polygres-sdk 0.2.0__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.0 → polygres_sdk-0.3.0}/CONTRIBUTING.md +8 -5
  2. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/PKG-INFO +28 -4
  3. {polygres_sdk-0.2.0 → 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.0/src/polygres/spec → polygres_sdk-0.3.0/compatibility/0.2.1}/python-sdk-v1.methods.json +24 -5
  6. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/docs/reference-v1.md +49 -1
  7. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/pyproject.toml +1 -1
  8. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/__init__.py +8 -0
  9. {polygres_sdk-0.2.0 → 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.0 → 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.0 → polygres_sdk-0.3.0}/src/polygres/client.py +57 -5
  17. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/context.py +110 -2
  18. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/context_wait.py +23 -14
  19. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/errors.py +15 -32
  20. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/models.py +101 -0
  21. polygres_sdk-0.3.0/src/polygres/rows.py +384 -0
  22. polygres_sdk-0.3.0/src/polygres/spec/python-sdk-v1.methods.json +4892 -0
  23. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/spec/runtime-v1.openapi.json +3805 -2057
  24. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/version_notices.py +72 -0
  25. polygres_sdk-0.3.0/tests/conftest.py +11 -0
  26. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/fixtures/context/contract-fixtures.json +20 -15
  27. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_client.py +32 -4
  28. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_context.py +208 -13
  29. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_method_spec.py +2 -2
  30. polygres_sdk-0.3.0/tests/test_release_workflow.py +95 -0
  31. polygres_sdk-0.3.0/tests/test_rows.py +279 -0
  32. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_version.py +6 -6
  33. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_version_notices.py +60 -7
  34. polygres_sdk-0.3.0/tools/extract_release_notes.py +57 -0
  35. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/generate_sdk_method_spec.py +14 -0
  36. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/runtime_operation_exclusions.json +12 -0
  37. polygres_sdk-0.2.0/src/polygres/_vendor/polygres_lib/auth/errors.py +0 -895
  38. polygres_sdk-0.2.0/src/polygres/_vendor/polygres_lib/context/errors.py +0 -266
  39. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/.gitignore +0 -0
  40. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/LICENSE +0 -0
  41. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/compatibility/0.1.0/README.md +0 -0
  42. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/compatibility/0.1.0/python-sdk-v1.methods.json +0 -0
  43. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/migrations/README.md +0 -0
  44. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/migrations/vector-configuration-creation.md +0 -0
  45. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_api_version.py +0 -0
  46. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_method_metadata.py +0 -0
  47. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/__init__.py +0 -0
  48. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/__init__.py +0 -0
  49. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/api_versions.py +0 -0
  50. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/__init__.py +0 -0
  51. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/api_keys.py +0 -0
  52. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/models.py +0 -0
  53. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/ports.py +0 -0
  54. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/principals.py +0 -0
  55. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/records.py +0 -0
  56. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/auth/types.py +0 -0
  57. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/__init__.py +0 -0
  58. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/enums.py +0 -0
  59. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/context/validation.py +0 -0
  60. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/__init__.py +0 -0
  61. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/models.py +0 -0
  62. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_vendor/polygres_lib/core/types.py +0 -0
  63. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/_version.py +0 -0
  64. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/context_models.py +0 -0
  65. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/context_validation.py +0 -0
  66. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/py.typed +0 -0
  67. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/spec/__init__.py +0 -0
  68. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/src/polygres/spec/python-sdk-v1.methods.schema.json +0 -0
  69. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_compatibility.py +0 -0
  70. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tests/test_packaged_resources.py +0 -0
  71. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/check_sdk_compatibility.py +0 -0
  72. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/generate_runtime_openapi_snapshot.py +0 -0
  73. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/run_context_live_test.sh +0 -0
  74. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/test_context_live.py +0 -0
  75. {polygres_sdk-0.2.0 → polygres_sdk-0.3.0}/tools/verify_distribution.py +0 -0
  76. {polygres_sdk-0.2.0 → 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.0
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.0
78
- git push origin polygres-sdk-v0.2.0
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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: polygres-sdk
3
- Version: 0.2.0
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.0`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.0).
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.0 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.0) 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.0`](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.0).
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.0 release notes](https://github.com/Evokoa/polygres-sdk/releases/tag/polygres-sdk-v0.2.0) 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.
@@ -178,6 +178,14 @@
178
178
  "reason": "Readiness checks are operational endpoints; the SDK uses retrieval readiness instead.",
179
179
  "review_date": "2027-02-04"
180
180
  },
