Skip to content

Add enum, additionalProperties, validation, auth generation, and per-call config support#258

Draft
phalt wants to merge 8 commits into
mainfrom
claude/stoic-cray-xg5qn8
Draft

Add enum, additionalProperties, validation, auth generation, and per-call config support#258
phalt wants to merge 8 commits into
mainfrom
claude/stoic-cray-xg5qn8

Conversation

@phalt

@phalt phalt commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Change summary

This PR delivers the first two releases of the feature roadmap (committed as PLAN.md): three generator-robustness features, plus two headline features for authentication and multi-tenant use.

  1. Non-string enum support: Integer, float, and mixed-type enums now generate correctly instead of crashing with AttributeError. Integer enums use enum.IntEnum, others use enum.Enum with synthesized VALUE_ member names. Negative numbers and decimals are sanitized appropriately, and colliding names are deduplicated with numeric suffixes.

  2. Typed additionalProperties (map types): Object schemas with schema-valued additionalProperties now generate typed dictionaries (dict[str, ValueType]). Pure map schemas (no properties of their own) generate a new DictResponse base class that provides dict-like access while validating values. Free-form objects (additionalProperties: true or absent) retain the existing dict[str, typing.Any] behavior.

  3. clientele validate CLI command: A new pre-flight validation command walks OpenAPI specs and reports errors (unresolvable $refs) and warnings (constructs that degrade, like cookie parameters, multipart bodies, or unsupported security schemes). Exits with status 1 on errors to gate CI, status 0 on warnings or clean specs.

  4. Auth config generation from securitySchemes: The generator now reads components.securitySchemes and renders typed credential fields on the generated Configbearer_token for HTTP bearer, basic_username/basic_password for HTTP basic, and api_key for header API keys. Credentials inject the right header when set (never overriding explicit headers), and environment variable / .env support comes free via pydantic-settings. Unsupported schemes (oauth2, openIdConnect, query/cookie API keys) get an explanatory comment in config.py plus warnings from generation and validate.

  5. Per-call config overrides (closes OpenAPI client generator factory mode #233): Decorated functions and the direct request/arequest methods accept a reserved config keyword argument that overrides the client-wide configuration for a single call — base URL, headers, logger, and HTTP backend. The client-wide config is never mutated, so concurrent calls against different hosts (multi-tenant APIs, e.g. OCPI) are safe, and each override config lazily creates and caches its own HTTP backend/connection pool. Works in already-generated clients without regeneration.

Note: the branch has been merged with main's 2.3.0 restructure — the new test files live under the reorganized layout (tests/generators/, tests/api/) and the moved SchemasGenerator/request_context paths are used throughout.

Implementation details

  • Enums: Modified generate_enum_properties() in clientele/generators/shared/schemas.py to accept a list of values instead of a dict, synthesize member names based on type, and emit the correct base class. Added _enum_member_name() and _enum_base_class() helpers.
  • additionalProperties: Extended get_type() in clientele/generators/shared/utils.py to detect and type schema-valued additionalProperties (carried through cicerone_compat and 3.1 normalization). Added DictResponse base class in clientele/schemas.py with dict-like methods (__getitem__, keys(), items(), etc.). Pure map schemas emit DictResponse subclasses via the shared root-model template.
  • Validation: New SpecValidator class in clientele/generators/validation.py walks the spec tree, checks $ref resolution, and reports unsupported constructs. Integrated into CLI via the new validate command, which also handles unparseable files gracefully.
  • Auth generation: New clientele/generators/shared/security.py classifies security schemes (shared by generator and validator). config_py.jinja2 renders credential fields and a model_post_init that injects headers via setdefault. Basic-auth fields deliberately avoid username/password names so pydantic-settings' case-insensitive env matching cannot pick up the OS $USERNAME variable.
  • Per-call config: A reserved config kwarg in _prepare_call, following the exact pattern of the existing reserved query/headers kwargs, carried on PreparedCall (clientele/api/request_context.py) and resolved in all four execution paths (sync, async, both streaming variants) plus direct requests. Functions that declare their own config parameter keep it as a normal argument.

Related issue(s)

Pull request tasks

The following have been completed for this task:

  • Code changes
  • Tests written (test-first for every feature)
    • tests/generators/test_enum_generation.py — all enum variants, pydantic round-trips, name collisions
    • tests/generators/test_additional_properties.py — map type generation, validation, response_map compatibility
    • tests/generators/test_validation.py — every finding type plus CLI exit codes
    • tests/generators/test_auth_generation.py — per-scheme generation, header injection, env vars, end-to-end via FakeHTTPBackend
    • tests/api/test_config_override.py — tenant routing, non-mutation, async, streaming, lazy backend caching, collision guard
  • Documentation changes for new or changed features
    • docs/openapi-cli.mdvalidate command, generated enums and map types
    • docs/api-authentication.md — rewritten to lead with generated auth support
    • docs/api-configuration.md — new "Per-call configuration" section
  • Pre-review preparation
    • make test — full suite passes (662 tests, post-merge with main's 2.3.0 restructure)
    • make format — code formatted
    • make ty — type checking passes
    • make generate-test-clients — test clients regenerated with new enum/map support
  • Changelog updated (docs/CHANGELOG.md, folded into the 2.3.0 UNRELEASED section)
  • Merged with main — conflicts from the 2.3.0 project restructure resolved

Code examples

Enum generation:

# Input: {"type": "integer", "enum": [1, 2, 3]}
class StatusCode(enum.IntEnum):
    VALUE_1 = 1
    VALUE_2 = 2
    VALUE_3 = 3

# Input: {"type": "string", "enum": ["red", "green"]}
class Colour(str, enum.Enum):
    RED = "red"
    GREEN = "green"

Map types:

# Property with typed map
class Report(pydantic.BaseModel):
    scores: typing.Optional[dict[str, int]] = None

# Pure map schema
class ScoreMap(DictResponse[int]):
    pass

scores = ScoreMap.model_validate({"alice": 3, "bob": 5})
scores["alice"]  # 3

Generated auth config:

# Input: {"bearerAuth": {"type": "http", "scheme": "bearer"}}
class Config(clientele_api.BaseConfig):
    base_url: str = "https://api.example.com"
    bearer_token: str = ""  # sends "Authorization: Bearer <token>" when set

Per-call config override:

party_a = BaseConfig(base_url="https://party-a.example.com", headers={"Authorization": "Bearer <token-a>"})
party_b = BaseConfig(base_url="https://party-b.example.com", headers={"Authorization": "Bearer <token-b>"})

user_a = get_user(user_id=1, config=party_a)
user_b = get_user(user_id=1, config=party_b)  # safe concurrently, no global mutation

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE

claude added 7 commits June 10, 2026 12:48
Documents a prioritized plan of features identified by auditing the
generator and runtime against real-world OpenAPI specs and the issue
tracker. Each item includes user value, difficulty, an implementation
sketch grounded in the current code, and risks. Release A (generator
robustness) covers: non-string enum support, typed additionalProperties,
and a new 'clientele validate' pre-flight command.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
OpenAPI enums are not restricted to strings: integer enums (status
codes, versions), number enums, and mixed-type enums are all legal and
common in real-world specs. The generator previously assumed string
values and crashed with 'AttributeError: int object has no attribute
upper' on any non-string member, blocking generation entirely.

Enum classes are now generated with a base class chosen from the member
values: all-string enums keep the existing (str, enum.Enum) shape,
all-integer enums become enum.IntEnum, and number or mixed-type enums
become plain enum.Enum. Non-string members are named VALUE_<n>, with
MINUS_ for negative numbers and underscores for decimal points. Member
names that collide after sanitisation are deduplicated with a numeric
suffix.

Tests were written first against a new enums.json example spec and
exercise generation, importability, pydantic validation round-trips,
and name collisions. Documented in docs/openapi-cli.md.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
OpenAPI object schemas with a schema-valued additionalProperties
describe maps - error maps, translations, metrics keyed by id - and
are common in real-world specs. The generator previously dropped
additionalProperties entirely (the compat layer never copied it out of
the parsed cicerone model), so every map degraded to
dict[str, typing.Any] and lost all value validation.

Map-valued properties now generate dict[str, <type>], including refs
to other models (values are hydrated and validated by pydantic).
Component schemas that are purely maps (additionalProperties with no
properties) generate subclasses of a new clientele.schemas.DictResponse
root model - the map sibling of the existing ListResponse - so they are
real types that pydantic accepts in response_map, with dict-style
access (indexing, len, iteration, keys/values/items/get).

Free-form objects (additionalProperties: true, {}, or absent) and
objects that declare their own properties keep their existing
behaviour. additionalProperties schemas are also normalized for
OpenAPI 3.1 type arrays like every other nested schema position.

Tests were written first: unit tests for type resolution and 3.1
normalization, plus integration tests against a new
additional_properties.json example spec covering validation
round-trips, model hydration, and response_map compatibility.
Documented in docs/openapi-cli.md.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
Incompatible OpenAPI specs previously failed half-way through
generation: $refs to missing components crash with an opaque
AttributeError or silently produce broken output, and unsupported
constructs (cookie parameters, multipart bodies) are silently dropped
with no trace. This is the most common source of confusing failures
for new users pointing clientele at real-world specs.

The new 'clientele validate' command walks a schema without generating
anything and reports findings in two severities:

- errors: constructs that break generation, i.e. $ref references to
  schemas or parameters missing from components
- warnings: constructs that degrade - non-component $refs (become
  typing.Any), cookie parameters (skipped), multipart/form-data bodies
  (no file upload support), operations missing responses, and response
  content without a schema

It exits 1 on errors and 0 otherwise, so it can gate CI ahead of
generation. Validation logic lives in a standalone SpecValidator
(clientele/generators/validation.py) reusing the existing
cicerone_compat conversion layer, with the CLI as a thin wrapper that
also handles unparseable files gracefully.

Tests were written first: unit tests for every finding type plus CLI
tests for exit codes, usage errors, and a real example spec. Documented
in docs/openapi-cli.md.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
Picks up the DictResponse import and the now-typed additionalProperties
map in best.json (dict[str, str] instead of dict[str, typing.Any]).
Regenerated via the existing make generate-test-clients commands.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
Nearly every real-world API declares its authentication in
components.securitySchemes, but the generator ignored that section
entirely - the docs told every user to hand-edit config.py with the
right headers. This was the most visible 'it almost works' gap for
generated clients.

The generator now classifies the spec's security schemes and renders
typed credential fields on the generated Config:

- http/bearer       -> bearer_token field; sends
                       'Authorization: Bearer <token>'
- http/basic        -> basic_username/basic_password fields; sends
                       'Authorization: Basic <base64>'
- apiKey in header  -> api_key field; sends the scheme's named header
                       (multiple key schemes get fields named after
                       the scheme, deduplicated against BaseConfig)

Injection happens in model_post_init via setdefault, so credentials
are only sent when set and never override headers the user provides
explicitly. Because they are pydantic-settings fields, environment
variable and .env support come for free (e.g. BEARER_TOKEN).

Unsupported schemes - oauth2, openIdConnect, and API keys in query or
cookie - get an explanatory comment in config.py, a console warning at
generation time, and a warning from clientele validate. Classification
logic lives in clientele/generators/shared/security.py so the generator
and validator stay in agreement.

Basic auth field names deliberately avoid 'username'/'password' to
prevent accidental pickup of the OS-level USERNAME environment
variable through pydantic-settings' case-insensitive matching.

Tests were written first: per-scheme generation and header-injection
tests against inline specs, an end-to-end test asserting the credential
reaches the wire via FakeHTTPBackend, and validator warning coverage.
Documented in docs/api-authentication.md.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
Generated clients create one module-global APIClient, binding every
decorated function to a single configuration. Users integrating with
multi-tenant APIs - one OpenAPI spec served by many hosts, such as the
OCPI EV-roaming protocol from issue #233 - could not direct individual
calls at different base URLs or credentials. The only workaround,
client.configure(), mutates global state and races under concurrency.

Decorated functions and the direct request/arequest methods now accept
a reserved 'config' keyword argument, following the exact pattern of
the existing reserved 'query' and 'headers' kwargs:

- the call uses the override's base_url, headers, logger, and HTTP
  backend; the client-wide config is never touched, so interleaved
  calls with different overrides are safe
- an override without an http_backend gets one lazily, cached on the
  override config itself, so each tenant keeps its own connection pool
- functions that declare their own 'config' parameter keep it as a
  normal argument (the reserved kwarg is disabled, like 'query')
- all four execution paths are covered: sync, async, and both
  streaming variants

No template changes are needed - every already-generated client
function gains multi-tenant support without regeneration.

Tests were written first: routing to per-tenant backends and headers,
non-mutation of the client config, interleaved tenants, async, direct
requests, streaming, lazy backend caching, and the parameter-collision
guard. Documented in docs/api-configuration.md.

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
@phalt phalt changed the title Add enum, additionalProperties, and validation support Add enum, additionalProperties, validation, auth generation, and per-call config support Jun 10, 2026
Resolves conflicts with main's project restructure:
- generator.py: combine main's flattened import paths with the new
  security-scheme classification import
- shared/schemas.py: keep the non-string enum implementation at the
  module's new location; drop the vestigial
  generated_response_class_names attribute that main removed
- docs/CHANGELOG.md: fold this branch's feature entries into the new
  '2.3.0 UNRELEASED' section

Follow-ups to match the new layout: moved the new generator test files
under tests/generators/, updated the one import of the relocated
SchemasGenerator, and hardened the generated-module import helpers in
the new tests to purge cached schemas/config/client modules on entry
(main's reorganized suite runs more tests that import generated
modules of the same names).

https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAPI client generator factory mode

2 participants