Skip to main content

Sanshain Conan Plugin

A Conan extension to manage API specifications (OpenAPI, AsyncAPI, gRPC/Proto) during the C++ build process.

Compatibility: sanshain-conan 2.x speaks Sanshain Service 2.x. The catalog command needs Sanshain Service 2.4.0 or later; all other commands work with any 2.x service.

Description

Sanshain Conan Plugin allows you to:

  • Provide: Upload your service's API specification to the Sanshain service under the version declared in the spec file itself.
  • Require: Download API snippets of other services, pinned to an exact version, to generate client code.
  • Catalog: Push the project's Backstage catalog-info.yaml, so Sanshain's network graph shows the resources (databases, buckets, queues) the service depends on. Needs Sanshain Service 2.4.0 or later.

The 2.0 model

Sanshain 2.0 replaced branches with producer-declared versions:

  • The version lives in the spec file. For OpenAPI/AsyncAPI it is read from info.version; proto files must carry a // sanshain-version: MAJOR.MINOR.PATCH comment. Strict three-part semver, no suffixes.
  • Stability is declared on every provide. Every provide is a snapshot (overwritable work-in-progress) unless the ga switch is set — then it is ga (immutable; the number is permanently claimed).
  • Consumers pin exact versions. Each requires entry names the exact version to build against. No ranges, no latest, no fallback, no waiting.

Installation

Install from PyPI, into the same environment Conan runs in:

pip install sanshain-conan

Then import the helper directly in your conanfile.py:

from conan import ConanFile
from sanshainconan import Sanshain

class MyProject(ConanFile):
    def generate(self):
        sanshain = Sanshain(self)
        # Download required API specs (runs during `conan install`)
        sanshain.require()
        # proceed with client generation and build

The 1.x/2.0 python_requires = "sanshain-conan/x.y.z" pattern is gone — it resolved from a Conan remote, and the package now ships on PyPI instead.

Configuration

The plugin uses the standard sanshain.yaml file in the project root:

sanshainUrl: "https://sanshain.example.com"
serviceName: "my-cpp-service"
catalogInfo: "catalog-info.yaml"  # optional, pushed by the catalog command

provides:
  - file: "openapi.yaml"        # version read from its info.version

requires:
  - serviceName: "other-service"
    version: "1.2.0"            # exact pin (MAJOR.MINOR.PATCH)
    outputDirectory: "generated/sanshain"
    endpoints:
      - method: GET
        path: /api/v1/user

Every relative path in sanshain.yaml (file, openApiFile, asyncApiFile, protoFile, outputDirectory, catalogInfo) resolves against the folder that contains sanshain.yaml, never the working directory; absolute paths stay absolute. A config passed via --config (CLI) or config_path (Conan helper) therefore finds the same files as one in the current folder.

sanshain.yaml carries no stability and no version for provides — the version travels inside the spec file (MAJOR[.MINOR[.PATCH]], optionally v-prefixed; omitted parts are zero and the stored form is always three-part), and stability is decided by the ga switch (below). Branch-era fields (branch, timeout, baseVersion, releaseBranches) are rejected at parse time with a migration hint.

requires entry fields

Field Required Description
serviceName yes The Producer to require from.
version yes Exact pin, MAJOR.MINOR.PATCH.
outputDirectory yes Where the spec is written, relative to sanshain.yaml.
endpoints yes method + path pairs; several are downloaded as one bundle.
apiType no openapi (default), asyncapi or proto.
format no Only openapi, only with apiType: asyncapi: the AsyncAPI answer converted to OpenAPI.

AsyncAPI as OpenAPI (format)

format: openapi on an apiType: asyncapi entry asks Sanshain for the required channels as an OpenAPI document, for generators that only speak OpenAPI. It is the only conversion; any other format value or apiType is a configuration error. The file keeps its usual name (<serviceName>.yaml), so to get both formats list the entry twice with different outputDirectorys:

requires:
  - serviceName: "notifier"
    apiType: asyncapi
    version: "1.2.0"
    outputDirectory: "generated/asyncapi"
    endpoints:
      - method: PUB
        path: user/signup
  - serviceName: "notifier"
    apiType: asyncapi
    format: openapi
    version: "1.2.0"
    outputDirectory: "generated/openapi"
    endpoints:
      - method: PUB
        path: user/signup

