From 6da0e8966bf1132f0a9b0720795a842091764b2e Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 26 Apr 2026 00:29:07 +0200 Subject: [PATCH] repository CRUD --- README.md | 2 ++ src/repo_registry/core/service.py | 18 +++++++++++ src/repo_registry/storage/sqlite.py | 46 +++++++++++++++++++++++++++++ src/repo_registry/web_api/app.py | 46 +++++++++++++++++++++++++++++ tests/test_registry_service.py | 31 +++++++++++++++++++ tests/test_web_api.py | 17 ++++++++++- 6 files changed, 159 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87c2b0d..4097d2d 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ The v0.1 API covers the main registration, analysis, review, search, and inspect GET /repos POST /repos GET /repos/{id} +PATCH /repos/{id} +DELETE /repos/{id} POST /repos/{id}/analysis-runs GET /repos/{id}/analysis-runs GET /repos/{id}/analysis-runs/{run_id} diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index fa794d9..b56eefb 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -62,6 +62,24 @@ class RegistryService: def get_repository(self, repository_id: int) -> Repository: return self.store.get_repository(repository_id) + def update_repository( + self, + repository_id: int, + *, + name: str | None = None, + description: str | None = None, + branch: str | None = None, + ) -> Repository: + return self.store.update_repository( + repository_id, + name=name, + description=description, + branch=branch, + ) + + def delete_repository(self, repository_id: int) -> None: + self.store.delete_repository(repository_id) + def analyze_repository( self, repository_id: int, diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py index 5120a5f..1ea9f40 100644 --- a/src/repo_registry/storage/sqlite.py +++ b/src/repo_registry/storage/sqlite.py @@ -82,6 +82,52 @@ class RegistryStore: repository_id = int(cursor.lastrowid) return self.get_repository(repository_id) + def update_repository( + self, + repository_id: int, + *, + name: str | None = None, + description: str | None = None, + branch: str | None = None, + ) -> Repository: + self.get_repository(repository_id) + assignments: list[str] = [] + values: list[str | int | None] = [] + if name is not None: + assignments.append("name = ?") + values.append(name) + if description is not None: + assignments.append("description = ?") + values.append(description) + if branch is not None: + assignments.append("branch = ?") + values.append(branch) + if not assignments: + return self.get_repository(repository_id) + + values.append(repository_id) + with self.connect() as connection: + cursor = connection.execute( + f""" + UPDATE repositories + SET {", ".join(assignments)}, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, + values, + ) + if cursor.rowcount == 0: + raise NotFoundError(f"repository {repository_id} was not found") + return self.get_repository(repository_id) + + def delete_repository(self, repository_id: int) -> None: + with self.connect() as connection: + cursor = connection.execute( + "DELETE FROM repositories WHERE id = ?", + (repository_id,), + ) + if cursor.rowcount == 0: + raise NotFoundError(f"repository {repository_id} was not found") + def update_repository_status(self, repository_id: int, status: str) -> None: with self.connect() as connection: cursor = connection.execute( diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index f49563e..93663cd 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -48,6 +48,24 @@ class RepositoryCreate(BaseModel): } +class RepositoryUpdate(BaseModel): + name: str | None = None + description: str | None = None + branch: str | None = None + + model_config = { + "json_schema_extra": { + "examples": [ + { + "name": "Renamed Repository", + "description": "Updated curator-facing summary.", + "branch": "main", + } + ] + } + } + + class AbilityCreate(BaseModel): name: str description: str = "" @@ -305,6 +323,34 @@ def get_repository( raise HTTPException(status_code=404, detail=str(exc)) from exc +@app.patch("/repos/{repository_id}") +def update_repository( + repository_id: int, + payload: RepositoryUpdate, + service: RegistryService = Depends(get_service), +) -> dict[str, object]: + try: + return asdict( + service.update_repository( + repository_id, + **payload.model_dump(exclude_unset=True), + ) + ) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.delete("/repos/{repository_id}", status_code=204) +def delete_repository( + repository_id: int, + service: RegistryService = Depends(get_service), +) -> None: + try: + service.delete_repository(repository_id) + except NotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/repos/{repository_id}/analysis-runs", status_code=201) def create_analysis_run( repository_id: int, diff --git a/tests/test_registry_service.py b/tests/test_registry_service.py index c0b405a..8b0ef2a 100644 --- a/tests/test_registry_service.py +++ b/tests/test_registry_service.py @@ -61,6 +61,37 @@ def test_manual_registry_builds_ability_map(tmp_path): assert capability.evidence[0].strength == "strong" +def test_repository_update_and_delete(tmp_path): + service = make_service(tmp_path) + repository = service.register_repository( + name="Original", + url="https://example.com/original.git", + description="Original description.", + ) + ability_id = service.add_ability(repository.id, name="Original Ability") + + updated = service.update_repository( + repository.id, + name="Updated", + description="Updated description.", + branch="develop", + ) + + assert updated.name == "Updated" + assert updated.description == "Updated description." + assert updated.branch == "develop" + assert service.ability_map(repository.id).abilities[0].id == ability_id + + service.delete_repository(repository.id) + + try: + service.get_repository(repository.id) + except NotFoundError as exc: + assert "repository" in str(exc) + else: + raise AssertionError("expected a NotFoundError") + + def test_search_matches_approved_abilities_and_capabilities(tmp_path): service = make_service(tmp_path) repository = service.register_repository( diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 6d6202c..08fc9e2 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -24,6 +24,16 @@ def test_api_manual_registry_loop(tmp_path): assert repository_response.status_code == 201 repository_id = repository_response.json()["id"] + update_response = client.patch( + f"/repos/{repository_id}", + json={ + "name": "MailRouter Updated", + "description": "Updated mail routing summary.", + }, + ) + assert update_response.status_code == 200 + assert update_response.json()["name"] == "MailRouter Updated" + ability_response = client.post( f"/repos/{repository_id}/abilities", json={ @@ -60,7 +70,7 @@ def test_api_manual_registry_loop(tmp_path): map_response = client.get(f"/repos/{repository_id}/ability-map") assert map_response.status_code == 200 ability_map = map_response.json() - assert ability_map["repository"]["name"] == "MailRouter" + assert ability_map["repository"]["name"] == "MailRouter Updated" assert ability_map["abilities"][0]["capabilities"][0]["name"] == ( "Classify Incoming Email" ) @@ -68,6 +78,11 @@ def test_api_manual_registry_loop(tmp_path): search_response = client.get("/search", params={"q": "email"}) assert search_response.status_code == 200 assert search_response.json() + + delete_response = client.delete(f"/repos/{repository_id}") + assert delete_response.status_code == 204 + missing_response = client.get(f"/repos/{repository_id}") + assert missing_response.status_code == 404 finally: app.dependency_overrides.clear()