scriptgini 1.4.0__tar.gz → 1.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {scriptgini-1.4.0 → scriptgini-1.5.2}/PKG-INFO +6 -4
  2. {scriptgini-1.4.0 → scriptgini-1.5.2}/README.md +5 -3
  3. scriptgini-1.5.2/app/__init__.py +3 -0
  4. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/main.py +46 -0
  5. scriptgini-1.5.2/app/models/script_revision.py +37 -0
  6. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/analytics.py +96 -0
  7. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/api_key.py +28 -1
  8. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/scripts.py +289 -15
  9. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/analytics.py +31 -0
  10. scriptgini-1.5.2/app/schemas/script_revision.py +48 -0
  11. {scriptgini-1.4.0 → scriptgini-1.5.2}/pyproject.toml +1 -1
  12. {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/PKG-INFO +6 -4
  13. {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/SOURCES.txt +4 -1
  14. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_auth.py +88 -19
  15. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_coverage.py +18 -3
  16. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint3_execution.py +1 -1
  17. scriptgini-1.5.2/tests/test_sprint6_coverage_lifecycle.py +502 -0
  18. scriptgini-1.4.0/app/__init__.py +0 -3
  19. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/__init__.py +0 -0
  20. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/prompts.py +0 -0
  21. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/agents/script_gini_agent.py +0 -0
  22. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/cache.py +0 -0
  23. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/celery_app.py +0 -0
  24. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/config.py +0 -0
  25. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/database.py +0 -0
  26. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/llm/__init__.py +0 -0
  27. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/llm/provider.py +0 -0
  28. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/__init__.py +0 -0
  29. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/api_key.py +0 -0
  30. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/bulk_job.py +0 -0
  31. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/execution_job.py +0 -0
  32. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/generated_script.py +0 -0
  33. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/membership.py +0 -0
  34. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/organization.py +0 -0
  35. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/project.py +0 -0
  36. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/script_run.py +0 -0
  37. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/test_case.py +0 -0
  38. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/models/user.py +0 -0
  39. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/__init__.py +0 -0
  40. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/auth.py +0 -0
  41. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/bulk_jobs.py +0 -0
  42. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/demo.py +0 -0
  43. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/execution.py +0 -0
  44. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/organizations.py +0 -0
  45. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/projects.py +0 -0
  46. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/reports.py +0 -0
  47. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/routers/test_cases.py +0 -0
  48. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/__init__.py +0 -0
  49. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/api_key.py +0 -0
  50. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/auth.py +0 -0
  51. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/bulk_job.py +0 -0
  52. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/execution.py +0 -0
  53. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/generated_script.py +0 -0
  54. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/membership.py +0 -0
  55. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/organization.py +0 -0
  56. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/project.py +0 -0
  57. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/reports.py +0 -0
  58. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/schemas/test_case.py +0 -0
  59. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/api_key.py +0 -0
  60. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/auth.py +0 -0
  61. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/auth_dependencies.py +0 -0
  62. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/git_export.py +0 -0
  63. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/services/rbac.py +0 -0
  64. {scriptgini-1.4.0 → scriptgini-1.5.2}/app/tasks.py +0 -0
  65. {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/dependency_links.txt +0 -0
  66. {scriptgini-1.4.0 → scriptgini-1.5.2}/scriptgini.egg-info/top_level.txt +0 -0
  67. {scriptgini-1.4.0 → scriptgini-1.5.2}/setup.cfg +0 -0
  68. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_api.py +0 -0
  69. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_infra_services_coverage.py +0 -0
  70. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint2_rbac.py +0 -0
  71. {scriptgini-1.4.0 → scriptgini-1.5.2}/tests/test_sprint5_reporting_analytics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptgini
3
- Version: 1.4.0
3
+ Version: 1.5.2
4
4
  Summary: Agentic AI system that converts functional test cases into automation test scripts.
5
5
  Author: ScriptGini Team
6
6
  License: Proprietary
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
18
18
 
19
- Current release: v1.3.1 (Sprint 4 hardening increment)
19
+ Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
20
20
 
21
21
  ---
22
22
 
@@ -39,8 +39,10 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
39
39
  - **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
40
40
  - **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
41
41
  - **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
42
+ - **Coverage analytics** — Module-level coverage and execution visibility for test cases
42
43
  - **Richer test case intake** — Import `.txt`, `.md`, `.json`, `.csv`, `.feature`, `.yml/.yaml`, and `.xlsx`
43
44
  - **Import preview mapping** — Preview parsed scenarios in the UI before creating a project workspace
45
+ - **Script lifecycle tracking** — Refactor, version history, diff, and rollback support
44
46
  - **REST API** — FastAPI with auto-generated Swagger UI
45
47
  - **Alembic migrations** — Safe, versioned schema management over SQLite
46
48
 
@@ -442,9 +444,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
442
444
  | **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
443
445
  | **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
444
446
  | **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
445
- | **Sprint 4** | Security & Hardening | 30-36pts | 🟡 In progress (isolation boundary + breakout tests pending) |
447
+ | **Sprint 4** | Security & Hardening | 30-36pts | Hardening increment completed (isolation + negative tests + audit controls) |
446
448
  | **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
447
- | **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
449
+ | **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
448
450
 
449
451
  ---
450
452
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Enterprise-grade Agentic AI system that converts functional test cases into high-quality, review-ready automation test scripts.**
4
4
 
5
- Current release: v1.3.1 (Sprint 4 hardening increment)
5
+ Current release: v1.5.2 (Sprint 6 hardening + API key audit/revocation patch)
6
6
 
7
7
  ---
8
8
 
@@ -25,8 +25,10 @@ ScriptGini is an AI-powered test automation engine built for Quality Engineering
25
25
  - **Hardened execution sandbox** — Script runs use isolated Python mode, static safety validation, and restricted environment variables
26
26
  - **Bulk job orchestration** — Project-level bulk generate and bulk run with pollable job status
27
27
  - **Run analytics dashboard** — Project-level pass/fail/timeout metrics and recent failure feed
28
+ - **Coverage analytics** — Module-level coverage and execution visibility for test cases
28
29
  - **Richer test case intake** — Import `.txt`, `.md`, `.json`, `.csv`, `.feature`, `.yml/.yaml`, and `.xlsx`
29
30
  - **Import preview mapping** — Preview parsed scenarios in the UI before creating a project workspace
31
+ - **Script lifecycle tracking** — Refactor, version history, diff, and rollback support
30
32
  - **REST API** — FastAPI with auto-generated Swagger UI
31
33
  - **Alembic migrations** — Safe, versioned schema management over SQLite
32
34
 
@@ -428,9 +430,9 @@ The project follows an **enterprise-grade development roadmap** with 6 sprints c
428
430
  | **Sprint 1** | IAM Core | 30-36pts | 🟡 Core delivered (auth hardening pending) |
429
431
  | **Sprint 2** | RBAC + Multi-Tenancy | 32-38pts | 🟡 Core delivered (RBAC hardening pending) |
430
432
  | **Sprint 3** | Durable Execution | 34-40pts | ✅ Completed (Redis + Celery queue foundation) |
431
- | **Sprint 4** | Security & Hardening | 30-36pts | 🟡 In progress (isolation boundary + breakout tests pending) |
433
+ | **Sprint 4** | Security & Hardening | 30-36pts | Hardening increment completed (isolation + negative tests + audit controls) |
432
434
  | **Sprint 5** | Reporting & Analytics | 28-34pts | ✅ Completed (Reports APIs, trends/flakiness, retention cleanup) |
433
- | **Sprint 6** | Advanced Features | 24-30pts | 🔲 Pending (Webhooks, defect sync, versioning) |
435
+ | **Sprint 6** | Coverage Analytics, Script Lifecycle, and DX | 24-30pts | Completed (coverage analytics, refactor/version history/diff/rollback, 100% quality gate) |
434
436
 
435
437
  ---
436
438
 
@@ -0,0 +1,3 @@
1
+ __version__ = "1.5.2"
2
+ __api_version__ = "v1.5.2"
3
+
@@ -10,6 +10,8 @@ from fastapi.responses import JSONResponse
10
10
  from fastapi.responses import FileResponse
11
11
  from fastapi.middleware.cors import CORSMiddleware
12
12
  from fastapi.staticfiles import StaticFiles
13
+ from fastapi.exceptions import RequestValidationError
14
+ from starlette.exceptions import HTTPException as StarletteHTTPException
13
15
 
14
16
  from app import __version__
15
17
  from app.config import settings
@@ -114,6 +116,50 @@ app.add_middleware(
114
116
  )
115
117
 
116
118
 
119
+ def _error_envelope(request: Request, code: str, message: str, context: dict | None = None, status_code: int = 500) -> JSONResponse:
120
+ request_id = getattr(request.state, "request_id", None) or request.headers.get("X-Request-ID", "unknown")
121
+ return JSONResponse(
122
+ status_code=status_code,
123
+ content={
124
+ "error": {
125
+ "code": code,
126
+ "message": message,
127
+ "context": context or {},
128
+ "request_id": request_id,
129
+ }
130
+ },
131
+ headers={"X-Request-ID": request_id},
132
+ )
133
+
134
+
135
+ @app.exception_handler(StarletteHTTPException)
136
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
137
+ code = {
138
+ 400: "bad_request",
139
+ 401: "unauthorized",
140
+ 403: "forbidden",
141
+ 404: "not_found",
142
+ 409: "conflict",
143
+ 422: "unprocessable_entity",
144
+ 429: "rate_limit_exceeded",
145
+ 500: "internal_server_error",
146
+ 503: "service_unavailable",
147
+ 504: "gateway_timeout",
148
+ }.get(exc.status_code, f"http_{exc.status_code}")
149
+ return _error_envelope(request, code, str(exc.detail), status_code=exc.status_code)
150
+
151
+
152
+ @app.exception_handler(RequestValidationError)
153
+ async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
154
+ return _error_envelope(
155
+ request,
156
+ "validation_error",
157
+ "Request validation failed",
158
+ context={"errors": exc.errors()},
159
+ status_code=422,
160
+ )
161
+
162
+
117
163
  @app.middleware("http")
118
164
  async def apply_security_controls(request: Request, call_next):
119
165
  request_id = request.headers.get("X-Request-ID") or str(uuid4())
@@ -0,0 +1,37 @@
1
+ import enum
2
+ from datetime import datetime, timezone
3
+
4
+ from sqlalchemy import String, Text, DateTime, ForeignKey, Enum as SAEnum, Integer
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from app.database import Base
8
+
9
+
10
+ class RevisionChangeType(str, enum.Enum):
11
+ generated = "generated"
12
+ refactored = "refactored"
13
+ rolled_back = "rolled_back"
14
+
15
+
16
+ class ScriptRevision(Base):
17
+ __tablename__ = "script_revisions"
18
+
19
+ id: Mapped[int] = mapped_column(primary_key=True, index=True)
20
+ script_id: Mapped[int] = mapped_column(
21
+ ForeignKey("generated_scripts.id", ondelete="CASCADE"), nullable=False, index=True
22
+ )
23
+ project_id: Mapped[int] = mapped_column(
24
+ ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
25
+ )
26
+ test_case_id: Mapped[int] = mapped_column(
27
+ ForeignKey("test_cases.id", ondelete="CASCADE"), nullable=False, index=True
28
+ )
29
+ revision_number: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
30
+ script_content: Mapped[str] = mapped_column(Text, nullable=False)
31
+ change_type: Mapped[RevisionChangeType] = mapped_column(
32
+ SAEnum(RevisionChangeType), default=RevisionChangeType.generated, nullable=False
33
+ )
34
+ change_summary: Mapped[str | None] = mapped_column(String(1024), nullable=True)
35
+ created_at: Mapped[datetime] = mapped_column(
36
+ DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
37
+ )
@@ -8,7 +8,11 @@ from app.database import get_db
8
8
  from app.models.project import Project
9
9
  from app.models.script_run import ScriptRun, ScriptRunStatus
10
10
  from app.models.test_case import TestCase
11
+ from app.models.generated_script import GeneratedScript
11
12
  from app.schemas.analytics import (
13
+ CoverageModuleItem,
14
+ CoverageResponse,
15
+ CoverageTestCaseItem,
12
16
  FlakinessItemResponse,
13
17
  FlakinessResponse,
14
18
  RecentFailureResponse,
@@ -196,3 +200,95 @@ def get_flakiness(
196
200
  min_runs=min_runs,
197
201
  items=items,
198
202
  )
203
+
204
+
205
+ @insights_router.get("/coverage", response_model=CoverageResponse)
206
+ def get_coverage(
207
+ project_id: int | None = None,
208
+ db: Session = Depends(get_db),
209
+ ):
210
+ """Map test cases to scripted/executed/passed state, grouped by project module (derived from test case title prefix)."""
211
+ if project_id is not None:
212
+ project = db.query(Project).filter(Project.id == project_id).first()
213
+ if not project:
214
+ raise HTTPException(status_code=404, detail="Project not found")
215
+
216
+ tc_query = db.query(TestCase)
217
+ if project_id is not None:
218
+ tc_query = tc_query.filter(TestCase.project_id == project_id)
219
+ test_cases = tc_query.all()
220
+
221
+ # Collect latest script per test_case_id
222
+ script_query = db.query(GeneratedScript).order_by(GeneratedScript.test_case_id, GeneratedScript.id.desc())
223
+ if project_id is not None:
224
+ script_query = script_query.filter(GeneratedScript.project_id == project_id)
225
+ scripted_by_tc: dict[int, GeneratedScript] = {}
226
+ for gs in script_query.all():
227
+ if gs.test_case_id not in scripted_by_tc:
228
+ scripted_by_tc[gs.test_case_id] = gs
229
+
230
+ # Collect latest run per test_case_id
231
+ run_query = db.query(ScriptRun).order_by(ScriptRun.test_case_id, ScriptRun.id.desc())
232
+ if project_id is not None:
233
+ run_query = run_query.filter(ScriptRun.project_id == project_id)
234
+ run_by_tc: dict[int, ScriptRun] = {}
235
+ for sr in run_query.all():
236
+ if sr.test_case_id not in run_by_tc:
237
+ run_by_tc[sr.test_case_id] = sr
238
+
239
+ # Group by module (first word of title or "General")
240
+ modules_map: dict[str, list[CoverageTestCaseItem]] = {}
241
+ for tc in test_cases:
242
+ parts = tc.title.split()
243
+ module = parts[0] if parts else "General"
244
+
245
+ has_script = tc.id in scripted_by_tc
246
+ has_run = tc.id in run_by_tc
247
+ last_run_success: bool | None = None
248
+ if has_run:
249
+ last_run_success = run_by_tc[tc.id].success
250
+
251
+ item = CoverageTestCaseItem(
252
+ test_case_id=tc.id,
253
+ title=tc.title,
254
+ format=tc.format.value,
255
+ has_script=has_script,
256
+ has_run=has_run,
257
+ last_run_success=last_run_success,
258
+ )
259
+ modules_map.setdefault(module, []).append(item)
260
+
261
+ module_items: list[CoverageModuleItem] = []
262
+ for module_name, cases in sorted(modules_map.items()):
263
+ total = len(cases)
264
+ scripted = sum(1 for c in cases if c.has_script)
265
+ executed = sum(1 for c in cases if c.has_run)
266
+ passed = sum(1 for c in cases if c.last_run_success is True)
267
+ module_items.append(
268
+ CoverageModuleItem(
269
+ module=module_name,
270
+ total_cases=total,
271
+ scripted_cases=scripted,
272
+ executed_cases=executed,
273
+ passed_cases=passed,
274
+ coverage_rate=round(scripted / total * 100, 2) if total else 0.0,
275
+ execution_rate=round(executed / total * 100, 2) if total else 0.0,
276
+ test_cases=cases,
277
+ )
278
+ )
279
+
280
+ total_cases = len(test_cases)
281
+ scripted_cases = sum(1 for tc in test_cases if tc.id in scripted_by_tc)
282
+ executed_cases = sum(1 for tc in test_cases if tc.id in run_by_tc)
283
+ passed_cases = sum(1 for tc in test_cases if run_by_tc.get(tc.id) and run_by_tc[tc.id].success)
284
+
285
+ return CoverageResponse(
286
+ project_id=project_id,
287
+ total_cases=total_cases,
288
+ scripted_cases=scripted_cases,
289
+ executed_cases=executed_cases,
290
+ passed_cases=passed_cases,
291
+ overall_coverage_rate=round(scripted_cases / total_cases * 100, 2) if total_cases else 0.0,
292
+ overall_execution_rate=round(executed_cases / total_cases * 100, 2) if total_cases else 0.0,
293
+ modules=module_items,
294
+ )
@@ -1,6 +1,9 @@
1
+ import logging
2
+
1
3
  from fastapi import APIRouter, Depends, HTTPException, status
2
4
  from sqlalchemy.orm import Session
3
5
 
6
+ from app.config import settings
4
7
  from app.database import get_db
5
8
  from app.services import api_key as api_key_service
6
9
  from app.services.auth_dependencies import get_current_user
@@ -13,6 +16,26 @@ from app.schemas.api_key import (
13
16
  from app.models.api_key import APIKey
14
17
 
15
18
  router = APIRouter(prefix="/auth/api-keys", tags=["auth"])
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _audit_api_key_event(
23
+ event: str,
24
+ actor_user_id: int,
25
+ api_key_id: int | None,
26
+ api_key_prefix: str | None,
27
+ result: str,
28
+ ) -> None:
29
+ if not settings.SECURITY_AUDIT_LOG_ENABLED:
30
+ return
31
+ logger.info(
32
+ "security_audit event=%s actor_user_id=%s api_key_id=%s api_key_prefix=%s result=%s",
33
+ event,
34
+ actor_user_id,
35
+ api_key_id,
36
+ api_key_prefix,
37
+ result,
38
+ )
16
39
 
17
40
 
18
41
  @router.post("", response_model=APIKeyRevealResponse, status_code=status.HTTP_201_CREATED)
@@ -36,6 +59,7 @@ def create_api_key(
36
59
  request.scopes,
37
60
  request.expires_at,
38
61
  )
62
+ _audit_api_key_event("api_key_created", current_user.id, api_key.id, api_key.prefix, "success")
39
63
 
40
64
  return APIKeyRevealResponse(
41
65
  id=api_key.id,
@@ -89,6 +113,7 @@ def update_api_key(
89
113
  existing_key = api_key_service.get_api_key_by_id(db, key_id)
90
114
 
91
115
  if not existing_key:
116
+ _audit_api_key_event("api_key_revoked", current_user.id, key_id, None, "not_found")
92
117
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API key not found")
93
118
 
94
119
  # Check that the key belongs to the current user
@@ -120,7 +145,9 @@ def delete_api_key(
120
145
 
121
146
  # Check that the key belongs to the current user
122
147
  if existing_key.user_id != current_user.id:
148
+ _audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "forbidden")
123
149
  raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to delete this API key")
124
150
 
125
- api_key_service.delete_api_key(db, key_id)
151
+ api_key_service.revoke_api_key(db, key_id)
152
+ _audit_api_key_event("api_key_revoked", current_user.id, existing_key.id, existing_key.prefix, "success")
126
153
  return None