ragwell 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.
Files changed (142) hide show
  1. ragwell/__init__.py +50 -0
  2. ragwell/_common.py +393 -0
  3. ragwell/_contract/manifest.json +140 -0
  4. ragwell/_contract/openapi.json +8778 -0
  5. ragwell/_deadline.py +78 -0
  6. ragwell/_decoding.py +61 -0
  7. ragwell/_generated/__init__.py +8 -0
  8. ragwell/_generated/api/__init__.py +1 -0
  9. ragwell/_generated/api/document_deletion/__init__.py +1 -0
  10. ragwell/_generated/api/document_deletion/delete_document_v1_projects_project_id_documents_document_id_delete.py +228 -0
  11. ragwell/_generated/api/document_deletion/get_document_deletion_v1_projects_project_id_document_deletions_receipt_id_get.py +201 -0
  12. ragwell/_generated/api/document_deletion/retry_document_deletion_v1_projects_project_id_document_deletions_receipt_id_retry_post.py +233 -0
  13. ragwell/_generated/api/document_inspection/__init__.py +1 -0
  14. ragwell/_generated/api/document_inspection/create_document_export_v1_projects_project_id_documents_document_id_exports_post.py +246 -0
  15. ragwell/_generated/api/document_inspection/download_document_export_part_v1_projects_project_id_documents_document_id_exports_export_id_parts_part_number_get.py +239 -0
  16. ragwell/_generated/api/document_inspection/get_document_activity_v1_projects_project_id_documents_document_id_activity_get.py +234 -0
  17. ragwell/_generated/api/document_inspection/get_document_export_v1_projects_project_id_documents_document_id_exports_export_id_get.py +220 -0
  18. ragwell/_generated/api/document_inspection/inspect_document_source_v1_projects_project_id_documents_document_id_generations_generation_id_sources_source_id_get.py +275 -0
  19. ragwell/_generated/api/document_inspection/inspect_document_v1_projects_project_id_documents_document_id_inspection_get.py +234 -0
  20. ragwell/_generated/api/document_inspection/list_document_chunks_v1_projects_project_id_documents_document_id_generations_generation_id_chunks_get.py +306 -0
  21. ragwell/_generated/api/document_inspection/list_document_generations_v1_projects_project_id_documents_document_id_generations_get.py +249 -0
  22. ragwell/_generated/api/documents/__init__.py +1 -0
  23. ragwell/_generated/api/documents/create_upload_v1_projects_project_id_uploads_post.py +273 -0
  24. ragwell/_generated/api/documents/finalize_upload_v1_projects_project_id_uploads_upload_id_finalize_post.py +211 -0
  25. ragwell/_generated/api/documents/get_document_upload_policy_v1_document_upload_policy_get.py +144 -0
  26. ragwell/_generated/api/documents/get_document_v1_projects_project_id_documents_document_id_get.py +201 -0
  27. ragwell/_generated/api/documents/get_upload_state.py +206 -0
  28. ragwell/_generated/api/documents/list_documents_v1_projects_project_id_documents_get.py +366 -0
  29. ragwell/_generated/api/documents/replace_document_metadata_v1_projects_project_id_documents_document_id_metadata_put.py +251 -0
  30. ragwell/_generated/api/documents/upload_content_v1_projects_project_id_uploads_upload_id_content_put.py +281 -0
  31. ragwell/_generated/api/embedding_connections/__init__.py +1 -0
  32. ragwell/_generated/api/embedding_connections/inspect_embedding_connection_v1_projects_project_id_embedding_connection_get.py +192 -0
  33. ragwell/_generated/api/ingestion/__init__.py +1 -0
  34. ragwell/_generated/api/ingestion/cancel_job_v1_projects_project_id_jobs_job_id_cancel_post.py +233 -0
  35. ragwell/_generated/api/ingestion/get_job_v1_projects_project_id_jobs_job_id_get.py +201 -0
  36. ragwell/_generated/api/ingestion/list_jobs_v1_projects_project_id_jobs_get.py +387 -0
  37. ragwell/_generated/api/ingestion/retry_job_v1_projects_project_id_jobs_job_id_retry_post.py +233 -0
  38. ragwell/_generated/api/projects/__init__.py +1 -0
  39. ragwell/_generated/api/projects/get_project_v1_projects_project_id_get.py +187 -0
  40. ragwell/_generated/api/retrieval/__init__.py +1 -0
  41. ragwell/_generated/api/retrieval/search_v1_projects_project_id_search_post.py +235 -0
  42. ragwell/_generated/client.py +274 -0
  43. ragwell/_generated/errors.py +16 -0
  44. ragwell/_generated/models/__init__.py +191 -0
  45. ragwell/_generated/models/accepted_document_format_response.py +96 -0
  46. ragwell/_generated/models/accepted_document_format_response_media_type.py +10 -0
  47. ragwell/_generated/models/attempt_status.py +11 -0
  48. ragwell/_generated/models/chunk_part.py +171 -0
  49. ragwell/_generated/models/chunk_part_kind.py +10 -0
  50. ragwell/_generated/models/chunk_stage_progress.py +53 -0
  51. ragwell/_generated/models/chunk_stage_progress_phase.py +9 -0
  52. ragwell/_generated/models/command_acceptance.py +121 -0
  53. ragwell/_generated/models/command_acceptance_operation.py +12 -0
  54. ragwell/_generated/models/create_document_export_request.py +48 -0
  55. ragwell/_generated/models/create_upload_request.py +96 -0
  56. ragwell/_generated/models/create_upload_request_declared_media_type.py +10 -0
  57. ragwell/_generated/models/document_annotation_field_response.py +81 -0
  58. ragwell/_generated/models/document_annotation_field_response_key.py +11 -0
  59. ragwell/_generated/models/document_annotation_policy_response.py +116 -0
  60. ragwell/_generated/models/document_chunk_activity.py +78 -0
  61. ragwell/_generated/models/document_chunk_info.py +228 -0
  62. ragwell/_generated/models/document_chunk_page.py +105 -0
  63. ragwell/_generated/models/document_deletion_response.py +268 -0
  64. ragwell/_generated/models/document_deletion_state.py +12 -0
  65. ragwell/_generated/models/document_embedding_usage.py +227 -0
  66. ragwell/_generated/models/document_embedding_usage_totals.py +152 -0
  67. ragwell/_generated/models/document_export_part_response.py +70 -0
  68. ragwell/_generated/models/document_export_response.py +205 -0
  69. ragwell/_generated/models/document_export_response_state.py +12 -0
  70. ragwell/_generated/models/document_format.py +10 -0
  71. ragwell/_generated/models/document_generation_info.py +260 -0
  72. ragwell/_generated/models/document_generation_page.py +95 -0
  73. ragwell/_generated/models/document_inspection_response.py +325 -0
  74. ragwell/_generated/models/document_job_info.py +275 -0
  75. ragwell/_generated/models/document_list_response.py +95 -0
  76. ragwell/_generated/models/document_metadata_input.py +106 -0
  77. ragwell/_generated/models/document_metadata_response.py +102 -0
  78. ragwell/_generated/models/document_profile_info.py +174 -0
  79. ragwell/_generated/models/document_response.py +158 -0
  80. ragwell/_generated/models/document_retrieval_activity.py +156 -0
  81. ragwell/_generated/models/document_source_info.py +102 -0
  82. ragwell/_generated/models/document_source_preview.py +161 -0
  83. ragwell/_generated/models/document_stage_attempt.py +133 -0
  84. ragwell/_generated/models/document_state.py +12 -0
  85. ragwell/_generated/models/document_upload_policy_response.py +94 -0
  86. ragwell/_generated/models/document_version_response.py +109 -0
  87. ragwell/_generated/models/document_version_state.py +11 -0
  88. ragwell/_generated/models/embedding_connection_notice_response.py +124 -0
  89. ragwell/_generated/models/embedding_connection_notice_response_lane.py +9 -0
  90. ragwell/_generated/models/embedding_connection_notice_response_state.py +9 -0
  91. ragwell/_generated/models/embedding_connection_response.py +196 -0
  92. ragwell/_generated/models/embedding_connection_response_funding_source.py +9 -0
  93. ragwell/_generated/models/embedding_connection_response_health.py +10 -0
  94. ragwell/_generated/models/embedding_connection_response_status.py +11 -0
  95. ragwell/_generated/models/embedding_serialization.py +65 -0
  96. ragwell/_generated/models/error_detail.py +91 -0
  97. ragwell/_generated/models/error_response.py +62 -0
  98. ragwell/_generated/models/field_error.py +70 -0
  99. ragwell/_generated/models/finalized_intake_response.py +88 -0
  100. ragwell/_generated/models/index_generation_status.py +10 -0
  101. ragwell/_generated/models/ingestion_job_list_response.py +83 -0
  102. ragwell/_generated/models/ingestion_job_response.py +318 -0
  103. ragwell/_generated/models/ingestion_job_status.py +14 -0
  104. ragwell/_generated/models/ingestion_stage.py +13 -0
  105. ragwell/_generated/models/limit_key.py +13 -0
  106. ragwell/_generated/models/limit_unit.py +12 -0
  107. ragwell/_generated/models/plan_limit_error_detail.py +178 -0
  108. ragwell/_generated/models/plan_limit_error_detail_code.py +9 -0
  109. ragwell/_generated/models/plan_limit_error_response.py +62 -0
  110. ragwell/_generated/models/processing_profile_editability_response.py +99 -0
  111. ragwell/_generated/models/processing_profile_editability_response_reason_code_type_0.py +9 -0
  112. ragwell/_generated/models/processing_profile_summary_response.py +153 -0
  113. ragwell/_generated/models/processing_profile_technical_response.py +174 -0
  114. ragwell/_generated/models/processing_service_class_id.py +10 -0
  115. ragwell/_generated/models/processing_service_class_response.py +72 -0
  116. ragwell/_generated/models/project_response.py +126 -0
  117. ragwell/_generated/models/replace_document_metadata_request.py +57 -0
  118. ragwell/_generated/models/retrieval_citation_response.py +91 -0
  119. ragwell/_generated/models/retrieval_item_response.py +178 -0
  120. ragwell/_generated/models/retrieval_scores_response.py +82 -0
  121. ragwell/_generated/models/search_filters.py +110 -0
  122. ragwell/_generated/models/search_request.py +84 -0
  123. ragwell/_generated/models/search_response.py +98 -0
  124. ragwell/_generated/models/source_coordinate_kind.py +9 -0
  125. ragwell/_generated/models/source_span.py +155 -0
  126. ragwell/_generated/models/structural_unit.py +141 -0
  127. ragwell/_generated/models/structural_unit_kind.py +13 -0
  128. ragwell/_generated/models/text_retrieval_citation_response.py +98 -0
  129. ragwell/_generated/models/upload_session_response.py +262 -0
  130. ragwell/_generated/models/upload_state.py +12 -0
  131. ragwell/_generated/types.py +54 -0
  132. ragwell/_sync_http.py +217 -0
  133. ragwell/_transport.py +891 -0
  134. ragwell/async_client.py +1118 -0
  135. ragwell/client.py +1099 -0
  136. ragwell/errors.py +205 -0
  137. ragwell/py.typed +0 -0
  138. ragwell/types.py +34 -0
  139. ragwell-0.1.0.dist-info/METADATA +179 -0
  140. ragwell-0.1.0.dist-info/RECORD +142 -0
  141. ragwell-0.1.0.dist-info/WHEEL +4 -0
  142. ragwell-0.1.0.dist-info/licenses/LICENSE +21 -0
