Repository navigation
Authenticate with Authorization: Bearer for programmatic tokens - #94
Conversation
- .gitignore: local virtualenvs are non-portable (absolute interpreter paths, platform-specific native extensions), so they must never be committed. - docker-compose.yml: Compose V2 ignores the top-level `version` key and warns on every invocation. Both `docker compose` and `docker-compose` resolve to V2 locally, and CI only uses `docker compose`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a Development section covering virtualenv creation, dependency installation and editor interpreter selection, written to apply to any editor rather than a specific one. Supporting changes the new instructions require: - pyproject.toml: declare flake8 in a `dev` extra. It was previously installed ad hoc by the Dockerfile and by nothing else, so local and CI versions were unconstrained and free to diverge. Bounded to 7.x because flake8 majors add checks, which fails CI on untouched code. - Dockerfile: install `.[dev]` instead of flake8 and the package separately. - .flake8: exclude .venv. flake8's default exclude list does not cover it, so a project-root virtualenv makes it lint site-packages. Uses extend-exclude to keep the defaults. - .dockerignore: exclude .venv, which `COPY . /code/` would otherwise copy into the image. - README: the Tests command named the image, `sapi-python-client`, rather than a service, and failed with "no such service". Corrected to `ci`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Storage API tokens are project-bound and go in X-StorageApi-Token.
Programmatic tokens (kbc_at_* session, kbc_pat_* personal access) are not,
so they are sent as Authorization: Bearer alongside the project id in
X-KBC-ProjectId.
`kbcstorage.auth` holds the two strategies and `coerce`, which turns the
`token` argument into a strategy object. A bare string still becomes a
StorageApiToken, so every existing caller is unaffected:
Client(url, 'token') # unchanged
Client(url, BearerToken('kbc_at_...', 1234)) # bearer
Endpoint now stores the strategy on `self.auth` and builds its header
block from `auth.headers()`. Endpoints constructed internally by other
endpoints are passed `self.auth` rather than the raw token, so the scheme
propagates through the multi-step paths in tables, workspaces and buckets.
`self.token` still exposes the raw string.
Buckets.list_tables no longer hardcodes X-StorageApi-Token, which would
have sent both schemes at once under a bearer token.
The four request verbs merge caller headers into a new dict instead of
mutating the caller's, which the previous `headers.update()` did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Types ----- `coerce` was unannotated, so a type checker inferred `Unknown | StorageApiToken` for its result. It is now `Union[str, Auth] -> Auth` with `Auth = Union[StorageApiToken, BearerToken]`, and the strategies annotate their own constructors and `headers()`. `isinstance` replaces the `hasattr` duck-typing check so the union narrows; nothing consumes `kbcstorage.auth` yet, so no contract is lost. basedpyright now reports `StorageApiToken | BearerToken` for all three input forms. Programmatic tokens are rejected as Storage API tokens ------------------------------------------------------ A `kbc_at_*`/`kbc_pat_*` token cannot authenticate as `X-StorageApi-Token`; the backend answers 401. `StorageApiToken` now raises instead, so the mistake surfaces at construction rather than at the request. The check sits in the constructor so both `coerce` and an explicit `StorageApiToken(...)` are covered, and the message never echoes the token. The `token` property -------------------- Under a bearer token `.token` holds the programmatic token, not a Storage API token, and anything that forwards it into a Storage API header breaks. `Client.token` and `Endpoint.token` document that, and `Endpoint.token` became a read-only property: it was a plain attribute whose assignment silently did nothing, since the header block is built in `__init__`. `coerce` also warns once when a bearer token is used, and warns that a bare string is deprecated. Warning attribution ------------------- Endpoints build other endpoints, so a fixed `stacklevel` named a line inside kbcstorage. One `Client(BearerToken(...))` emitted 13 warnings, 12 of them pointing at library internals, and a DeprecationWarning attributed to a library module is dropped by the default filter, which hid the bare-string deprecation for every Endpoint subclass. `_caller_stacklevel` walks out of the package, which collapses those 13 to one and makes the deprecation visible. Tables and Configurations passed the raw token to their metadata endpoint instead of `self.auth`, coercing twice and building a redundant strategy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Resolve the Python-version compatibility issues and prevent warnings during valid bearer-token usage.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds strategy-based authentication for Storage API and programmatic bearer tokens, with propagation through nested endpoints and updated tests and development tooling.
Changes:
- Adds
StorageApiTokenandBearerTokenauthentication strategies. - Propagates authentication through nested endpoints and improves header handling.
- Updates tests, documentation, and local development configuration.
File summaries
| File | Summary |
|---|---|
tests/mocks/test_workspaces.py |
Tests bearer authentication propagation |
tests/mocks/test_client.py |
Tests endpoint authentication |
tests/mocks/test_buckets.py |
Tests exclusive authentication headers |
tests/mocks/test_base.py |
Tests base header behavior |
tests/mocks/test_auth.py |
Tests authentication strategies |
README.md |
Documents authentication and development setup |
pyproject.toml |
Adds development dependency |
kbcstorage/workspaces.py |
Propagates authentication strategy |
kbcstorage/tables.py |
Propagates authentication strategy |
kbcstorage/configurations.py |
Propagates authentication strategy |
kbcstorage/client.py |
Adds strategy-aware client setup |
kbcstorage/buckets.py |
Uses strategy-aware requests |
kbcstorage/base.py |
Builds authentication headers |
kbcstorage/auth.py |
Implements authentication strategies |
Dockerfile |
Installs development extras |
docker-compose.yml |
Removes obsolete version key |
.gitignore |
Ignores virtual environments |
.flake8 |
Excludes virtual environments |
.dockerignore |
Excludes virtual environments |
Review details
- Files reviewed: 18/19 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| ### Local environment | ||
|
|
||
| The package requires Python 3.7 or newer; CI builds and tests on 3.11. |
There was a problem hiding this comment.
Fixed in d4d0b69. requires-python is now >=3.10 and the README matches, naming int | str in workspaces.load_tables as the reason. Added the 3.10 and 3.11 classifiers too. Both the annotation and the >=3.7 predate this PR — this branch only newly asserted 3.7 in the README, so the fix goes here.
| warnings.warn( | ||
| "Authenticating with a bearer token: `.token` returns the " | ||
| "programmatic token, not a Storage API token, and will be " | ||
| "rejected if sent as X-StorageApi-Token. Pass `.auth` when " | ||
| "building further endpoints.", | ||
| UserWarning, | ||
| stacklevel=_caller_stacklevel(), | ||
| ) | ||
| return token |
There was a problem hiding this comment.
Fixed in d4d0b69. coerce() warned on every BearerToken it received, including ones already constructed, so it fired 13 times for a single Client(url, BearerToken(...)) and again per Tables.load / Workspaces.create call. Both warnings are removed along with _caller_stacklevel(); the caveat stays in the .token docstrings on Endpoint and Client, which is where it belongs. tests/mocks/test_auth.py::TestCoerce::test_no_warnings asserts coerce() is silent for all three input forms, and the mock suite now passes under -W error.
|
|
||
| [project.optional-dependencies] | ||
| dev = [ | ||
| "flake8>=7,<8", |
There was a problem hiding this comment.
Resolved in d4d0b69 from the other direction: rather than loosening the flake8 range, requires-python is now >=3.10, which was already the real floor — workspaces.load_tables uses int | str annotations that fail at import below 3.10. flake8>=7 needs 3.8.1+, so the conflict is gone. Docker and CI both run 3.11.
|
@jirkasemmler can you review it please when you find time? There is public API breakign change. |
|
@jirkasemmler can you review it pleas, if not at least let me know I will find someone else. |
|
on today's list |
jirkasemmler
left a comment
There was a problem hiding this comment.
Code review
The design is good. Passing the auth strategy through the internally built endpoints is the right approach, and the Buckets.list_tables and header-mutation fixes are correct. A few things to address before merging:
BearerTokenemits aUserWarningon the documented happy path.coerce()warns every time it receives aBearerToken, and every endpoint calls it.Client(url, BearerToken(...))by itself produces 13 warnings, and more follow at runtime becauseTables.load,Workspaces.createand similar methods build newFiles/Jobs/Tokensendpoints on each call. Withpython -W erroror pytestfilterwarnings = error, constructing the client raises. The note about.tokenbelongs in the.tokendocstring (it is already there), not in a runtime warning.
sapi-python-client/kbcstorage/auth.py
Lines 81 to 91 in 36673e2
- Passing the token as a string now emits a
DeprecationWarning. The PR description says "every existing caller is unaffected", and the README marks the string form as# unchanged. Because of_caller_stacklevel(), the warning is attributed to user code, so it will be shown there. Either document the deprecation (README and changelog) or drop the warning for now.
sapi-python-client/kbcstorage/auth.py
Lines 91 to 99 in 36673e2
- Breaking change:
kbc_at_/kbc_pat_strings now raiseValueError.Client(url, 'kbc_pat_...')used to send the token asX-StorageApi-Tokenand now fails at construction. That may be intended, but it should be called out as a breaking change in the release notes, with a version bump to match.
sapi-python-client/kbcstorage/auth.py
Lines 34 to 40 in 36673e2
- The Python 3.7 claim is inconsistent (also raised by Copilot). The README and
requires-pythonsay 3.7+, butflake8>=7requires 3.8+, andWorkspaces.load_tablesusesint | strannotations, which fail at import time below 3.10. Please alignrequires-python/README with what is actually supported.
sapi-python-client/pyproject.toml
Lines 11 to 13 in 36673e2
sapi-python-client/pyproject.toml
Lines 36 to 38 in 36673e2
🤖 Generated with Claude Code
coerce() runs once per endpoint, and endpoints build further endpoints, so a warning there fires 13 times for a single Client(). The bearer caveat is in the .token docstrings; the string form is supported, not deprecated. requires-python and the README claimed 3.7, but workspaces.load_tables has `int | str` annotations that fail at import below 3.10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@jirkasemmler all four addressed in d4d0b69. 1. 2. 3. Breaking change. Intended, now a README section under Authentication. Worth noting what it replaces: the prior behaviour sent 4. Python version. 77 mock tests pass, and pass under 🤖 Generated with Claude Code |
jirkasemmler
left a comment
There was a problem hiding this comment.
Re-review after d4d0b69
All four points from my previous review are resolved and verified: auth.py has no warnings import and the mock suite passes under python -W error (77 tests), the string form is not deprecated, the kbc_at_/kbc_pat_ rejection is documented under "Breaking change in 0.11.0", and requires-python/README agree on 3.10. Flake8 is clean. I also checked the backend contract in keboola/connection: BearerTokenAuthenticator reads X-KBC-ProjectId, and kbc_at_/kbc_pat_ are exactly the prefixes the Storage routes accept (kbc_apc_, kbc_rt_, kbc_mfa_ are not storage bearers), so the client-side guard matches.
Approving. The rest is minor and can be addressed here or deferred.
Minor
- README shows two canonical forms for a Storage API token. The lead example at
README.md:21-23was changed toStorageApiToken('your-token'), while the Authentication section says the bare string is fully supported. I would keep the bare string in the lead example (and in "Endpoint Classes Usage") and introduceStorageApiTokenonly in the Authentication section. - The Python floor bump is an install-time break for 3.8/3.9 and is not listed under "Breaking change". It is justified (
int | stratworkspaces.py:233already failed at import below 3.10), but one line in the breaking-change section would help users whose pipelines pin 3.9. _post/_put/_deleteheader merging is untested (base.py:132,157,182).test_caller_headers_are_kept_and_not_mutatedcould be parametrized over the four verbs.Endpoint.tokenis now a read-only property (base.py:58-67). Assigningendpoint.token = ...worked on 0.10.0 and now raisesAttributeError. No practical impact (it never affected_auth_header), but worth a line in the release notes.
Nits
base.py:47duplicates theToken is required.guard thatcoerce()already raises.coerce()with a non-string token (bytes, int,Mock()) fails withAttributeErrorfromstartswithatauth.py:14; anisinstance(token, str)check would give a clearTypeError.BearerToken(tok, True)andBearerToken(tok, 1234.0)sendTrue/1234.0asX-KBC-ProjectId; the backend rejects both with "must be a positive integer" and the client gives no hint.Client._token(client.py:35) is a second source of truth beside_auth;tokencould just returnself._auth.tokenlikeEndpoint.tokendoes.- PR description still says 73 tests; it is 77 now.
Notes
- Bearer auth is feature-gated server-side:
BearerTokenAuthenticator::supports()returns false on stacks withoutprogrammatic-auth, soBearerTokengets a 401 there. Worth one sentence in the README Authentication section. - The functional suites run only with a Storage token, so the bearer path is mock-covered only. The backend E2E confirms
tokens/verifyunder bearer resolves to the admin's master storage token, soWorkspaces._get_default_backendshould work, but this repo does not prove it. - Version comes from git tags and README already says 0.11.0; tag
0.11.0after merge.
🤖 Generated with Claude Code
What
Storage API tokens are project-bound and travel in
X-StorageApi-Token. Programmatic tokens —kbc_at_*session tokens andkbc_pat_*personal access tokens — are not bound to a project, so they must be sent asAuthorization: Bearertogether with the project id inX-KBC-ProjectId.How
kbcstorage/auth.pyholds two strategies,StorageApiTokenandBearerToken, pluscoerce(), which normalises thetokenargument into a strategy object. A bare string still becomes aStorageApiToken, so every existing caller is unaffected — including positional construction and directly built endpoints.Endpointkeeps the strategy onself.authand derives its header block fromauth.headers().self.tokenstill exposes the raw string.Endpoints that other endpoints build internally now receive
self.authrather than the raw token, so the scheme propagates through the multi-step paths intables.py,workspaces.pyandbuckets.py.Two fixes fell out of this:
Buckets.list_tableshardcodedX-StorageApi-Token, which under a bearer token would have sent both schemes on one request.headers.update()did.Tests
73 mock tests pass; 14 are auth-specific. Both schemes are asserted mutually exclusive — each test checks the other scheme's header is absent, so a regression sending both would fail rather than pass quietly.
Covered: every branch of
auth.py;Endpointheader construction; all 10 client endpoints plus nested.metadataendpoints under both schemes;Buckets.list_tables; and bearer propagation into theTokensendpoint thatWorkspaces.createbuilds internally.Two known gaps, neither a defect — the call sites are correct, but a future refactor could break them silently:
test_base.pyexercises_getonly;_post/_put/_deletetook the same header change untested.tables.pyhas seven sites buildingFiles/Jobsfromself.authwith no bearer test, as doesbuckets.py:122.test_workspaces.pysets the precedent for how to cover these.Unrelated commits on this branch
Two housekeeping commits precede the feature and can be reviewed independently, or split out if you'd prefer:
0b53bc2— ignore.venv, drop the obsolete composeversionkey6dfcab4— document local development setup; declare flake8 in adevextra; exclude.venvfrom flake8 and the Docker build context; fix the Tests command, which named the image rather than theciservice and failed withno such service🤖 Generated with Claude Code