Skip to content

refactor(client): split client.py into a client/ package by endpoint family (#520)#524

Merged
padak merged 6 commits into
mainfrom
refactor/520-client-split
Jul 22, 2026
Merged

refactor(client): split client.py into a client/ package by endpoint family (#520)#524
padak merged 6 commits into
mainfrom
refactor/520-client-split

Conversation

@padak

@padak padak commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Splits src/keboola_agent_cli/client.py (3,962 LOC on main, ~1.9× over CONTRIBUTING.md's 2,000-line hard ceiling) into a client/ package by endpoint family, via mixin composition. KeboolaClient stays a single class exposing every Storage/Queue method at its original signature — so keboola_agent_cli.Client and its .raw accessor are unaffected.

Flagged as NB-2 in the PR #516 review (pre-existing systemic debt, deferred to this follow-up).

Closes #520.

Pure move — no method bodies changed

Every KeboolaClient method and every module-level helper moves verbatim. This is mechanically verifiable: extract each method's source segment (via ast) from origin/main:client.py and from the new package, and compare. Result:

  • 124 / 124 methods present — none missing, none extra.
  • 121 byte-identical.
  • 3 with only a documented type-annotation change (see below).
  • All 11 module-level helpers (incl. the _CloudDownloader / _IterBytesReader classes) byte-identical.

The three annotation changes, each required to satisfy ty after the move (no # type: ignore added):

method change why
_CoreClient.__enter__ -> "KeboolaClient"-> Self once on the shared base, return self typed as the subclass is unsound; -> Self is the correct base-class annotation and matches the existing BaseHttpClient.__enter__ in this repo
_MiscMixin.get_oauth_url selfself: "KeboolaClient" calls self.create_short_lived_token (a _TokensMixin method) across the mixin boundary — the canonical typed-mixin idiom so ty sees the composed surface
_StorageFilesMixin.upload_file selfself: "KeboolaClient" calls self.prepare_file_upload / self._upload_to_cloud (_StorageTablesMixin) — same idiom

These are the only cross-family self.<method> calls in the whole client (confirmed by an ast cross-family edge scan). Runtime behavior is identical — KeboolaClient inherits both mixins, so every call resolves exactly as before.

Design

  • Mixin composition, not sub-client delegation.raw.list_tables(...) etc. must keep working, so KeboolaClient remains one class whose methods are physically split across _XMixin modules and recombined by inheritance.
  • Every mixin inherits a shared typed base _CoreClient(BaseHttpClient) (client/_core.py) holding the plumbing (__init__, _request/_queue_request/_query_request/_encrypt_request/_sync_actions_request, the _*_base_url props, _get_or_create_sub_client, _wait_for_storage_job, close, __enter__/__exit__). This is what lets ty statically resolve self._request(...) inside each mixin. _CoreClient.__init__ runs once (single diamond apex).
  • client/__init__.py re-exports the 9 importable names so from keboola_agent_cli.client import X is unchanged for all importers (verified byte-identical with the grep in the issue). It also re-exports time, QUERY_RESULTS_PAGE_SIZE and _CloudDownloader, which existing tests reach as keboola_agent_cli.client.<name> (two time.sleep/time.monotonic patch sites, a _CloudDownloader.create patch, and a page-size assertion).

Per-module LOC (all under the 2,000 hard ceiling)

module LOC contents
storage_tables.py 1,164 buckets / tables / snapshots / sharing (35 methods)
_transfer.py 665 module-level cloud up/download + query-error parsers + URL safety + inline pagination
configs.py 507 components / configs / rows / metadata (17)
storage_files.py 342 files incl. sliced download (10)
queue.py 276 jobs (7)
tokens.py 206 verify / project / scoped tokens / features (8)
_core.py 193 _CoreClient plumbing base
misc.py 184 search / oauth / encrypt / sync-actions (4)
workspaces.py 168 workspaces / sandboxes (8)
query.py 157 query service (6)
branches.py 148 dev branches + branch metadata (7)
stream.py 144 stream sources → StreamClient (7)
_client.py 46 KeboolaClient(<10 mixins>, _CoreClient) composition
__init__.py 43 re-export surface

Commit structure

One clean commit per step of the issue's 5-step plan, each leaving make check green:

  1. client.pyclient/ package; module-level helpers → _transfer.py.
  2. Extract _CoreClient plumbing into _core.py.
  3. Peel small families (stream, branches, tokens, query, workspaces, misc).
  4. Peel configs + queue.
  5. Peel storage_tables + storage_files; finalize composition; monolith gone.

Test coupling (necessary, not behavior changes)

A package split relocates module globals, so 4 test sites coupled to the old file layout were repointed at the new internal home — same behavioral assertions, no weakening:

  • test_workspace_service.py (×3): monkeypatch.setattr(client_module, "QUERY_RESULTS_PAGE_SIZE", …) → patches client._transfer (where _collect_inline_results now reads it).
  • test_lib.py (×1): InlineQueryResult.__module__ == "keboola_agent_cli.client".startswith("keboola_agent_cli.client") (it now lives in the client._transfer submodule).

Verification

  • make check green (4,648 passed, 8 skipped) at every step.
  • ty clean — no new diagnostics (only the 3 pre-existing unrelated warnings in scripts/hatch_build.py, tests/test_mcp_deprecation_warnings.py, tests/test_tool_call_permissions.py), no new # type: ignore.
  • Import surface byte-identical: grep -rhoE "from (\.+|keboola_agent_cli\.)client import [^\n]+" src/ tests/ | sort -u unchanged before/after.
  • kbagent --help and kbagent storage --help render.

A couple of prose mentions of client.py in docs/sdk.md and CONTRIBUTING.md are now slightly stale but were left untouched to keep this a pure code move (CONTRIBUTING.md "split by endpoint family" is in fact the guideline this PR fulfills).


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

padak added 6 commits July 22, 2026 14:06
…-level helpers to _transfer.py (#520)

Pure movement, zero behavior change. Converts the 3,962-LOC single-file
client.py into a client/ package. The ~525 LOC of module-level helpers
(cloud up/download plumbing, query-error parsers, URL-safety, inline
Query Service pagination, Queue poll scheduling) move verbatim into
client/_transfer.py. The whole KeboolaClient class stays intact in
client/_client.py for now (peeled into mixins in later steps).

client/__init__.py re-exports the 9 public names so
`from keboola_agent_cli.client import X` is unchanged for all importers,
plus `time`, `QUERY_RESULTS_PAGE_SIZE` and `_CloudDownloader` which
existing tests reach as `keboola_agent_cli.client.<name>` (patch targets
and a page-size assertion).

Relative parent-package imports shift one level (`.constants` ->
`..constants`, etc.). A TYPE_CHECKING import of KeboolaClient in
_transfer.py keeps the `_collect_inline_results` forward-ref resolvable
for ty.

Test layout couplings updated (necessary consequence of a module split,
not a behavior change): the two workspace pagination tests and the page-
boundary test monkeypatch QUERY_RESULTS_PAGE_SIZE at its new reader
module (client._transfer); test_lib relaxes the InlineQueryResult
`__module__` equality to a client-package prefix check.

make check green: 4648 passed, 8 skipped. ty clean (3 pre-existing
unrelated warnings only).
…_core.py (#520)

Pure movement. Moves the shared HTTP plumbing verbatim into a new
_CoreClient(BaseHttpClient) base in client/_core.py: __init__, the four
_*_base_url properties, close, __enter__/__exit__, _request,
_get_or_create_sub_client, _queue_request/_query_request/_encrypt_request/
_sync_actions_request and _wait_for_storage_job. KeboolaClient now inherits
_CoreClient; every endpoint-family mixin peeled out in later steps will
inherit the same base so ty statically resolves self._request(...) etc.
with no # type: ignore.

Judgment call: the relocated __enter__ is annotated `-> Self` instead of
the former `-> "KeboolaClient"`. Once __enter__ lives on the _CoreClient
base, `return self` typed as the KeboolaClient subclass is unsound and ty
flags invalid-return-type; `-> Self` is the correct base-class annotation
and matches the existing BaseHttpClient.__enter__ in this repo
(http_base.py already imports typing.Self for exactly this). Behavior is
identical -- the context manager still returns the concrete instance. This
also drops the TYPE_CHECKING import of KeboolaClient that a verbatim
forward ref would have required.

make check green: 4648 passed, 8 skipped. ty clean.
…branches, tokens, query, workspaces, misc) (#520)

Pure movement. Extracts six endpoint families out of the KeboolaClient
monolith into per-family mixins, each `class _XMixin(_CoreClient)` so ty
statically resolves the plumbing (self._request, self._queue_request,
shared instance attrs) with no # type: ignore:

  client/stream.py      _StreamMixin      (7 methods)
  client/branches.py    _BranchesMixin    (7)
  client/tokens.py      _TokensMixin      (8)
  client/query.py       _QueryMixin       (6)
  client/workspaces.py  _WorkspacesMixin  (8)
  client/misc.py        _MiscMixin        (4: search/oauth/encrypt/sync-actions)

KeboolaClient now composes these six mixins over _CoreClient; every method
body, signature and docstring moved verbatim, and each module gets exactly
the imports its methods use.

Judgment call: _MiscMixin.get_oauth_url calls self.create_short_lived_token
(a _TokensMixin method). Across the mixin boundary ty cannot see it on the
bare _CoreClient base, so the method's `self` is annotated
`self: "KeboolaClient"` (via a TYPE_CHECKING import) -- the canonical typed-
mixin idiom. Runtime is unchanged: KeboolaClient inherits both mixins, so
the call resolves at run time exactly as before. This is the only cross-
family self-call among these six families.

_client.py: 3165 -> 2258 LOC. make check green: 4648 passed, 8 skipped. ty clean.
…#520)

Pure movement. Extracts two mid-sized families into per-family mixins over
_CoreClient:

  client/configs.py  _ConfigsMixin  (17 methods: components, configs, rows,
                     metadata, incl. delete_config)
  client/queue.py    _QueueMixin    (7 methods: job list/create/kill/events/poll)

Every method body, signature and docstring moved verbatim; each module gets
exactly the imports its methods use. No cross-family self-calls in either.

_client.py: 2258 -> 1508 LOC. make check green: 4648 passed, 8 skipped. ty clean.
…ion; monolith gone (#520)

Pure movement, final step. Extracts the two storage families into mixins
over _CoreClient:

  client/storage_tables.py  _StorageTablesMixin  (35 methods: buckets, tables,
                            snapshots, bucket sharing/linking)  1,164 LOC
  client/storage_files.py   _StorageFilesMixin   (10 methods: upload/download
                            incl. sliced, tagging)               342 LOC

_client.py is now a pure composition module: KeboolaClient assembled from all
ten mixins over _CoreClient, in the order named in issue #520, keeping its
class docstring. The residual monolith (all method bodies + their imports) is
gone -- every method now lives in exactly one family module and the stale
`logger`/imports left behind were dropped.

Judgment call: _StorageFilesMixin.upload_file calls self.prepare_file_upload
and self._upload_to_cloud (both _StorageTablesMixin methods -- the shared
cloud-upload path used by upload_table too). As with get_oauth_url in step 3,
its `self` is annotated `self: "KeboolaClient"` so ty resolves the cross-
family call; runtime is unchanged.

Every client/ module is now under the 2,000-LOC hard ceiling (largest:
storage_tables.py at 1,164). make check green: 4648 passed, 8 skipped. ty
clean (only the 3 pre-existing unrelated warnings).
The monolithic client.py became the client/ package; refresh the prose that
named the old file so a reader (and the kbagent-pr-reviewer agent) is not
misdirected. CLAUDE.md project-structure map + 3-layer prose, CONTRIBUTING.md
layer table / file-size budget / split guidance / new-command checklist,
docs/sdk.md diagram, and the pr-reviewer BLOCKING httpx rule now say the
`client/` package. No behavior change; convention #17 silent-drift sync.
@padak
padak force-pushed the refactor/520-client-split branch from bc3ff5c to 5c2dcc4 Compare July 22, 2026 12:40
@padak
padak merged commit e93349e into main Jul 22, 2026
4 checks passed
@padak
padak deleted the refactor/520-client-split branch July 22, 2026 12:48
padak added a commit that referenced this pull request Jul 22, 2026
…(internal) (#525)

Internal-only maintenance release: client.py -> client/ package split (#520/#524) + nightly E2E flake fixes (#521/#523). No user-facing behavior changes. Version bumped, changelog added, manifests synced, make check green.
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.

Split client.py by endpoint family — 3,842 LOC vs 2,000 hard ceiling (PR #516 NB-2)

1 participant