polygres-sdk 0.2.1__tar.gz → 0.3.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 (76) hide show
  1. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/CONTRIBUTING.md +8 -5
  2. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/PKG-INFO +27 -3
  3. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/README.md +26 -2
  4. polygres_sdk-0.3.0/compatibility/0.2.1/README.md +8 -0
  5. {polygres_sdk-0.2.1/src/polygres/spec → polygres_sdk-0.3.0/compatibility/0.2.1}/python-sdk-v1.methods.json +7 -4
  6. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/docs/reference-v1.md +42 -1
  7. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/pyproject.toml +1 -1
  8. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/__init__.py +8 -0
  9. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/enums.py +2 -0
  10. polygres_sdk-0.3.0/src/polygres/_vendor/polygres_lib/auth/errors.py +230 -0
  11. polygres_sdk-0.3.0/src/polygres/_vendor/polygres_lib/context/errors.py +103 -0
  12. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/models.py +3 -3
  13. polygres_sdk-0.3.0/src/polygres/_vendor/polygres_lib/errors/__init__.py +27 -0
  14. polygres_sdk-0.3.0/src/polygres/_vendor/polygres_lib/errors/catalog.py +205 -0
  15. polygres_sdk-0.3.0/src/polygres/_vendor/polygres_lib/errors/generated.py +9385 -0
  16. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/client.py +55 -4
  17. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/context_wait.py +23 -14
  18. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/errors.py +15 -32
  19. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/models.py +99 -0
  20. polygres_sdk-0.3.0/src/polygres/rows.py +384 -0
  21. polygres_sdk-0.3.0/src/polygres/spec/python-sdk-v1.methods.json +4892 -0
  22. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/spec/runtime-v1.openapi.json +3554 -2114
  23. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/fixtures/context/contract-fixtures.json +20 -15
  24. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_client.py +15 -2
  25. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_context.py +13 -0
  26. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_method_spec.py +1 -1
  27. polygres_sdk-0.3.0/tests/test_release_workflow.py +95 -0
  28. polygres_sdk-0.3.0/tests/test_rows.py +279 -0
  29. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_version.py +6 -6
  30. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_version_notices.py +9 -9
  31. polygres_sdk-0.3.0/tools/extract_release_notes.py +57 -0
  32. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/generate_sdk_method_spec.py +2 -0
  33. polygres_sdk-0.2.1/src/polygres/_vendor/polygres_lib/auth/errors.py +0 -895
  34. polygres_sdk-0.2.1/src/polygres/_vendor/polygres_lib/context/errors.py +0 -266
  35. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/.gitignore +0 -0
  36. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/LICENSE +0 -0
  37. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/compatibility/0.1.0/README.md +0 -0
  38. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/compatibility/0.1.0/python-sdk-v1.methods.json +0 -0
  39. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/migrations/README.md +0 -0
  40. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/migrations/vector-configuration-creation.md +0 -0
  41. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_api_version.py +0 -0
  42. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_method_metadata.py +0 -0
  43. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/__init__.py +0 -0
  44. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/__init__.py +0 -0
  45. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/api_versions.py +0 -0
  46. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/__init__.py +0 -0
  47. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/api_keys.py +0 -0
  48. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/models.py +0 -0
  49. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/ports.py +0 -0
  50. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/principals.py +0 -0
  51. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/records.py +0 -0
  52. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/types.py +0 -0
  53. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/__init__.py +0 -0
  54. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/enums.py +0 -0
  55. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/validation.py +0 -0
  56. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/__init__.py +0 -0
  57. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/models.py +0 -0
  58. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/types.py +0 -0
  59. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/_version.py +0 -0
  60. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/context.py +0 -0
  61. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/context_models.py +0 -0
  62. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/context_validation.py +0 -0
  63. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/py.typed +0 -0
  64. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/spec/__init__.py +0 -0
  65. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/spec/python-sdk-v1.methods.schema.json +0 -0
  66. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/src/polygres/version_notices.py +0 -0
  67. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/conftest.py +0 -0
  68. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_compatibility.py +0 -0
  69. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tests/test_packaged_resources.py +0 -0
  70. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/check_sdk_compatibility.py +0 -0
  71. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/generate_runtime_openapi_snapshot.py +0 -0
  72. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/run_context_live_test.sh +0 -0
  73. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/runtime_operation_exclusions.json +0 -0
  74. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/test_context_live.py +0 -0
  75. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/verify_distribution.py +0 -0
  76. {polygres_sdk-0.2.1 → polygres_sdk-0.3.0}/tools/verify_release_version.py +0 -0
@@ -23,7 +23,7 @@ python tools/generate_runtime_openapi_snapshot.py --check
23
23
  python tools/generate_sdk_method_spec.py --check
24
24
  python tools/generate_sdk_method_spec.py --validate-only
25
25
  python tools/check_sdk_compatibility.py \
26
- --baseline compatibility/0.1.0/python-sdk-v1.methods.json
26
+ --baseline compatibility/0.2.1/python-sdk-v1.methods.json
27
27
  python -m build