ragwell/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """Typed synchronous and asynchronous clients for the Ragwell retrieval API."""
2
+
3
+ from .async_client import AsyncRagwell
4
+ from .client import Ragwell
5
+ from .errors import (
6
+ ApiError,
7
+ AuthenticationError,
8
+ ConfigurationError,
9
+ ConflictError,
10
+ DownloadIntegrityError,
11
+ NotFoundError,
12
+ OperationFailedError,
13
+ PaginationError,
14
+ PermissionDeniedError,
15
+ ProtocolError,
16
+ QuotaExceededError,
17
+ RagwellError,
18
+ RateLimitError,
19
+ ServerError,
20
+ TransportError,
21
+ TransportTimeout,
22
+ ValidationError,
23
+ WaitTimeoutError,
24
+ )
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ __all__ = [
29
+ "ApiError",
30
+ "AsyncRagwell",
31
+ "AuthenticationError",
32
+ "ConfigurationError",
33
+ "ConflictError",
34
+ "DownloadIntegrityError",
35
+ "NotFoundError",
36
+ "OperationFailedError",
37
+ "PaginationError",
38
+ "PermissionDeniedError",
39
+ "ProtocolError",
40
+ "QuotaExceededError",
41
+ "Ragwell",
42
+ "RagwellError",
43
+ "RateLimitError",
44
+ "ServerError",
45
+ "TransportError",
46
+ "TransportTimeout",
47
+ "ValidationError",
48
+ "WaitTimeoutError",
49
+ "__version__",
50
+ ]
ragwell/_common.py ADDED
@@ -0,0 +1,393 @@
1
+ """Shared validation, pagination, and bounded file helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import io
8
+ import os
9
+ import re
10
+ import tempfile
11
+ from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping
12
+ from pathlib import Path
13
+ from types import TracebackType
14
+ from typing import Any, BinaryIO, Self, TypeAlias, TypeVar
15
+ from urllib.parse import urlsplit
16
+ from uuid import UUID, uuid4
17
+
18
+ import httpx
19
+
20
+ from .errors import ConfigurationError
21
+
22
+ UploadInput: TypeAlias = str | os.PathLike[str] | bytes | BinaryIO
23
+ _IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
24
+ _MEDIA_BY_SUFFIX = {
25
+ ".pdf": "application/pdf",
26
+ ".txt": "text/plain",
27
+ ".md": "text/markdown",
28
+ ".markdown": "text/markdown",
29
+ }
30
+ SUPPORTED_MEDIA_TYPES = frozenset(_MEDIA_BY_SUFFIX.values())
31
+
32
+
33
+ def resolve_configuration(
34
+ *, base_url: str | None, api_key: str | None
35
+ ) -> tuple[httpx.URL, str]:
36
+ resolved_base = base_url if base_url is not None else os.getenv("RAGWELL_BASE_URL")
37
+ resolved_key = api_key if api_key is not None else os.getenv("RAGWELL_API_KEY")
38
+ if not resolved_base:
39
+ raise ConfigurationError(
40
+ "Set base_url or RAGWELL_BASE_URL; the beta SDK has no default endpoint"
41
+ )
42
+ if not resolved_key or not resolved_key.strip():
43
+ raise ConfigurationError("Set api_key or RAGWELL_API_KEY")
44
+ parts = urlsplit(resolved_base)
45
+ if parts.scheme not in {"https", "http"} or not parts.hostname:
46
+ raise ConfigurationError("base_url must be an absolute HTTP(S) URL")
47
+ if parts.username is not None or parts.password is not None:
48
+ raise ConfigurationError("base_url must not contain credentials")
49
+ if parts.query or parts.fragment:
50
+ raise ConfigurationError("base_url must not contain a query or fragment")
51
+ if parts.scheme == "http" and parts.hostname not in {
52
+ "localhost",
53
+ "127.0.0.1",
54
+ "::1",
55
+ }:
56
+ raise ConfigurationError("Plain HTTP is allowed only for loopback development")
57
+ return httpx.URL(resolved_base.rstrip("/")), resolved_key.strip()
58
+
59
+
60
+ def resource_id(value: UUID | str, name: str) -> UUID:
61
+ if isinstance(value, UUID):
62
+ return value
63
+ try:
64
+ return UUID(value)
65
+ except (TypeError, ValueError, AttributeError) as exc:
66
+ raise ValueError(f"{name} must be a UUID") from exc
67
+
68
+
69
+ def idempotency_key(value: str | None) -> str:
70
+ key = value if value is not None else f"rw_{uuid4()}"
71
+ if not _IDEMPOTENCY_RE.fullmatch(key):
72
+ raise ValueError(
73
+ "idempotency_key must be 8-128 characters from A-Z, a-z, 0-9, . _ : -"
74
+ )
75
+ return key
76
+
77
+
78
+ def _query_value(value: Any) -> str | int | float | bool | None:
79
+ if value is None or isinstance(value, (str, int, float, bool)):
80
+ return value
81
+ if hasattr(value, "value"):
82
+ return _query_value(value.value)
83
+ return str(value)
84
+
85
+
86
+ def query_params(**values: Any) -> list[tuple[str, str | int | float | bool | None]]:
87
+ result: list[tuple[str, str | int | float | bool | None]] = []
88
+ for name, value in values.items():
89
+ if value is None:
90
+ continue
91
+ if isinstance(value, (list, tuple, set, frozenset)):
92
+ result.extend((name, _query_value(item)) for item in value)
93
+ else:
94
+ result.append((name, _query_value(value)))
95
+ return result
96
+
97
+
98
+ def metadata_dict(value: Any) -> Mapping[str, Any]:
99
+ if hasattr(value, "to_dict"):
100
+ result = value.to_dict()
101
+ if isinstance(result, dict):
102
+ return result
103
+ if isinstance(value, Mapping):
104
+ return value
105
+ raise TypeError("request body must be a generated model or mapping")
106
+
107
+
108
+ class PreparedUpload:
109
+ """Seekable upload content whose original cursor/ownership are preserved."""
110
+
111
+ def __init__(
112
+ self,
113
+ *,
114
+ stream: BinaryIO,
115
+ owned: bool,
116
+ start: int,
117
+ filename: str,
118
+ media_type: str,
119
+ size: int,
120
+ sha256: str,
121
+ ) -> None:
122
+ self.stream = stream
123
+ self.owned = owned
124
+ self.start = start
125
+ self.filename = filename
126
+ self.media_type = media_type
127
+ self.size = size
128
+ self.sha256 = sha256
129
+
130
+ def rewind(self) -> None:
131
+ self.stream.seek(self.start)
132
+
133
+ def close(self) -> None:
134
+ if self.owned:
135
+ self.stream.close()
136
+ else:
137
+ self.stream.seek(self.start)
138
+
139
+ def __enter__(self) -> Self:
140
+ return self
141
+
142
+ def __exit__(
143
+ self,
144
+ exc_type: type[BaseException] | None,
145
+ exc_value: BaseException | None,
146
+ traceback: TracebackType | None,
147
+ ) -> None:
148
+ self.close()
149
+
150
+
151
+ class AsyncFileContent(httpx.AsyncByteStream):
152
+ """Read a caller-owned seekable stream without blocking the event loop."""
153
+
154
+ def __init__(self, stream: BinaryIO, *, chunk_size: int = 1024 * 1024) -> None:
155
+ self.stream = stream
156
+ self.chunk_size = chunk_size
157
+
158
+ async def __aiter__(self) -> AsyncIterator[bytes]:
159
+ while True:
160
+ chunk = await file_call(self.stream.read, self.chunk_size)
161
+ if not chunk:
162
+ return
163
+ yield chunk
164
+
165
+
166
+ class SyncFileContent(httpx.SyncByteStream):
167
+ """Read a caller-owned stream in fixed-size chunks."""
168
+
169
+ def __init__(self, stream: BinaryIO, *, chunk_size: int = 1024 * 1024) -> None:
170
+ self.stream = stream
171
+ self.chunk_size = chunk_size
172
+
173
+ def __iter__(self) -> Iterator[bytes]:
174
+ while True:
175
+ chunk = self.stream.read(self.chunk_size)
176
+ if not chunk:
177
+ return
178
+ yield chunk
179
+
180
+
181
+ def prepare_upload(
182
+ source: UploadInput,
183
+ *,
184
+ filename: str | None,
185
+ media_type: str | None,
186
+ chunk_size: int = 1024 * 1024,
187
+ check: Callable[[], None] = lambda: None,
188
+ ) -> PreparedUpload:
189
+ check()
190
+ owned = False
191
+ if isinstance(source, bytes):
192
+ if not filename:
193
+ raise ValueError("filename is required when uploading bytes")
194
+ stream: BinaryIO = io.BytesIO(source)
195
+ owned = True
196
+ start = 0
197
+ resolved_filename = filename
198
+ elif isinstance(source, (str, os.PathLike)):
199
+ path = Path(source)
200
+ stream = path.open("rb")
201
+ owned = True
202
+ start = 0
203
+ resolved_filename = filename or path.name
204
+ else:
205
+ stream = source
206
+ if not filename:
207
+ raise ValueError("filename is required when uploading a file object")
208
+ if not stream.seekable():
209
+ raise ValueError(
210
+ "high-level upload requires a seekable stream; use uploads.upload_content "
211
+ "with declared metadata for a bounded non-seekable source"
212
+ )
213
+ try:
214
+ start = stream.tell()
215
+ except (OSError, ValueError) as exc:
216
+ raise ValueError("upload stream must expose a stable cursor") from exc
217
+ resolved_filename = filename
218
+
219
+ suffix = Path(resolved_filename).suffix.lower()
220
+ resolved_media = media_type or _MEDIA_BY_SUFFIX.get(suffix)
221
+ if resolved_media not in SUPPORTED_MEDIA_TYPES:
222
+ if owned:
223
+ stream.close()
224
+ raise ValueError(
225
+ "media_type must be application/pdf, text/plain, or text/markdown"
226
+ )
227
+
228
+ digest = hashlib.sha256()
229
+ size = 0
230
+ try:
231
+ while True:
232
+ check()
233
+ chunk = stream.read(chunk_size)
234
+ check()
235
+ if not chunk:
236
+ break
237
+ if not isinstance(chunk, bytes):
238
+ raise TypeError("upload stream must be opened in binary mode")
239
+ size += len(chunk)
240
+ digest.update(chunk)
241
+ stream.seek(start)
242
+ except BaseException:
243
+ if owned:
244
+ stream.close()
245
+ else:
246
+ try:
247
+ stream.seek(start)
248
+ except (OSError, ValueError):
249
+ pass
250
+ raise
251
+ return PreparedUpload(
252
+ stream=stream,
253
+ owned=owned,
254
+ start=start,
255
+ filename=resolved_filename,
256
+ media_type=resolved_media,
257
+ size=size,
258
+ sha256=digest.hexdigest(),
259
+ )
260
+
261
+
262
+ def create_temporary_destination(
263
+ destination: Path, *, overwrite: bool
264
+ ) -> tuple[BinaryIO, Path]:
265
+ destination = destination.expanduser()
266
+ destination.parent.mkdir(parents=True, exist_ok=True)
267
+ if destination.exists() and not overwrite:
268
+ raise FileExistsError(destination)
269
+ descriptor, temporary_name = tempfile.mkstemp(
270
+ dir=destination.parent,
271
+ prefix=f".{destination.name}.",
272
+ suffix=".part",
273
+ )
274
+ return os.fdopen(descriptor, "wb"), Path(temporary_name)
275
+
276
+
277
+ def remove_temporary(path: Path) -> None:
278
+ try:
279
+ path.unlink()
280
+ except FileNotFoundError:
281
+ pass
282
+
283
+
284
+ def finalize_temporary(temporary: Path, destination: Path, *, overwrite: bool) -> None:
285
+ if destination.exists() and not overwrite:
286
+ raise FileExistsError(destination)
287
+ os.replace(temporary, destination)
288
+
289
+
290
+ def repeated_cursor(cursor: object, seen: set[object]) -> bool:
291
+ if cursor is None:
292
+ return False
293
+ if cursor in seen:
294
+ return True
295
+ seen.add(cursor)
296
+ return False
297
+
298
+
299
+ def as_uuid_list(values: Iterable[UUID | str] | None) -> list[UUID] | None:
300
+ if values is None:
301
+ return None
302
+ return [resource_id(value, "document_id") for value in values]
303
+
304
+
305
+ __all__ = [
306
+ "AsyncFileContent",
307
+ "PreparedUpload",
308
+ "SUPPORTED_MEDIA_TYPES",
309
+ "SyncFileContent",
310
+ "UploadInput",
311
+ "as_uuid_list",
312
+ "create_temporary_destination",
313
+ "finalize_temporary",
314
+ "idempotency_key",
315
+ "metadata_dict",
316
+ "prepare_upload",
317
+ "query_params",
318
+ "remove_temporary",
319
+ "repeated_cursor",
320
+ "resolve_configuration",
321
+ "resource_id",
322
+ ]
323
+
324
+
325
+ FileT = TypeVar("FileT")
326
+
327
+
328
+ async def file_call(
329
+ function: Callable[..., FileT],
330
+ *args: Any,
331
+ _cancel_result: Callable[[FileT], None] | None = None,
332
+ **kwargs: Any,
333
+ ) -> FileT:
334
+ """Join in-flight file I/O before restoring/closing its handle on cancellation."""
335
+ task = asyncio.create_task(asyncio.to_thread(function, *args, **kwargs))
336
+ try:
337
+ return await asyncio.shield(task)
338
+ except asyncio.CancelledError:
339
+ try:
340
+ value = await _join_file(task)
341
+ except Exception:
342
+ pass
343
+ else:
344
+ if _cancel_result is not None:
345
+ _cancel_result(value)
346
+ raise
347
+
348
+
349
+ async def prepare_upload_async(
350
+ source: UploadInput,
351
+ *,
352
+ filename: str | None,
353
+ media_type: str | None,
354
+ check: Callable[[], None],
355
+ ) -> PreparedUpload:
356
+ # A worker owns preparation until handoff. Cancellation must not lose an
357
+ # opened SDK file or leave a caller's cursor at the end of the hash pass.
358
+ task = asyncio.create_task(
359
+ asyncio.to_thread(
360
+ prepare_upload,
361
+ source,
362
+ filename=filename,
363
+ media_type=media_type,
364
+ check=check,
365
+ )
366
+ )
367
+ try:
368
+ return await asyncio.shield(task)
369
+ except asyncio.CancelledError:
370
+ try:
371
+ prepared = await _join_file(task)
372
+ except Exception:
373
+ pass
374
+ else:
375
+ await file_call(prepared.close)
376
+ raise
377
+
378
+
379
+ async def _join_file(task: asyncio.Task[FileT]) -> FileT:
380
+ while True:
381
+ try:
382
+ return await asyncio.shield(task)
383
+ except asyncio.CancelledError:
384
+ if task.cancelled():
385
+ raise
386
+
387
+
388
+ def discard_temporary(value: tuple[BinaryIO, Path]) -> None:
389
+ output, temporary = value
390
+ try:
391
+ output.close()
392
+ finally:
393
+ remove_temporary(temporary)
@@ -0,0 +1,140 @@
1
+ {
2
+ "api_info_version": "0.1.0",
3
+ "artifact_version": "2026-09-19.1",
4
+ "canonical_sha256": "57a1f942f7ca8b89186753a20320588b57e2425663b8529b36037066c02328bb",
5
+ "compatibility_notes": "See contracts/machine/README.md. Artifact identity is its digest, not runtime version equality. Installed Python SDK qualification and public publication remain separate release gates.",
6
+ "machine_sha256": "8e05de4ae0f3aa76261e97ad07be1fc77756f317c3e2c20ab2be3efb2cb6db90",
7
+ "operation_count": 26,
8
+ "operations": [
9
+ {
10
+ "method": "GET",
11
+ "operation_id": "get_document_upload_policy_v1_document_upload_policy_get",
12
+ "path": "/v1/document-upload-policy"
13
+ },
14
+ {
15
+ "method": "GET",
16
+ "operation_id": "get_project_v1_projects__project_id__get",
17
+ "path": "/v1/projects/{project_id}"
18
+ },
19
+ {
20
+ "method": "GET",
21
+ "operation_id": "get_document_deletion_v1_projects__project_id__document_deletions__receipt_id__get",
22
+ "path": "/v1/projects/{project_id}/document-deletions/{receipt_id}"
23
+ },
24
+ {
25
+ "method": "POST",
26
+ "operation_id": "retry_document_deletion_v1_projects__project_id__document_deletions__receipt_id__retry_post",
27
+ "path": "/v1/projects/{project_id}/document-deletions/{receipt_id}/retry"
28
+ },
29
+ {
30
+ "method": "GET",
31
+ "operation_id": "list_documents_v1_projects__project_id__documents_get",
32
+ "path": "/v1/projects/{project_id}/documents"
33
+ },
34
+ {
35
+ "method": "DELETE",
36
+ "operation_id": "delete_document_v1_projects__project_id__documents__document_id__delete",
37
+ "path": "/v1/projects/{project_id}/documents/{document_id}"
38
+ },
39
+ {
40
+ "method": "GET",
41
+ "operation_id": "get_document_v1_projects__project_id__documents__document_id__get",
42
+ "path": "/v1/projects/{project_id}/documents/{document_id}"
43
+ },
44
+ {
45
+ "method": "GET",
46
+ "operation_id": "get_document_activity_v1_projects__project_id__documents__document_id__activity_get",
47
+ "path": "/v1/projects/{project_id}/documents/{document_id}/activity"
48
+ },
49
+ {
50
+ "method": "POST",
51
+ "operation_id": "create_document_export_v1_projects__project_id__documents__document_id__exports_post",
52
+ "path": "/v1/projects/{project_id}/documents/{document_id}/exports"
53
+ },
54
+ {
55
+ "method": "GET",
56
+ "operation_id": "get_document_export_v1_projects__project_id__documents__document_id__exports__export_id__get",
57
+ "path": "/v1/projects/{project_id}/documents/{document_id}/exports/{export_id}"
58
+ },
59
+ {
60
+ "method": "GET",
61
+ "operation_id": "download_document_export_part_v1_projects__project_id__documents__document_id__exports__export_id__parts__part_number__get",
62
+ "path": "/v1/projects/{project_id}/documents/{document_id}/exports/{export_id}/parts/{part_number}"
63
+ },
64
+ {
65
+ "method": "GET",
66
+ "operation_id": "list_document_generations_v1_projects__project_id__documents__document_id__generations_get",
67
+ "path": "/v1/projects/{project_id}/documents/{document_id}/generations"
68
+ },
69
+ {
70
+ "method": "GET",
71
+ "operation_id": "list_document_chunks_v1_projects__project_id__documents__document_id__generations__generation_id__chunks_get",
72
+ "path": "/v1/projects/{project_id}/documents/{document_id}/generations/{generation_id}/chunks"
73
+ },
74
+ {
75
+ "method": "GET",
76
+ "operation_id": "inspect_document_source_v1_projects__project_id__documents__document_id__generations__generation_id__sources__source_id__get",
77
+ "path": "/v1/projects/{project_id}/documents/{document_id}/generations/{generation_id}/sources/{source_id}"
78
+ },
79
+ {
80
+ "method": "GET",
81
+ "operation_id": "inspect_document_v1_projects__project_id__documents__document_id__inspection_get",
82
+ "path": "/v1/projects/{project_id}/documents/{document_id}/inspection"
83
+ },
84
+ {
85
+ "method": "PUT",
86
+ "operation_id": "replace_document_metadata_v1_projects__project_id__documents__document_id__metadata_put",
87
+ "path": "/v1/projects/{project_id}/documents/{document_id}/metadata"
88
+ },
89
+ {
90
+ "method": "GET",
91
+ "operation_id": "inspect_embedding_connection_v1_projects__project_id__embedding_connection_get",
92
+ "path": "/v1/projects/{project_id}/embedding-connection"
93
+ },
94
+ {
95
+ "method": "GET",
96
+ "operation_id": "list_jobs_v1_projects__project_id__jobs_get",
97
+ "path": "/v1/projects/{project_id}/jobs"
98
+ },
99
+ {
100
+ "method": "GET",
101
+ "operation_id": "get_job_v1_projects__project_id__jobs__job_id__get",
102
+ "path": "/v1/projects/{project_id}/jobs/{job_id}"
103
+ },
104
+ {
105
+ "method": "POST",
106
+ "operation_id": "cancel_job_v1_projects__project_id__jobs__job_id__cancel_post",
107
+ "path": "/v1/projects/{project_id}/jobs/{job_id}/cancel"
108
+ },
109
+ {
110
+ "method": "POST",
111
+ "operation_id": "retry_job_v1_projects__project_id__jobs__job_id__retry_post",
112
+ "path": "/v1/projects/{project_id}/jobs/{job_id}/retry"
113
+ },
114
+ {
115
+ "method": "POST",
116
+ "operation_id": "search_v1_projects__project_id__search_post",
117
+ "path": "/v1/projects/{project_id}/search"
118
+ },
119
+ {
120
+ "method": "POST",
121
+ "operation_id": "create_upload_v1_projects__project_id__uploads_post",
122
+ "path": "/v1/projects/{project_id}/uploads"
123
+ },
124
+ {
125
+ "method": "GET",
126
+ "operation_id": "get_upload_state",
127
+ "path": "/v1/projects/{project_id}/uploads/{upload_id}"
128
+ },
129
+ {
130
+ "method": "PUT",
131
+ "operation_id": "upload_content_v1_projects__project_id__uploads__upload_id__content_put",
132
+ "path": "/v1/projects/{project_id}/uploads/{upload_id}/content"
133
+ },
134
+ {
135
+ "method": "POST",
136
+ "operation_id": "finalize_upload_v1_projects__project_id__uploads__upload_id__finalize_post",
137
+ "path": "/v1/projects/{project_id}/uploads/{upload_id}/finalize"
138
+ }
139
+ ]
140
+ }