docforge-sdk 0.1.0__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 (58) hide show
  1. docforge_sdk-0.1.0/.gitignore +85 -0
  2. docforge_sdk-0.1.0/.python-version +1 -0
  3. docforge_sdk-0.1.0/LICENSE +21 -0
  4. docforge_sdk-0.1.0/PKG-INFO +108 -0
  5. docforge_sdk-0.1.0/README.md +85 -0
  6. docforge_sdk-0.1.0/docforge_sdk/__init__.py +161 -0
  7. docforge_sdk-0.1.0/docforge_sdk/_exceptions.py +108 -0
  8. docforge_sdk-0.1.0/docforge_sdk/_requestspec.py +34 -0
  9. docforge_sdk-0.1.0/docforge_sdk/_transport_async.py +168 -0
  10. docforge_sdk-0.1.0/docforge_sdk/_transport_base.py +141 -0
  11. docforge_sdk-0.1.0/docforge_sdk/_transport_sync.py +168 -0
  12. docforge_sdk-0.1.0/docforge_sdk/_version.py +5 -0
  13. docforge_sdk-0.1.0/docforge_sdk/client.py +131 -0
  14. docforge_sdk-0.1.0/docforge_sdk/models/__init__.py +132 -0
  15. docforge_sdk-0.1.0/docforge_sdk/models/_shared.py +121 -0
  16. docforge_sdk-0.1.0/docforge_sdk/models/auth.py +130 -0
  17. docforge_sdk-0.1.0/docforge_sdk/models/blobs.py +25 -0
  18. docforge_sdk-0.1.0/docforge_sdk/models/collections.py +142 -0
  19. docforge_sdk-0.1.0/docforge_sdk/models/documents.py +49 -0
  20. docforge_sdk-0.1.0/docforge_sdk/models/explorer.py +238 -0
  21. docforge_sdk-0.1.0/docforge_sdk/models/health.py +20 -0
  22. docforge_sdk-0.1.0/docforge_sdk/models/ir.py +139 -0
  23. docforge_sdk-0.1.0/docforge_sdk/models/jobs.py +100 -0
  24. docforge_sdk-0.1.0/docforge_sdk/models/pipelines.py +182 -0
  25. docforge_sdk-0.1.0/docforge_sdk/models/search.py +110 -0
  26. docforge_sdk-0.1.0/docforge_sdk/py.typed +0 -0
  27. docforge_sdk-0.1.0/docforge_sdk/resources/__init__.py +53 -0
  28. docforge_sdk-0.1.0/docforge_sdk/resources/_base.py +46 -0
  29. docforge_sdk-0.1.0/docforge_sdk/resources/auth.py +264 -0
  30. docforge_sdk-0.1.0/docforge_sdk/resources/blobs.py +63 -0
  31. docforge_sdk-0.1.0/docforge_sdk/resources/collections.py +204 -0
  32. docforge_sdk-0.1.0/docforge_sdk/resources/documents.py +153 -0
  33. docforge_sdk-0.1.0/docforge_sdk/resources/explorer.py +342 -0
  34. docforge_sdk-0.1.0/docforge_sdk/resources/health.py +53 -0
  35. docforge_sdk-0.1.0/docforge_sdk/resources/jobs.py +161 -0
  36. docforge_sdk-0.1.0/docforge_sdk/resources/pipelines.py +291 -0
  37. docforge_sdk-0.1.0/docforge_sdk/resources/search.py +70 -0
  38. docforge_sdk-0.1.0/pyproject.toml +83 -0
  39. docforge_sdk-0.1.0/tests/__init__.py +0 -0
  40. docforge_sdk-0.1.0/tests/check_schema_drift.py +115 -0
  41. docforge_sdk-0.1.0/tests/live/__init__.py +0 -0
  42. docforge_sdk-0.1.0/tests/live/test_openapi_parity_live.py +53 -0
  43. docforge_sdk-0.1.0/tests/openapi_snapshot.json +1 -0
  44. docforge_sdk-0.1.0/tests/parity_map.py +104 -0
  45. docforge_sdk-0.1.0/tests/unit/__init__.py +0 -0
  46. docforge_sdk-0.1.0/tests/unit/test_auth.py +135 -0
  47. docforge_sdk-0.1.0/tests/unit/test_blobs.py +42 -0
  48. docforge_sdk-0.1.0/tests/unit/test_collections.py +91 -0
  49. docforge_sdk-0.1.0/tests/unit/test_documents.py +53 -0
  50. docforge_sdk-0.1.0/tests/unit/test_explorer.py +96 -0
  51. docforge_sdk-0.1.0/tests/unit/test_health.py +37 -0
  52. docforge_sdk-0.1.0/tests/unit/test_jobs.py +54 -0
  53. docforge_sdk-0.1.0/tests/unit/test_models_offline_parity.py +48 -0
  54. docforge_sdk-0.1.0/tests/unit/test_pipelines.py +74 -0
  55. docforge_sdk-0.1.0/tests/unit/test_resource_parity.py +55 -0
  56. docforge_sdk-0.1.0/tests/unit/test_search.py +59 -0
  57. docforge_sdk-0.1.0/tests/unit/test_transport.py +131 -0
  58. docforge_sdk-0.1.0/uv.lock +504 -0