format needs Sanshain Service 2.4.0 or later. An older service rejects the single-endpoint request or returns AsyncAPI; the client reports that it needs 2.4.0. The message is format: openapi needs Sanshain Service 2.4.0 or later (server is <version>) (a warning with best-effort, and nothing is written).

Unknown fields

A field no Sanshain client knows — usually a typo — is reported once per run (CLI: on stderr; Conan helper: as warnings) and otherwise ignored:

   /\_/\  ?
  ( O_o )   sanshain.yaml: unknown field 'catalogInf' - ignored
 ---U-U---

With strict: true the same message ends in rejected (strict) and the command fails; --best-effort does not soften this. Fields other Sanshain clients support but this one does not (force, insecure, combine, compression) are accepted silently. The branch-era fields above keep failing with their migration hint.

Environment

  • SANSHAIN_URL overrides sanshainUrl from sanshain.yaml (and makes it optional there), so the service address can change without a commit.
  • SANSHAIN_TOKEN is sent as the Bearer token.
  • The switches SANSHAIN_GA, SANSHAIN_TRUNK and SANSHAIN_BEST_EFFORT are on for true or 1 (trimmed, case-insensitive); any other value — yes, on, empty — is off. A CLI flag (--ga, --trunk, --best-effort) turns a switch on regardless of the environment.

Streams: trunk and release branches

Alongside stability, the pipeline declares which dependency graph its calls belong to. Like the ga switch this is a property of the invocation and never appears in sanshain.yaml:

  • Trunk CI sets SANSHAIN_TRUNK=true (CLI: --trunk) — provides mark trunk's current version and requires feed the main graph.
  • Release and hotfix pipelines set SANSHAIN_TAG=<branch> (CLI: --tag) — the calls update that sanshain-branch instead of trunk. The branch must already exist and is never created by a build; with an unknown one the call still succeeds but is recorded in no release graph, and Sanshain warns no sanshain-branch '<name>' - this build was not recorded in a release graph.

Declaring both is not an error either: the client warns trunk and tag are mutually exclusive - this build is recorded in neither and sends neither, so the build only feeds the dev graph. With strict: true it fails before any request is sent.

Sanshain warnings

The dependency graph is informational and never fails a build. When Sanshain accepts a call but could not record all of it, it says so in a Sanshain-Warning response header. Every such header line is printed as a warning (CLI: ⚠ <text>; Conan helper: output.warning("Sanshain: <text>")), on provide, retire, require and catalog calls alike, also on failed calls. With strict: true a warning fails the command after the call with strict: Sanshain warned: <text> (the first warning of that call); --best-effort does not soften this.

Retiring a protocol

Removing a provides entry tells Sanshain nothing — it cannot distinguish a dropped protocol from a pipeline that stopped running. Keep the entry and mark it:

provides:
  - apiType: asyncapi
    retired: true

The next provide retires that family: the capability tag is cleared, it leaves the current dependency graph, and its AsyncAPI channel contracts are released. Version history and existing Consumer pins are untouched. Retiring needs the releaser role — which a release pipeline already holds — or a maintainer grant on the Producer; publishing GA needs releaser too, and a 403 names the role and the snapshot fallback.

⚠️ AsyncAPI 2.x perspective. Sanshain reads 2.x publish/subscribe from the application's perspective: publish means this service publishes to the channel, subscribe means this service consumes it. The AsyncAPI 2.x specification defines those keywords from the client's perspective — exactly inverted. Sanshain deliberately uses the application-perspective reading because it matches the unambiguous 3.x send/receive mapping. A document authored with the spec-literal reading registers its contracts, and has its subscriptions harvested, exactly backwards. Harvested subscriptions are printed after every AsyncAPI provide; those with drift or no publisher yet are warnings and never fail the build.

Stability: the ga switch

Every provide is a snapshot by default — a developer building locally can never accidentally release. ga is an explicit act:

  • Environment variable: SANSHAIN_GA=true (works for the CLI and the Conan integration)
  • CLI flag: --ga

