Skip to content

Authoring a product (products/<product>/sdk.yml)

phantasos builds a vendored, self-contained Python SDK for an OpenAPI spec from a declarative product directory.

Quickstart

Create the product directory:

products/<product>/
├── openapi.yml                 # OpenAPI source document
├── sdk.yml                     # SDK config (see config reference below)
├── overrides/
│   └── README.md.jinja         # required — becomes the generated SDK's README
└── hooks.py                    # optional — Python preprocess/patch hooks

Write a minimal sdk.yml:

package: my_product              # Python import name (snake_case)
output: ../../../my-product-sdk  # where to write the SDK (relative to sdk.yml)
base_url: https://api.example.com
facade: true                     # bind generated *Api classes onto one client
project:                         # required to scaffold a full project
  distribution: my-product-sdk
  author: Jane Smith
  author_email: jane@example.com
  repo_url: https://github.com/org/my-product-sdk

Build the SDK, then the CLI:

phantasos sdk build <product>

For what each build stage does, see Architecture. The full configuration surface is the reference below.


Build-config fields (sdk.yml)

Field Type Default Description
package string — (required) Python package name for the generated SDK
output string — (required) Path to write the SDK to (relative to sdk.yml)
base_url string — (required) Default API host injected into component templates
generator block see below OpenAPI Generator invocation options (generator: section)
spec string "./openapi.yml" Path to the OpenAPI document (relative to sdk.yml)
apply_generic_patches bool true Apply apostrophe-enum, lenient-enum, and oneOf first-match patches

generator:

Options passed to the OpenAPI Generator invocation.

Field Type Default Description
library string "urllib3" OpenAPI Generator HTTP library (urllib3 or httpx)
oneof_discriminator_lookup bool true Dispatch oneOf deserialization via the spec's discriminator mapping (OAG useOneOfDiscriminatorLookup). Without it, oneOf payloads resolve by trial deserialization, which mis-types variants once enums are lenient. Disable only for a spec whose discriminator mapping is wrong.
generator:
  library: urllib3
  oneof_discriminator_lookup: true

No-op for specs without discriminator blocks (e.g. adem) — those keep trial deserialization.


Components

Each component produces a vendored file under <package>/extras/. Set a component to the built-in type name, a custom template path, or omit it entirely to skip vendoring.

auth

Writes extras/auth.py.

Built-in type: scm_oauth

Strata Cloud Manager (SCM) OAuth2 client-credentials grant (Basic creds, form body), auto-refreshing token. The token_url is baked in as https://auth.apps.paloaltonetworks.com/oauth2/access_token — override only if targeting a different IdP endpoint.

auth:
  type: scm_oauth
  # token_url: https://auth.apps.paloaltonetworks.com/oauth2/access_token  # baked default
  scope_env: SCOPE                    # default: SCOPE
  client_id_env: CLIENT_ID            # default: CLIENT_ID
  client_secret_env: CLIENT_SECRET    # default: CLIENT_SECRET
  base_url_env: BASE_URL              # default: BASE_URL
  config_class_name: SdkConfiguration # default: SdkConfiguration

pagination

Writes extras/pagination.py.

Built-in type: cursor and offset

Cursor paging: items under data_field, cursor under page_info.

pagination:
  type: cursor
  data_field: data               # default
  page_info_field: page_info     # default
  cursor_field: cursor           # default
  has_next_field: has_next_page  # default

errors

Writes extras/errors.py.

Built-in type: nested and list

Helpers over typed exceptions; extracts a message from body[error_field][message_field].

errors:
  type: nested
  error_field: error     # default
  message_field: message # default
  code_field: code       # default

facade

Writes extras/facade.py. Binds each generated *Api class as client.<resource> and exposes client.paginate(...) when pagination is present. Resources are auto-discovered from the generated api/__init__.py.

facade: true   # shorthand for type: default (no config fields)

Custom templates

Set any component's type to a relative path ending in .jinja to use a per-product template instead of a built-in:

auth:
  type: ./templates/api_key.py.jinja
  header_name: X-API-Key

phantasos resolves the path relative to sdk.yml's directory, verifies it exists, and passes all other fields as template variables.


docs:

Opt-in. Add a docs: block and the build emits a complete documentation site inside the generated SDK — guides for authentication, pagination, and CRUD, plus an API reference auto-generated from the emitted docstrings. Omit the block and no docs are scaffolded. The only required field is showcase_resource.

docs:
  showcase_resource: applications
Field Type Default Description
showcase_resource string — (required) The resource (facade attribute) whose CRUD operations drive the worked examples on the home and CRUD pages. Must match a discovered resource.
showcase_variant string null Optional variant name passed to the example synthesizer, selecting an alternate set of synthesized request bodies.
site_name string project distribution Title of the generated site. Defaults to the project's distribution name.
operations block auto-classified Per-verb override of which SDK method maps to each CRUD slot (see below).
examples block auto-synthesized Per-slot verbatim replacement of a generated example code block (see below).

operations:

By default phantasos classifies the showcase resource's methods into the five CRUD slots automatically. Override any slot by naming the exact SDK method:

docs:
  showcase_resource: applications
  operations:
    create: create_application
    read: get_application_by_id
    list: list_applications
    update: patch_application_by_type_and_id
    delete: delete_application_by_id

