Skip to content

feat: a header an operation declares is a keyword of its own - #20

Merged
briankfung merged 2 commits into
mainfrom
header-parameter-options
Oct 23, 2026
Merged

briankfung merged 2 commits into
mainfrom
header-parameter-options

Conversation

@briankfung

Copy link
Copy Markdown
Member

What changes

  • An operation's own header is a keyword of its own. The generator reads each operation's optional header parameters, where it skipped them before. Each becomes a keyword of that method's own, named from the header in snake_case and typed from the spec. process_message takes payer_authorization, sent as Payer-Authorization, and every existing call works as it did:

    checked = client.process_message(message, payer_authorization=f"Bearer {access_token}")
  • Held where nothing prints it. A header can carry a credential, so the method holds the keyword at once in HeldHeaders, which prints as <headers hidden> and is never pickled, and deletes the name, as it already does for a request body. A traceback that records each frame's local variables shows nothing of it.

  • Checked before sending. The value is checked against the parameter's schema (its maxLength and pattern, plus no line break, since Python's $ would match before a final one) in a frame of its own. A value the API would refuse raises Haau3Error before anything is sent, and the message names the keyword, never the value.

  • Precedence. The keyword wins over the same header in extra_headers, as correlation_id does for X-Request-Id.

  • The generator stops on a required header, a cookie, a header whose name clashes with a query parameter or a call option, a schema that is not a string, a pattern Python cannot read, and a header on an operation a batch can carry.

  • Docs: api.md, the method's docstring and the README's Options say how it works.

Why

The spec declares Payer-Authorization on process_message, but the generator built methods from path and query parameters only, so the header could be sent only through extra_headers, unchecked, as an ordinary local a traceback would print. The value is the whole header (Bearer and the token), as the spec's schema types it.

Tests

tests/test_header_options.py, each run sync, on asyncio and on trio:

  • the header is sent beside the key's own Authorization, and not sent when unset (the control);
  • any case of the scheme is taken;
  • a bare token, another scheme, an empty value, a final line break and 4,097 characters are each refused before anything is sent, with the value nowhere in the error or in the SDK's own frames printed with their local variables, and 4,096 characters are taken;
  • the keyword wins over extra_headers, is sent again on a retry, and is carried by no error the API answers;
  • the holder never prints or pickles what it holds, and only process_message takes the keyword.

A positive control checks that the SDK's frames are printed with their locals. tests/test_generate.py covers the generator's new keyword and each case it stops on.

Negative controls, each failing its tests: the keyword's name kept in the method's frame (5 fail), the pattern unchecked (9), extra_headers winning (3), the refusal raised in the frame that holds the value (9).

pytest (672 passed), ruff check, ruff format --check, mypy, pyright, pyright --verifytypes (100%) and ty check pass. The generated files are current.

🤖 Generated with Claude Code

briankfung and others added 2 commits October 23, 2026 10:49
The generator now reads an operation's optional header parameters, where it skipped them before.
Each becomes a keyword of that method's own, named from the header in snake_case and typed from the
spec, so process_message takes payer_authorization, sent as Payer-Authorization. Every existing
call works as it did.

A header can carry a credential, so the method holds the keyword at once in HeldHeaders, which
prints as <headers hidden> and is never pickled, and deletes the name, as it already does for a
request body. A traceback that records each frame's local variables shows nothing of it. Before
sending, the value is checked against the parameter's schema (its maxLength and pattern) in a frame
of its own. A value the API would refuse is refused in a message that names the keyword, never the
value. The keyword wins over the same header in extra_headers, as correlation_id does for
X-Request-Id.

The generator stops on a required header, on a parameter in a cookie, on a header whose name clashes
with a query parameter or a call option, on a pattern Python cannot read, and on a header declared by
an operation a batch can carry. api.md, each method's docstring and the README's Options say how it
works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here nothing prints it

Each test runs sync, on asyncio and on trio. The header is sent beside the key's own
Authorization, and not sent when unset (the control). Any case of the scheme is taken. A bare
token, another scheme, an empty value, a final line break and 4,097 characters are each refused
before anything is sent, with the value nowhere in the error or in the SDK's frames printed with
their local variables; 4,096 characters are taken. The keyword wins over the same header in
extra_headers, is sent again on a retry, and is carried by no error the API answers. The holder
never prints or pickles what it holds, and only process_message takes the keyword. A positive
control checks that the SDK's frames are printed with their locals.

The generator's tests: an optional header becomes a keyword of the operation's own, and the
generator stops on a required header, a cookie, a name that clashes with a call option or a query
parameter, a schema that is not a string, a pattern Python cannot read, and a header on an
operation a batch can carry. The README test gives the new block its access token.

Negative controls, each failing its tests and restored with cp: the keyword's name kept in the
method's frame (5 fail), the pattern unchecked (9), extra_headers winning (3), the refusal raised in
the frame that holds the value (9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@briankfung
briankfung merged commit 94e5ed2 into main Oct 23, 2026
10 checks passed
@briankfung
briankfung deleted the header-parameter-options branch October 23, 2026 14:51
@haau3-bot haau3-bot Bot mentioned this pull request Oct 23, 2026
briankfung pushed a commit that referenced this pull request Oct 23, 2026
The next release of haau3, from the pull requests merged since the last
one. Merging this makes the GitHub Release and builds the version, which
is published to PyPI once a maintainer approves it.
---


##
[0.2.0](v0.1.4...v0.2.0)
(2026-10-23)


### ⚠ BREAKING CHANGES

* the API's spec at 1.0.0-preview.12
([#16](#16))

### Features

* a header an operation declares is a keyword of its own
([#20](#20))
([94e5ed2](94e5ed2))
* the API's spec at 1.0.0-preview.12
([#16](#16))
([303f24c](303f24c))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: haau3-bot[bot] <344534095+haau3-bot[bot]@users.noreply.github.com>
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.

1 participant