CI sets the switch on its protected-branch pipelines; that is the whole mechanism. There is no git detection and no branch matching.

# Local / feature pipeline: snapshot
python3 -m sanshainconan.cli provide

# Release pipeline: ga
python3 -m sanshainconan.cli --ga provide
# or: SANSHAIN_GA=true python3 -m sanshainconan.cli provide

Usage

Conan Integration

Import the installed package in your conanfile.py:

from conan import ConanFile
from sanshainconan import Sanshain

class MyProject(ConanFile):
    def generate(self):
        sanshain = Sanshain(self)
        # Download required API specs (runs during `conan install`)
        sanshain.require()

Why generate() and not build()?

Conan best practice recommends keeping build() simple — it should only compile. The generate() method runs during conan install and is designed to prepare all build inputs (toolchain files, downloaded specs, generated code). Placing require() in generate() means developers can run conan install . followed by a native build (e.g., cmake --build .) without needing Conan during the actual compilation step.

Why is provide() not in a Conan lifecycle method?

Uploading your API spec to Sanshain is a publish action with side effects — it should only happen once after tests pass, typically as a CI step. Use the CLI:

CLI Tool

# Provide (upload) spec — run in CI after tests pass; add --ga on release pipelines
python3 -m sanshainconan.cli provide

# Require (download) specs — alternative to generate() integration
python3 -m sanshainconan.cli require

# Push catalog-info.yaml (needs Sanshain Service 2.4.0)
sanshain-conan catalog

The installed package also provides the sanshain-conan command; sanshain-conan provide is the same as python3 -m sanshainconan.cli provide.

Catalog

catalogInfo in sanshain.yaml names the project's Backstage catalog-info.yaml. The catalog command (CLI: sanshain-conan catalog; Conan helper: Sanshain(self).catalog()) pushes it with PUT /catalog/<serviceName>. Sanshain reads only the Component whose metadata.name equals serviceName, and of it only the resource: refs in spec.dependsOn; each push replaces the previous set.

Needs Sanshain Service 2.4.0 or later. An older service answers 404, reported as sanshain catalog needs Sanshain Service 2.4.0 or later (server is <version>). Every other command works with any 2.x service.

  • A trunk build (--trunk / SANSHAIN_TRUNK=true) updates the main and the dev graph; any other build only the dev graph. A sanshain-branch (--tag) is not sent.

  • On success:

    Catalog recorded for orders-service (main+dev): 2 resources
      resource:default/orders-bucket
      resource:default/orders-db
    
  • Without catalogInfo the command warns No catalogInfo in sanshain.yaml - skipping catalog. and exits 0; with strict: true it fails. Strict checks only serviceName and catalogInfo here, never provides or requires.

  • A missing file fails naming the resolved path. With --best-effort (or SANSHAIN_BEST_EFFORT / bestEffort: true) every failed push is a warning and the command exits 0.

  • If Sanshain cannot read the file as this project's Component — invalid YAML, or usually a metadata.name that is not the serviceName — it records nothing (the previous periphery stays) and answers with a warning naming the reason, which is printed; the command exits 0. With strict: true it fails with strict: Sanshain warned: <reason>.

Add it as a CI step next to provide, e.g. on trunk:

SANSHAIN_TRUNK=true sanshain-conan catalog

Provide Response Summary

After each successful provide, the CLI logs a human-readable summary:

✓ Provided 1.4.0 (snapshot): 2 new, 1 updated, 0 deleted endpoints

Re-providing byte-identical content is a no-op on the server (changes reports all zero), so CI re-runs of the same commit never fight.

Failure Modes

The 2.0 server fails immediately — nothing waits:

  • 404 Unknown: the producer or the pinned version does not exist (in either stability). A configuration error — fix the version pin. List what exists: GET /producers/<serviceName>/versions.

  • 410 Absent: the pinned version exists but deliberately does not include the requested endpoint(s).

  • 409 Version conflict (provide): rejected by the version rules — e.g. re-providing an existing GA version with different content. The CLI surfaces the server's message and the proposed next free version:

    ✗ Version conflict: GA 1.2.0 is immutable
      Publish as 1.3.0 — update info.version in specs/openapi.yaml
    

    The plugin never modifies your spec files — bump the version yourself and republish.

  • Wrong server: if a provide/require fails and the instance turns out to be pre-2.0, the confusing error is replaced with Sanshain server at <url> is <version>; this client requires Sanshain 2.x — upgrade the server.