@@ -0,0 +1,85 @@
1
+ # ── Python ────────────────────────────────────────────────
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ *.egg
11
+ .eggs/
12
+
13
+ # ── Virtual environments ───────────────────────────────────
14
+ .venv/
15
+ venv/
16
+ env/
17
+ ENV/
18
+
19
+ # ── uv ────────────────────────────────────────────────────
20
+ .uv/
21
+
22
+ # ── Secrets / credentials ─────────────────────────────────
23
+ services/**/.env
24
+ *.env
25
+ !services/**/.env.example
26
+ !.env.example
27
+ # SeaweedFS S3 identity (real secret) — track only the placeholder template.
28
+ services/**/s3_config.json
29
+ !services/**/s3_config.json.example
30
+
31
+ # ── Logs ──────────────────────────────────────────────────
32
+ logs/
33
+ *.log
34
+
35
+ # ── Node / frontend build artifacts (location-independent) ──
36
+ # Globbed so they match wherever the frontend lives (now src/docforge-rework/app/frontend/).
37
+ **/node_modules/
38
+ **/frontend/dist/
39
+ # Stale .js outputs that shadow .tsx/.ts source — Vite resolves them with priority
40
+ # and serves an old bundle. We're TS-only (tsconfig has noEmit); any .js under a
41
+ # frontend src/ is build artefact noise.
42
+ **/frontend/src/**/*.js
43
+ **/frontend/src/**/*.js.map
44
+ # NOTE: src/api/generated.ts is NOW committed (tracked) so the build is reproducible
45
+ # without a live backend — regenerate it with `npm run gen:types` after schema changes.
46
+
47
+ # ── IDE / OS ──────────────────────────────────────────────
48
+ .idea/
49
+ .vscode/
50
+ *.swp
51
+ *.swo
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # ── Claude Code / AI dev tooling (kept LOCAL, never published) ──
56
+ # The whole .claude/ tree (agents, rules, commands, agent-memory, session data, knowledge graph)
57
+ # and the MCP client config are internal AI-assisted-workflow tooling — not part of the public repo.
58
+ .claude/
59
+ .mcp.json
60
+
61
+ # ── Alembic / migrations (keep structure, not generated) ──
62
+ # src/docforge-rework/migrations/versions/*.py # comment in to exclude
63
+
64
+ # ── Test artifacts ────────────────────────────────────────
65
+ .pytest_cache/
66
+ .coverage
67
+ htmlcov/
68
+ # Note: the synthetic test corpus under src/docforge-rework/tests/corpus/documents/<ext>/ IS committed
69
+ # (deterministic, loaded at test time); it is regenerated via tests/corpus/generation/ when builders change.
70
+
71
+ # Vite dependency pre-bundling cache (regenerated by the dev server)
72
+ **/.vite/
73
+
74
+ # Playwright MCP session artifacts (snapshots / console logs from browser-driven checks)
75
+ .playwright-mcp/
76
+
77
+ # The pipeline BUILD module (blob models + PipelineBuilder) is SOURCE, not a build
78
+ # artifact - re-include it from the global build/ rule above.
79
+ !src/docforge-rework/shared/libs/pipelines/build/
80
+ !src/docforge-rework/shared/libs/pipelines/build/**
81
+
82
+ # The build/ unit-test package mirrors that source module - also SOURCE, not an artifact.
83
+ !src/docforge-rework/tests/units/build/
84
+ !src/docforge-rework/tests/units/build/**
85
+ src/docforge-rework/app/frontend/scripts/node_modules/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian Barré
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: docforge-sdk
3
+ Version: 0.1.0
4
+ Summary: Typed async + sync Python client for the DocForge REST API (standalone, zero server deps).
5
+ Project-URL: Homepage, https://github.com/Florian-BARRE/docforge
6
+ Project-URL: Repository, https://github.com/Florian-BARRE/docforge
7
+ Project-URL: Issues, https://github.com/Florian-BARRE/docforge/issues
8
+ Author: Florian Barré
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api-client,async,client,docforge,document-intelligence,rag,retrieval,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: httpx>=0.28
21
+ Requires-Dist: pydantic>=2.7
22
+ Description-Content-Type: text/markdown
23
+
24
+ # docforge-sdk
25
+
26
+ A typed Python client for the [DocForge](https://github.com/Florian-BARRE/docforge) REST API. It
27
+ ships **both** an asynchronous and a synchronous client with an identical surface, is fully
28
+ type-hinted (`py.typed`), and has **zero dependency on the DocForge server tree** — it is a
29
+ clean-room client that talks to the API over HTTP only (`httpx` + `pydantic` + hand-written models
30
+ that mirror the public REST contract), so it can be vendored or published independently.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install docforge-sdk
36
+ ```
37
+
38
+ ## Async usage
39
+
40
+ ```python
41
+ import asyncio
42
+
43
+ from docforge_sdk import AsyncClient, SearchRequest
44
+
45
+
46
+ async def main() -> None:
47
+ async with AsyncClient("http://localhost:10040", api_token="df_root_...") as client:
48
+ collections = await client.collections.list()
49
+ for collection in collections:
50
+ print(collection.id, collection.name)
51
+
52
+ hits = await client.search.search(
53
+ collections[0].id,
54
+ SearchRequest(query="quarterly revenue", limit=5),
55
+ )
56
+ for hit in hits.hits:
57
+ print(hit.score, hit.text)
58
+
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ ## Sync usage
64
+
65
+ ```python
66
+ from docforge_sdk import Client, SearchRequest
67
+
68
+ with Client("http://localhost:10040", api_token="df_root_...") as client:
69
+ collections = client.collections.list()
70
+ hits = client.search.search(
71
+ collections[0].id,
72
+ SearchRequest(query="quarterly revenue", limit=5),
73
+ )
74
+ for hit in hits.hits:
75
+ print(hit.score, hit.text)
76
+ ```
77
+
78
+ The two clients expose the same resources and method signatures; the sync methods are the async ones
79
+ without `await`. Available resource groups: `auth`, `health`, `collections`, `documents`, `explorer`,
80
+ `search`, `jobs`, `blobs`, `pipelines`.
81
+
82
+ ## License
83
+
84
+ MIT — see [LICENSE](LICENSE). This SDK is deliberately licensed **MIT even though the parent DocForge
85
+ repository is GPLv3**: it is a standalone, clean-room client (HTTP models only, no server code), so a
86
+ permissive per-directory license is intentional and lets any project depend on it freely.
87
+
88
+ ## Publishing (maintainers)
89
+
90
+ Releases publish to PyPI via **Trusted Publishing (OIDC)** — there is no API token stored in the
91
+ repo. To cut a release, tag a commit with the `sdk-v<version>` prefix (the version must match
92
+ `docforge_sdk/_version.py`) and push the tag:
93
+
94
+ ```bash
95
+ git tag sdk-v0.1.0
96
+ git push origin sdk-v0.1.0
97
+ ```
98
+
99
+ The `.github/workflows/release-sdk.yml` workflow then builds and uploads the sdist + wheel.
100
+
101
+ **One-time PyPI setup** (done once by the maintainer, before the first release):
102
+
103
+ 1. Reserve the project name `docforge-sdk` on PyPI.
104
+ 2. Under the project's *Publishing* settings, add a **GitHub trusted publisher** with:
105
+ - Owner: `Florian-BARRE`
106
+ - Repository: `docforge`
107
+ - Workflow name: `release-sdk.yml`
108
+ - Environment: `pypi`
@@ -0,0 +1,85 @@
1
+ # docforge-sdk
2
+
3
+ A typed Python client for the [DocForge](https://github.com/Florian-BARRE/docforge) REST API. It
4
+ ships **both** an asynchronous and a synchronous client with an identical surface, is fully
5
+ type-hinted (`py.typed`), and has **zero dependency on the DocForge server tree** — it is a
6
+ clean-room client that talks to the API over HTTP only (`httpx` + `pydantic` + hand-written models
7
+ that mirror the public REST contract), so it can be vendored or published independently.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install docforge-sdk
13
+ ```
14
+
15
+ ## Async usage
16
+
17
+ ```python
18
+ import asyncio
19
+
20
+ from docforge_sdk import AsyncClient, SearchRequest
21
+
22
+
23
+ async def main() -> None:
24
+ async with AsyncClient("http://localhost:10040", api_token="df_root_...") as client:
25
+ collections = await client.collections.list()
26
+ for collection in collections:
27
+ print(collection.id, collection.name)
28
+
29
+ hits = await client.search.search(
30
+ collections[0].id,
31
+ SearchRequest(query="quarterly revenue", limit=5),
32
+ )
33
+ for hit in hits.hits:
34
+ print(hit.score, hit.text)
35
+
36
+
37
+ asyncio.run(main())
38
+ ```
39
+
40
+ ## Sync usage
41
+
42
+ ```python
43
+ from docforge_sdk import Client, SearchRequest
44
+
45
+ with Client("http://localhost:10040", api_token="df_root_...") as client:
46
+ collections = client.collections.list()
47
+ hits = client.search.search(
48
+ collections[0].id,
49
+ SearchRequest(query="quarterly revenue", limit=5),
50
+ )
51
+ for hit in hits.hits:
52
+ print(hit.score, hit.text)
53
+ ```
54
+
55
+ The two clients expose the same resources and method signatures; the sync methods are the async ones
56
+ without `await`. Available resource groups: `auth`, `health`, `collections`, `documents`, `explorer`,
57
+ `search`, `jobs`, `blobs`, `pipelines`.
58
+
59
+ ## License
60
+
61
+ MIT — see [LICENSE](LICENSE). This SDK is deliberately licensed **MIT even though the parent DocForge
62
+ repository is GPLv3**: it is a standalone, clean-room client (HTTP models only, no server code), so a
63
+ permissive per-directory license is intentional and lets any project depend on it freely.
64
+
65
+ ## Publishing (maintainers)
66
+
67
+ Releases publish to PyPI via **Trusted Publishing (OIDC)** — there is no API token stored in the
68
+ repo. To cut a release, tag a commit with the `sdk-v<version>` prefix (the version must match
69
+ `docforge_sdk/_version.py`) and push the tag:
70
+
71
+ ```bash
72
+ git tag sdk-v0.1.0
73
+ git push origin sdk-v0.1.0
74
+ ```
75
+
76
+ The `.github/workflows/release-sdk.yml` workflow then builds and uploads the sdist + wheel.
77
+
78
+ **One-time PyPI setup** (done once by the maintainer, before the first release):
79
+
80
+ 1. Reserve the project name `docforge-sdk` on PyPI.
81
+ 2. Under the project's *Publishing* settings, add a **GitHub trusted publisher** with:
82
+ - Owner: `Florian-BARRE`
83
+ - Repository: `docforge`
84
+ - Workflow name: `release-sdk.yml`
85
+ - Environment: `pypi`
@@ -0,0 +1,161 @@
1
+ # ------------------- Exceptions ------------------- #
2
+ from ._exceptions import (
3
+ APIConnectionError,
4
+ APIStatusError,
5
+ APITimeoutError,
6
+ AuthError,
7
+ ConflictError,
8
+ DocForgeError,
9
+ NotFoundError,
10
+ UnprocessableError,
11
+ )
12
+ from ._version import __version__
13
+
14
+ # ------------------- Clients ------------------- #
15
+ from .client import AsyncClient, Client
16
+
17
+ # ------------------- Shared vocabulary ------------------- #
18
+ from .models._shared import (
19
+ Capability,
20
+ DocumentStatus,
21
+ EnrichmentKind,
22
+ EnrichmentStatus,
23
+ FieldOrigin,
24
+ FieldScope,
25
+ FieldType,
26
+ KeyPermissions,
27
+ SourceKind,
28
+ )
29
+
30
+ # ------------------- Auth models ------------------- #
31
+ from .models.auth import CreatedKey, CreateKeyRequest, KeyInfo, RotateKeyRequest
32
+
33
+ # ------------------- Blobs models ------------------- #
34
+ from .models.blobs import BlobContent
35
+
36
+ # ------------------- Collections models ------------------- #
37
+ from .models.collections import (
38
+ CollectionModel,
39
+ CreateCollectionRequest,
40
+ FieldSpec,
41
+ UpdateCollectionRequest,
42
+ )
43
+
44
+ # ------------------- Documents models ------------------- #
45
+ from .models.documents import DocumentEnabledResponse, EnabledPatch, UploadAccepted
46
+
47
+ # ------------------- Explorer models ------------------- #
48
+ from .models.explorer import (
49
+ BulkChunkEnabledPatch,
50
+ BulkChunkEnabledResponse,
51
+ ChunkEnabledPatch,
52
+ ChunkEnabledResult,
53
+ ChunkInfo,
54
+ DocumentDetail,
55
+ DocumentListItem,
56
+ MetadataValue,
57
+ PageInfo,
58
+ )
59
+
60
+ # ------------------- Health models ------------------- #
61
+ from .models.health import HealthStatus
62
+
63
+ # ------------------- IR models ------------------- #
64
+ from .models.ir import DocumentIRModel, IRBlock, IREnrichment, IRFigure, IRTable
65
+
66
+ # ------------------- Jobs models ------------------- #
67
+ from .models.jobs import JobEvent, JobStatus, JobTrace, WorkerActivity, WorkersLive
68
+
69
+ # ------------------- Pipelines models ------------------- #
70
+ from .models.pipelines import (
71
+ EditResponse,
72
+ InspectResponse,
73
+ PipelineDesignResponse,
74
+ PipelineIndexResponse,
75
+ PipelineSurface,
76
+ StageApplyResponse,
77
+ StageViewResponse,
78
+ )
79
+
80
+ # ------------------- Search models ------------------- #
81
+ from .models.search import SearchHit, SearchRequest, SearchResponse, SearchTarget
82
+
83
+ # ------------------- Public API ------------------- #
84
+ __all__ = [
85
+ "__version__",
86
+ # Clients
87
+ "AsyncClient",
88
+ "Client",
89
+ # Shared vocabulary
90
+ "Capability",
91
+ "KeyPermissions",
92
+ "FieldType",
93
+ "FieldOrigin",
94
+ "FieldScope",
95
+ "SourceKind",
96
+ "DocumentStatus",
97
+ "EnrichmentKind",
98
+ "EnrichmentStatus",
99
+ # Auth
100
+ "CreateKeyRequest",
101
+ "RotateKeyRequest",
102
+ "CreatedKey",
103
+ "KeyInfo",
104
+ # Health
105
+ "HealthStatus",
106
+ # Collections
107
+ "FieldSpec",
108
+ "CollectionModel",
109
+ "CreateCollectionRequest",
110
+ "UpdateCollectionRequest",
111
+ # Documents
112
+ "UploadAccepted",
113
+ "EnabledPatch",
114
+ "DocumentEnabledResponse",
115
+ # Explorer
116
+ "MetadataValue",
117
+ "DocumentListItem",
118
+ "DocumentDetail",
119
+ "PageInfo",
120
+ "ChunkInfo",
121
+ "ChunkEnabledPatch",
122
+ "BulkChunkEnabledPatch",
123
+ "ChunkEnabledResult",
124
+ "BulkChunkEnabledResponse",
125
+ # IR
126
+ "IRBlock",
127
+ "IRTable",
128
+ "IRFigure",
129
+ "IREnrichment",
130
+ "DocumentIRModel",
131
+ # Search
132
+ "SearchTarget",
133
+ "SearchRequest",
134
+ "SearchHit",
135
+ "SearchResponse",
136
+ # Jobs
137
+ "JobStatus",
138
+ "JobEvent",
139
+ "JobTrace",
140
+ "WorkerActivity",
141
+ "WorkersLive",
142
+ # Blobs
143
+ "BlobContent",
144
+ # Pipelines
145
+ "PipelineSurface",
146
+ "PipelineIndexResponse",
147
+ "PipelineDesignResponse",
148
+ "InspectResponse",
149
+ "EditResponse",
150
+ "StageViewResponse",
151
+ "StageApplyResponse",
152
+ # Exceptions
153
+ "DocForgeError",
154
+ "APIConnectionError",
155
+ "APITimeoutError",
156
+ "APIStatusError",
157
+ "AuthError",
158
+ "NotFoundError",
159
+ "ConflictError",
160
+ "UnprocessableError",
161
+ ]
@@ -0,0 +1,108 @@
1
+ # ====== Code Summary ======
2
+ # The public exception hierarchy for the SDK. Every failure a caller can catch descends from
3
+ # ``DocForgeError``. Transport-level failures (no connection, timeout) and HTTP error statuses map to
4
+ # dedicated subclasses so callers can branch on failure kind without inspecting raw httpx objects.
5
+
6
+ # ====== Standard Library Imports ======
7
+ from typing import Any
8
+
9
+ # ====== Third-Party Library Imports ======
10
+ import httpx
11
+
12
+
13
+ class DocForgeError(Exception):
14
+ """Base class for every error raised by the DocForge SDK."""
15
+
16
+
17
+ class APIConnectionError(DocForgeError):
18
+ """Raised when the API could not be reached (DNS, refused connection, network drop)."""
19
+
20
+
21
+ class APITimeoutError(APIConnectionError):
22
+ """Raised when a request exceeded the configured timeout before a response arrived."""
23
+
24
+
25
+ class APIStatusError(DocForgeError):
26
+ """
27
+ Raised when the API returned a 4xx/5xx HTTP status.
28
+
29
+ Attributes:
30
+ status_code (int): The HTTP status code returned by the API.
31
+ body (Any): The parsed response body (JSON when decodable, else raw text) — an opaque,
32
+ server-shaped error payload.
33
+ """
34
+
35
+ def __init__(self, message: str, *, status_code: int, body: Any) -> None:
36
+ """
37
+ Initialize the status error with the failing code and response body.
38
+
39
+ Args:
40
+ message (str): Human-readable summary of the failure.
41
+ status_code (int): The HTTP status code returned by the API.
42
+ body (Any): The parsed response body (opaque server error payload).
43
+ """
44
+ super().__init__(message)
45
+ self.status_code: int = status_code
46
+ self.body: Any = body
47
+
48
+
49
+ class AuthError(APIStatusError):
50
+ """Raised on 401 (unauthenticated) or 403 (unauthorized) responses."""
51
+
52
+
53
+ class NotFoundError(APIStatusError):
54
+ """Raised on 404 responses (the target resource does not exist)."""
55
+
56
+
57
+ class ConflictError(APIStatusError):
58
+ """Raised on 409 responses (the request conflicts with the resource's current state)."""
59
+
60
+
61
+ class UnprocessableError(APIStatusError):
62
+ """Raised on 422 responses (the request body failed server-side validation)."""
63
+
64
+
65
+ # Maps an HTTP status code to the most specific exception class for it.
66
+ _STATUS_TO_EXCEPTION: dict[int, type[APIStatusError]] = {
67
+ 401: AuthError,
68
+ 403: AuthError,
69
+ 404: NotFoundError,
70
+ 409: ConflictError,
71
+ 422: UnprocessableError,
72
+ }
73
+
74
+
75
+ def exception_from_response(response: httpx.Response) -> APIStatusError:
76
+ """
77
+ Build the most specific ``APIStatusError`` for an errored HTTP response.
78
+
79
+ Args:
80
+ response (httpx.Response): The response whose status is 4xx/5xx.
81
+
82
+ Returns:
83
+ APIStatusError: A ``NotFoundError`` / ``AuthError`` / ``ConflictError`` /
84
+ ``UnprocessableError`` when the status is recognised, else a plain ``APIStatusError``.
85
+ """
86
+ # 1. Decode the body as JSON when possible; fall back to raw text for non-JSON errors.
87
+ try:
88
+ body: Any = response.json()
89
+ except ValueError:
90
+ body = response.text
91
+
92
+ # 2. Pick the dedicated subclass for this status, defaulting to the generic status error.
93
+ exception_class = _STATUS_TO_EXCEPTION.get(response.status_code, APIStatusError)
94
+ message = f"API request failed with status {response.status_code}"
95
+ return exception_class(message, status_code=response.status_code, body=body)
96
+
97
+
98
+ __all__ = [
99
+ "DocForgeError",
100
+ "APIConnectionError",
101
+ "APITimeoutError",
102
+ "APIStatusError",
103
+ "AuthError",
104
+ "NotFoundError",
105
+ "ConflictError",
106
+ "UnprocessableError",
107
+ "exception_from_response",
108
+ ]
@@ -0,0 +1,34 @@
1
+ # ====== Code Summary ======
2
+ # The immutable description of a single HTTP call. A RequestSpec is pure data — it carries the method,
3
+ # API-relative path and payload, and performs no I/O. Resource classes build specs; transports execute
4
+ # them. Centralising URL/body shape here keeps it out of the async/sync transport bodies.
5
+
6
+ # ====== Standard Library Imports ======
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class RequestSpec:
13
+ """
14
+ An immutable, I/O-free description of one API request.
15
+
16
+ The ``json`` and ``files`` payloads are typed ``Any`` because they are opaque, endpoint-shaped
17
+ bodies already serialised by the calling resource (a model dump or an httpx multipart mapping).
18
+
19
+ Attributes:
20
+ method (str): The HTTP method (e.g. ``"GET"``, ``"POST"``, ``"DELETE"``).
21
+ path (str): The path relative to the API root (e.g. ``"/auth/keys"``).
22
+ params (dict[str, Any] | None): Query parameters; ``None`` values are dropped at execution.
23
+ json (Any): The JSON request body, or ``None`` for bodyless requests.
24
+ files (Any): The multipart file mapping for uploads, or ``None``.
25
+ """
26
+
27
+ method: str
28
+ path: str
29
+ params: dict[str, Any] | None = None
30
+ json: Any = None
31
+ files: Any = None
32
+
33
+
34
+ __all__ = ["RequestSpec"]