Skip to content

Phase 2: Project management (client, config store, service, CLI) - #2

Merged
padak merged 1 commit into
mainfrom
phase-2-projects
Feb 26, 2026
Merged

Phase 2: Project management (client, config store, service, CLI)#2
padak merged 1 commit into
mainfrom
phase-2-projects

Conversation

@padak

@padak padak commented Feb 26, 2026

Copy link
Copy Markdown
Member

Implementation Summary

Full implementation of the project management lifecycle (Phase 2): add, list, remove, edit, status. Includes the Keboola API client with retry/timeouts, config persistence with secure file permissions, project service with dependency injection, and all CLI commands with proper exit codes and dual JSON/Rich output.

Files Changed

New files

  • src/keboola_agent_cli/client.py — KeboolaClient with httpx, retry (exponential backoff 1s/2s/4s for 429/5xx, max 3), timeouts (connect=5s, read=30s, write=10s, pool=5s), User-Agent header, verify_token(), list_components(), get_config_detail()
  • src/keboola_agent_cli/services/project_service.py — ProjectService with DI (client_factory callable), add/remove/edit/list/status operations
  • tests/test_client.py — 17 tests: verify_token success/401/403, retry on 503/429, retry exhaustion, no retry on 400, timeout, connect error, token masking, headers, context manager, list_components, get_config_detail
  • tests/test_config_store.py — 25 tests: load empty, save/load round-trip, file permissions 0600, add/remove/edit/get project, default project management, duplicate alias, version check, invalid JSON, invalid structure
  • tests/test_services.py — 18 tests: add project success/failure/duplicate/network, remove success/nonexistent, edit url/token/no-changes/nonexistent, list empty/multiple/token-masked, status ok/mixed/specific/nonexistent/token-masked
  • tests/test_cli.py — 14 tests: project add JSON/human/invalid-token-exit-3/timeout-exit-4, project list JSON-empty/JSON-with-data/human/human-empty, project remove success/nonexistent-exit-5, project status JSON/human, project edit url/config-error-exit-5

Modified files

  • src/keboola_agent_cli/models.py — Added TokenVerifyResponse model
  • src/keboola_agent_cli/config_store.py — Full implementation with version checking, error handling (ConfigError), duplicate alias protection
  • src/keboola_agent_cli/commands/project.py — Replaced stubs with real commands (Rich tables, proper exit codes, env var support)
  • src/keboola_agent_cli/cli.py — Wired up ConfigStore and ProjectService in context object

Acceptance Criteria

  • project add succeeds, verifies token via API, saves to config.json
  • project list shows projects in Rich table
  • --json project list outputs valid JSON with project details
  • project status shows connectivity with response time (OK/ERROR)
  • project remove removes project from config
  • project edit updates fields (re-verifies token if changed)
  • Config file permissions 0600
  • Token never printed fully — always masked (901-...pt0k)
  • Invalid token returns exit code 3 with structured error in --json mode
  • Network timeout returns exit code 4
  • Retry works on 5xx (verified via test mock: 503 then success)
  • Support KBC_TOKEN and KBC_STORAGE_API_URL env vars

Tests

  • uv run pytest tests/ -v114 tests passing (40 Phase 1 + 74 Phase 2, 0 failures)

🤖 Generated with Claude Code

Implement the full project management lifecycle: add, list, remove, edit, status.

- client.py: KeboolaClient with httpx, retry (exponential backoff for 429/5xx),
  timeouts (connect=5s, read=30s, write=10s, pool=5s), User-Agent header,
  verify_token(), list_components(), get_config_detail(), token masking in errors
- config_store.py: Full ConfigStore with platformdirs, load/save with 0o600
  permissions, add/remove/edit/get project, version checking, error handling
- services/project_service.py: ProjectService with DI (client_factory callable),
  add/remove/edit/list/status with token verification and masked output
- commands/project.py: All commands fully flag-based (non-interactive), proper
  exit codes (0/3/4/5), Rich table output for human mode, JSON for machine mode
- cli.py: Wired up context object with config_store and project_service
- models.py: Added TokenVerifyResponse model
- Support for KBC_TOKEN and KBC_STORAGE_API_URL env vars

