Skip to content

fix(cli): inherit path-item-level OpenAPI parameters - #7606

Open
DerMayer1 wants to merge 10 commits into
OpenBB-finance:v5from
DerMayer1:bugfix/openapi-path-item-parameters
Open

fix(cli): inherit path-item-level OpenAPI parameters#7606
DerMayer1 wants to merge 10 commits into
OpenBB-finance:v5from
DerMayer1:bugfix/openapi-path-item-parameters

Conversation

@DerMayer1

Copy link
Copy Markdown

What

OpenAPI 3.x lets a Path Item Object declare parameters that apply to every operation under that path, with operation-level entries overriding them by (name, in). The V5 codegen only read op.get("parameters"), so path-item parameters were dropped — silently. The generated command kept the URL template but had nothing registered to satisfy it.

This is the same silent-success failure surface as #7585, from a different root cause: the generator reports success, and the breakage only shows up when the user calls the command.

Real-world repro

The Xero Accounting API declares its required xero-tenant-id header once per path item, across all 138 paths:

/Accounts/{AccountID}:
  parameters:
    - $ref: '#/components/parameters/requiredHeader'   # xero-tenant-id, in: header, required: true
  get:
    parameters:
      - $ref: '#/components/parameters/AccountID'

Running build_command_spec against the live spec:

before after
commands generated 78 78
carrying required xero-tenant-id 0 78

Xero returns 403 without that header, so before this change every generated command was dead on arrival with no warning.

How

Adds operation_parameters(spec, path_item, op) to openapi_schema.py. It merges the two levels, dereferences $ref on both, and applies the spec's override rule keyed on (name, in) — so a same-named parameter in a different location is correctly kept rather than swallowed.

Both consumers route through it:

  • build_command_spec_build_operation_entry — the pre-computed --generate-spec path
  • build_command_indexbuild_parser_from_operation — the interactive --server path

build_router_map, build_reference, and detect_api_prefix only read path keys and descriptions, so they're unaffected.

Unresolvable $refs, non-dict entries, and nameless parameters are skipped exactly as before. build_parser_from_operation keeps its previous operation-only behavior when called without a spec, and specs with no path-item parameters produce byte-identical output.

Tests

14 new tests covering: inheritance, $ref resolution on both levels, operation-over-path-item override, (name, in) keying, inheritance across multiple operations under one path item, and the defensive skip paths.

2019 passed
ruff format --check   clean
ruff check            clean
ty check              clean

(2005 before this change, plus the 14 new.)

OpenAPI 3.x lets a Path Item Object declare `parameters` shared by every
operation under that path, with operation-level entries overriding them by
(name, in). The codegen read only `op.get("parameters")`, so inherited
parameters were dropped silently — the generated command kept the URL
template but had nothing registered to fill it.

Real-world impact: the Xero Accounting API declares its required
`xero-tenant-id` header once per path item across all 138 paths. Before this
change `build_command_spec` produced 78 commands and none carried the
header, so every generated command 403s. Nothing warned.

Adds `operation_parameters()`, which merges the two levels and dereferences
both, and routes the pre-computed spec path (`build_command_spec`) and the
interactive path (`build_command_index` -> `build_parser_from_operation`)
through it. Unresolvable refs, non-dict entries, and nameless parameters are
skipped as before. Specs without path-item parameters are unaffected.
@CLAassistant

CLAassistant commented Jul 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@DerMayer1

Copy link
Copy Markdown
Author

Some additional data on how common this pattern is, in case it helps with prioritizing.

I ran a path-item-parameter check across the APIs.guru directory — 2,510 published specs fetched successfully. 673 of them (27%) declare parameters at the path-item level, so every command generated from those specs currently drops them:

API affected paths
Microsoft Graph (beta) 12,001 / 14,227
Microsoft Graph 6,517 / 7,419
AWS EC2 591 / 591
Google Compute 487 / 487
Kubernetes 368 / 423
Xero Accounting 132 / 132
Codat (accounting) 118 / 124
Rebilly (billing) 104 / 105

