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.PATCHcomment. 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 isga(immutable; the number is permanently claimed). - Consumers pin exact versions. Each
requiresentry names the exactversionto build against. No ranges, nolatest, 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_URLoverridessanshainUrlfromsanshain.yaml(and makes it optional there), so the service address can change without a commit.SANSHAIN_TOKENis sent as the Bearer token.- The switches
SANSHAIN_GA,SANSHAIN_TRUNKandSANSHAIN_BEST_EFFORTare on fortrueor1(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 warnsno 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/subscribefrom the application's perspective:publishmeans this service publishes to the channel,subscribemeans 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.xsend/receivemapping. 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 notbuild()?Conan best practice recommends keeping
build()simple — it should only compile. Thegenerate()method runs duringconan installand is designed to prepare all build inputs (toolchain files, downloaded specs, generated code). Placingrequire()ingenerate()means developers can runconan 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
catalogInfothe command warnsNo catalogInfo in sanshain.yaml - skipping catalog.and exits 0; withstrict: trueit fails. Strict checks onlyserviceNameandcatalogInfohere, never provides or requires. -
A missing file fails naming the resolved path. With
--best-effort(orSANSHAIN_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.namethat is not theserviceName— it records nothing (the previous periphery stays) and answers with a warning naming the reason, which is printed; the command exits 0. Withstrict: trueit fails withstrict: 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:
-
404Unknown: the producer or the pinned version does not exist (in either stability). A configuration error — fix theversionpin. List what exists:GET /producers/<serviceName>/versions. -
410Absent: the pinned version exists but deliberately does not include the requested endpoint(s). -
409Version 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.yamlThe 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
providesconfigured → exit with error - No
requiresconfigured → exit with error catalogwithoutcatalogInfo→ exit with error (catalogchecks onlyserviceNameandcatalogInfo)- An unknown field in
sanshain.yaml→ exit with error (see Unknown fields) - A
Sanshain-Warningfrom 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_TOKENenvironment variable. - GZIP Support: Efficiently downloads large specifications.
Development
Local Development with uv
It is recommended to use uv for managing the development environment.
-
Install uv: Follow the official installation guide.
-
Sync environment:
uv sync --dev
-
Run tests:
uv run python -m unittest discover tests
-
Linting and Checks:
uv run flake8 . uv run black --check . uv run bandit -r . uv run mypy .
-
Running Tools without Install (uvx):
uvx black .
Traditional Installation
If you prefer pip, you can still use it:
-
Install dependencies:
pip install -r requirements-dev.txt
-
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| sanshain_conan-2.3.0.tar.gz | 87.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.