feat(python-sdk): support omitting username/password from basic auth when configured in IR#14407
feat(python-sdk): support omitting username/password from basic auth when configured in IR#14407Swimburger wants to merge 20 commits intomainfrom
Conversation
…en configured in IR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
| writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') | ||
| if either_omitted: | ||
| writer.write_node( | ||
| AST.ClassInstantiation( | ||
| class_=httpx.HttpX.BASIC_AUTH, | ||
| args=[ | ||
| AST.Expression(f"{username_var}"), | ||
| AST.Expression(f"{password_var}"), | ||
| AST.Expression(f'self.{names.get_username_getter_name(basic_auth_scheme)}() or ""'), | ||
| AST.Expression(f'self.{names.get_password_getter_name(basic_auth_scheme)}() or ""'), | ||
| ], | ||
| ) | ||
| ) |
There was a problem hiding this comment.
When auth is mandatory and either_omitted is True, the code unconditionally sets the Authorization header even when both credentials are None. This conflicts with the stated behavior that "When neither is provided, the Authorization header is omitted entirely."
If both get_username() and get_password() return None, the generated code will still encode an Authorization header as ":" (empty username and password), which may cause authentication failures.
The fix should add a conditional check similar to the non-mandatory branch:
username_val = self.get_username()
password_val = self.get_password()
if username_val is not None or password_val is not None:
headers["Authorization"] = httpx.BasicAuth(username_val or "", password_val or "")._auth_header| writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') | |
| if either_omitted: | |
| writer.write_node( | |
| AST.ClassInstantiation( | |
| class_=httpx.HttpX.BASIC_AUTH, | |
| args=[ | |
| AST.Expression(f"{username_var}"), | |
| AST.Expression(f"{password_var}"), | |
| AST.Expression(f'self.{names.get_username_getter_name(basic_auth_scheme)}() or ""'), | |
| AST.Expression(f'self.{names.get_password_getter_name(basic_auth_scheme)}() or ""'), | |
| ], | |
| ) | |
| ) | |
| username_val = AST.Expression(f'self.{names.get_username_getter_name(basic_auth_scheme)}()') | |
| password_val = AST.Expression(f'self.{names.get_password_getter_name(basic_auth_scheme)}()') | |
| writer.write("username_val = ") | |
| writer.write_node(username_val) | |
| writer.write_newline_if_last_line_not() | |
| writer.write("password_val = ") | |
| writer.write_node(password_val) | |
| writer.write_newline_if_last_line_not() | |
| writer.write("if username_val is not None or password_val is not None:") | |
| with writer.indent(): | |
| writer.write(f'headers["{ClientWrapperGenerator.AUTHORIZATION_HEADER}"] = ') | |
| writer.write_node( | |
| AST.ClassInstantiation( | |
| class_=httpx.HttpX.BASIC_AUTH, | |
| args=[ | |
| AST.Expression('username_val or ""'), | |
| AST.Expression('password_val or ""'), | |
| ], | |
| ) | |
| ) | |
Spotted by Graphite
Is this helpful? React 👍 or 👎 to let us know.
There was a problem hiding this comment.
This was already addressed in the "complete field removal" update (commit af66447). The current code:
- Non-mandatory path: Only creates variables/conditions for non-omitted fields. Omitted fields use
AST.Expression('""')directly — noor ""fallbacks. - Mandatory path: Omitted fields use
'""'directly, non-omitted fields call the getter. - Constructor: Omitted fields are completely removed from constructor params, not just made optional.
The diff this comment references is from a previous revision.
…y instead of coarse eitherOmitted flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… per-field omit fix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…c-auth-optional-python-sdk
…ms, use empty string internally Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…s non-mandatory Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ngelog Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…c-auth-optional-python-sdk
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…from main, keep 5.4.0 for feat) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… generator Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| return [ | ||
| { | ||
| // usernameOmit/passwordOmit may exist in newer IR versions | ||
| const authRecord = auth as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
🔴 as unknown as Record<string, unknown> violates CLAUDE.md rule and relies on fragile data smuggling
CLAUDE.md explicitly prohibits as unknown as X type assertions: "Never use as any or as unknown as X. These are escape hatches that bypass the type system entirely. If the types don't line up, fix the types."
Beyond the rule violation, this pattern exists because usernameOmit/passwordOmit are smuggled as extra properties on a DynamicSnippets.BasicAuth object (which only defines username and password per packages/ir-sdk/src/sdk/api/resources/dynamic/resources/auth/types/BasicAuth.ts:5-8). The converter at packages/cli/generation/ir-generator/src/dynamic-snippets/DynamicSnippetsConverter.ts:736-749 attaches these as ad-hoc properties, relying on JavaScript spread to propagate them. If the dynamic snippet IR passes through any schema-based serialization/deserialization boundary (as is typical when IR is passed to generators via JSON), these undeclared fields may be silently stripped, causing !!authRecord.usernameOmit to always evaluate to false and the feature to silently not work. The proper fix is to extend the DynamicSnippets.BasicAuth type in the IR definition to include the new fields.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This is a known limitation. The FernIr.dynamic.BasicAuth type comes from @fern-fern/ir-sdk (the published IR SDK package), which doesn't have typed usernameOmit/passwordOmit fields yet. The fields exist in the IR schema (packages/ir-sdk/src/sdk/api/resources/auth/types/BasicAuthScheme.ts) but the dynamic IR types haven't been updated to include them. Updating the IR types is out of scope for this PR per maintainer instruction ("Fix the non-IR changes"). The cast is necessary until the published IR SDK is updated.
…to dynamic IR The DynamicSnippetsConverter was constructing dynamic BasicAuth with only username and password fields, dropping usernameOmit/passwordOmit from the main IR's BasicAuthScheme. This caused dynamic snippets generators to always include omitted auth fields (e.g. $password) since they couldn't detect the omit flags in the dynamic IR data. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Description
Split from #14378 (one PR per generator).
Adds conditional support for omitting username or password from basic auth in the Python SDK generator. When
usernameOmitorpasswordOmitflags are set in the IR'sBasicAuthScheme, the flagged field is completely removed from the end-user SDK API (constructor parameters, type hints, getters). Internally, the omitted field is treated as an empty string when encoding theAuthorization: Basicheader (e.g.,password: omit: true→ header encodesusername:). Default behavior (both required) is preserved when no omit flags are set.Changes Made
client_wrapper_generator.py:_get_constructor_info(): Conditionally skips adding constructor parameters for omitted fields entirely (not just making them nullable). Usesgetattr(basic_auth_scheme, "username_omit", None)defensively since the Python IR SDK may not expose these fields in type stubs._get_write_get_headers_body(): Omitted fields use""directly viaAST.Expression('""')instead of reading from options. Only non-omitted fields get null-checked. Per-field conditions (not coarseeither_omitted).versions.yml: New 5.2.2 entrybasic-auth-pw-omittedtest fixture: Fern definition withpassword: omit: true, plus full seed output forseed/python-sdk/basic-auth-pw-omitted/Testing
basic-auth-pw-omittedfixtureseed/python-sdk/basic-auth-pw-omitted/src/seed/client.py— confirm the constructor has nopasswordparameter and_get_auth_headersuses""for the password inhttpx.BasicAuth().getattrdefensive access:getattr(basic_auth_scheme, "username_omit", None)is used instead of direct attribute access. If the Python IR SDK (v65) already defines these fields, direct access would be cleaner.passwordOmit: true, only the password is removed — username remains required. Verify this matches the generated seed output.pass). When auth is mandatory, it still sets the header withBasicAuth("", ""). Verify this asymmetry is acceptable.testCI failure: Thetestcheck fails with an IR deserialization error (singleBaseUrlvsmultipleBaseUrls) — this is a pre-existing issue in the Python generator's test suite unrelated to this PR.Updates since last revision
basic-auth-optional→basic-auth-pw-omittedto accurately reflect what the fixture tests (password omission, not generic optionality)omit: trueare now completely removed from the SDK API (constructor params, type hints, getters), not just made optional/nullable. This matches the IR doc comment: "the field will be omitted from the SDK."either_omittedflag with per-fieldusername_omitted/password_omittedchecksAST.Expression('""')directly instead ofor ""fallback on nullable paramsLink to Devin session: https://app.devin.ai/sessions/0786b963284f4799acb409d5373cde0a
Requested by: @Swimburger