Skip to content

refactor(sync): split sync_service.py -- extract bindings + clone + models (tech-debt) - #437

Merged
padak merged 4 commits into
mainfrom
refactor/split-sync-service
Jun 17, 2026
Merged

refactor(sync): split sync_service.py -- extract bindings + clone + models (tech-debt)#437
padak merged 4 commits into
mainfrom
refactor/split-sync-service

Conversation

@padak

@padak padak commented Jun 17, 2026

Copy link
Copy Markdown
Member

Tech-debt split of the oversized sync_service.py, deferred from #432.

Stacked on #432 (base = feat/sdk-hardening-426-427-428, so the diff is split-only). Merge #432 first, then retarget this to main (or merge into the collector before #432 lands).

Why

sync_service.py reached ~4033 lines -- ~2.7x the 1500-LOC hard ceiling for services/*.py in CONTRIBUTING.md "Code Quality Patterns", which names this file as the known extreme case and says the next PR adding material should split first. #432 added ~321 LOC (push Phase-D flow remap + clone_project) without splitting; this is that split.

What (no behavior change -- public SyncService API identical)

Eight new cohesive modules; sync_service.py: 4033 -> 2244 lines (-1789, ~44%).

Module LOC Contents
_sync_models.py 97 the 5 dataclasses + VARIABLES/FLOW component-id constants (re-exports CreatedConfig)
_sync_bindings.py 456 push Phase C (variable links) + Phase D (keboola.flow task configId remap)
_sync_clone.py 163 the clone_project orchestration
_sync_writeback.py 167 manifest + local-file writeback after a push
_sync_storage.py 376 pull-side data writers (storage metadata, per-config jobs, samples) + _ensure_path_within
_sync_bulk.py 228 the all-projects orchestrators (pull_all / diff_all / push_all)
_sync_branch.py 191 git-branch <-> Keboola-dev-branch linking
_sync_push_ops.py 369 per-change create/update/delete CRUD for configs + rows

The remainder (2244 lines) is the genuine core: init_sync / pull / diff / push / status orchestration + the shared branch-path / manifest / conflict-detection helpers. Splitting those too would turn SyncService into a near-empty delegating shell -- left as a judgement-call follow-up.

Approach

Free functions taking the SyncService as first arg (not mixins): keeps typing explicit and ty-clean (a TYPE_CHECKING SyncService import types the service param) and makes each group unit-testable in isolation. Most extracted functions are pure; only those needing an on-disk helper (_read_config_file / _write_config_file / _compute_config_hashes / _file_hash / _resolve_source_branch_path / _resolve_max_workers) take service. Public methods (clone_project, pull_all/diff_all/push_all, branch_link/branch_unlink/branch_status) are preserved as thin delegators so commands + tests are unchanged. A handful of direct test calls to relocated private helpers were updated.

Patch-point fidelity: _sync_branch imports get_current_branch LOCALLY inside each function (as the original did) so tests that patch("keboola_agent_cli.sync.git_utils.get_current_branch") keep working -- a module-top import would capture the reference at import time and defeat source-module patching.

Verification

  • make check green: 4092 passed, 8 skipped -- identical to pre-split, so behavior is unchanged. ty clean on all new modules.
  • An 8-way adversarial review (one agent per module, comparing each extracted function against the original method on the base branch) found no behavior-affecting change -- only the intentional method->function / underscore-prefix change.

@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

Base automatically changed from feat/sdk-hardening-426-427-428 to main June 17, 2026 07:50
padak added 4 commits June 17, 2026 09:52
…y (tech-debt)

`sync_service.py` had grown to ~4033 lines -- ~2.7x the 1500-LOC ceiling for
`services/*.py` (CONTRIBUTING.md calls this file out by name). #432 added ~321
LOC without splitting; this is the deferred split.

No behavior change -- pure mechanical extraction, public `SyncService` API
identical:

- `_sync_models.py`: the 5 dataclasses (WritebackResult, CreatedConfig,
  VariableBindingResult, FlowBindingResult, LocalConfigHashes) + the
  VARIABLES/FLOW component-id constants. `sync_service` re-exports `CreatedConfig`
  (the only externally-imported name) for back-compat.
- `_sync_bindings.py`: the push-time link backfill -- Phase C (variable links)
  and Phase D (keboola.flow task configIds) -- as free functions taking the
  `SyncService` as first arg (for the 3 on-disk helpers they need). push() now
  calls `resolve_variable_bindings(self, ...)` / `resolve_flow_task_bindings(self, ...)`.
- `_sync_clone.py`: the `clone_project` orchestration. `SyncService.clone_project`
  is now a thin delegator (public method preserved).

`sync_service.py`: 4033 -> 3435 lines (-598). The free-function-taking-service
pattern keeps typing explicit (ty-clean via a TYPE_CHECKING `SyncService` import)
and makes the bindings/clone unit-testable in isolation. Further extractions
(writeback, storage-metadata/jobs/samples) are natural follow-ups in the same
pattern. make check green (4092 passed).
…h-debt)

Continues the sync_service.py split. No behavior change.

- `_sync_writeback.py`: manifest + local-file writeback after a push
  (writeback_create_config_in_manifest, writeback_create_row_in_manifest,
  propagate_kbc_metadata, writeback_after_push). Only writeback_after_push needs
  the service (for _write_config_file); the rest are pure.
- `_sync_storage.py`: pull-side data writers (write_storage_metadata,
  fetch_jobs_per_config, write_per_config_jobs, fetch_samples,
  mask_encrypted_columns) + the `_ensure_path_within` storage-write traversal
  guard moved here with its only callers. Only fetch_jobs_per_config needs the
  service (for _resolve_max_workers).

push()/pull() call the free functions; tests updated to the relocated symbols
(test_sync_service.py writeback calls, test_sync_storage_jobs.py storage calls).

`sync_service.py`: 3435 -> 2931 lines. Cumulative over this PR: 4033 -> 2931
(-1102, ~27%), now across 5 cohesive modules (_sync_models, _sync_bindings,
_sync_clone, _sync_writeback, _sync_storage). make check green (4092 passed,
identical to pre-split).
…h-debt)

Continues the sync_service.py split. No behavior change.

- `_sync_bulk.py`: the all-projects orchestrators (pull_all, diff_all,
  push_all) as free functions taking the service; the public methods are now
  thin delegators.
- `_sync_branch.py`: git-branch <-> Keboola-dev-branch linking (branch_link,
  branch_unlink, branch_status). branch_link takes the service; the others are
  pure. `get_current_branch` is imported LOCALLY inside each function (as the
  original did) so tests that patch `keboola_agent_cli.sync.git_utils.
  get_current_branch` keep working -- a top-level import would capture the
  reference at import time and defeat source-module patching.

Public methods preserved as shims (commands + tests call them unchanged).
`sync_service.py`: 2581 lines (was 2931 this commit; 4033 at the PR base).
make check green (4092 passed).
Continues the sync_service.py split. No behavior change.

- `_sync_push_ops.py`: the per-change create/update/delete operations for
  configs and rows (push_create, push_update, push_row_change + the row
  create/update/delete helpers) as free functions taking the service. push()
  calls push_create(self, ...) / push_update(self, ...) / push_row_change(self,
  ...). The 2 direct test calls to _push_update_row were updated to the
  relocated push_update_row.

`sync_service.py`: 2244 lines. Cumulative over this PR: 4033 -> 2244 (-1789,
~44%), across 8 cohesive modules (_sync_models, _sync_bindings, _sync_clone,
_sync_writeback, _sync_storage, _sync_bulk, _sync_branch, _sync_push_ops). The
remainder is the core orchestration (init/pull/diff/push/status) + shared
branch-path/manifest helpers. make check green (4092 passed).
@padak
padak force-pushed the refactor/split-sync-service branch from 80850c1 to 12b9f83 Compare June 17, 2026 07:54
@padak
padak merged commit e428d36 into main Jun 17, 2026
4 checks passed
@padak
padak deleted the refactor/split-sync-service branch June 17, 2026 08:13
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