Tests: 114 passing (40 original + 74 new)
- test_client.py: verify_token success/401, retry on 503/429, timeout, token masking
- test_config_store.py: load empty, add/remove/edit, permissions, version check
- test_services.py: add/remove/edit/list/status with mocks, mixed success/failure
- test_cli.py: project add/list/remove/edit/status via CliRunner, JSON and human mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@padak
padak merged commit 33cd47b into main Feb 26, 2026
@padak
padak deleted the phase-2-projects branch March 2, 2026 23:05
Matovidlo added a commit that referenced this pull request Jun 15, 2026
…, dedup

Thermo-nuclear review follow-ups (behavior-preserving):
- #1/#6: move inline nfpm loop -> build/package/linux/build_packages.sh and the
  zip+sha256 -> build/package/zip_binary.sh (explicit per-OS archiver); workflow
  is now pure orchestration.
- #2: .github/actions/setup-build composite (uv + optional Node) replaces the
  repeated setup preamble in pypi/freeze.
- #3: index.sh deb/rpm/apk collapsed into a publish_repo() helper + per-format indexers.
- #5: sign.sh parses the Azure token with jq instead of a sed regex.
- #7: nfpm.yaml notes it's rendered via envsubst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Matovidlo added a commit that referenced this pull request Jun 17, 2026
…, dedup

Thermo-nuclear review follow-ups (behavior-preserving):
- #1/#6: move inline nfpm loop -> build/package/linux/build_packages.sh and the
  zip+sha256 -> build/package/zip_binary.sh (explicit per-OS archiver); workflow
  is now pure orchestration.
- #2: .github/actions/setup-build composite (uv + optional Node) replaces the
  repeated setup preamble in pypi/freeze.
- #3: index.sh deb/rpm/apk collapsed into a publish_repo() helper + per-format indexers.
- #5: sign.sh parses the Azure token with jq instead of a sed regex.
- #7: nfpm.yaml notes it's rendered via envsubst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
martinsifra added a commit that referenced this pull request Aug 26, 2026
The big one (#2, upgraded): the diff sides' wire shape was WRONG in the
implementation and in the test fixtures that defined it. Verified against
connection (ConfigurationVersionResponse + ConfigurationDiffData OA
schemas, now recorded in the notes wire-truth table): each side is
{version, isDeleted, diff: {name, description, changeDescription,
isDisabled, configuration, rows}} -- content NESTED under diff,
version/deletion as side metadata. The classification and both take modes
now read the envelope; the flat-side code would have been dead on arrival
against the live API.

#1: resolve_conflict no longer takes branch_id -- it derives the branch
from the MR itself (branches.branchFromId), so the conflict-set guard and
the branch being written to can never disagree; a caller-supplied id could
point the REPLACing rebase at an unrelated dev branch the guard never
checked. A published/canceled MR (null branchFromId) is refused readably.

#3: take=theirs of a deleted side collapses to the delete resolution,
symmetric with ours ('production deleted it, dev changed it' is a live
conflict shape).
#4: deletion surfaces as top-level ours_deleted/theirs_deleted booleans on
get_config_diff (None = side never existed) -- it is side metadata, not a
content path.
#5: a 'both' row where the sides agree on the identical value carries
agreed: true -- agreement, not a conflict hotspot.
#2 (message half): a take side missing required envelope keys is reported
as a backend contract violation pointing at the resolved-body workaround,
not as caller error.
#6: wire ids compared via _same_id / int-coerced (find_merge_request_for_
branch, merge()'s was_active) -- a string-serialized branchFromId can no
longer silently defeat the post-merge cleanup.
#7: the feature pre-flight raises FeatureNotEnabledError carrying the new
ErrorCode.FEATURE_NOT_ENABLED (value matches the string SearchService
already emits; categorized 'configuration' like PAYG_NOT_AVAILABLE).
#8: ConfigError imported from ..errors like every other service; the
isDefault scan hoisted to services.base.find_default_branch_id and the
copies in config/sync/workspace services migrated (lib.py keeps its own
loop -- the SDK facade does not import the services layer); verify_token
is skipped when the server already serialized viewer (the polyfill's cost
dies with the polyfill); list --state validates against the closed
vocabulary instead of returning a silent count: 0 on a typo.
#9: test imports hoisted (no mid-file noqa), mocks spec'd at the L3 seam
(KeboolaClient + MergeRequests -- a renamed L3 method now fails the tests),
and regression tests added for every finding (82 tests total).

Doc drift: layer2/layer3 references updated to BRANCHES_MERGE_REQUESTS_
FEATURE, the layer3 open nit closed, tokens.py line ref fixed.

Review: tasks/pr-703-review.md (2026-08-27).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.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