sediment-api 0.7.0__tar.gz → 0.7.1__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.
- {sediment_api-0.7.0 → sediment_api-0.7.1}/PKG-INFO +6 -6
- {sediment_api-0.7.0 → sediment_api-0.7.1}/pyproject.toml +6 -6
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/__init__.py +1 -1
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/deps.py +1 -2
- sediment_api-0.7.1/sediment_api/ingest_envelopes.py +55 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/ci_vendor.py +3 -4
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/compute.py +3 -4
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/forge.py +21 -22
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/gateway.py +4 -3
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/otlp.py +3 -3
- sediment_api-0.7.1/tests/test_ingest_envelopes.py +139 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_v1.py +1 -1
- {sediment_api-0.7.0 → sediment_api-0.7.1}/.gitignore +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/LICENSE +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/config.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/database.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/main.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/mirror_gc.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/__init__.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/abandonment_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/attribution_share_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/compute_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/dataset_diagnostics.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/label_confidence_inspection.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/lifecycle_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/merge_retention_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/model_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/precision_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/recovery_yield_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/__init__.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/reports.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/routers/v1.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/services/__init__.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/services/operational_reports.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/worker.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/workers.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/conftest.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_attribution_share_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_baseline_gap_repro.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_bug_repro_offset.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_capture_record_isolation.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_ci_vendor.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_compute_ingest.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_compute_report_cli.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_config.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_context_discovery_authority.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_context_discovery_query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_context_discovery_workers.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_context_evidence_query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_context_query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_credential_authorities.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_database_lifecycle.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_database_outage.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_database_role_startup.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_dataset_diagnostics_report.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_e2e_smoke.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_evidence_query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_evidence_worker_imports.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_evidence_workers.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_gateway_capture_receipt.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_gateway_identity.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_lifecycle_report_cli.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_merge_retention_cli.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_one_shot_database.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_operational_reports.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_operational_reports_service.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_postgres_gateway_tracer.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_push_mirror.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_query.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_report_attachment_scope.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_report_routes.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_repository_identity_ingest.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_repository_identity_model_service.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_repository_identity_queries.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_retrieval_authority.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_worker_processes.py +0 -0
- {sediment_api-0.7.0 → sediment_api-0.7.1}/tests/test_worker_routes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sediment-api
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Sediment ingest API: facts in through three doors
|
|
5
5
|
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
6
|
Project-URL: Documentation, https://github.com/sediment-ai/sediment/tree/main/docs
|
|
@@ -9,12 +9,12 @@ Project-URL: Changelog, https://github.com/sediment-ai/sediment/blob/main/CHANGE
|
|
|
9
9
|
License-Expression: AGPL-3.0-or-later
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
|
-
Requires-Dist: anyio>=4.
|
|
12
|
+
Requires-Dist: anyio>=4.16.0
|
|
13
13
|
Requires-Dist: fastapi>=0.142.2
|
|
14
14
|
Requires-Dist: httpx>=0.27
|
|
15
15
|
Requires-Dist: pydantic-settings>=2.16.2
|
|
16
|
-
Requires-Dist: sediment-capture==0.7.
|
|
17
|
-
Requires-Dist: sediment-core==0.7.
|
|
18
|
-
Requires-Dist: sediment-derive==0.7.
|
|
19
|
-
Requires-Dist: sediment-export==0.7.
|
|
16
|
+
Requires-Dist: sediment-capture==0.7.1
|
|
17
|
+
Requires-Dist: sediment-core==0.7.1
|
|
18
|
+
Requires-Dist: sediment-derive==0.7.1
|
|
19
|
+
Requires-Dist: sediment-export==0.7.1
|
|
20
20
|
Requires-Dist: uvicorn>=0.54.0
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sediment-api"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "Sediment ingest API: facts in through three doors"
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
license = "AGPL-3.0-or-later"
|
|
7
7
|
license-files = ["LICENSE"]
|
|
8
8
|
dependencies = [
|
|
9
9
|
# Security floor for HTTPX/Starlette, including installations from wheels.
|
|
10
|
-
"anyio>=4.
|
|
10
|
+
"anyio>=4.16.0",
|
|
11
11
|
"fastapi>=0.142.2",
|
|
12
12
|
"httpx>=0.27",
|
|
13
13
|
"uvicorn>=0.54.0",
|
|
14
14
|
"pydantic-settings>=2.16.2",
|
|
15
|
-
"sediment-core==0.7.
|
|
16
|
-
"sediment-capture==0.7.
|
|
17
|
-
"sediment-derive==0.7.
|
|
18
|
-
"sediment-export==0.7.
|
|
15
|
+
"sediment-core==0.7.1",
|
|
16
|
+
"sediment-capture==0.7.1",
|
|
17
|
+
"sediment-derive==0.7.1",
|
|
18
|
+
"sediment-export==0.7.1",
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[project.urls]
|
|
@@ -14,8 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import json
|
|
15
15
|
import secrets
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Literal
|
|
18
|
-
from typing import Any
|
|
17
|
+
from typing import Any, Literal
|
|
19
18
|
|
|
20
19
|
from fastapi import Depends, Header, HTTPException, Request
|
|
21
20
|
from sediment_capture import verify_signature
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Typed response envelopes for the ingest doors (AGENTS.md §API Conventions).
|
|
3
|
+
|
|
4
|
+
Routers return these instead of bare dicts so the generated OpenAPI names
|
|
5
|
+
each outcome. The wire bodies stay exactly the documented shapes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Annotated, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
_CLOSED = ConfigDict(extra="forbid")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class IngestReceipt:
|
|
20
|
+
"""The retained Fact for one single-Fact ingest request."""
|
|
21
|
+
|
|
22
|
+
__pydantic_config__ = _CLOSED
|
|
23
|
+
|
|
24
|
+
fact_id: Annotated[
|
|
25
|
+
str,
|
|
26
|
+
Field(description="ID of the retained Fact, including on redelivery"),
|
|
27
|
+
]
|
|
28
|
+
stored: Annotated[
|
|
29
|
+
bool,
|
|
30
|
+
Field(
|
|
31
|
+
description=(
|
|
32
|
+
"False when a redelivery collapsed on a UNIQUE index (ADR "
|
|
33
|
+
"0003); success, not an error"
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class IngestSkipped:
|
|
41
|
+
"""A declined delivery that the sender shouldn't retry."""
|
|
42
|
+
|
|
43
|
+
__pydantic_config__ = _CLOSED
|
|
44
|
+
|
|
45
|
+
skipped: Literal[True]
|
|
46
|
+
reason: Annotated[
|
|
47
|
+
str, Field(description="Why the route stored nothing for this delivery")
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class OTLPExportLogsResponse:
|
|
53
|
+
"""The empty OTLP/HTTP JSON ExportLogsServiceResponse: full success."""
|
|
54
|
+
|
|
55
|
+
__pydantic_config__ = _CLOSED
|
|
@@ -16,8 +16,6 @@ collapses redeliveries; ``stored: false`` means success, not an error.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from typing import Any
|
|
20
|
-
|
|
21
19
|
from fastapi import APIRouter, Depends
|
|
22
20
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
23
21
|
from sediment_core import (
|
|
@@ -40,6 +38,7 @@ from sediment_core import (
|
|
|
40
38
|
|
|
41
39
|
from ..config import settings
|
|
42
40
|
from ..deps import get_store, verify_ingest_token
|
|
41
|
+
from ..ingest_envelopes import IngestReceipt
|
|
43
42
|
|
|
44
43
|
router = APIRouter(tags=["ci-vendor"])
|
|
45
44
|
|
|
@@ -121,7 +120,7 @@ def ingest_ci_vendor(
|
|
|
121
120
|
body: VendorCIRequest,
|
|
122
121
|
_: None = Depends(verify_ingest_token),
|
|
123
122
|
store: FactStore = Depends(get_store),
|
|
124
|
-
) ->
|
|
123
|
+
) -> IngestReceipt:
|
|
125
124
|
outcome = CIOutcome(
|
|
126
125
|
org_id=settings.org_id,
|
|
127
126
|
provider=body.provider,
|
|
@@ -146,4 +145,4 @@ def ingest_ci_vendor(
|
|
|
146
145
|
source_event_id=body.source_event_id,
|
|
147
146
|
)
|
|
148
147
|
receipt = store.store_ci_outcome_receipt(outcome)
|
|
149
|
-
return
|
|
148
|
+
return IngestReceipt(fact_id=receipt.fact_id, stored=receipt.stored)
|
|
@@ -17,8 +17,6 @@ not an error.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import Any
|
|
21
|
-
|
|
22
20
|
from fastapi import APIRouter, Depends
|
|
23
21
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
24
22
|
from sediment_core import (
|
|
@@ -31,6 +29,7 @@ from sediment_core import (
|
|
|
31
29
|
|
|
32
30
|
from ..config import settings
|
|
33
31
|
from ..deps import get_store, verify_ingest_token
|
|
32
|
+
from ..ingest_envelopes import IngestReceipt
|
|
34
33
|
|
|
35
34
|
router = APIRouter(tags=["compute"])
|
|
36
35
|
|
|
@@ -89,7 +88,7 @@ def ingest_compute_allocation(
|
|
|
89
88
|
body: ComputeAllocationRequest,
|
|
90
89
|
_: None = Depends(verify_ingest_token),
|
|
91
90
|
store: FactStore = Depends(get_store),
|
|
92
|
-
) ->
|
|
91
|
+
) -> IngestReceipt:
|
|
93
92
|
allocation = ComputeAllocation(
|
|
94
93
|
org_id=settings.org_id,
|
|
95
94
|
session_id=body.session_id,
|
|
@@ -99,4 +98,4 @@ def ingest_compute_allocation(
|
|
|
99
98
|
occurred_at=body.occurred_at,
|
|
100
99
|
)
|
|
101
100
|
receipt = store.store_compute_allocation_receipt(allocation)
|
|
102
|
-
return
|
|
101
|
+
return IngestReceipt(fact_id=receipt.fact_id, stored=receipt.stored)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""
|
|
3
3
|
GitHub webhook ingest routes, mounted under /ingest/github in main.py.
|
|
4
4
|
|
|
5
|
-
One module for
|
|
6
|
-
which parser/store pair the payload walks through. The signature is
|
|
5
|
+
One module for all four routes: they share the HMAC intake and differ only
|
|
6
|
+
in which parser/store pair the payload walks through. The signature is
|
|
7
7
|
verified BEFORE the event type is examined: a wrong-secret delivery must
|
|
8
8
|
401 even for event types a route skips, or GitHub's setup ping would
|
|
9
9
|
green-light a misconfigured webhook. The push route stores the Push fact,
|
|
@@ -19,7 +19,6 @@ from __future__ import annotations
|
|
|
19
19
|
import logging
|
|
20
20
|
from collections import Counter
|
|
21
21
|
from datetime import UTC, datetime
|
|
22
|
-
from typing import Any
|
|
23
22
|
|
|
24
23
|
from fastapi import APIRouter, BackgroundTasks, Depends, Header, Request
|
|
25
24
|
from sediment_capture import (
|
|
@@ -53,6 +52,7 @@ from starlette.concurrency import run_in_threadpool
|
|
|
53
52
|
|
|
54
53
|
from ..config import settings
|
|
55
54
|
from ..deps import get_store, read_verified_webhook
|
|
55
|
+
from ..ingest_envelopes import IngestReceipt, IngestSkipped
|
|
56
56
|
|
|
57
57
|
logger = logging.getLogger("sediment.api.forge")
|
|
58
58
|
router = APIRouter(tags=["forge"])
|
|
@@ -295,15 +295,15 @@ async def ingest_push(
|
|
|
295
295
|
x_hub_signature_256: str | None = Header(None),
|
|
296
296
|
x_github_event: str = Header(...),
|
|
297
297
|
store: FactStore = Depends(get_store),
|
|
298
|
-
) ->
|
|
298
|
+
) -> IngestReceipt | IngestSkipped:
|
|
299
299
|
payload = await read_verified_webhook(request, x_hub_signature_256)
|
|
300
300
|
# GitHub delivers every subscribed event (incl. ping) to the one URL;
|
|
301
301
|
# the X-GitHub-Event header is the discriminator.
|
|
302
302
|
if x_github_event != "push":
|
|
303
|
-
return
|
|
303
|
+
return IngestSkipped(skipped=True, reason="not a push event")
|
|
304
304
|
push = parse_push(payload, org_id=settings.org_id, github_host=settings.github_host)
|
|
305
305
|
if push is None:
|
|
306
|
-
return
|
|
306
|
+
return IngestSkipped(skipped=True, reason="not a storable push")
|
|
307
307
|
receipt = await run_in_threadpool(store.store_push_receipt, push)
|
|
308
308
|
fetch_repo, fetch_clone_url = push.repo, push.clone_url
|
|
309
309
|
push = receipt.fact
|
|
@@ -325,12 +325,11 @@ async def ingest_push(
|
|
|
325
325
|
push, fetch_repo=fetch_repo, fetch_clone_url=fetch_clone_url
|
|
326
326
|
)
|
|
327
327
|
background.add_task(request.app.state.workers.wait_background, task)
|
|
328
|
-
return
|
|
328
|
+
return IngestReceipt(fact_id=push.push_id, stored=stored)
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
@router.post(
|
|
332
332
|
"/repository",
|
|
333
|
-
response_model=dict[str, Any],
|
|
334
333
|
responses={
|
|
335
334
|
503: {
|
|
336
335
|
"description": "Mirror work capacity, storage, or execution budget exceeded."
|
|
@@ -343,11 +342,11 @@ async def ingest_repository(
|
|
|
343
342
|
x_github_event: str = Header(...),
|
|
344
343
|
x_github_delivery: str | None = Header(None),
|
|
345
344
|
store: FactStore = Depends(get_store),
|
|
346
|
-
) ->
|
|
345
|
+
) -> IngestReceipt | IngestSkipped:
|
|
347
346
|
"""Store an immutable rename receipt independently of mirror availability."""
|
|
348
347
|
payload = await read_verified_webhook(request, x_hub_signature_256)
|
|
349
348
|
if x_github_event != "repository":
|
|
350
|
-
return
|
|
349
|
+
return IngestSkipped(skipped=True, reason="not a repository event")
|
|
351
350
|
rename, reason = parse_repository_rename(
|
|
352
351
|
payload,
|
|
353
352
|
org_id=settings.org_id,
|
|
@@ -356,12 +355,12 @@ async def ingest_repository(
|
|
|
356
355
|
)
|
|
357
356
|
if rename is None:
|
|
358
357
|
assert reason is not None
|
|
359
|
-
return
|
|
358
|
+
return IngestSkipped(skipped=True, reason=reason.value)
|
|
360
359
|
receipt = await run_in_threadpool(store.store_repository_rename_receipt, rename)
|
|
361
360
|
logger.info(
|
|
362
361
|
"repository_rename_stored fact_id=%s stored=%s", receipt.fact_id, receipt.stored
|
|
363
362
|
)
|
|
364
|
-
return
|
|
363
|
+
return IngestReceipt(fact_id=receipt.fact_id, stored=receipt.stored)
|
|
365
364
|
|
|
366
365
|
|
|
367
366
|
@router.post("/ci")
|
|
@@ -371,10 +370,10 @@ async def ingest_ci(
|
|
|
371
370
|
x_github_event: str = Header(...),
|
|
372
371
|
x_github_delivery: str | None = Header(None),
|
|
373
372
|
store: FactStore = Depends(get_store),
|
|
374
|
-
) ->
|
|
373
|
+
) -> IngestReceipt | IngestSkipped:
|
|
375
374
|
payload = await read_verified_webhook(request, x_hub_signature_256)
|
|
376
375
|
if x_github_event != "workflow_run":
|
|
377
|
-
return
|
|
376
|
+
return IngestSkipped(skipped=True, reason="not a workflow_run event")
|
|
378
377
|
outcome = parse_workflow_run(
|
|
379
378
|
payload,
|
|
380
379
|
org_id=settings.org_id,
|
|
@@ -382,7 +381,7 @@ async def ingest_ci(
|
|
|
382
381
|
github_host=settings.github_host,
|
|
383
382
|
)
|
|
384
383
|
if outcome is None:
|
|
385
|
-
return
|
|
384
|
+
return IngestSkipped(skipped=True, reason="not a completed workflow_run")
|
|
386
385
|
receipt = await run_in_threadpool(store.store_ci_outcome_receipt, outcome)
|
|
387
386
|
outcome = receipt.fact
|
|
388
387
|
stored = receipt.stored
|
|
@@ -393,7 +392,7 @@ async def ingest_ci(
|
|
|
393
392
|
outcome.repo,
|
|
394
393
|
outcome.result,
|
|
395
394
|
)
|
|
396
|
-
return
|
|
395
|
+
return IngestReceipt(fact_id=outcome.outcome_id, stored=stored)
|
|
397
396
|
|
|
398
397
|
|
|
399
398
|
@router.post("/pull-request")
|
|
@@ -403,10 +402,10 @@ async def ingest_pull_request(
|
|
|
403
402
|
x_github_event: str = Header(...),
|
|
404
403
|
x_github_delivery: str | None = Header(None),
|
|
405
404
|
store: FactStore = Depends(get_store),
|
|
406
|
-
) ->
|
|
405
|
+
) -> IngestReceipt | IngestSkipped:
|
|
407
406
|
payload = await read_verified_webhook(request, x_hub_signature_256)
|
|
408
407
|
if x_github_event != "pull_request":
|
|
409
|
-
return
|
|
408
|
+
return IngestSkipped(skipped=True, reason="not a pull_request event")
|
|
410
409
|
if payload.get("action") == "closed":
|
|
411
410
|
merge = parse_pull_request_merge(
|
|
412
411
|
payload,
|
|
@@ -415,7 +414,7 @@ async def ingest_pull_request(
|
|
|
415
414
|
github_host=settings.github_host,
|
|
416
415
|
)
|
|
417
416
|
if merge is None:
|
|
418
|
-
return
|
|
417
|
+
return IngestSkipped(skipped=True, reason="not a merged pull_request")
|
|
419
418
|
receipt = await run_in_threadpool(store.store_pull_request_merge_receipt, merge)
|
|
420
419
|
merge = receipt.fact
|
|
421
420
|
stored = receipt.stored
|
|
@@ -426,7 +425,7 @@ async def ingest_pull_request(
|
|
|
426
425
|
merge.repo,
|
|
427
426
|
merge.pr_number,
|
|
428
427
|
)
|
|
429
|
-
return
|
|
428
|
+
return IngestReceipt(fact_id=merge.merge_id, stored=stored)
|
|
430
429
|
|
|
431
430
|
revision, skip_reason = parse_pull_request_revision(
|
|
432
431
|
payload,
|
|
@@ -436,7 +435,7 @@ async def ingest_pull_request(
|
|
|
436
435
|
)
|
|
437
436
|
if revision is None:
|
|
438
437
|
assert skip_reason is not None
|
|
439
|
-
return
|
|
438
|
+
return IngestSkipped(skipped=True, reason=skip_reason.value)
|
|
440
439
|
receipt = await run_in_threadpool(
|
|
441
440
|
store.store_pull_request_revision_receipt, revision
|
|
442
441
|
)
|
|
@@ -449,4 +448,4 @@ async def ingest_pull_request(
|
|
|
449
448
|
revision.repo,
|
|
450
449
|
revision.pr_number,
|
|
451
450
|
)
|
|
452
|
-
return
|
|
451
|
+
return IngestReceipt(fact_id=revision.revision_id, stored=stored)
|
|
@@ -20,6 +20,7 @@ from sediment_core.models import AwareDatetime
|
|
|
20
20
|
|
|
21
21
|
from ..config import settings
|
|
22
22
|
from ..deps import get_store, verify_ingest_token
|
|
23
|
+
from ..ingest_envelopes import IngestReceipt, IngestSkipped
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger("sediment.api.gateway")
|
|
25
26
|
router = APIRouter(tags=["gateway"])
|
|
@@ -57,7 +58,7 @@ def ingest_gateway(
|
|
|
57
58
|
body: GatewayIngestRequest,
|
|
58
59
|
_: None = Depends(verify_ingest_token),
|
|
59
60
|
store: FactStore = Depends(get_store),
|
|
60
|
-
) ->
|
|
61
|
+
) -> IngestReceipt | IngestSkipped:
|
|
61
62
|
adapter = ADAPTERS.get(body.provider)
|
|
62
63
|
if adapter is None:
|
|
63
64
|
raise HTTPException(
|
|
@@ -85,7 +86,7 @@ def ingest_gateway(
|
|
|
85
86
|
body.provider,
|
|
86
87
|
body.payload.get("litellm_call_id"),
|
|
87
88
|
)
|
|
88
|
-
return
|
|
89
|
+
return IngestSkipped(skipped=True, reason="no_session")
|
|
89
90
|
|
|
90
91
|
try:
|
|
91
92
|
inference_call = adapter.normalize(
|
|
@@ -134,4 +135,4 @@ def ingest_gateway(
|
|
|
134
135
|
receipt.fact_id,
|
|
135
136
|
receipt.stored,
|
|
136
137
|
)
|
|
137
|
-
return
|
|
138
|
+
return IngestReceipt(fact_id=receipt.fact_id, stored=receipt.stored)
|
|
@@ -12,7 +12,6 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
import logging
|
|
15
|
-
from typing import Any
|
|
16
15
|
|
|
17
16
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
18
17
|
from sediment_capture import parse_otlp_logs
|
|
@@ -21,6 +20,7 @@ from starlette.concurrency import run_in_threadpool
|
|
|
21
20
|
|
|
22
21
|
from ..config import settings
|
|
23
22
|
from ..deps import get_store, verify_ingest_token
|
|
23
|
+
from ..ingest_envelopes import OTLPExportLogsResponse
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger("sediment.api.otlp")
|
|
26
26
|
router = APIRouter(tags=["otlp"])
|
|
@@ -31,7 +31,7 @@ async def ingest_otlp_logs(
|
|
|
31
31
|
request: Request,
|
|
32
32
|
_: None = Depends(verify_ingest_token),
|
|
33
33
|
store: FactStore = Depends(get_store),
|
|
34
|
-
) ->
|
|
34
|
+
) -> OTLPExportLogsResponse:
|
|
35
35
|
try:
|
|
36
36
|
payload = await request.json()
|
|
37
37
|
except HTTPException:
|
|
@@ -98,7 +98,7 @@ async def ingest_otlp_logs(
|
|
|
98
98
|
},
|
|
99
99
|
)
|
|
100
100
|
# Empty ExportLogsServiceResponse = full success (OTLP/HTTP JSON).
|
|
101
|
-
return
|
|
101
|
+
return OTLPExportLogsResponse()
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def _store_translated_facts(
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Ingest response envelopes: the committed OpenAPI names each outcome, and
|
|
3
|
+
representative stored, duplicate, and skipped bodies validate against it
|
|
4
|
+
while keeping their exact wire shapes (AGENTS.md §API Conventions)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import pytest
|
|
14
|
+
import yaml
|
|
15
|
+
from fastapi.testclient import TestClient
|
|
16
|
+
from jsonschema import Draft202012Validator
|
|
17
|
+
from sediment_capture import sign_payload
|
|
18
|
+
|
|
19
|
+
from sediment_api.config import settings
|
|
20
|
+
|
|
21
|
+
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
22
|
+
FIXTURES = REPO_ROOT / "packages/capture/tests/fixtures"
|
|
23
|
+
SPEC = yaml.safe_load((REPO_ROOT / "openapi.yaml").read_text(encoding="utf-8"))
|
|
24
|
+
RECEIPT = "#/components/schemas/IngestReceipt"
|
|
25
|
+
SKIPPED = "#/components/schemas/IngestSkipped"
|
|
26
|
+
OTLP = "#/components/schemas/OTLPExportLogsResponse"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ok_schema(path: str) -> dict[str, Any]:
|
|
30
|
+
response = SPEC["paths"][path]["post"]["responses"]["200"]
|
|
31
|
+
return response["content"]["application/json"]["schema"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _assert_matches_spec(path: str, response: httpx.Response) -> None:
|
|
35
|
+
assert response.status_code == 200
|
|
36
|
+
schema = {**_ok_schema(path), "components": SPEC["components"]}
|
|
37
|
+
Draft202012Validator(schema).validate(response.json())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
("path", "refs"),
|
|
42
|
+
[
|
|
43
|
+
("/ingest/gateway", [RECEIPT, SKIPPED]),
|
|
44
|
+
("/ingest/github/push", [RECEIPT, SKIPPED]),
|
|
45
|
+
("/ingest/github/repository", [RECEIPT, SKIPPED]),
|
|
46
|
+
("/ingest/github/ci", [RECEIPT, SKIPPED]),
|
|
47
|
+
("/ingest/github/pull-request", [RECEIPT, SKIPPED]),
|
|
48
|
+
("/ingest/ci", [RECEIPT]),
|
|
49
|
+
("/ingest/compute", [RECEIPT]),
|
|
50
|
+
("/v1/logs", [OTLP]),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_each_ingest_route_names_its_envelopes(path: str, refs: list[str]) -> None:
|
|
54
|
+
schema = _ok_schema(path)
|
|
55
|
+
options = schema.get("anyOf", [schema])
|
|
56
|
+
assert [option["$ref"] for option in options] == refs
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_envelope_schemas_are_closed() -> None:
|
|
60
|
+
schemas = SPEC["components"]["schemas"]
|
|
61
|
+
receipt, skipped, otlp = (
|
|
62
|
+
schemas[name]
|
|
63
|
+
for name in ("IngestReceipt", "IngestSkipped", "OTLPExportLogsResponse")
|
|
64
|
+
)
|
|
65
|
+
assert receipt["required"] == ["fact_id", "stored"]
|
|
66
|
+
assert receipt["properties"]["fact_id"]["type"] == "string"
|
|
67
|
+
assert receipt["properties"]["stored"]["type"] == "boolean"
|
|
68
|
+
assert skipped["required"] == ["skipped", "reason"]
|
|
69
|
+
assert skipped["properties"]["skipped"]["const"] is True
|
|
70
|
+
assert otlp["properties"] == {}
|
|
71
|
+
for schema in (receipt, skipped, otlp):
|
|
72
|
+
assert schema["additionalProperties"] is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _bearer() -> dict[str, str]:
|
|
76
|
+
return {"Authorization": f"Bearer {settings.api_bearer_token}"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _webhook(
|
|
80
|
+
client: TestClient, path: str, event: str, payload: dict[str, Any]
|
|
81
|
+
) -> httpx.Response:
|
|
82
|
+
body = json.dumps(payload).encode()
|
|
83
|
+
return client.post(
|
|
84
|
+
path,
|
|
85
|
+
content=body,
|
|
86
|
+
headers={
|
|
87
|
+
"X-Hub-Signature-256": sign_payload(body, settings.github_webhook_secret),
|
|
88
|
+
"X-GitHub-Event": event,
|
|
89
|
+
"X-GitHub-Delivery": "delivery-envelopes",
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_gateway_stored_duplicate_and_skipped_bodies(client: TestClient) -> None:
|
|
96
|
+
payload = json.loads(
|
|
97
|
+
(FIXTURES / "litellm_standard_logging_object.json").read_text()
|
|
98
|
+
)
|
|
99
|
+
body = {"provider": "litellm", "session_id": "sess-envelope", "payload": payload}
|
|
100
|
+
|
|
101
|
+
first = client.post("/ingest/gateway", json=body, headers=_bearer())
|
|
102
|
+
again = client.post("/ingest/gateway", json=body, headers=_bearer())
|
|
103
|
+
skipped = client.post(
|
|
104
|
+
"/ingest/gateway",
|
|
105
|
+
json={"provider": "litellm", "payload": {"litellm_call_id": "no-session"}},
|
|
106
|
+
headers=_bearer(),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
for response in (first, again, skipped):
|
|
110
|
+
_assert_matches_spec("/ingest/gateway", response)
|
|
111
|
+
fact_id = first.json()["fact_id"]
|
|
112
|
+
assert first.json() == {"fact_id": fact_id, "stored": True}
|
|
113
|
+
assert again.json() == {"fact_id": fact_id, "stored": False}
|
|
114
|
+
assert skipped.json() == {"skipped": True, "reason": "no_session"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_webhook_stored_duplicate_and_skipped_bodies(client: TestClient) -> None:
|
|
118
|
+
push = json.loads((FIXTURES / "github_push.json").read_text())
|
|
119
|
+
|
|
120
|
+
first = _webhook(client, "/ingest/github/push", "push", push)
|
|
121
|
+
again = _webhook(client, "/ingest/github/push", "push", push)
|
|
122
|
+
skipped = _webhook(client, "/ingest/github/push", "ping", {"zen": "ok"})
|
|
123
|
+
|
|
124
|
+
for response in (first, again, skipped):
|
|
125
|
+
_assert_matches_spec("/ingest/github/push", response)
|
|
126
|
+
fact_id = first.json()["fact_id"]
|
|
127
|
+
assert first.json() == {"fact_id": fact_id, "stored": True}
|
|
128
|
+
assert again.json() == {"fact_id": fact_id, "stored": False}
|
|
129
|
+
assert skipped.json() == {"skipped": True, "reason": "not a push event"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_otlp_success_stays_empty(client: TestClient) -> None:
|
|
133
|
+
[fixture] = sorted(FIXTURES.glob("otlp/claude_code/tool_decision_accept_user.json"))
|
|
134
|
+
payload = json.loads(fixture.read_text())
|
|
135
|
+
|
|
136
|
+
for _ in range(2):
|
|
137
|
+
response = client.post("/v1/logs", json=payload, headers=_bearer())
|
|
138
|
+
_assert_matches_spec("/v1/logs", response)
|
|
139
|
+
assert response.content == b"{}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sediment_api-0.7.0 → sediment_api-0.7.1}/sediment_api/reports/label_confidence_inspection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|