docforge-sdk 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,168 @@
1
+ # ====== Code Summary ======
2
+ # The synchronous HTTP transport (over httpx.Client). Inherits every pure helper from _TransportBase
3
+ # and supplies only the sync I/O primitive (_send); spec execution, upload and raw byte fetch route
4
+ # through that single chokepoint, so no endpoint hand-rolls its own parsing.
5
+
6
+ # ====== Standard Library Imports ======
7
+ from typing import Any
8
+
9
+ # ====== Third-Party Library Imports ======
10
+ import httpx
11
+
12
+ # ====== Local Project Imports ======
13
+ from ._exceptions import APIConnectionError, APITimeoutError
14
+ from ._requestspec import RequestSpec
15
+ from ._transport_base import T, _TransportBase
16
+
17
+
18
+ class SyncTransport(_TransportBase):
19
+ """Synchronous transport backed by an ``httpx.Client``."""
20
+
21
+ def __init__(self, base_url: str, timeout: float, api_token: str = "") -> None:
22
+ """
23
+ Create the underlying sync httpx client with the resolved headers and timeout.
24
+
25
+ Args:
26
+ base_url (str): The API origin.
27
+ timeout (float): Per-request timeout in seconds.
28
+ api_token (str): Bearer token; empty means unauthenticated.
29
+ """
30
+ super().__init__(base_url, timeout, api_token)
31
+ self._client = httpx.Client(timeout=timeout, headers=self._headers)
32
+
33
+ def _send(
34
+ self,
35
+ method: str,
36
+ url: str,
37
+ *,
38
+ params: dict[str, Any] | None = None,
39
+ json: Any = None,
40
+ data: Any = None,
41
+ files: Any = None,
42
+ ) -> httpx.Response:
43
+ """
44
+ Execute a single httpx call, mapping transport failures to SDK exceptions.
45
+
46
+ Args:
47
+ method (str): The HTTP method.
48
+ url (str): The absolute request URL.
49
+ params (dict[str, Any] | None): Cleaned query parameters.
50
+ json (Any): The JSON body, if any.
51
+ data (Any): The form body for multipart uploads, if any.
52
+ files (Any): The multipart file mapping, if any.
53
+
54
+ Returns:
55
+ httpx.Response: The raw response (status not yet checked).
56
+
57
+ Raises:
58
+ APITimeoutError: When the request timed out.
59
+ APIConnectionError: When the API could not be reached.
60
+ """
61
+ # 1. A timeout is a specific connection failure — catch it before the broader transport error.
62
+ try:
63
+ return self._client.request(
64
+ method, url, params=params, json=json, data=data, files=files
65
+ )
66
+ except httpx.TimeoutException as error:
67
+ raise APITimeoutError(str(error)) from error
68
+ except httpx.TransportError as error:
69
+ raise APIConnectionError(str(error)) from error
70
+
71
+ def request(self, spec: RequestSpec, model: type[T]) -> T:
72
+ """
73
+ Execute a request spec and validate its response into the target model.
74
+
75
+ Args:
76
+ spec (RequestSpec): The request description.
77
+ model (type[T]): The target type to validate the response into.
78
+
79
+ Returns:
80
+ T: The validated response model.
81
+ """
82
+ # 1. Execute, 2. surface any error status, 3. parse — the single per-transport pipeline.
83
+ response = self._send(
84
+ spec.method,
85
+ self._url(spec.path),
86
+ params=self._clean(spec.params),
87
+ json=spec.json,
88
+ files=spec.files,
89
+ )
90
+ self._raise_for_status(response)
91
+ return self._parse(response, model)
92
+
93
+ def request_bare(self, spec: RequestSpec, model: type[T]) -> T:
94
+ """
95
+ Execute a request spec against the BARE origin (outside ``/api/v1``) and parse it.
96
+
97
+ Mirrors ``request`` but targets an un-versioned route (e.g. the public ``/health`` probe).
98
+
99
+ Args:
100
+ spec (RequestSpec): The request description (its path is origin-relative).
101
+ model (type[T]): The target type to validate the response into.
102
+
103
+ Returns:
104
+ T: The validated response model.
105
+ """
106
+ response = self._send(
107
+ spec.method,
108
+ self._bare_url(spec.path),
109
+ params=self._clean(spec.params),
110
+ json=spec.json,
111
+ files=spec.files,
112
+ )
113
+ self._raise_for_status(response)
114
+ return self._parse(response, model)
115
+
116
+ def upload(self, path: str, files: Any, data: Any, model: type[T]) -> T:
117
+ """
118
+ Send a multipart upload and validate the response into the target model.
119
+
120
+ Args:
121
+ path (str): The API-relative path.
122
+ files (Any): The multipart file mapping.
123
+ data (Any): The accompanying form fields, if any.
124
+ model (type[T]): The target response type.
125
+
126
+ Returns:
127
+ T: The validated response model.
128
+ """
129
+ response = self._send("POST", self._url(path), files=files, data=data)
130
+ self._raise_for_status(response)
131
+ return self._parse(response, model)
132
+
133
+ def get_bytes(self, path: str) -> bytes:
134
+ """
135
+ Fetch a raw binary body (e.g. a stored blob) without JSON parsing.
136
+
137
+ Args:
138
+ path (str): The API-relative path.
139
+
140
+ Returns:
141
+ bytes: The raw response content.
142
+ """
143
+ response = self._send("GET", self._url(path))
144
+ self._raise_for_status(response)
145
+ return response.content
146
+
147
+ def get_bytes_typed(self, path: str) -> tuple[bytes, str]:
148
+ """
149
+ Fetch a raw binary body together with its server-declared media type.
150
+
151
+ Args:
152
+ path (str): The API-relative path.
153
+
154
+ Returns:
155
+ tuple[bytes, str]: The raw content and its ``Content-Type`` (a generic octet-stream
156
+ fallback when the header is absent).
157
+ """
158
+ response = self._send("GET", self._url(path))
159
+ self._raise_for_status(response)
160
+ mime_type = response.headers.get("content-type", "application/octet-stream")
161
+ return response.content, mime_type
162
+
163
+ def close(self) -> None:
164
+ """Close the underlying httpx client and release its connections."""
165
+ self._client.close()
166
+
167
+
168
+ __all__ = ["SyncTransport"]
@@ -0,0 +1,5 @@
1
+ # ====== Code Summary ======
2
+ # Single source of truth for the package version. Hatchling reads ``__version__`` from this file to
3
+ # populate the distribution metadata (see ``[tool.hatch.version]`` in pyproject.toml).
4
+
5
+ __version__ = "0.1.0"
docforge_sdk/client.py ADDED
@@ -0,0 +1,131 @@
1
+ # ====== Code Summary ======
2
+ # The public entry points: AsyncClient and Client. Each owns a transport and wires every resource
3
+ # group onto itself (auth, health, collections, documents, explorer, search, jobs, blobs, pipelines).
4
+ # Both support context-manager use so the underlying httpx client is always closed.
5
+
6
+ # ====== Standard Library Imports ======
7
+ from types import TracebackType
8
+
9
+ # ====== Local Project Imports ======
10
+ from ._transport_async import AsyncTransport
11
+ from ._transport_sync import SyncTransport
12
+ from .resources.auth import AsyncAuth, SyncAuth
13
+ from .resources.blobs import AsyncBlobs, SyncBlobs
14
+ from .resources.collections import AsyncCollections, SyncCollections
15
+ from .resources.documents import AsyncDocuments, SyncDocuments
16
+ from .resources.explorer import AsyncExplorer, SyncExplorer
17
+ from .resources.health import AsyncHealth, SyncHealth
18
+ from .resources.jobs import AsyncJobs, SyncJobs
19
+ from .resources.pipelines import AsyncPipelines, SyncPipelines
20
+ from .resources.search import AsyncSearch, SyncSearch
21
+
22
+
23
+ class AsyncClient:
24
+ """
25
+ Asynchronous DocForge API client.
26
+
27
+ Attributes:
28
+ auth (AsyncAuth): API-key management resource.
29
+ health (AsyncHealth): Liveness probe.
30
+ collections (AsyncCollections): Collection CRUD.
31
+ documents (AsyncDocuments): Document admission + searchability control.
32
+ explorer (AsyncExplorer): Document/chunk read surface + IR + toggles.
33
+ search (AsyncSearch): Hybrid search.
34
+ jobs (AsyncJobs): Ingestion-job monitoring.
35
+ blobs (AsyncBlobs): Content-addressed blob fetch.
36
+ pipelines (AsyncPipelines): Pipeline discovery + design.
37
+ """
38
+
39
+ def __init__(self, base_url: str, timeout: float = 30.0, api_token: str = "") -> None:
40
+ """
41
+ Create the client and wire its resource groups onto the shared transport.
42
+
43
+ Args:
44
+ base_url (str): The API origin, e.g. ``"http://localhost:10040"``.
45
+ timeout (float): Per-request timeout in seconds.
46
+ api_token (str): Bearer token; empty means unauthenticated requests.
47
+ """
48
+ self._transport = AsyncTransport(base_url, timeout, api_token)
49
+ self.auth = AsyncAuth(self._transport)
50
+ self.health = AsyncHealth(self._transport)
51
+ self.collections = AsyncCollections(self._transport)
52
+ self.documents = AsyncDocuments(self._transport)
53
+ self.explorer = AsyncExplorer(self._transport)
54
+ self.search = AsyncSearch(self._transport)
55
+ self.jobs = AsyncJobs(self._transport)
56
+ self.blobs = AsyncBlobs(self._transport)
57
+ self.pipelines = AsyncPipelines(self._transport)
58
+
59
+ async def __aenter__(self) -> "AsyncClient":
60
+ """Enter the async context, returning the client itself."""
61
+ return self
62
+
63
+ async def __aexit__(
64
+ self,
65
+ exc_type: type[BaseException] | None,
66
+ exc_value: BaseException | None,
67
+ traceback: TracebackType | None,
68
+ ) -> None:
69
+ """Close the client on context exit."""
70
+ await self.aclose()
71
+
72
+ async def aclose(self) -> None:
73
+ """Close the underlying transport and release its connections."""
74
+ await self._transport.aclose()
75
+
76
+
77
+ class Client:
78
+ """
79
+ Synchronous DocForge API client.
80
+
81
+ Attributes:
82
+ auth (SyncAuth): API-key management resource.
83
+ health (SyncHealth): Liveness probe.
84
+ collections (SyncCollections): Collection CRUD.
85
+ documents (SyncDocuments): Document admission + searchability control.
86
+ explorer (SyncExplorer): Document/chunk read surface + IR + toggles.
87
+ search (SyncSearch): Hybrid search.
88
+ jobs (SyncJobs): Ingestion-job monitoring.
89
+ blobs (SyncBlobs): Content-addressed blob fetch.
90
+ pipelines (SyncPipelines): Pipeline discovery + design.
91
+ """
92
+
93
+ def __init__(self, base_url: str, timeout: float = 30.0, api_token: str = "") -> None:
94
+ """
95
+ Create the client and wire its resource groups onto the shared transport.
96
+
97
+ Args:
98
+ base_url (str): The API origin, e.g. ``"http://localhost:10040"``.
99
+ timeout (float): Per-request timeout in seconds.
100
+ api_token (str): Bearer token; empty means unauthenticated requests.
101
+ """
102
+ self._transport = SyncTransport(base_url, timeout, api_token)
103
+ self.auth = SyncAuth(self._transport)
104
+ self.health = SyncHealth(self._transport)
105
+ self.collections = SyncCollections(self._transport)
106
+ self.documents = SyncDocuments(self._transport)
107
+ self.explorer = SyncExplorer(self._transport)
108
+ self.search = SyncSearch(self._transport)
109
+ self.jobs = SyncJobs(self._transport)
110
+ self.blobs = SyncBlobs(self._transport)
111
+ self.pipelines = SyncPipelines(self._transport)
112
+
113
+ def __enter__(self) -> "Client":
114
+ """Enter the context, returning the client itself."""
115
+ return self
116
+
117
+ def __exit__(
118
+ self,
119
+ exc_type: type[BaseException] | None,
120
+ exc_value: BaseException | None,
121
+ traceback: TracebackType | None,
122
+ ) -> None:
123
+ """Close the client on context exit."""
124
+ self.close()
125
+
126
+ def close(self) -> None:
127
+ """Close the underlying transport and release its connections."""
128
+ self._transport.close()
129
+
130
+
131
+ __all__ = ["AsyncClient", "Client"]
@@ -0,0 +1,132 @@
1
+ # ------------------- Shared vocabulary ------------------- #
2
+ from ._shared import (
3
+ Capability,
4
+ DocumentStatus,
5
+ EnrichmentKind,
6
+ EnrichmentStatus,
7
+ FieldOrigin,
8
+ FieldScope,
9
+ FieldType,
10
+ KeyPermissions,
11
+ SourceKind,
12
+ )
13
+
14
+ # ------------------- Auth models ------------------- #
15
+ from .auth import CreatedKey, CreateKeyRequest, KeyInfo, RotateKeyRequest
16
+
17
+ # ------------------- Blobs models ------------------- #
18
+ from .blobs import BlobContent
19
+
20
+ # ------------------- Collections models ------------------- #
21
+ from .collections import (
22
+ CollectionModel,
23
+ CreateCollectionRequest,
24
+ FieldSpec,
25
+ UpdateCollectionRequest,
26
+ )
27
+
28
+ # ------------------- Documents models ------------------- #
29
+ from .documents import DocumentEnabledResponse, EnabledPatch, UploadAccepted
30
+
31
+ # ------------------- Explorer models ------------------- #
32
+ from .explorer import (
33
+ BulkChunkEnabledPatch,
34
+ BulkChunkEnabledResponse,
35
+ ChunkEnabledPatch,
36
+ ChunkEnabledResult,
37
+ ChunkInfo,
38
+ DocumentDetail,
39
+ DocumentListItem,
40
+ MetadataValue,
41
+ PageInfo,
42
+ )
43
+
44
+ # ------------------- Health models ------------------- #
45
+ from .health import HealthStatus
46
+
47
+ # ------------------- IR models ------------------- #
48
+ from .ir import DocumentIRModel, IRBlock, IREnrichment, IRFigure, IRTable
49
+
50
+ # ------------------- Jobs models ------------------- #
51
+ from .jobs import JobEvent, JobStatus, JobTrace, WorkerActivity, WorkersLive
52
+
53
+ # ------------------- Pipelines models ------------------- #
54
+ from .pipelines import (
55
+ EditResponse,
56
+ InspectResponse,
57
+ PipelineDesignResponse,
58
+ PipelineIndexResponse,
59
+ PipelineSurface,
60
+ StageApplyResponse,
61
+ StageViewResponse,
62
+ )
63
+
64
+ # ------------------- Search models ------------------- #
65
+ from .search import SearchHit, SearchRequest, SearchResponse, SearchTarget
66
+
67
+ # ------------------- Public API ------------------- #
68
+ __all__ = [
69
+ # Shared vocabulary
70
+ "Capability",
71
+ "KeyPermissions",
72
+ "FieldType",
73
+ "FieldOrigin",
74
+ "FieldScope",
75
+ "SourceKind",
76
+ "DocumentStatus",
77
+ "EnrichmentKind",
78
+ "EnrichmentStatus",
79
+ # Auth
80
+ "CreateKeyRequest",
81
+ "RotateKeyRequest",
82
+ "CreatedKey",
83
+ "KeyInfo",
84
+ # Health
85
+ "HealthStatus",
86
+ # Collections
87
+ "FieldSpec",
88
+ "CollectionModel",
89
+ "CreateCollectionRequest",
90
+ "UpdateCollectionRequest",
91
+ # Documents
92
+ "UploadAccepted",
93
+ "EnabledPatch",
94
+ "DocumentEnabledResponse",
95
+ # Explorer
96
+ "MetadataValue",
97
+ "DocumentListItem",
98
+ "DocumentDetail",
99
+ "PageInfo",
100
+ "ChunkInfo",
101
+ "ChunkEnabledPatch",
102
+ "BulkChunkEnabledPatch",
103
+ "ChunkEnabledResult",
104
+ "BulkChunkEnabledResponse",
105
+ # IR
106
+ "IRBlock",
107
+ "IRTable",
108
+ "IRFigure",
109
+ "IREnrichment",
110
+ "DocumentIRModel",
111
+ # Search
112
+ "SearchTarget",
113
+ "SearchRequest",
114
+ "SearchHit",
115
+ "SearchResponse",
116
+ # Jobs
117
+ "JobStatus",
118
+ "JobEvent",
119
+ "JobTrace",
120
+ "WorkerActivity",
121
+ "WorkersLive",
122
+ # Blobs
123
+ "BlobContent",
124
+ # Pipelines
125
+ "PipelineSurface",
126
+ "PipelineIndexResponse",
127
+ "PipelineDesignResponse",
128
+ "InspectResponse",
129
+ "EditResponse",
130
+ "StageViewResponse",
131
+ "StageApplyResponse",
132
+ ]
@@ -0,0 +1,121 @@
1
+ # ====== Code Summary ======
2
+ # Vocabulary shared across resources, mirrored from the DocForge backend (never imported from it, to
3
+ # keep the SDK standalone). Holds the authorization enum + per-key permission scope, the metadata
4
+ # contract enums (FieldType/FieldOrigin/FieldScope), the document lifecycle enums (DocumentStatus/
5
+ # SourceKind) and the IR enrichment enums (EnrichmentKind/EnrichmentStatus).
6
+
7
+ # ====== Standard Library Imports ======
8
+ from enum import StrEnum
9
+
10
+ # ====== Third-Party Library Imports ======
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class Capability(StrEnum):
15
+ """A coarse action class an endpoint requires of the calling key."""
16
+
17
+ READ = "read"
18
+ WRITE = "write"
19
+ SEARCH = "search"
20
+ ADMIN = "admin"
21
+
22
+
23
+ class FieldType(StrEnum):
24
+ """Data type of a metadata field — drives validation and storage."""
25
+
26
+ STRING = "string"
27
+ INTEGER = "integer"
28
+ FLOAT = "float"
29
+ BOOL = "bool"
30
+ KEYWORD_LIST = "keyword_list"
31
+ DATETIME = "datetime"
32
+ ENUM = "enum"
33
+ TEXT = "text"
34
+ INTEGER_LIST = "integer_list"
35
+ FLOAT_LIST = "float_list"
36
+ TEXT_LIST = "text_list"
37
+
38
+
39
+ class FieldOrigin(StrEnum):
40
+ """Where a metadata field's value comes from — drives who may fill it."""
41
+
42
+ SYSTEM = "system"
43
+ USER = "user"
44
+ GENERATED = "generated"
45
+
46
+
47
+ class FieldScope(StrEnum):
48
+ """At which granularity a field's value lives — one per document, or one per chunk."""
49
+
50
+ DOCUMENT = "document"
51
+ CHUNK = "chunk"
52
+
53
+
54
+ class SourceKind(StrEnum):
55
+ """How a document's pages were acquired — routes parsing (text vs OCR/VLM)."""
56
+
57
+ DIGITAL_BORN = "digital_born"
58
+ SCANNED = "scanned"
59
+ MIXED = "mixed"
60
+
61
+
62
+ class DocumentStatus(StrEnum):
63
+ """A document's ingestion lifecycle state."""
64
+
65
+ PENDING = "pending"
66
+ PROCESSING = "processing"
67
+ DONE = "done"
68
+ FAILED = "failed"
69
+
70
+
71
+ class EnrichmentKind(StrEnum):
72
+ """What an IR block enrichment produced."""
73
+
74
+ CLASSIFY = "classify"
75
+ OCR = "ocr"
76
+ VLM = "vlm"
77
+ CHART_TO_DATA = "chart_to_data"
78
+ TABLE_SUMMARY = "table_summary"
79
+
80
+
81
+ class EnrichmentStatus(StrEnum):
82
+ """The outcome of a block enrichment run."""
83
+
84
+ OK = "ok"
85
+ FAILED = "failed"
86
+ SKIPPED = "skipped"
87
+
88
+
89
+ class KeyPermissions(BaseModel):
90
+ """
91
+ The per-key permission scope stored on an API key.
92
+
93
+ A key with ``null`` permissions is full access (root) and is represented as ``None`` at the call
94
+ site, never as an instance of this model — this model always describes a SCOPED key.
95
+
96
+ Attributes:
97
+ capabilities (list[Capability]): The action classes the key grants (an empty list = a key
98
+ that can authenticate but is authorized for nothing).
99
+ collections (list[str]): Either ``["*"]`` (every collection) or an explicit list of
100
+ collection UUID strings the key is scoped to.
101
+ """
102
+
103
+ capabilities: list[Capability] = Field(
104
+ description="The action classes this key grants (READ / WRITE / SEARCH / ADMIN)."
105
+ )
106
+ collections: list[str] = Field(
107
+ description="Collection scope: ['*'] for all, else explicit collection UUID strings."
108
+ )
109
+
110
+
111
+ __all__ = [
112
+ "Capability",
113
+ "FieldType",
114
+ "FieldOrigin",
115
+ "FieldScope",
116
+ "SourceKind",
117
+ "DocumentStatus",
118
+ "EnrichmentKind",
119
+ "EnrichmentStatus",
120
+ "KeyPermissions",
121
+ ]
@@ -0,0 +1,130 @@
1
+ # ====== Code Summary ======
2
+ # Request/response models for the auth (API-key management) resource, mirrored field-for-field from the
3
+ # DocForge backend router models. The response ``permissions`` field is the opaque JSONB blob the server
4
+ # stores, so it is typed as a plain dict rather than the structured KeyPermissions.
5
+
6
+ # ====== Standard Library Imports ======
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ # ====== Third-Party Library Imports ======
11
+ from pydantic import BaseModel, Field
12
+
13
+ # ====== Local Project Imports ======
14
+ from ._shared import KeyPermissions
15
+
16
+
17
+ class CreateKeyRequest(BaseModel):
18
+ """
19
+ Body of a create-key request.
20
+
21
+ Attributes:
22
+ name (str): Human-readable label for the key.
23
+ permissions (KeyPermissions | None): Per-key capability + collection scope; ``None`` = full
24
+ access (root).
25
+ expires_at (datetime | None): Absolute expiry instant; ``None`` = never expires.
26
+ """
27
+
28
+ name: str = Field(min_length=1, description="Human-readable label for the key.")
29
+ permissions: KeyPermissions | None = Field(
30
+ default=None,
31
+ description="Per-key capability + collection scope; null = full access (root).",
32
+ )
33
+ expires_at: datetime | None = Field(
34
+ default=None,
35
+ description="Absolute expiry instant; null = never expires.",
36
+ )
37
+
38
+
39
+ class RotateKeyRequest(BaseModel):
40
+ """
41
+ Body of a rotate-key request — every field optional so absence clones from the source key.
42
+
43
+ Serialization is CRUCIAL here: the backend distinguishes "field absent" (clone the source key's
44
+ value) from "field present but null" (a meaningful value — ``permissions=null`` means full access,
45
+ ``expires_at=null`` means never expires). Callers must therefore dump this model with
46
+ ``exclude_unset=True`` so a field that was never set is omitted from the body, while an explicitly
47
+ provided ``None`` is sent as JSON ``null``. The auth resource does this for you.
48
+
49
+ Attributes:
50
+ name (str | None): New label; absent = keep the source name.
51
+ permissions (KeyPermissions | None): New scope; absent = keep the source blob, null = full
52
+ access.
53
+ expires_at (datetime | None): New expiry; absent = keep the source expiry, null = never
54
+ expires.
55
+ """
56
+
57
+ name: str | None = Field(
58
+ default=None, min_length=1, description="New label; absent = keep the source name."
59
+ )
60
+ permissions: KeyPermissions | None = Field(
61
+ default=None,
62
+ description="New scope; absent = keep the source blob, null = full access.",
63
+ )
64
+ expires_at: datetime | None = Field(
65
+ default=None,
66
+ description="New expiry instant; absent = keep the source expiry, null = never expires.",
67
+ )
68
+
69
+
70
+ class CreatedKey(BaseModel):
71
+ """
72
+ The create/rotate response — the ONLY place the plaintext key is ever returned.
73
+
74
+ Attributes:
75
+ id (str): The key row's UUID.
76
+ name (str): The label given at creation.
77
+ prefix (str): The stored display prefix (safe to show; not a secret on its own).
78
+ permissions (dict[str, Any] | None): The opaque per-scope JSONB blob; ``None`` = full access.
79
+ created_at (datetime): When the key was created.
80
+ expires_at (datetime | None): The absolute expiry instant; ``None`` = never expires.
81
+ key (str): The plaintext key — shown ONCE here, never stored or returned again.
82
+ """
83
+
84
+ id: str = Field(description="The key row's UUID.")
85
+ name: str = Field(description="The label given at creation.")
86
+ prefix: str = Field(description="The stored display prefix.")
87
+ permissions: dict[str, Any] | None = Field(
88
+ default=None, description="Per-scope blob; null = full access."
89
+ )
90
+ created_at: datetime = Field(description="When the key was created.")
91
+ expires_at: datetime | None = Field(
92
+ default=None, description="Absolute expiry instant; null = never expires."
93
+ )
94
+ key: str = Field(description="Plaintext key — shown ONCE, never recoverable afterwards.")
95
+
96
+
97
+ class KeyInfo(BaseModel):
98
+ """
99
+ A key as listed — metadata only, never the hash or the plaintext.
100
+
101
+ Attributes:
102
+ id (str): The key row's UUID.
103
+ name (str): The label given at creation.
104
+ prefix (str): The stored display prefix.
105
+ permissions (dict[str, Any] | None): The opaque per-scope JSONB blob; ``None`` = full access.
106
+ created_at (datetime): When the key was created.
107
+ expires_at (datetime | None): The absolute expiry instant; ``None`` = never expires.
108
+ last_used_at (datetime | None): Last successful authentication instant; ``None`` = never used.
109
+ revoked_at (datetime | None): Soft-revocation timestamp; ``None`` when the key is active.
110
+ """
111
+
112
+ id: str = Field(description="The key row's UUID.")
113
+ name: str = Field(description="The label given at creation.")
114
+ prefix: str = Field(description="The stored display prefix.")
115
+ permissions: dict[str, Any] | None = Field(
116
+ default=None, description="Per-scope blob; null = full access."
117
+ )
118
+ created_at: datetime = Field(description="When the key was created.")
119
+ expires_at: datetime | None = Field(
120
+ default=None, description="Absolute expiry instant; null = never expires."
121
+ )
122
+ last_used_at: datetime | None = Field(
123
+ default=None, description="Last successful authentication instant; null = never used."
124
+ )
125
+ revoked_at: datetime | None = Field(
126
+ default=None, description="Soft-revocation timestamp; null when the key is active."
127
+ )
128
+
129
+
130
+ __all__ = ["CreateKeyRequest", "RotateKeyRequest", "CreatedKey", "KeyInfo"]