refactor(client): split client.py into a client/ package by endpoint family (#520)#524
Merged
Conversation
4 tasks
…-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
force-pushed
the
refactor/520-client-split
branch
from
July 22, 2026 12:40
bc3ff5c to
5c2dcc4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Splits
src/keboola_agent_cli/client.py(3,962 LOC onmain, ~1.9× over CONTRIBUTING.md's 2,000-line hard ceiling) into aclient/package by endpoint family, via mixin composition.KeboolaClientstays a single class exposing every Storage/Queue method at its original signature — sokeboola_agent_cli.Clientand its.rawaccessor 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
KeboolaClientmethod and every module-level helper moves verbatim. This is mechanically verifiable: extract each method's source segment (viaast) fromorigin/main:client.pyand from the new package, and compare. Result:_CloudDownloader/_IterBytesReaderclasses) byte-identical.The three annotation changes, each required to satisfy
tyafter the move (no# type: ignoreadded):_CoreClient.__enter__-> "KeboolaClient"→-> Selfreturn selftyped as the subclass is unsound;-> Selfis the correct base-class annotation and matches the existingBaseHttpClient.__enter__in this repo_MiscMixin.get_oauth_urlself→self: "KeboolaClient"self.create_short_lived_token(a_TokensMixinmethod) across the mixin boundary — the canonical typed-mixin idiom sotysees the composed surface_StorageFilesMixin.upload_fileself→self: "KeboolaClient"self.prepare_file_upload/self._upload_to_cloud(_StorageTablesMixin) — same idiomThese are the only cross-family
self.<method>calls in the whole client (confirmed by anastcross-family edge scan). Runtime behavior is identical —KeboolaClientinherits both mixins, so every call resolves exactly as before.Design
.raw.list_tables(...)etc. must keep working, soKeboolaClientremains one class whose methods are physically split across_XMixinmodules and recombined by inheritance._CoreClient(BaseHttpClient)(client/_core.py) holding the plumbing (__init__,_request/_queue_request/_query_request/_encrypt_request/_sync_actions_request, the_*_base_urlprops,_get_or_create_sub_client,_wait_for_storage_job,close,__enter__/__exit__). This is what letstystatically resolveself._request(...)inside each mixin._CoreClient.__init__runs once (single diamond apex).client/__init__.pyre-exports the 9 importable names sofrom keboola_agent_cli.client import Xis unchanged for all importers (verified byte-identical with the grep in the issue). It also re-exportstime,QUERY_RESULTS_PAGE_SIZEand_CloudDownloader, which existing tests reach askeboola_agent_cli.client.<name>(twotime.sleep/time.monotonicpatch sites, a_CloudDownloader.createpatch, and a page-size assertion).Per-module LOC (all under the 2,000 hard ceiling)
storage_tables.py_transfer.pyconfigs.pystorage_files.pyqueue.pytokens.py_core.py_CoreClientplumbing basemisc.pyworkspaces.pyquery.pybranches.pystream.pyStreamClient(7)_client.pyKeboolaClient(<10 mixins>, _CoreClient)composition__init__.pyCommit structure
One clean commit per step of the issue's 5-step plan, each leaving
make checkgreen:client.py→client/package; module-level helpers →_transfer.py._CoreClientplumbing into_core.py.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", …)→ patchesclient._transfer(where_collect_inline_resultsnow reads it).test_lib.py(×1):InlineQueryResult.__module__ == "keboola_agent_cli.client"→.startswith("keboola_agent_cli.client")(it now lives in theclient._transfersubmodule).Verification
make checkgreen (4,648 passed, 8 skipped) at every step.tyclean — no new diagnostics (only the 3 pre-existing unrelated warnings inscripts/hatch_build.py,tests/test_mcp_deprecation_warnings.py,tests/test_tool_call_permissions.py), no new# type: ignore.grep -rhoE "from (\.+|keboola_agent_cli\.)client import [^\n]+" src/ tests/ | sort -uunchanged before/after.kbagent --helpandkbagent storage --helprender.A couple of prose mentions of
client.pyindocs/sdk.mdandCONTRIBUTING.mdare 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).