28
28
  python tools/verify_distribution.py dist/*.whl dist/*.tar.gz
29
29
  ```
@@ -60,7 +60,7 @@ Release rehearsal happens first on TestPyPI:
60
60
  /tmp/polygres-sdk-testpypi/bin/python -m pip install \
61
61
  --index-url https://test.pypi.org/simple/ \
62
62
  --extra-index-url https://pypi.org/simple/ \
63
- polygres-sdk==0.2.1
63
+ polygres-sdk==0.3.0
64
64
  /tmp/polygres-sdk-testpypi/bin/python - <<'PY'
65
65
  import importlib.metadata
66
66
  from polygres import Polygres
@@ -74,12 +74,15 @@ Publish to PyPI only after the same commit has passed TestPyPI validation. Push
74
74
  a release tag from the public repository:
75
75
 
76
76
  ```bash
77
- git tag polygres-sdk-v0.2.1
78
- git push origin polygres-sdk-v0.2.1
77
+ git tag polygres-sdk-v0.3.0
78
+ git push origin polygres-sdk-v0.3.0
79
79
  ```
80
80
 
81
81
  The `pypi` GitHub environment must be approved before the upload proceeds.
82
- Project URLs in `pyproject.toml` must use valid HTTPS URLs accepted by PyPI.
82
+ After PyPI accepts the package, the workflow creates the matching GitHub
83
+ Release from the version's changelog entry and attaches the wheel and source
84
+ distribution. Project URLs in `pyproject.toml` must use valid HTTPS URLs
85
+ accepted by PyPI.
83
86
 
84
87
  ## Scope
85
88
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: polygres-sdk
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for Polygres
5
5
  Project-URL: Homepage, https://polygres.com
6
6
  Project-URL: Documentation, https://docs.polygres.com/sdk
@@ -285,13 +285,37 @@ print(connection.direct_url_without_password)
285
285
 
286
286
  Use a PostgreSQL driver such as psycopg or SQLAlchemy when your application needs a database connection. The Polygres SDK is an HTTP retrieval client and does not bundle a PostgreSQL driver.
287
287
 
288
+ ## Single-row writes
289
+
290
+ Use `project.rows` for one JSON-native row. Context reconciliation is explicit:
291
+ omit both Context options for a generic table, or select one collection so the
292
+ same operation writes the row and creates its pgContext point.
293
+
294
+ ```python
295
+ result = project.rows.upsert(
296
+ schema="public",
297
+ table="memories",
298
+ row={"id": "memory_123", "content": "Remember the deployment window."},
299
+ conflict_columns=["id"],
300
+ returning=["id"],
301
+ context_collection_id="2e172638-bd77-4a2c-bc42-406f4f2938d7",
302
+ idempotency_key="memory-123-v1",
303
+ wait_for_context=True,
304
+ )
305
+ ```
306
+
307
+ UUIDs and timestamps are JSON strings. Arrays and vectors are JSON arrays.
308
+ Never automatically retry a row-only write after a timeout; its outcome may be
309
+ ambiguous. A Context-backed request may be resumed only with the exact same
310
+ payload and idempotency key.
311
+
288
312
  ## Version and support
289
313
 
290
- Package version: [`0.2.1`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.1).
314
+ Package version: [`0.3.0`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.3.0).
291
315
 
292
316
  When contacting support, include the installed SDK version and the request ID.
293
317
 
294
- See the [SDK 0.2.1 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.1) for release changes.
318
+ See the [SDK 0.3.0 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.3.0) for release changes.
295
319
 
296
320
  ## Optional Agent Skill
297
321
 
@@ -243,13 +243,37 @@ print(connection.direct_url_without_password)
243
243
 
244
244
  Use a PostgreSQL driver such as psycopg or SQLAlchemy when your application needs a database connection. The Polygres SDK is an HTTP retrieval client and does not bundle a PostgreSQL driver.
245
245
 
246
+ ## Single-row writes
247
+
248
+ Use `project.rows` for one JSON-native row. Context reconciliation is explicit:
249
+ omit both Context options for a generic table, or select one collection so the
250
+ same operation writes the row and creates its pgContext point.
251
+
252
+ ```python
253
+ result = project.rows.upsert(
254
+ schema="public",
255
+ table="memories",
256
+ row={"id": "memory_123", "content": "Remember the deployment window."},
257
+ conflict_columns=["id"],
258
+ returning=["id"],
259
+ context_collection_id="2e172638-bd77-4a2c-bc42-406f4f2938d7",
260
+ idempotency_key="memory-123-v1",
261
+ wait_for_context=True,
262
+ )
263
+ ```
264
+
265
+ UUIDs and timestamps are JSON strings. Arrays and vectors are JSON arrays.
266
+ Never automatically retry a row-only write after a timeout; its outcome may be
267
+ ambiguous. A Context-backed request may be resumed only with the exact same
268
+ payload and idempotency key.
269
+
246
270
  ## Version and support
247
271
 
248
- Package version: [`0.2.1`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.1).
272
+ Package version: [`0.3.0`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.3.0).
249
273
 
250
274
  When contacting support, include the installed SDK version and the request ID.
251
275
 
252
- See the [SDK 0.2.1 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.1) for release changes.
276
+ See the [SDK 0.3.0 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.3.0) for release changes.
253
277
 
254
278
  ## Optional Agent Skill
255
279
 
@@ -0,0 +1,8 @@
1
+ # SDK compatibility baseline 0.2.1
2
+
3
+ `python-sdk-v1.methods.json` is the explicit released 0.2.1 public network
4
+ surface used by CI and release checks for 0.3.0. It is copied byte-for-byte
5
+ from the generated method contract at the 0.2.1 release boundary.
6
+
7
+ Release automation must name this file with `--baseline`; the compatibility
8
+ tool refuses to infer a baseline from an arbitrary local artifact.
@@ -3653,7 +3653,8 @@
3653
3653
  "limit",
3654
3654
  "max_depth",
3655
3655
  "relationship_types",
3656
- "start"
3656
+ "start",
3657
+ "target_table"
3657
3658
  ],
3658
3659
  "required_fields": [
3659
3660
  "start"
@@ -3723,7 +3724,8 @@
3723
3724
  "limit",
3724
3725
  "max_depth",
3725
3726
  "relationship_types",
3726
- "start"
3727
+ "start",
3728
+ "target_table"
3727
3729
  ],
3728
3730
  "required_fields": [
3729
3731
  "start"
@@ -3862,7 +3864,8 @@
3862
3864
  "limit",
3863
3865
  "max_depth",
3864
3866
  "relationship_types",
3865
- "start"
3867
+ "start",
3868
+ "target_table"
3866
3869
  ],
3867
3870
  "required_fields": [
3868
3871
  "start"
@@ -4454,7 +4457,7 @@
4454
4457
  "stability": "stable"
4455
4458
  }
4456
4459
  ],
4457
- "openapi_sha256": "7a0960535e680ffd590776b2f6fea4188cb857c75bf5f5568680e6189f746b5f",
4460
+ "openapi_sha256": "7dac7258e9dd8e7c92019025effee85ec71a19bca98154d20d7c00f7d8450f8a",
4458
4461
  "schema_version": 1,
4459
4462
  "sdk": "polygres-sdk",
4460
4463
  "sdk_version": "0.2.1"
@@ -2,7 +2,7 @@
2
2
 
3
3
  This file is generated. Edit Python method metadata, docstrings, or the Runtime OpenAPI contract instead.
4
4
 
5
- - SDK package version: `0.2.1`
5
+ - SDK package version: `0.3.0`
6
6
  - HTTP API family: `v1`
7
7
  - Default API behavior version: `2026-08-04`
8
8
  - Method specification schema: `1`
@@ -503,6 +503,47 @@ Run vector-first hybrid retrieval.
503
503
  page = project.hybrid.vector_first(embedding)
504
504
  ```
505
505
 
506
+ ## `project.rows`
507
+
508
+ | SDK method | HTTP operation | Since | Retry | Pagination | Deprecated |
509
+ | --- | --- | --- | --- | --- | --- |
510
+ | `project.rows.ignore()` | `POST /v1/tables/{schema_name}/{table_name}/rows` (`runtime_write_row`) | `0.3.0` | `none` | `none` | No |
511
+ | `project.rows.insert()` | `POST /v1/tables/{schema_name}/{table_name}/rows` (`runtime_write_row`) | `0.3.0` | `none` | `none` | No |
512
+ | `project.rows.upsert()` | `POST /v1/tables/{schema_name}/{table_name}/rows` (`runtime_write_row`) | `0.3.0` | `none` | `none` | No |
513
+ | `project.rows.validate()` | `POST /v1/tables/{schema_name}/{table_name}/rows/validate` (`runtime_validate_row_write`) | `0.3.0` | `read_only_post` | `none` | No |
514
+
515
+ ### `project.rows.ignore()`
516
+
517
+ Insert one row and ignore a matching primary or unique conflict.
518
+
519
+ ```python
520
+ result = project.rows.ignore(schema="public", table="events", row={"id": "1"}, conflict_columns=["id"])
521
+ ```
522
+
523
+ ### `project.rows.insert()`
524
+
525
+ Insert one row into an eligible project-owned table.
526
+
527
+ ```python
528
+ result = project.rows.insert(schema="public", table="events", row={"id": "1"})
529
+ ```
530
+
531
+ ### `project.rows.upsert()`
532
+
533
+ Upsert one row through a primary or unique constraint.
534
+
535
+ ```python
536
+ result = project.rows.upsert(schema="public", table="events", row={"id": "1"}, conflict_columns=["id"])
537
+ ```
538
+
539
+ ### `project.rows.validate()`
540
+
541
+ Validate a single-row write without executing it.
542
+
543
+ ```python
544
+ validation = project.rows.validate(schema="public", table="events", row={"id": "1"})
545
+ ```
546
+
506
547
  ## `project.text`
507
548
 
508
549
  | SDK method | HTTP operation | Since | Retry | Pagination | Deprecated |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polygres-sdk"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Python SDK for Polygres"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,6 +3,7 @@ from polygres.client import Polygres
3
3
  from polygres.context_models import * # noqa: F403
4
4
  from polygres.context_models import __all__ as _context_model_exports
5
5
  from polygres.errors import (
6
+ PolygresAmbiguousWriteError,
6
7
  PolygresAPIError,
7
8
  PolygresAuthError,
8
9
  PolygresError,
@@ -23,6 +24,9 @@ from polygres.models import (
23
24
  HybridResult,
24
25
  Page,
25
26
  RetrievalReadiness,
27
+ RowContextReconciliationResult,
28
+ RowWriteResult,
29
+ RowWriteValidation,
26
30
  TextResult,
27
31
  VectorResult,
28
32
  )
@@ -39,6 +43,7 @@ __all__ = [
39
43
  "Page",
40
44
  "Polygres",
41
45
  "PolygresAPIError",
46
+ "PolygresAmbiguousWriteError",
42
47
  "PolygresAuthError",
43
48
  "PolygresError",
44
49
  "PolygresMaintenanceError",
@@ -50,6 +55,9 @@ __all__ = [
50
55
  "PolygresVersionNotice",
51
56
  "PolygresVersionWarning",
52
57
  "RetrievalReadiness",
58
+ "RowContextReconciliationResult",
59
+ "RowWriteResult",
60
+ "RowWriteValidation",
53
61
  "TextResult",
54
62
  "VectorResult",
55
63
  "__version__",
@@ -233,6 +233,7 @@ class RuntimeScope(str, Enum):
233
233
  HYBRID_READ = "hybrid:read"
234
234
  CONTEXT_READ = "context:read"
235
235
  CONTEXT_MANAGE = "context:manage"
236
+ ROWS_WRITE = "rows:write"
236
237
 
237
238
 
238
239
  class RuntimeClientKind(str, Enum):
@@ -366,6 +367,7 @@ class PolicyId(str, Enum):
366
367
  PROJECT_PERMISSION = "project_permission"
367
368
  PROJECT_API_KEY = "project_api_key"
368
369
  GATEWAY_RUNTIME = "gateway_runtime"
370
+ RUNTIME_ROW_WRITE = "runtime_row_write"
369
371
  CLI_FLOW = "cli_flow"
370
372
  RECOVERY_SESSION = "recovery_session"
371
373
  LEGACY_DEMO = "legacy_demo"
@@ -0,0 +1,230 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from types import MappingProxyType
9
+ from uuid import UUID
10
+
11
+ from ..errors import ERROR_CATALOG, PolygresError, error_record
12
+ from .enums import (
13
+ CliExitCode,
14
+ PasswordRequirement,
15
+ ProjectStatus,
16
+ ResetClass,
17
+ RetryClass,
18
+ )
19
+
20
+
21
+ class AuthErrorCategory(str, Enum):
22
+ VALIDATION = "validation"
23
+ AUTHENTICATION = "authentication"
24
+ AUTHORIZATION = "authorization"
25
+ ACCOUNT_GATE = "account_gate"
26
+ CONFLICT = "conflict"
27
+ RATE_LIMIT = "rate_limit"
28
+ DEPENDENCY = "dependency"
29
+ TERMINAL_SESSION = "terminal_session"
30
+ UNEXPECTED = "unexpected"
31
+
32
+
33
+ class AuthErrorCode(str, Enum):
34
+ AUTH_REQUIRED = "AUTH_REQUIRED"
35
+ AUTH_MODE_NOT_ALLOWED = "AUTH_MODE_NOT_ALLOWED"
36
+ AUTH_NOT_CONFIGURED = "AUTH_NOT_CONFIGURED"
37
+ AUTH_INVALID_CREDENTIALS = "AUTH_INVALID_CREDENTIALS"
38
+ AUTH_EMAIL_REQUIRED = "AUTH_EMAIL_REQUIRED"
39
+ INVALID_TOKEN = "INVALID_TOKEN"
40
+ TOKEN_EXPIRED = "TOKEN_EXPIRED"
41
+ LOCAL_SESSION_CORRUPT = "LOCAL_SESSION_CORRUPT"
42
+ LOCAL_SESSION_RESET_LIMIT_REACHED = "LOCAL_SESSION_RESET_LIMIT_REACHED"
43
+ AUTH_PROVIDER_UNAVAILABLE = "AUTH_PROVIDER_UNAVAILABLE"
44
+ AUTH_PKCE_EXCHANGE_FAILED = "AUTH_PKCE_EXCHANGE_FAILED"
45
+ AUTH_PKCE_VERIFIER_MISSING = "AUTH_PKCE_VERIFIER_MISSING"
46
+ AUTH_ACTION_INVALID = "AUTH_ACTION_INVALID"
47
+ AUTH_ACTION_EXPIRED = "AUTH_ACTION_EXPIRED"
48
+ AUTH_RECOVERY_REQUIRED = "AUTH_RECOVERY_REQUIRED"
49
+ RECOVERY_GRANT_INVALID = "RECOVERY_GRANT_INVALID"
50
+ AUTH_REAUTHENTICATION_REQUIRED = "AUTH_REAUTHENTICATION_REQUIRED"
51
+ AUTH_REAUTHENTICATION_INVALID = "AUTH_REAUTHENTICATION_INVALID"
52
+ AUTH_PASSWORD_MODE_MISMATCH = "AUTH_PASSWORD_MODE_MISMATCH"
53
+ AUTH_WEAK_PASSWORD = "AUTH_WEAK_PASSWORD"
54
+ AUTH_IDENTITY_CHANGED = "AUTH_IDENTITY_CHANGED"
55
+ EMAIL_NOT_VERIFIED = "EMAIL_NOT_VERIFIED"
56
+ ACCOUNT_NOT_ELIGIBLE = "ACCOUNT_NOT_ELIGIBLE"
57
+ APPROVAL_REQUIRED = "APPROVAL_REQUIRED"
58
+ ADMIN_NOT_ALLOWED = "ADMIN_NOT_ALLOWED"
59
+ PERMISSION_DENIED = "PERMISSION_DENIED"
60
+ NOT_FOUND = "NOT_FOUND"
61
+ PROJECT_NOT_READY = "PROJECT_NOT_READY"
62
+ INVITATION_NOT_FOUND = "INVITATION_NOT_FOUND"
63
+ INVITATION_EMAIL_MISMATCH = "INVITATION_EMAIL_MISMATCH"
64
+ INVITATION_EXISTS = "INVITATION_EXISTS"
65
+ INVITATION_EXPIRED = "INVITATION_EXPIRED"
66
+ INVITATION_NOT_PENDING = "INVITATION_NOT_PENDING"
67
+ INVITATION_NOT_RESENDABLE = "INVITATION_NOT_RESENDABLE"
68
+ INVITATION_RESEND_TOO_SOON = "INVITATION_RESEND_TOO_SOON"
69
+ INVITATION_DELIVERY_FAILED = "INVITATION_DELIVERY_FAILED"
70
+ SELF_INVITE_NOT_ALLOWED = "SELF_INVITE_NOT_ALLOWED"
71
+ INVALID_INVITATION_ROLE = "INVALID_INVITATION_ROLE"
72
+ ORG_MEMBERSHIP_LIMIT_EXCEEDED = "ORG_MEMBERSHIP_LIMIT_EXCEEDED"
73
+ LEGAL_ACCEPTANCE_REQUIRED = "LEGAL_ACCEPTANCE_REQUIRED"
74
+ VALIDATION_ERROR = "VALIDATION_ERROR"
75
+ CLI_AUTH_NOT_CONFIGURED = "CLI_AUTH_NOT_CONFIGURED"
76
+ CLI_AUTH_SESSION_CREATE_FAILED = "CLI_AUTH_SESSION_CREATE_FAILED"
77
+ CLI_AUTH_SESSION_INVALID = "CLI_AUTH_SESSION_INVALID"
78
+ CLI_AUTH_SESSION_NOT_FOUND = "CLI_AUTH_SESSION_NOT_FOUND"
79
+ CLI_AUTH_SESSION_TERMINAL = "CLI_AUTH_SESSION_TERMINAL"
80
+ CLI_AUTH_DENIED = "CLI_AUTH_DENIED"
81
+ CLI_AUTH_EXPIRED = "CLI_AUTH_EXPIRED"
82
+ CLI_AUTH_TIMEOUT = "CLI_AUTH_TIMEOUT"
83
+ CLI_AUTH_RESPONSE_INVALID = "CLI_AUTH_RESPONSE_INVALID"
84
+ API_KEY_INVALID = "API_KEY_INVALID"
85
+ API_KEY_NOT_FOUND = "API_KEY_NOT_FOUND"
86
+ PROJECT_HEADER_REQUIRED = "PROJECT_HEADER_REQUIRED"
87
+ RUNTIME_API_KEY_SNAPSHOT_UNAVAILABLE = "RUNTIME_API_KEY_SNAPSHOT_UNAVAILABLE"
88
+ GATEWAY_RUNTIME_JWT_REQUIRED = "GATEWAY_RUNTIME_JWT_REQUIRED"
89
+ GATEWAY_RUNTIME_JWT_INVALID = "GATEWAY_RUNTIME_JWT_INVALID"
90
+ GATEWAY_RUNTIME_PROJECT_MISMATCH = "GATEWAY_RUNTIME_PROJECT_MISMATCH"
91
+ GATEWAY_RUNTIME_SCOPE_INVALID = "GATEWAY_RUNTIME_SCOPE_INVALID"
92
+ GATEWAY_RUNTIME_SCOPE_DENIED = "GATEWAY_RUNTIME_SCOPE_DENIED"
93
+ GATEWAY_RUNTIME_JWKS_UNAVAILABLE = "GATEWAY_RUNTIME_JWKS_UNAVAILABLE"
94
+ RUNTIME_HOST_INVALID = "RUNTIME_HOST_INVALID"
95
+ RUNTIME_ROUTING_HEADER_REJECTED = "RUNTIME_ROUTING_HEADER_REJECTED"
96
+ RUNTIME_PROJECT_NOT_FOUND = "RUNTIME_PROJECT_NOT_FOUND"
97
+ RUNTIME_PROJECT_NOT_READY = "RUNTIME_PROJECT_NOT_READY"
98
+ EMAIL_DELIVERY_FAILED = "EMAIL_DELIVERY_FAILED"
99
+ RATE_LIMITED = "RATE_LIMITED"
100
+ RATE_LIMIT_UNAVAILABLE = "RATE_LIMIT_UNAVAILABLE"
101
+ INTERNAL_ERROR = "INTERNAL_ERROR"
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class AuthErrorDescriptor:
106
+ code: AuthErrorCode
107
+ category: AuthErrorCategory
108
+ safe_message: str
109
+ message_key: str
110
+ http_status: int | None
111
+ cli_exit_code: CliExitCode
112
+ retry_class: RetryClass
113
+ reset_class: ResetClass
114
+ safe_detail_keys: frozenset[str]
115
+
116
+
117
+ AUTH_ERROR_CATALOG: Mapping[AuthErrorCode, AuthErrorDescriptor] = MappingProxyType(
118
+ {
119
+ code: AuthErrorDescriptor(
120
+ code=code,
121
+ category=AuthErrorCategory(ERROR_CATALOG[code.value].category),
122
+ safe_message=ERROR_CATALOG[code.value].message,
123
+ message_key=ERROR_CATALOG[code.value].message_key,
124
+ http_status=ERROR_CATALOG[code.value].http_status,
125
+ cli_exit_code=CliExitCode(ERROR_CATALOG[code.value].cli_exit_code),
126
+ retry_class=RetryClass(ERROR_CATALOG[code.value].retry_class),
127
+ reset_class=ResetClass(ERROR_CATALOG[code.value].reset_class),
128
+ safe_detail_keys=ERROR_CATALOG[code.value].safe_detail_fields,
129
+ )
130
+ for code in AuthErrorCode
131
+ }
132
+ )
133
+
134
+ if set(AUTH_ERROR_CATALOG) != set(AuthErrorCode):
135
+ raise RuntimeError("global error catalog must define every AuthErrorCode exactly once")
136
+
137
+
138
+ _FIELD_PATH = re.compile(r"^[a-z][a-z0-9_.]{0,127}$")
139
+
140
+
141
+ def _validate_safe_details(code: AuthErrorCode, details: Mapping[str, object]) -> None:
142
+ if code is AuthErrorCode.VALIDATION_ERROR and details:
143
+ field = details.get("field")
144
+ if not isinstance(field, str) or _FIELD_PATH.fullmatch(field) is None:
145
+ raise ValueError("field must be a schema-owned field path")
146
+ elif code is AuthErrorCode.AUTH_WEAK_PASSWORD and details:
147
+ raw_requirements = details.get("requirements")
148
+ if not isinstance(raw_requirements, (list, tuple)):
149
+ raise ValueError("requirements must be present")
150
+ try:
151
+ requirements = tuple(PasswordRequirement(item) for item in raw_requirements)
152
+ except (TypeError, ValueError) as exc:
153
+ raise ValueError("unknown password requirement") from exc
154
+ canonical = tuple(item for item in PasswordRequirement if item in requirements)
155
+ if requirements != canonical:
156
+ raise ValueError("password requirements must be unique and canonical")
157
+ minimum = details.get("minimum_length")
158
+ if minimum is not None and (
159
+ PasswordRequirement.LENGTH not in requirements
160
+ or isinstance(minimum, bool)
161
+ or not isinstance(minimum, int)
162
+ or not 1 <= minimum <= 1024
163
+ ):
164
+ raise ValueError("minimum_length requires a known length requirement")
165
+ elif (
166
+ code in {AuthErrorCode.PROJECT_NOT_READY, AuthErrorCode.RUNTIME_PROJECT_NOT_READY}
167
+ and details
168
+ ):
169
+ try:
170
+ ProjectStatus(details.get("status"))
171
+ except (TypeError, ValueError) as exc:
172
+ raise ValueError("status must be a ProjectStatus") from exc
173
+ elif code is AuthErrorCode.INVITATION_RESEND_TOO_SOON and details:
174
+ timestamp = details.get("resend_available_at")
175
+ seconds = details.get("remaining_seconds")
176
+ if not isinstance(timestamp, str):
177
+ raise ValueError("resend_available_at must be an RFC 3339 timestamp")
178
+ try:
179
+ parsed = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
180
+ except ValueError as exc:
181
+ raise ValueError("resend_available_at must be an RFC 3339 timestamp") from exc
182
+ if parsed.tzinfo is None or parsed.utcoffset() is None:
183
+ raise ValueError("resend_available_at must include a UTC offset")
184
+ if isinstance(seconds, bool) or not isinstance(seconds, int) or seconds < 0:
185
+ raise ValueError("remaining_seconds must be a non-negative integer")
186
+ elif code is AuthErrorCode.ORG_MEMBERSHIP_LIMIT_EXCEEDED and details:
187
+ try:
188
+ for key in ("current_organization_id", "invited_organization_id"):
189
+ raw = details[key]
190
+ if not isinstance(raw, str) or str(UUID(raw)) != raw:
191
+ raise ValueError
192
+ except (KeyError, TypeError, ValueError) as exc:
193
+ raise ValueError("organization details must contain canonical UUIDs") from exc
194
+ elif code is AuthErrorCode.AUTH_IDENTITY_CHANGED and details:
195
+ for key in ("previous_epoch", "current_epoch"):
196
+ value = details.get(key)
197
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
198
+ raise ValueError("identity epochs must be non-negative integers")
199
+
200
+
201
+ class AuthError(PolygresError):
202
+ def __init__(
203
+ self,
204
+ code: AuthErrorCode,
205
+ *,
206
+ details: Mapping[str, object] | None = None,
207
+ retry_after_seconds: int | None = None,
208
+ ) -> None:
209
+ descriptor = AUTH_ERROR_CATALOG[code]
210
+ supplied = dict(details or {})
211
+ unknown = supplied.keys() - descriptor.safe_detail_keys
212
+ if unknown:
213
+ raise ValueError(f"unsafe error detail keys: {sorted(unknown)!r}")
214
+ _validate_safe_details(code, supplied)
215
+ if retry_after_seconds is not None and (
216
+ isinstance(retry_after_seconds, bool)
217
+ or not isinstance(retry_after_seconds, int)
218
+ or not 0 <= retry_after_seconds <= 86_400
219
+ or descriptor.retry_class is RetryClass.NEVER
220
+ ):
221
+ raise ValueError("invalid retry_after_seconds for auth error")
222
+ super().__init__(error_record(code.value, details=supplied))
223
+ self.code = code
224
+ self.retry_class = descriptor.retry_class
225
+ self.reset_class = descriptor.reset_class
226
+ self.message_key = descriptor.message_key
227
+ self.retry_after_seconds = retry_after_seconds
228
+
229
+ def __repr__(self) -> str:
230
+ return f"AuthError(code={self.code.value!r})"
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from ..errors import ERROR_CATALOG
7
+
8
+
9
+ class ContextErrorCode(str, Enum):
10
+ REQUEST_INVALID = "CONTEXT_REQUEST_INVALID"
11
+ IDENTIFIER_INVALID = "CONTEXT_IDENTIFIER_INVALID"
12
+ SOURCE_INVALID = "CONTEXT_SOURCE_INVALID"
13
+ DELETE_CONFIRMATION_INVALID = "CONTEXT_DELETE_CONFIRMATION_INVALID"
14
+ POINT_KEY_INVALID = "CONTEXT_POINT_KEY_INVALID"
15
+ EMBEDDING_INVALID = "CONTEXT_EMBEDDING_INVALID"
16
+ FILTER_INVALID = "CONTEXT_FILTER_INVALID"
17
+ LIMIT_EXCEEDED = "CONTEXT_LIMIT_EXCEEDED"
18
+ GRAPH_START_REQUIRED = "CONTEXT_GRAPH_START_REQUIRED"
19
+ GRAPH_DIRECTION_INVALID = "CONTEXT_GRAPH_DIRECTION_INVALID"
20
+ RANKING_WEIGHTS_INVALID = "CONTEXT_RANKING_WEIGHTS_INVALID"
21
+ RESOURCE_NAMESPACE_MISMATCH = "CONTEXT_RESOURCE_NAMESPACE_MISMATCH"
22
+ COLLECTION_NOT_FOUND = "CONTEXT_COLLECTION_NOT_FOUND"
23
+ OPERATION_NOT_FOUND = "CONTEXT_OPERATION_NOT_FOUND"
24
+ COLLECTION_NOT_READY = "CONTEXT_COLLECTION_NOT_READY"
25
+ TEXT_COLUMN_REQUIRED = "CONTEXT_TEXT_COLUMN_REQUIRED"
26
+ GRAPH_NOT_READY = "CONTEXT_GRAPH_NOT_READY"
27
+ SOURCE_KEY_ALIGNMENT_INVALID = "CONTEXT_SOURCE_KEY_ALIGNMENT_INVALID"
28
+ CAPABILITY_UNAVAILABLE = "CONTEXT_CAPABILITY_UNAVAILABLE"
29
+ SOURCE_KEY_NOT_FOUND = "CONTEXT_SOURCE_KEY_NOT_FOUND"
30
+ SOURCE_VECTOR_INVALID = "CONTEXT_SOURCE_VECTOR_INVALID"
31
+ FILTER_REGISTRATION_CONFLICT = "CONTEXT_FILTER_REGISTRATION_CONFLICT"
32
+ POINT_CURSOR_INVALID = "CONTEXT_POINT_CURSOR_INVALID"
33
+ RECALL_UNAVAILABLE = "CONTEXT_RECALL_UNAVAILABLE"
34
+ COLLECTION_NAME_CONFLICT = "CONTEXT_COLLECTION_NAME_CONFLICT"
35
+ PREFLIGHT_BLOCKED = "CONTEXT_PREFLIGHT_BLOCKED"
36
+ OPERATION_CONFLICT = "CONTEXT_OPERATION_CONFLICT"
37
+ IDEMPOTENCY_CONFLICT = "CONTEXT_IDEMPOTENCY_CONFLICT"
38
+ OPERATION_NOT_CANCELLABLE = "CONTEXT_OPERATION_NOT_CANCELLABLE"
39
+ OPERATION_NOT_RETRYABLE = "CONTEXT_OPERATION_NOT_RETRYABLE"
40
+ OPERATION_CANCELLED = "CONTEXT_OPERATION_CANCELLED"
41
+ ONBOARDING_NOT_ELIGIBLE = "CONTEXT_ONBOARDING_NOT_ELIGIBLE"
42
+ ONBOARDING_NOT_AVAILABLE = "CONTEXT_ONBOARDING_NOT_AVAILABLE"
43
+ RUNTIME_UNSUPPORTED = "CONTEXT_RUNTIME_UNSUPPORTED"
44
+ EXTENSION_UNAVAILABLE = "CONTEXT_EXTENSION_UNAVAILABLE"
45
+ SOURCE_TABLE_UNSUPPORTED = "CONTEXT_SOURCE_TABLE_UNSUPPORTED"
46
+ SOURCE_KEY_REQUIRED = "CONTEXT_SOURCE_KEY_REQUIRED"
47
+ SOURCE_KEY_TYPE_UNSUPPORTED = "CONTEXT_SOURCE_KEY_TYPE_UNSUPPORTED"
48
+ SOURCE_KEY_TEXT_CONSTRAINT_REQUIRED = "CONTEXT_SOURCE_KEY_TEXT_CONSTRAINT_REQUIRED"
49
+ SOURCE_PRIVILEGE_REQUIRED = "CONTEXT_SOURCE_PRIVILEGE_REQUIRED"
50
+ VECTOR_COLUMN_MISSING = "CONTEXT_VECTOR_COLUMN_MISSING"
51
+ VECTOR_COLUMN_CONFLICT = "CONTEXT_VECTOR_COLUMN_CONFLICT"
52
+ VECTOR_TYPE_UNSUPPORTED = "CONTEXT_VECTOR_TYPE_UNSUPPORTED"
53
+ VECTOR_NULLABLE = "CONTEXT_VECTOR_NULLABLE"
54
+ VECTOR_DIMENSION_INVALID = "CONTEXT_VECTOR_DIMENSION_INVALID"
55
+ COSINE_ZERO_VECTOR = "CONTEXT_COSINE_ZERO_VECTOR"
56
+ RESULT_COLUMN_INVALID = "CONTEXT_RESULT_COLUMN_INVALID"
57
+ FILTER_COLUMN_INVALID = "CONTEXT_FILTER_COLUMN_INVALID"
58
+ JSONB_FILTER_PATH_INVALID = "CONTEXT_JSONB_FILTER_PATH_INVALID"
59
+ RESERVED_FILTER_CONFLICT = "CONTEXT_RESERVED_FILTER_CONFLICT"
60
+ TEXT_COLUMN_INVALID = "CONTEXT_TEXT_COLUMN_INVALID"
61
+ HNSW_UNSUPPORTED = "CONTEXT_HNSW_UNSUPPORTED"
62
+ INDEX_CONFLICT = "CONTEXT_INDEX_CONFLICT"
63
+ MEMORY_PRESSURE = "CONTEXT_MEMORY_PRESSURE"
64
+ PGVECTOR_SOURCE = "CONTEXT_PGVECTOR_SOURCE"
65
+ UNSUPPORTED_VECTOR_TYPE = "CONTEXT_UNSUPPORTED_VECTOR_TYPE"
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class ContextErrorDescriptor:
70
+ status_code: int
71
+ message: str
72
+ safe_detail_fields: tuple[str, ...] = ()
73
+ retryable: bool = False
74
+ conflict: bool = False
75
+ availability: bool = False
76
+
77
+
78
+ CONTEXT_ERROR_CATALOG: dict[ContextErrorCode, ContextErrorDescriptor] = {
79
+ code: ContextErrorDescriptor(
80
+ status_code=ERROR_CATALOG[code.value].http_status,
81
+ message=ERROR_CATALOG[code.value].message,
82
+ safe_detail_fields=tuple(sorted(ERROR_CATALOG[code.value].safe_detail_fields)),
83
+ retryable=ERROR_CATALOG[code.value].retry_class
84
+ in {"bounded_retry", "dependency_retry", "after_delay"},
85
+ conflict=ERROR_CATALOG[code.value].http_status == 409,
86
+ availability=(
87
+ "UNAVAILABLE" in code.value
88
+ or "NOT_READY" in code.value
89
+ or code is ContextErrorCode.MEMORY_PRESSURE
90
+ ),
91
+ )
92
+ for code in ContextErrorCode
93
+ }
94
+
95
+ if set(CONTEXT_ERROR_CATALOG) != set(ContextErrorCode):
96
+ raise RuntimeError("global error catalog must define every ContextErrorCode exactly once")
97
+
98
+
99
+ __all__ = [
100
+ "CONTEXT_ERROR_CATALOG",
101
+ "ContextErrorCode",
102
+ "ContextErrorDescriptor",
103
+ ]
@@ -511,6 +511,7 @@ class JointSearchRequest(GraphSearchBase):
511
511
 
512
512
  class ErrorBody(ContextResponse):
513
513
  code: str
514
+ variant: str | None = None
514
515
  message: str
515
516
  details: dict[str, Any]
516
517
 
@@ -749,6 +750,7 @@ class CollectionGetResponse(ContextResponse):
749
750
 
750
751
  class ContextOperationFailure(ContextResponse):
751
752
  code: str
753
+ variant: str | None = None
752
754
  message: str
753
755
  details: dict[str, Any]
754
756
  http_status: int
@@ -1080,9 +1082,7 @@ class ContextJointResult(ContextSearchResult):
1080
1082
  raise ValueError("graph-introduced results require null baseline fields")
1081
1083
  if self.introduced_by_graph and self.graph is None:
1082
1084
  raise ValueError("graph-introduced results require graph evidence")
1083
- if not self.introduced_by_graph and (
1084
- self.baseline_rank is None or self.rank_lift is None
1085
- ):
1085
+ if not self.introduced_by_graph and (self.baseline_rank is None or self.rank_lift is None):
1086
1086
  raise ValueError("non-graph-introduced results require baseline fields")
1087
1087
  if not math.isclose(
1088
1088
  self.score,