Require-Side ETag Caching

The CLI stores the ETag from require responses and sends If-None-Match on subsequent runs. On 304 Not Modified:

⏭ user-service spec unchanged (304), skipping code generation.

A pin on a GA version can never change content; a pin on a snapshot can — which is exactly what the ETag detects.

State File

The local cache is stored at .sanshain-cache/state.json in the current working directory. The format is:

{
  "requires": {
    "user-service|1.2.0|GET|/api/v1/users": {
      "etag": "\"sha256:def456...\"",
      "last_fetched": "2026-04-25T12:00:00Z"
    }
  }
}

Strict Mode

By default, the CLI warns and skips when configuration is incomplete (no serviceName, no provides, no requires, no catalogInfo for catalog). This makes it safe to run both commands even if only one applies.

To fail on missing configuration, enable strict mode in sanshain.yaml:

strict: true

When strict mode is enabled:

  • Missing serviceName → exit with error
  • No provides configured → exit with error
  • No requires configured → exit with error
  • catalog without catalogInfo → exit with error (catalog checks only serviceName and catalogInfo)
  • An unknown field in sanshain.yaml → exit with error (see Unknown fields)
  • A Sanshain-Warning from the service (see Sanshain warnings) → exit with error after the call, also with --best-effort
  • Both trunk and tag declared → exit with error before any request

Note that a requires entry without a version, or any leftover branch-era field, is always a hard error — strict mode does not change that.

Features

  • Exact Version Pins: Requires resolve immediately against the pinned version — GA preferred, else the same-numbered snapshot, else failure.
  • Bundle Support: Downloads merged API specs for multiple endpoints with deduplicated schemas.
  • Bearer Token Auth: Uses SANSHAIN_TOKEN environment variable.
  • GZIP Support: Efficiently downloads large specifications.

Development

Local Development with uv

It is recommended to use uv for managing the development environment.

  1. Install uv: Follow the official installation guide.

  2. Sync environment:

    uv sync --dev
    
  3. Run tests:

    uv run python -m unittest discover tests
    
  4. Linting and Checks:

    uv run flake8 .
    uv run black --check .
    uv run bandit -r .
    uv run mypy .
    
  5. Running Tools without Install (uvx):

    uvx black .
    

Traditional Installation

If you prefer pip, you can still use it:

  1. Install dependencies:

    pip install -r requirements-dev.txt
    
  2. Run tests:

    export PYTHONPATH=$PYTHONPATH:.
    python3 -m unittest discover tests
    

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

Metadata

Release files for sanshain-conan 2.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sanshain-conan 2.3.0
File Size Uploaded Provenance
sanshain_conan-2.3.0.tar.gz 87.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sanshain-conan 2.3.0
File Interpreter ABI Platform
sanshain_conan-2.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 117.2 kB

Release files / sanshain_conan-2.3.0.tar.gz

Download URL sanshain_conan-2.3.0.tar.gz
Size 87.0 kB
Tags Source
SHA-256 checksum
How to use checksums
b58782a3c4b8ec94339922ae6a7595d13a236de783e78c301a71b9d7c450a99b
BLAKE2b-256 checksum
How to use checksums
e545d058c19ecfce005ac1d8e33e04a06e183eb67243b26098b38e4b3a76bce6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / sanshain_conan-2.3.0-py3-none-any.whl

Download URL sanshain_conan-2.3.0-py3-none-any.whl
Size 30.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3ce0165e444351f1848085684862bdb5cc25cdea03795ac22eb747f8fbd62b73
BLAKE2b-256 checksum
How to use checksums
5cedd97994f85dda525a9d2804c9681d434edc0cfd6e7ebf7c94d867d8159d5d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Security for sanshain-conan 2.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for sanshain_conan-2.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page