Each key is optional; an unset slot keeps the auto-classified method (or is omitted if the resource has no such operation).

examples:

For any slot, supply a verbatim code block to render instead of the auto-synthesized example. Use this when the synthesized body cannot capture a realistic payload:

docs:
  showcase_resource: applications
  examples:
    create: |
      app = client.applications.create_application(
          type="web",
          create_or_replace_app_input=CreateOrReplaceAppInput(name="Acme"),
      )

Each key (create, read, list, update, delete) is optional and overrides only that slot's example.

Building the generated SDK's docs

The generated SDK ships its own noxfile.py and docs workflow. Build its site from inside the generated SDK directory:

cd ../my-product-sdk
uv run nox -s docs        # strict mkdocs build
uv run nox -s docs-serve  # live-reload preview

phantasos's own nox -s sdk-docs session is an integration check that builds the prisma-browser SDK and its docs end-to-end; see Development.


transforms:

Declarative spec pre-processing applied before OpenAPI Generator runs.

hoist

Promote an inline array.items object to a named schema.

transforms:
  hoist:
    - schema: SomeControl   # component schema containing the array
      field: items          # property name of the array
      item: SomeEntry       # new name for the hoisted schema

tag_operations

Assign operationId and tag to a specific path+method.

transforms:
  tag_operations:
    - path: /v1/things
      method: get
      operation_id: ListThings
      tag: Things

hooks: ./hooks.py

Optional Python module (path relative to sdk.yml). Define either or both of:

def preprocess(spec: dict) -> None:
    """Called after declarative transforms, before OpenAPI Generator."""
    ...

def patch(pkg_dir) -> None:
    """Called after generic patches, on the generated package directory."""
    ...

Hooks run after the declarative transforms: block.


vars:

Supplemental template variables merged into the Jinja context for all component templates and include: templates.

vars:
  support_email: sdk@example.com
  api_version: v2

Reserved names (auto-exposed by phantasos — must NOT be shadowed): package, library, base_url, spec_version, spec_title, has_auth, has_pagination, has_errors, has_facade, config_class_name.


include:

Copy additional Jinja templates into <package>/extras/. Keys are destination filenames (under extras/); values are template paths relative to sdk.yml.

include:
  banner.py: ./templates/banner.py.jinja

Destination paths must stay within extras/ — path traversal is rejected.


project:

The project: block is required when building a scaffold (i.e. when phantasos renders pyproject.toml, GitHub workflows, docs, and other project files into the generated SDK).

project:
  distribution: my-sdk           # PyPI distribution name (required)
  author: Jane Smith             # (required)
  author_email: jane@example.com # (required)
  repo_url: https://github.com/org/my-sdk  # (required)
  description: "Python SDK for the My API" # default: ""
  license: Apache-2.0            # SPDX id; default: Apache-2.0
  python_versions: ["3.11", "3.12", "3.13", "3.14"]  # default
  dependencies:                  # default: urllib3/python-dateutil/pydantic/typing-extensions
    - "urllib3 >= 2.1.0, < 3.0.0"
    - "python-dateutil >= 2.8.2"
    - "pydantic >= 2.11"
    - "typing-extensions >= 4.7.1"

dependencies — the defaults match what OpenAPI Generator itself would emit for generator.library: urllib3, so you almost never need to override this field. Only set it when the SDK genuinely needs additional or different runtime deps.


overrides/

products/<product>/overrides/ mirrors the generated SDK tree. Any file placed here at the same relative path replaces the corresponding built-in scaffold template (same-path-wins). This is how per-product customisation is layered over the shared src/phantasos/scaffold/ templates without modifying the scaffold itself.

overrides/README.md.jinja — required. This becomes the README.md of the generated SDK. The Jinja context exposes all sdk.yml values plus the standard phantasos variables (package, base_url, spec_title, has_auth, has_pagination, has_errors, has_facade, config_class_name).

overrides/tests/ — optional. Jinja templates here are rendered into the generated SDK's tests/ directory alongside the gated component tests from the built-in scaffold. Use this for per-product integration, contract, or model-specific tests.

All files in overrides/ are version-controlled in this repo and are never lost across regenerations — the generated SDK is a pure build artifact.


Concrete examples

products/prisma-browser/sdk.yml

package: prisma_browser
output: ../../../prisma-browser-sdk
base_url: https://api.sase.paloaltonetworks.com
auth:
  type: scm_oauth
  scope_env: SCOPE
  base_url_env: PRISMA_SASE_BASE_URL
  config_class_name: PrismaSaseConfiguration
pagination: {type: cursor}
errors: {type: nested}
facade: true
transforms:
  hoist:
    - schema: AllowedOrBlockedExtensionsControl
      field: extensions
      item: AllowedOrBlockedExtensionEntry
    - schema: LaunchingExternalApplicationsControl
      field: exceptions
      item: ExternalApplicationLaunchException
  tag_operations:
    - path: /seb-api/v1/user-requests
      method: get
      operation_id: ListUserRequests
      tag: User Requests

products/adem/sdk.yml

package: adem
output: ../../../adem-sdk
base_url: https://api.sase.paloaltonetworks.com
auth:
  type: scm_oauth
  scope_env: SCOPE
  base_url_env: ADEM_BASE_URL
  config_class_name: AdemConfiguration
facade: true
hooks: ./hooks.py