181
+ {
182
+ "method": "POST",
183
+ "operation_id": "reindex_text_configuration_v1_text_configurations__config_id__reindex_post",
184
+ "owner": "Runtime Platform",
185
+ "path": "/v1/text/configurations/{config_id}/reindex",
186
+ "reason": "Text reindexing is a setup operation managed outside the retrieval SDK.",
187
+ "review_date": "2027-02-04"
188
+ },
181
189
  {
182
190
  "method": "POST",
183
191
  "operation_id": "reindex_vector_configuration_v1_vector_configurations__config_id__reindex_post",
@@ -201,6 +209,14 @@
201
209
  "path": "/v1/vector/configurations/{config_id}/compatibility-engine/{target_engine}",
202
210
  "reason": "Per-configuration compatibility engine selection is an operator rollback control, not an application retrieval method.",
203
211
  "review_date": "2027-02-04"
212
+ },
213
+ {
214
+ "method": "GET",
215
+ "operation_id": "text_configuration_diagnostics_v1_text_configurations__config_id__diagnostics_get",
216
+ "owner": "Runtime Platform",
217
+ "path": "/v1/text/configurations/{config_id}/diagnostics",
218
+ "reason": "Text index diagnostics are managed through the dashboard and control-plane workflow.",
219
+ "review_date": "2027-02-04"
204
220
  }
205
221
  ],
206
222
  "methods": [
@@ -3637,7 +3653,8 @@
3637
3653
  "limit",
3638
3654
  "max_depth",
3639
3655
  "relationship_types",
3640
- "start"
3656
+ "start",
3657
+ "target_table"
3641
3658
  ],
3642
3659
  "required_fields": [
3643
3660
  "start"
@@ -3707,7 +3724,8 @@
3707
3724
  "limit",
3708
3725
  "max_depth",
3709
3726
  "relationship_types",
3710
- "start"
3727
+ "start",
3728
+ "target_table"
3711
3729
  ],
3712
3730
  "required_fields": [
3713
3731
  "start"
@@ -3846,7 +3864,8 @@
3846
3864
  "limit",
3847
3865
  "max_depth",
3848
3866
  "relationship_types",
3849
- "start"
3867
+ "start",
3868
+ "target_table"
3850
3869
  ],
3851
3870
  "required_fields": [
3852
3871
  "start"
@@ -4438,8 +4457,8 @@
4438
4457
  "stability": "stable"
4439
4458
  }
4440
4459
  ],
4441
- "openapi_sha256": "bc828889aee0beb04d6c35cfd351d54d02ed547039a30d8316a4faa70cf8bc55",
4460
+ "openapi_sha256": "7dac7258e9dd8e7c92019025effee85ec71a19bca98154d20d7c00f7d8450f8a",
4442
4461
  "schema_version": 1,
4443
4462
  "sdk": "polygres-sdk",
4444
- "sdk_version": "0.2.0"
4463
+ "sdk_version": "0.2.1"
4445
4464
  }
@@ -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.0`
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`
@@ -190,6 +190,13 @@ Return pgContext capabilities for this Runtime.
190
190
  capabilities = project.context.get_capabilities()
191
191
  ```
192
192
 
193
+ Context retrieval methods fetch this response lazily on their first call and
194
+ cache it on the project Context namespace. They validate the requested mode,
195
+ dimensions, result and candidate limits, graph depth, and relationship-type
196
+ count before sending the retrieval request. Calling `get_capabilities()`
197
+ explicitly refreshes the cache. Cached capabilities expire after 60 seconds,
198
+ and an unavailable capability is refreshed on the next attempted call.
199
+
193
200
  ### `project.context.get_collection()`
194
201
 
195
202
  Return one Context collection by UUID.
@@ -496,6 +503,47 @@ Run vector-first hybrid retrieval.
496
503
  page = project.hybrid.vector_first(embedding)
497
504
  ```
498
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
+
499
547
  ## `project.text`
500
548
 
501
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.0"
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
+ ]