Restricted to the finance/banking/payments subset, it's 25 of 160 specs (16%). In Xero's and Codat's case the inherited parameter is a required auth header, so the generated commands can't succeed at all — which is how I ran into it.

While I had the corpus loaded I also checked three adjacent conformance gaps, and none of them look worth writing code for:

Gap specs affected (of 2,510)
parameter-level content instead of schema 4 (0.2%)
allOf in a parameter schema 24 (1%), mostly Swagger 2.0
path-level servers override 57 (2.3%)
operation-level servers override 33 (1.3%)

The servers ones are the only ones with a real severity story — when a spec overrides the host per path, generated commands would point at the wrong server silently. It's concentrated almost entirely in Twilio (121/121 paths on their main API, plus several of their other products), with Linode, Box and GitHub Enterprise using the operation-level form. Happy to open a separate issue or PR if multi-host specs are in scope for the codegen, but I didn't want to add handling for something that may be deliberately out of scope.

@deeleeramone

deeleeramone commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Hey @DerMayer1, thanks for the PR.

Did you try adding the -H xero-tenant-id {..value...} flag to the command? A header is not the same thing as a path param, those are handled differently.

  -H, --header KEY=VALUE
                        Custom HTTP header to send on every dispatch and on the OpenAPI fetch. Repeatable:
                        ``-H 'Authorization: Bearer xxx' -H 'X-Tenant: acme'``. Both ``KEY=VALUE`` and ``KEY:
                        VALUE`` forms are accepted.
  --header-file PATH    Read additional headers from a JSON file (object of string values). Headers from
                        --header take precedence on conflicts. (env: OPENBB_HEADER_FILE)
  -Q, --query-param KEY=VALUE
                        Query parameter injected on every request (e.g. APIs that authenticate via
                        ``?api_key=...`` like https://api.congress.gov). Repeatable. Also auto-loaded from
                        any env var prefixed ``OPENBB_HTTP_QUERY_`` — ``OPENBB_HTTP_QUERY_API_KEY=xxx``
                        becomes ``?api_key=xxx``. CLI flag takes precedence over env.
  --query-param-file PATH
                        Read additional query params from a JSON file (object of string values). --query-
                        param flags and ``OPENBB_HTTP_QUERY_*`` env vars take precedence on conflicts. (env:
                        OPENBB_QUERY_PARAM_FILE)

If these do not work as-expected, then that is a bug.

@DerMayer1

DerMayer1 commented Jul 25, 2026

Copy link
Copy Markdown
Author

You're right about the header, and my example was a bad one. A constant xero-tenant-id is exactly what -H is for, and I should have checked that before using it as the motivating case.

The general problem isn't headers though. Path-item parameters can be any location, so I went back to the corpus and broke it down by in: across the 673 specs that use them:

in: occurrences reachable via existing flags
header 70,442 yes — -H, as you say
query 50,973 mostly — -Q, when the value is constant for the run
path 48,447 no

108 of the 673 specs declare in: path parameters at the path-item level. Those become {placeholder} segments in the URL template, and -H/-Q can't fill them, so the command can't be dispatched regardless of flags. Three examples, before this PR vs after:

Codat Accounting — /companies/{companyId}/connections/{connectionId}/data/bankAccounts
  before: params ['Authorization','page','pageSize','query','orderBy']
          unsatisfiable placeholders: ['companyId','connectionId']
  after:  unsatisfiable placeholders: none

Linode — /account/entity-transfers/{token}
  before: unsatisfiable placeholders: ['token']

Kubernetes — /api/v1/componentstatuses/{name}
  before: zero parameters registered; unsatisfiable: ['name']

Others in that set: Microsoft Graph (12,001 paths), Keycloak (191), Apple App Store Connect (133), Bitbucket (117), Jira (115).

On scope, if you'd rather this only inherit non-header parameters and leave headers to -H, that's a small change and I'm happy to make it. The one thing I'm unsure of is whether the --generate-extension output has an equivalent of -H, since a generated provider package is consumed as a library rather than through CLI flags. If it does, narrowing to path/query is clearly the cleaner fix.

@deeleeramone

Copy link
Copy Markdown
Contributor

Can you try reframing the problem with clear scope? There are certainly API endpoints which will never universally translate into Python Interface functions, such as webhooks, OIDC, websockets, etc.

The codegen detects SecuritySchema and wraps it into the Credentials via the Provider instance. An OAuth2 flow requires a running server, so this type of authorization is not suitable for the Python interface. To use services like Microsoft Graph directly via Python, you would need to use their official package for managing identity and write custom code for the specific implementation.

Path and query parameters are already handled across all interfaces, but your example won't track across all the surfaces because a Python function does not have "headers" by its very nature. Only external HTTP requests to an API server carry headers - i.e, obb.some_function() will never have request headers. Converting "in:header" to become a Python parameter is likely undesirable for the majority of scenarios.

If the goal is to handle an Authorization header specifically, there could be an argument made for folding into the SecuritySchema detection and codegen paths.

The config system (openbb.toml, SystemSettings) allows you to set custom outbound headers for HTTP requests, at the user/environment level. This is likely the lever you are looking for.

SystemSettings.PythonSettings

model_config = ConfigDict(extra="allow").

Field Type Default
docstring_sections list[str] ["description", "parameters", "returns", "examples"]
docstring_max_length PositiveInt | None None
http dict | None {}
uvicorn dict | None {}

docstring_sections controls which sections are included in autogenerated docstrings.

docstring_max_length caps the length of those docstrings. None means no cap.

http is consumed by openbb_core.provider.utils.helpers.{make_request, amake_request, amake_requests, get_requests_session, get_async_requests_session}. Keys:

Key Applies to Purpose
cafile both Path to a CA certificate file.
certfile both Path to a client certificate file.
keyfile both Path to a client key file.
password aiohttp Password for the client key file.
verify_ssl both Verify SSL certificates.
fingerprint aiohttp SSL fingerprint.
proxy both Proxy URL.
proxy_auth aiohttp Proxy authentication.
proxy_headers aiohttp Proxy headers.
timeout both Request timeout.
auth both Basic authentication.
headers both Request headers.
cookies both Session cookies.

Additional keys are ignored unless explicitly implemented by custom code.

Per review: headers do not translate into a Python interface function, and
outbound headers are transport configuration
(SystemSettings.PythonSettings.http.headers, the CLI's -H) or a security-scheme
concern — not a per-command argument.

Path-item `in: header` and `in: cookie` entries are now dropped rather than
inherited. Operation-level parameters are untouched, so an operation that
declares its own header keeps today's behavior.

Scope is now only what cannot be supplied any other way: `in: path` and
`in: query` parameters declared on the Path Item Object.
@DerMayer1

Copy link
Copy Markdown
Author

That reframing is right, and I've pushed a commit that narrows the PR to match it.

Scope, explicitly:

Out. Headers and cookies. You're correct that obb.some_function() has no request headers, so turning in: header into a Python parameter is wrong for the majority case — and outbound headers already have a home in SystemSettings.PythonSettings.http.headers and the CLI's -H. Authorization specifically belongs in the SecuritySchema → Credentials path, not here. Also out: webhooks, OIDC, websockets, and OAuth2 flows, which need a running server and don't map onto a Python function at all.

In. in: path and in: query parameters declared on the Path Item Object rather than on the operation.

That's the whole remaining claim. Path and query parameters are handled across all interfaces when they're declared on the operation — what isn't handled is the same parameter declared one level up, which the codegen never read. End to end on Codat's accounting spec:

command: companies.connections.data.bankAccounts
url:     /companies/{companyId}/connections/{connectionId}/data/bankAccounts

before: registered params ['Authorization','page','pageSize','query','orderBy']
        $ openbb ... --companyId abc --connectionId xyz
          error: unrecognized arguments: --companyId abc --connectionId xyz
        dispatched URL: /companies/{companyId}/connections/{connectionId}/data/bankAccounts

after:  --companyId / --connectionId accepted
        dispatched URL: /companies/abc/connections/xyz/data/bankAccounts

The argument can't be passed, and the unsubstituted placeholder goes out on the wire.

Narrowing shrinks the blast radius: of the 673 specs in the APIs.guru corpus that use path-item parameters, 396 declare path or query there (108 path, 313 query) and are affected; the remaining 277 are header/cookie-only and this PR now correctly ignores them. Xero — my original example — is in that ignored group, which I think is the right outcome given -H exists.

Changed: inherited parameters are filtered to path/query before merging. Operation-level parameters are untouched, so an operation declaring its own header behaves exactly as today. 17 tests, full suite 2,022 green, ruff and ty clean.

@deeleeramone

deeleeramone commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the details, @DerMayer1. This resolves the params side, and the security schema builds correctly as a provider credential that gets inserted into the request headers; however, the response schema is not getting dug up from the $ref. If you can apply the same treatment for those, with consideration that some may contain nested references, that will round it out nicely!

Screenshot 2026-07-26 at 4 51 39 PM Screenshot 2026-07-26 at 5 02 35 PM
  /companies/{companyId}/connections/{connectionId}/data/bills:
    parameters:
    - $ref: '#/components/parameters/companyId'
    - $ref: '#/components/parameters/connectionId'
    - $ref: '#/components/parameters/page'
    - $ref: '#/components/parameters/pageSize'
    get:
      operationId: listBills
      tags:
      - Accounting
      summary: List bills
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DataList'
...
components:
  parameters:
    connectionId:
      name: connectionId
      in: path
      required: true
      description: Unique identifier for a connection.
      schema:
        type: string
        format: uuid
    companyId:
      name: companyId
      in: path
      required: true
      description: Unique identifier for a company.
      schema:
        type: string
        format: uuid
    pageSize:
      name: pageSize
      in: query
      description: Number of records per page.
      schema:
        type: integer
        default: 100
    page:
      name: page
      in: query
      description: Page number (1-indexed).
      schema:
        type: integer
        default: 1
  schemas:
    DataList:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
        pageNumber:
          type: integer
        pageSize:
          type: integer
        totalResults:
          type: integer
  securitySchemes:
    authHeader:
      type: apiKey
      in: header
      name: Authorization
      description: 'Codat expects your API key Base64-encoded and sent in the Authorization header using the Basic scheme, for example: `Authorization: Basic YOUR_BASE64_ENCODED_API_KEY`.'

Response-schema $refs were already expanded by deref_schema, but a schema
whose top level is an allOf composition was left unflattened. The properties
live inside the members, so a consumer reading them off the composition finds
none. fetcher_gen._data_schema looks for `properties.results`, gets nothing,
and falls through to _unwrap_schema_envelopes, which handles oneOf and anyOf
but not allOf. The generated Data model ends up with no fields.

Codat's listBills response is `allOf: [{properties: {results}}, {properties:
{_links, pageNumber, pageSize, totalResults}}]`, which produced an empty model.

merge_allof collapses a composition of object subschemas into one object
schema: properties unioned with the first declaration winning on conflict,
required unioned, and sibling keywords on the allOf node outranking members.
A composition that is not a plain intersection of object subschemas (a member
carrying its own oneOf/anyOf, or a non-object type) is returned unchanged
rather than flattened into something the spec does not describe. Nested
compositions are merged too, so a $ref inside a merged member still resolves.

Applied to extract_response_schema and extract_response_schemas.

Measured across five specs that use the pattern, counting operations whose
response schema exposes readable properties: 244 of 588 before, 394 after
(Codat Accounting 36 to 88, Codat Banking 0 to 8, Codat Commerce 1 to 11,
Rebilly 62 to 104, Box 145 to 183).
@DerMayer1

Copy link
Copy Markdown
Author

Pushed. I went in expecting the refs and spent a while there before realising they were already fine. deref_schema expands them recursively, nested included, and listBills on v5 comes back with zero unresolved $ref. What actually breaks it is the allOf sitting on top:

allOf[0] -> properties: [results]
allOf[1] -> properties: [_links, pageNumber, pageSize, totalResults]

Nothing merges those, so the properties are one level below where anything looks for them. _data_schema asks for properties.results, gets {}, and hands off to _unwrap_schema_envelopes, which knows oneOf and anyOf but not allOf. So the composition goes straight through untouched and the Data class comes out empty, which is what you're seeing.

merge_allof flattens it. Properties get unioned with the first declaration winning any conflict, required gets unioned, and anything written next to the allOf beats what the members say. It runs after deref_schema in both extract_response_schema and extract_response_schemas and recurses, so a composition buried in results.items also collapses and the refs inside members stay resolved.

I left compositions alone when they aren't a plain intersection of objects, so a member with its own oneOf/anyOf or a non-object type passes through as-is. Flattening those would mean guessing at a shape the spec never states.

listBills now:

before: ['allOf', 'x-internal'], properties []
after:  properties ['results', '_links', 'pageNumber', 'pageSize', 'totalResults']
        required   ['pageNumber', 'pageSize', 'totalResults', '_links']
        results.items -> Bill, resolved

Across the specs I had loaded, counting operations whose response schema actually exposes properties:

before after
Codat Accounting (127 ops) 36 88
Rebilly (184) 62 104
Box (258) 145 183
Codat Banking (8) + Commerce (11) 1 19

One thing I didn't touch: extract_request_body_schema has the same gap on the request side. Seemed better to keep this to the response schemas you asked about, but I can add it here or in its own PR if you want it.

7 new tests, 157 in that file. cli/tests gives me 1,945 passed vs 1,938 on the base, same 80 failures either way (platform extras I don't have installed). ruff clean.

@deeleeramone deeleeramone added bug Bugs and bug fixes cli OpenBB Platform CLI V5 PRs and issues for ODP V5 labels Jul 28, 2026
@deeleeramone

Copy link
Copy Markdown
Contributor

One thing I didn't touch: extract_request_body_schema has the same gap on the request side. Seemed better to keep this to the response schemas you asked about, but I can add it here or in its own PR if you want it.

Seems like this would be a good idea to handle it here, eh?

Same gap as the response side. request_body_parameters needs `type: object`
or `properties` to flatten a body into parameters, and an allOf composition
carries neither, so a POST or PUT generated from one gets no body parameters
at all.

extract_request_body_schema now runs merge_allof after deref_schema, matching
extract_response_schema.

Across the same five specs, counting request bodies that produce parameters:
138 of 206 before, 177 after (Codat Accounting 1 to 26 of its 30 bodies,
Rebilly 50 to 63, Box 87 to 88).
@DerMayer1
DerMayer1 force-pushed the bugfix/openapi-path-item-parameters branch from f4a3ed9 to 5886c6b Compare July 28, 2026 22:04
@DerMayer1

Copy link
Copy Markdown
Author

Done, pushed.

Same treatment on extract_request_body_schema. The failure mode there is a bit blunter than on the response side: request_body_parameters needs type: object or properties to flatten a body, an allOf gives it neither, so the command just ends up with no body parameters.

Request bodies that produce parameters, same specs as before:

before after
Codat Accounting (30 bodies) 1 26
Rebilly (75) 50 63
Box (101) 87 88

Codat is the one that really moves, since nearly every prototype schema there is a composition.

158 tests in that file now, cli/tests at 1,946 passed with the same 80 environment failures as the base. ruff clean.

@deeleeramone

deeleeramone commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Hey @DerMayer1, I'm still not seeing the response model being captured. It also seems to have duplicated some parameters, which generates invalid syntax. Potentially fixed one thing but broke another?

Screenshot 2026-07-29 133540 Screenshot 2026-07-29 133713

I'm not familiar with this service, so are these "options" endpoints valid or are they being interpreted incorrectly?

Screenshot 2026-07-29 132919

_mergeable_allof_members required every member's keys to come from a small
whitelist, so any unrecognized keyword disqualified the whole composition.
Codat's PagingInfo carries `definitions` next to its properties, which meant
the live Codat-Accounting.yaml never merged and its response models stayed
empty. The APIs.guru copy of the same API omits that key, which is why the
earlier measurements looked clean.

The check is now a blocklist. A member blocks the merge when it carries its
own combinator (oneOf/anyOf/allOf/not), an unresolved $ref left by a cycle,
a scalar or array constraint (enum/const/items), or a type other than object.
Annotation keywords that do not affect the intersection (title, description,
examples, definitions, additionalProperties, vendor x- keys) no longer block
it, and are dropped from the merged result along with the members.

On the live Codat spec, operations whose response schema exposes readable
properties go from 36 of 127 to 114. Across the five specs measured earlier:
244 of 588 before, 488 after (was 394 under the whitelist).

Corpus check over 2,816 response and request-body extractions in 6 specs:
567 changed, every one attributable to an allOf in the pre-merge output,
0 unexplained. Twilio declares no allOf compositions and is unchanged.
…perty

_signature_params appended request-body properties and then operation
parameters without deduplicating, so a name declared in both places produced
two function parameters with the same name and the generated module raised
SyntaxError.

Codat's push endpoints hit this: /push/bankAccounts/{accountId}/bankTransactions
declares accountId as a path parameter and repeats it in the request body, so
connections.push.bankAccounts.bankTransactions generated `accountId: str` and
`accountId: str = None` in one signature.

The operation parameter wins, since it carries the path placeholder and its own
required flag. The request payload is unchanged: _render_body_block derives body
fields from request_body_schema rather than from this list, so accountId is
still sent in the body.

This predates the path-item and allOf work in this PR. On origin/v5 the same
spec produces the same duplicate; on this branch the Codat spec generates 91
commands with 0 duplicated signatures, down from 1.
_render_body_block already writes `{'from': from_}`, keying the payload by the
wire name and reading a safe identifier, but _signature_params declared the raw
name. A body property named after a Python keyword therefore emitted
`from: str = None` and the generated module raised SyntaxError.

This did not fire before the allOf work in this PR, because a composed request
body produced no body parameters at all. Codat's Transfer body is such a
composition and has a `from` property, so connections.push.transfers started
generating invalid syntax once the body flattened.

_signature_params now emits safe_field_name(name)[0], the same helper
fetcher_gen and pydantic_gen already use. The body/query split in
_render_body_block compares against the safe names too: it previously matched
signature names against wire names, so `from_` was not recognized as a body
field and leaked into the query string as a `from_` parameter.

End to end on Codat-Accounting.yaml, generating the full extension and byte
compiling every emitted module: 136 files, 0 failures. origin/v5 emits 1 file
that does not compile (duplicate argument 'accountId').

Known gap left alone: an operation parameter named after a keyword would still
break, since the query and path lines use the raw name as a local variable.
None of the five specs checked declares one.
…ntifiers

Two defects found by generating extensions from real specs and byte compiling
every emitted module.

merge_allof and deref_schema walked every value in a schema dict, including
keywords that hold instance data rather than subschemas. A `default` of
`{"allOf": [{"a": 1}, {"b": 2}]}` was rewritten to `{"type": "object"}`, and a
`$ref` key inside an example was resolved as if it were a reference. Both now
copy `default`, `example`, `examples`, `enum`, `const` and vendor `x-` values
through untouched.

_signature_params emitted operation parameter names verbatim, so a wire name
that is not a Python identifier produced invalid syntax. Rebilly declares an
`Organization-Id` header and a `REB-APIKEY` security header, which emitted
`REB-APIKEY: str = None`. It now uses safe_field_name, matching fetcher_gen,
and _render_body_block keys the query dict by the wire name while reading the
safe identifier so the request is unchanged.

This surfaced through the allOf work rather than being caused by it: a composed
request body used to yield no properties, so `has_body` was false in
package_gen and the command was generated as a GET fetcher, where identifiers
were already sanitized. With bodies flattening, 14 of Codat's 24 POST endpoints
correctly move to the POST path, which had never been exercised by these specs.

Generating the full extension and compiling every module:

  Codat-Accounting.yaml   v5: 1 failure    branch: 0   (136 files)
  Rebilly 2.1             v5: 14 failures  branch: 1   (97 files)

The one remaining Rebilly failure is `async def 3dsecure(` in the router, which
fails identically on v5. Sanitizing that is a naming decision about the command
surface, so it is left alone here.

Known gap: a non-credential header parameter is still placed in the query
string on the POST path. Pre-existing, and unchanged by this PR.
The previous dedupe compared wire names, but the failure it prevents depends on
the sanitized identifier. Two wire names can collapse onto one (`from` and
`from_`, `Organization-Id` and `Organization_Id`), and operation_parameters
keeps a name declared twice under different `in` locations by design, so both
cases could still emit a duplicate argument.

_signature_params now tracks the identifiers it has emitted and skips anything
that would repeat one. Emission order is unchanged, so existing signatures are
byte-identical; only genuinely colliding names are dropped, and operation
parameters still win over body properties.

Neither collision occurs in Codat-Accounting.yaml or Rebilly 2.1. This closes
the class rather than the instance, since the instances are what this PR has
been finding.

Generation unchanged: Codat 136 files 0 failures, Rebilly 97 files 1 failure
(`async def 3dsecure(`, identical on v5).
@DerMayer1

Copy link
Copy Markdown
Author

Both real. The duplicate isn't from this PR, the empty response model was my fault for a reason I didn't expect, and chasing them turned up a third thing.

Taking the duplicate first. That path item declares no parameters at all, only post, so path-item inheritance never touches it. Codat declares accountId as a path parameter and repeats it as a body property, and _signature_params appended body properties and then operation parameters without deduplicating. I built a tree from origin/v5 and ran your spec through it:

v5      [cc, transactions, companyId, connectionId, accountId, accountId, timeoutInMinutes, allowSyncOnPushComplete]
branch  [cc, transactions, companyId, connectionId, accountId, timeoutInMinutes, allowSyncOnPushComplete]

The operation parameter wins now, since it carries the path placeholder. The payload is unchanged because _render_body_block builds the body from request_body_schema, so accountId still goes out in the body.

The response model is on me, though not for the reason you'd guess. I validated the merge against the APIs.guru copy of Codat 2.1.0 and you generate from codatio/oas/main/yaml/Codat-Accounting.yaml. The live PagingInfo carries a definitions key next to its properties, my member check was a whitelist of permitted keywords, and definitions disqualified the whole composition so it never merged. My numbers were real and measured on a document you weren't using, which is worse than having no numbers.

It's a blocklist now: a member blocks the merge only if it has its own combinator, an unresolved $ref from a cycle, enum/const/items, or a non-object type. definitions, title, examples and vendor keys don't. connections.data.accountTransactions now resolves to results, pageNumber, pageSize, totalResults, _links, and its Data class carries the AccountTransaction fields instead of pass.

The third thing is the one I'd look at first. package_gen decides between a fetcher and a POST command with has_body, which requires properties. An unmerged allOf body has none, so on v5 14 of Codat's 24 POST endpoints were generated as GET fetchers, including connections.push.bills, push.customers and push.invoices. They couldn't push at all. All 24 route correctly now, which is what the 81/10 to 67/24 split in the output is.

That reclassification then exposed two things I did break, both found by generating the extension and importing every module rather than reading the diff. Codat's Transfer body has a property named from, so once bodies flattened the signature emitted from: str = None. _render_body_block was already writing {'from': from_} via safe_field_name, so only the signature side was wrong. Fixing that left from_ leaking into the query string, because the body/query split compared signature names against wire names. Both fixed, and the same sanitization was missing for operation parameters, which Rebilly hits with Organization-Id and its REB-APIKEY security header.

Generating full extensions and importing every module:

v5 branch
Codat, modules that fail to import 5 0
Rebilly, files that fail to compile 14 1

The 5 on Codat is one SyntaxError cascading into routers.codat, routers.connections and routers.connections_push, so the router is unimportable on v5. Rebilly's remaining failure is async def 3dsecure( in the router, which fails the same way on v5; sanitizing it means choosing a name for the command, so I left it.

Also ran the wider check again after loosening the member rule: 2,816 response and request-body extractions across 6 specs, 567 changed, every change traceable to an allOf in the pre-merge output, none unexplained. Twilio declares no compositions and is byte-identical. Two consecutive generations produce byte-identical files apart from the provenance timestamp.

On the options endpoints, those are genuine. Codat's create/update model endpoints sit at /connections/{connectionId}/options/{dataType} and return the field metadata you need before pushing, which is why that Data class comes out as value / type / displayName / description / required.

Three things I deliberately left alone. Non-credential header parameters end up in the query string, but that's true on the fetcher path too and you've already said in: header shouldn't become a Python parameter, so I didn't want to reopen it here. write_text has no encoding= at any of its 13 call sites, which kills generation on Windows for specs containing non-cp1252 characters (Codat has a ); I worked around it with PYTHONUTF8=1 and it looks like its own fix. And a parameter named cc would now be dropped rather than colliding with the CommandContext argument, which is better than a SyntaxError but is still a silent drop; renaming it invents a public parameter name so it seemed like your call. None of the three occurs in Codat or Rebilly.

10 tests added across the two files. cli/tests at 1,952 passed against the same 80 environment failures as the base. ruff clean.

Mutation checking the new tests (revert each fix, confirm its test fails)
found two problems with the previous commits.

The guard that stops merge_allof and deref_schema from walking into `default`,
`example`, `enum`, `const` and vendor values shipped with no test at all. It is
now covered from both directions: a default holding an `allOf` key keeps its
value, and a `$ref` inside a default stays a literal while a real reference
beside it still resolves.

The body-property loop also carried a redundant check. It tested the raw wire
name against the operation parameter names before testing the sanitized one,
but `operation_param_names` holds both forms and `safe_field_name` is idempotent
on identifiers, so the first test could never fire alone. Reverting it left
every test passing, which is what surfaced it. Removed, and the sanitized name
is now computed once.

All nine mutations are caught by the suite: both data guards, the allOf
blocklist in each direction, the response merge wiring, body/parameter
precedence, the keyword identifier, the body/query split and the
emitted-identifier dedupe.

cli/tests at 1,954 passed against the same 80 environment failures as the base.
Generation unchanged: Codat 136 files 0 failures, Rebilly 97 files 1 failure.
@DerMayer1

Copy link
Copy Markdown
Author

Correction to my last comment: I said 10 tests added, it is 31 across the PR and 6 since that comment. Overstating my own verification is the wrong direction to be wrong in, so flagging it.

Went back and mutation checked the tests, reverting each fix and confirming its test fails. Two things came out of that.

The guard that stops merge_allof and deref_schema walking into default, example, enum, const and vendor values had no test at all. I had only checked it in a throwaway script. It is covered now from both directions: a default holding an allOf key keeps its value, and a $ref inside a default stays a literal while a real reference beside it still resolves.

The body-property loop also had a branch that could never fire. It tested the raw wire name against the operation parameter names before testing the sanitized one, but that set holds both forms and safe_field_name is idempotent on identifiers. Reverting it left every test green, which is how it surfaced. Removed.

All nine mutations are caught now: both data guards, the allOf blocklist in each direction, the response merge wiring, body/parameter precedence, the keyword identifier, the body/query split, and the emitted-identifier dedupe.

cli/tests at 1,954 passed against the same 80 environment failures as the base. Codat still generates 136 files with 0 failures, Rebilly 97 with the one pre-existing 3dsecure router failure. ruff clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bugs and bug fixes cli OpenBB Platform CLI V5 PRs and issues for ODP V5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants