Add enum, additionalProperties, validation, auth generation, and per-call config support#258
Draft
phalt wants to merge 8 commits into
Draft
Add enum, additionalProperties, validation, auth generation, and per-call config support#258phalt wants to merge 8 commits into
phalt wants to merge 8 commits into
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Non-string enum support: Integer, float, and mixed-type enums now generate correctly instead of crashing with
AttributeError. Integer enums useenum.IntEnum, others useenum.Enumwith synthesizedVALUE_member names. Negative numbers and decimals are sanitized appropriately, and colliding names are deduplicated with numeric suffixes.Typed additionalProperties (map types): Object schemas with schema-valued
additionalPropertiesnow generate typed dictionaries (dict[str, ValueType]). Pure map schemas (nopropertiesof their own) generate a newDictResponsebase class that provides dict-like access while validating values. Free-form objects (additionalProperties: trueor absent) retain the existingdict[str, typing.Any]behavior.clientele validateCLI 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.Auth config generation from
securitySchemes: The generator now readscomponents.securitySchemesand renders typed credential fields on the generatedConfig—bearer_tokenfor HTTP bearer,basic_username/basic_passwordfor HTTP basic, andapi_keyfor header API keys. Credentials inject the right header when set (never overriding explicit headers), and environment variable /.envsupport comes free via pydantic-settings. Unsupported schemes (oauth2,openIdConnect, query/cookie API keys) get an explanatory comment inconfig.pyplus warnings from generation andvalidate.Per-call config overrides (closes OpenAPI client generator factory mode #233): Decorated functions and the direct
request/arequestmethods accept a reservedconfigkeyword 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.Implementation details
generate_enum_properties()inclientele/generators/shared/schemas.pyto 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.get_type()inclientele/generators/shared/utils.pyto detect and type schema-valuedadditionalProperties(carried throughcicerone_compatand 3.1 normalization). AddedDictResponsebase class inclientele/schemas.pywith dict-like methods (__getitem__,keys(),items(), etc.). Pure map schemas emitDictResponsesubclasses via the shared root-model template.SpecValidatorclass inclientele/generators/validation.pywalks the spec tree, checks$refresolution, and reports unsupported constructs. Integrated into CLI via the newvalidatecommand, which also handles unparseable files gracefully.clientele/generators/shared/security.pyclassifies security schemes (shared by generator and validator).config_py.jinja2renders credential fields and amodel_post_initthat injects headers viasetdefault. Basic-auth fields deliberately avoidusername/passwordnames so pydantic-settings' case-insensitive env matching cannot pick up the OS$USERNAMEvariable.configkwarg in_prepare_call, following the exact pattern of the existing reservedquery/headerskwargs, carried onPreparedCall(clientele/api/request_context.py) and resolved in all four execution paths (sync, async, both streaming variants) plus direct requests. Functions that declare their ownconfigparameter keep it as a normal argument.Related issue(s)
Pull request tasks
The following have been completed for this task:
tests/generators/test_enum_generation.py— all enum variants, pydantic round-trips, name collisionstests/generators/test_additional_properties.py— map type generation, validation, response_map compatibilitytests/generators/test_validation.py— every finding type plus CLI exit codestests/generators/test_auth_generation.py— per-scheme generation, header injection, env vars, end-to-end viaFakeHTTPBackendtests/api/test_config_override.py— tenant routing, non-mutation, async, streaming, lazy backend caching, collision guarddocs/openapi-cli.md—validatecommand, generated enums and map typesdocs/api-authentication.md— rewritten to lead with generated auth supportdocs/api-configuration.md— new "Per-call configuration" sectionmake test— full suite passes (662 tests, post-merge with main's 2.3.0 restructure)make format— code formattedmake ty— type checking passesmake generate-test-clients— test clients regenerated with new enum/map supportdocs/CHANGELOG.md, folded into the2.3.0 UNRELEASEDsection)main— conflicts from the 2.3.0 project restructure resolvedCode examples
Enum generation:
Map types:
Generated auth config:
Per-call config override:
https://claude.ai/code/session_012ZEd8QZGvbqKabxchwhGkE