parallax-postgres 0.1.0__tar.gz → 0.2.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 (38) hide show
  1. {parallax_postgres-0.1.0/src/parallax_postgres.egg-info → parallax_postgres-0.2.0}/PKG-INFO +7 -6
  2. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/README.md +4 -4
  3. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/pyproject.toml +4 -2
  4. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/__init__.py +13 -1
  5. parallax_postgres-0.1.0/src/parallax/postgres/adapter.py → parallax_postgres-0.2.0/src/parallax/postgres/_adapter.py +2 -2
  6. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_authorization.py +4 -3
  7. parallax_postgres-0.2.0/src/parallax/postgres/_binding.py +324 -0
  8. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders.pxi +1 -1
  9. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_connection.py +62 -31
  10. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_context.py +3 -3
  11. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_endpoint.py +6 -7
  12. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_isolation.py +1 -1
  13. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_pool_metrics.py +2 -2
  14. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_route.py +3 -3
  15. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_route_plan.py +1 -1
  16. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_runtime.py +7 -6
  17. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_startup.py +2 -2
  18. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0/src/parallax_postgres.egg-info}/PKG-INFO +7 -6
  19. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/SOURCES.txt +2 -1
  20. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/requires.txt +2 -1
  21. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/LICENSE +0 -0
  22. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/NOTICE +0 -0
  23. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/setup.cfg +0 -0
  24. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/setup.py +0 -0
  25. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyi +0 -0
  26. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyx +0 -0
  27. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_c.pyi +0 -0
  28. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_c.pyx +0 -0
  29. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_compiled_loaders.py +0 -0
  30. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_options.py +0 -0
  31. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/cloader.pxi +0 -0
  32. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_binary/__init__.pxd +0 -0
  33. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_binary/_psycopg.pxd +0 -0
  34. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_c/__init__.pxd +0 -0
  35. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_c/_psycopg.pxd +0 -0
  36. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/py.typed +0 -0
  37. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/dependency_links.txt +0 -0
  38. {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parallax-postgres
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Parallax Postgres database adapter: the concrete database runtime over psycopg and its companion pool.
5
5
  Author: Parallax
6
6
  License-Expression: Apache-2.0
@@ -11,15 +11,16 @@ Requires-Python: >=3.14
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  License-File: NOTICE
14
- Requires-Dist: parallax-core==0.1.0
14
+ Requires-Dist: parallax-core==0.2.0
15
15
  Requires-Dist: psycopg[binary]<3.4,>=3.3.6
16
16
  Requires-Dist: psycopg-pool>=3.3.1
17
+ Requires-Dist: pyyaml>=6.0
17
18
  Dynamic: license-file
18
19
 
19
20
  # parallax-postgres
20
21
 
21
22
  Parallax's Postgres database adapter over `psycopg` and its companion
22
- `psycopg-pool`. Install `parallax-postgres==0.1.0` with the application packages
23
+ `psycopg-pool`. Install `parallax-postgres==0.2.0` with the application packages
23
24
  shown in the [getting-started guide](https://github.com/davidsunglee/parallax/blob/main/languages/python/docs/guide/01-getting-started.md#install).
24
25
  That guide links the authoritative Python, platform and PostgreSQL support
25
26
  declarations and the C-driver and source-build installation paths. The normal
@@ -31,7 +32,7 @@ for publication status. The distribution includes Apache-2.0 `LICENSE` and `NOTI
31
32
  ## Connecting
32
33
 
33
34
  ```python
34
- from parallax.core.db_port import Password
35
+ from parallax.core import Password
35
36
  from parallax.postgres import OnDemandOptions, PoolOptions, PostgresAdapter, PostgresEndpoint
36
37
  from parallax.snapshot import connect
37
38
 
@@ -63,10 +64,10 @@ PostgresEndpoint("postgresql://app:<password>@db.internal/app", credentials=Pass
63
64
  ValueError: connection_string must not carry a password; supply it through credentials.
64
65
  ```
65
66
 
66
- There are three spellings, all exported from `parallax.core.db_port`:
67
+ There are three spellings, all exported from `parallax.core`:
67
68
 
68
69
  ```python
69
- from parallax.core.db_port import DRIVER_MANAGED, Password
70
+ from parallax.core import DRIVER_MANAGED, Password
70
71
 
71
72
  # a constant secret; `Password` is its own source and keeps it out of every repr
72
73
  PostgresEndpoint("postgresql://app@db.internal:5432/app", credentials=Password("s3cret"))
@@ -1,7 +1,7 @@
1
1
  # parallax-postgres
2
2
 
3
3
  Parallax's Postgres database adapter over `psycopg` and its companion
4
- `psycopg-pool`. Install `parallax-postgres==0.1.0` with the application packages
4
+ `psycopg-pool`. Install `parallax-postgres==0.2.0` with the application packages
5
5
  shown in the [getting-started guide](https://github.com/davidsunglee/parallax/blob/main/languages/python/docs/guide/01-getting-started.md#install).
6
6
  That guide links the authoritative Python, platform and PostgreSQL support
7
7
  declarations and the C-driver and source-build installation paths. The normal
@@ -13,7 +13,7 @@ for publication status. The distribution includes Apache-2.0 `LICENSE` and `NOTI
13
13
  ## Connecting
14
14
 
15
15
  ```python
16
- from parallax.core.db_port import Password
16
+ from parallax.core import Password
17
17
  from parallax.postgres import OnDemandOptions, PoolOptions, PostgresAdapter, PostgresEndpoint
18
18
  from parallax.snapshot import connect
19
19
 
@@ -45,10 +45,10 @@ PostgresEndpoint("postgresql://app:<password>@db.internal/app", credentials=Pass
45
45
  ValueError: connection_string must not carry a password; supply it through credentials.
46
46
  ```
47
47
 
48
- There are three spellings, all exported from `parallax.core.db_port`:
48
+ There are three spellings, all exported from `parallax.core`:
49
49
 
50
50
  ```python
51
- from parallax.core.db_port import DRIVER_MANAGED, Password
51
+ from parallax.core import DRIVER_MANAGED, Password
52
52
 
53
53
  # a constant secret; `Password` is its own source and keeps it out of every repr
54
54
  PostgresEndpoint("postgresql://app@db.internal:5432/app", credentials=Password("s3cret"))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "parallax-postgres"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Parallax Postgres database adapter: the concrete database runtime over psycopg and its companion pool."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -8,7 +8,7 @@ license = "Apache-2.0"
8
8
  license-files = ["LICENSE", "NOTICE"]
9
9
  authors = [{ name = "Parallax" }]
10
10
  dependencies = [
11
- "parallax-core==0.1.0",
11
+ "parallax-core==0.2.0",
12
12
  # The binary extra bundles libpq so the adapter installs self-contained (no
13
13
  # system libpq required); the adapter owns that capability, so a sibling
14
14
  # that imports psycopg declares it without the extra. The compiled loaders
@@ -18,6 +18,8 @@ dependencies = [
18
18
  # The companion pool the runtime owns: the same project, released
19
19
  # separately.
20
20
  "psycopg-pool>=3.3.1",
21
+ # Reads and writes YAML binding documents; imported only by those calls.
22
+ "pyyaml>=6.0",
21
23
  ]
22
24
 
23
25
  [project.urls]
@@ -1,16 +1,28 @@
1
+ """Connect Parallax to PostgreSQL."""
2
+
1
3
  from __future__ import annotations
2
4
 
5
+ from parallax.postgres._adapter import PostgresAdapter
3
6
  from parallax.postgres._authorization import PostgresRole
7
+ from parallax.postgres._binding import (
8
+ PostgresBinding,
9
+ PostgresLocation,
10
+ PostgresSequence,
11
+ PostgresTable,
12
+ )
4
13
  from parallax.postgres._endpoint import PostgresEndpoint
5
14
  from parallax.postgres._isolation import isolation_spelling
6
15
  from parallax.postgres._options import OnDemandOptions, PoolOptions
7
- from parallax.postgres.adapter import PostgresAdapter
8
16
 
9
17
  __all__ = [
10
18
  "OnDemandOptions",
11
19
  "PoolOptions",
12
20
  "PostgresAdapter",
21
+ "PostgresBinding",
13
22
  "PostgresEndpoint",
23
+ "PostgresLocation",
14
24
  "PostgresRole",
25
+ "PostgresSequence",
26
+ "PostgresTable",
15
27
  "isolation_spelling",
16
28
  ]
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Final, cast, get_args
5
5
 
6
- from parallax.core.dialect import POSTGRES
6
+ from parallax.core.internal.dialect import POSTGRES
7
7
  from parallax.postgres._endpoint import PostgresEndpoint
8
8
  from parallax.postgres._options import positive_seconds
9
9
  from parallax.postgres._runtime import PostgresRuntime
@@ -140,7 +140,7 @@ class PostgresAdapter:
140
140
  unbounded instant, a structured document, and a Float32 and a Float64
141
141
  exactly, and it was given back — the reader's having authenticated the
142
142
  writer's login — unless an unreachable reader was deferred. A failure at
143
- any of those raises :class:`~parallax.core.db_port.DatabaseStartupError`
143
+ any of those raises :class:`~parallax.core.internal.db_port.DatabaseStartupError`
144
144
  naming the route and the phase, and publishes no runtime: every pool is
145
145
  closed and a startup connection already acquired goes through the
146
146
  ordinary cleanup path, which reports what it established rather than
@@ -5,7 +5,8 @@ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from psycopg.sql import SQL, Composed, Identifier
7
7
 
8
- from parallax.core.db_port import InvalidAuthorizationError
8
+ from parallax.core.internal.base import UsageError
9
+ from parallax.core.internal.db_port import AUTHORIZATION_INVALID
9
10
  from parallax.postgres._connection import (
10
11
  _complete_first_send, # pyright: ignore[reportPrivateUsage] - the lease's shared first-send markers
11
12
  _start_first_send, # pyright: ignore[reportPrivateUsage] - the lease's shared first-send markers
@@ -28,9 +29,9 @@ class PostgresRole:
28
29
 
29
30
  def __post_init__(self) -> None:
30
31
  if not isinstance(self.name, str): # pyright: ignore[reportUnnecessaryIsInstance] - validates untyped callers at the public boundary
31
- raise InvalidAuthorizationError("a PostgreSQL role name must be a string")
32
+ raise UsageError(AUTHORIZATION_INVALID, "a PostgreSQL role name must be a string")
32
33
  if not self.name:
33
- raise InvalidAuthorizationError("a PostgreSQL role name must not be empty")
34
+ raise UsageError(AUTHORIZATION_INVALID, "a PostgreSQL role name must not be empty")
34
35
 
35
36
 
36
37
  def _role_statement(role: PostgresRole) -> Composed:
@@ -0,0 +1,324 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import json
5
+ import re
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Final, Self
9
+
10
+ from parallax.core.internal.database_binding import (
11
+ BINDING_DEFINITION_REJECTED,
12
+ BINDING_SYNTAX_INVALID,
13
+ BindingError,
14
+ BindingIssue,
15
+ BindingParts,
16
+ LocationCodec,
17
+ binding_document,
18
+ binding_from_document,
19
+ )
20
+ from parallax.core.internal.dialect import POSTGRES, Dialect
21
+ from parallax.core.internal.metamodel import EntityIdentity
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+
26
+ import yaml
27
+
28
+ __all__ = ["PostgresBinding", "PostgresLocation", "PostgresSequence", "PostgresTable"]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class PostgresLocation:
33
+ """A PostgreSQL schema within the database the adapter's endpoint selects."""
34
+
35
+ schema: str
36
+
37
+ @property
38
+ def qualifier(self) -> tuple[str, ...]:
39
+ return (self.schema,)
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class PostgresTable:
44
+ """Overrides one mapping owner's schema, table name, or both.
45
+
46
+ An omitted schema comes from the binding's namespace or default location;
47
+ an omitted name is the model's declared table name.
48
+ """
49
+
50
+ schema: str | None = None
51
+ name: str | None = None
52
+
53
+ @property
54
+ def location(self) -> PostgresLocation | None:
55
+ return None if self.schema is None else PostgresLocation(self.schema)
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class PostgresSequence:
60
+ """Overrides one logical allocator's schema, sequence name, or both.
61
+
62
+ An omitted schema comes from the binding's default location, never a
63
+ namespace; an omitted name is the allocator key.
64
+ """
65
+
66
+ schema: str | None = None
67
+ name: str | None = None
68
+
69
+ @property
70
+ def location(self) -> PostgresLocation | None:
71
+ return None if self.schema is None else PostgresLocation(self.schema)
72
+
73
+
74
+ _CODEC: Final = LocationCodec(
75
+ backend="postgres",
76
+ keys=("schema",),
77
+ location_type=PostgresLocation,
78
+ table_type=PostgresTable,
79
+ sequence_type=PostgresSequence,
80
+ load=lambda components: PostgresLocation(components["schema"]),
81
+ dump=lambda location: {"schema": location.schema},
82
+ table=lambda location, name: PostgresTable(None if location is None else location.schema, name),
83
+ sequence=lambda location, name: PostgresSequence(
84
+ None if location is None else location.schema, name
85
+ ),
86
+ )
87
+
88
+
89
+ @dataclass(frozen=True, slots=True, init=False, repr=False)
90
+ class PostgresBinding:
91
+ """Where one deployment places a model's tables and sequences in PostgreSQL.
92
+
93
+ ``default`` places every table and sequence nothing more specific places;
94
+ ``namespaces`` places the tables of each exact Entity Namespace;
95
+ ``entities`` overrides individual mapping owners, keyed by canonical Entity
96
+ name; ``sequences`` overrides individual allocators, keyed by allocator name.
97
+ The value is immutable and owns copies of the maps it was given, keyed by
98
+ ``EntityIdentity`` for ``entities``. Construction, ``from_document``,
99
+ ``from_json``, and ``from_yaml`` all raise :class:`~parallax.core.BindingError`
100
+ with every problem at its document path.
101
+ """
102
+
103
+ default: PostgresLocation | None
104
+ namespaces: Mapping[str, PostgresLocation]
105
+ entities: Mapping[EntityIdentity, PostgresTable]
106
+ sequences: Mapping[str, PostgresSequence]
107
+
108
+ def __init__(
109
+ self,
110
+ *,
111
+ default: PostgresLocation | None = None,
112
+ namespaces: Mapping[str, PostgresLocation] | None = None,
113
+ entities: Mapping[str, PostgresTable]
114
+ | Mapping[EntityIdentity, PostgresTable]
115
+ | None = None,
116
+ sequences: Mapping[str, PostgresSequence] | None = None,
117
+ ) -> None:
118
+ document = binding_document(
119
+ _CODEC,
120
+ default=default,
121
+ namespaces=namespaces,
122
+ entities=entities,
123
+ sequences=sequences,
124
+ )
125
+ self._adopt(binding_from_document(document, _CODEC))
126
+
127
+ @property
128
+ def dialect(self) -> Dialect:
129
+ """The backend this binding places objects in."""
130
+ return POSTGRES
131
+
132
+ def _adopt(
133
+ self, parts: BindingParts[PostgresLocation, PostgresTable, PostgresSequence]
134
+ ) -> None:
135
+ object.__setattr__(self, "default", parts.default)
136
+ object.__setattr__(self, "namespaces", parts.namespaces)
137
+ object.__setattr__(self, "entities", parts.entities)
138
+ object.__setattr__(self, "sequences", parts.sequences)
139
+
140
+ @classmethod
141
+ def _from_parts(
142
+ cls, parts: BindingParts[PostgresLocation, PostgresTable, PostgresSequence]
143
+ ) -> Self:
144
+ binding = cls.__new__(cls)
145
+ binding._adopt(parts)
146
+ return binding
147
+
148
+ @classmethod
149
+ def from_document(cls, document: Mapping[str, object]) -> Self:
150
+ """The binding a decoded binding document describes."""
151
+ return cls._from_parts(binding_from_document(document, _CODEC))
152
+
153
+ @classmethod
154
+ def from_json(cls, text: str | bytes) -> Self:
155
+ """The binding a JSON binding document describes."""
156
+ document = _decoded(text, json.loads, json.JSONDecodeError, "JSON")
157
+ return cls._from_parts(binding_from_document(document, _CODEC))
158
+
159
+ @classmethod
160
+ def from_yaml(cls, text: str | bytes) -> Self:
161
+ """The binding a YAML binding document describes, read under the YAML 1.2
162
+ core schema: a plain scalar is null, a boolean, an integer, or a float
163
+ only in that schema's spellings, and otherwise the string written."""
164
+ import yaml
165
+
166
+ loader = _yaml_loader()
167
+ document = _decoded(
168
+ text, lambda source: yaml.load(source, Loader=loader), yaml.YAMLError, "YAML"
169
+ )
170
+ return cls._from_parts(binding_from_document(document, _CODEC))
171
+
172
+ def to_document(self) -> dict[str, object]:
173
+ """This binding as a canonical binding document."""
174
+ return binding_document(
175
+ _CODEC,
176
+ default=self.default,
177
+ namespaces=self.namespaces,
178
+ entities=self.entities,
179
+ sequences=self.sequences,
180
+ )
181
+
182
+ def to_json(self) -> str:
183
+ """This binding as canonical, byte-deterministic JSON text."""
184
+ return json.dumps(self.to_document(), indent=2, ensure_ascii=False) + "\n"
185
+
186
+ def to_yaml(self) -> str:
187
+ """This binding as canonical, byte-deterministic YAML text.
188
+
189
+ Every string a YAML 1.1 or 1.2 core reader would resolve to another type
190
+ is quoted, so either reads it back as the string it is.
191
+ """
192
+ import yaml
193
+
194
+ return yaml.dump(
195
+ self.to_document(),
196
+ Dumper=_yaml_dumper(),
197
+ sort_keys=False,
198
+ allow_unicode=True,
199
+ default_flow_style=False,
200
+ )
201
+
202
+ def __hash__(self) -> int:
203
+ return hash(
204
+ (
205
+ self.default,
206
+ tuple(self.namespaces.items()),
207
+ tuple(self.entities.items()),
208
+ tuple(self.sequences.items()),
209
+ )
210
+ )
211
+
212
+ def __repr__(self) -> str:
213
+ return (
214
+ f"PostgresBinding(default={self.default!r}, namespaces={dict(self.namespaces)!r}, "
215
+ f"entities={dict(self.entities)!r}, sequences={dict(self.sequences)!r})"
216
+ )
217
+
218
+
219
+ # The YAML 1.2 core schema's implicit resolvers (`m-database-binding`), each with
220
+ # the first characters a matching plain scalar can start with; the empty string
221
+ # is PyYAML's bucket for an empty plain scalar, which resolves to null.
222
+ _CORE_SCHEMA: Final[tuple[tuple[str, str, tuple[str, ...]], ...]] = (
223
+ ("tag:yaml.org,2002:null", r"^(?:null|Null|NULL|~|)$", ("~", "n", "N", "")),
224
+ ("tag:yaml.org,2002:bool", r"^(?:true|True|TRUE|false|False|FALSE)$", ("t", "T", "f", "F")),
225
+ (
226
+ "tag:yaml.org,2002:int",
227
+ r"^(?:[-+]?[0-9]+|0o[0-7]+|0x[0-9a-fA-F]+)$",
228
+ ("-", "+", *"0123456789"),
229
+ ),
230
+ (
231
+ "tag:yaml.org,2002:float",
232
+ r"^(?:[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?"
233
+ r"|[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN))$",
234
+ ("-", "+", ".", *"0123456789"),
235
+ ),
236
+ )
237
+
238
+
239
+ def _core_int(text: str) -> int:
240
+ if text.lstrip("-+")[:2] in ("0o", "0x"):
241
+ return int(text, 0)
242
+ return int(text)
243
+
244
+
245
+ def _core_float(text: str) -> float:
246
+ lowered = text.lower()
247
+ if lowered.lstrip("-+") == ".inf":
248
+ return float("-inf") if text.startswith("-") else float("inf")
249
+ if lowered == ".nan":
250
+ return float("nan")
251
+ return float(text)
252
+
253
+
254
+ @functools.cache
255
+ def _yaml_loader() -> type[yaml.SafeLoader]:
256
+ import yaml
257
+
258
+ class _CoreLoader(yaml.SafeLoader):
259
+ pass
260
+
261
+ resolvers: dict[str, list[tuple[str, re.Pattern[str]]]] = {}
262
+ for tag, pattern, first_characters in _CORE_SCHEMA:
263
+ compiled = re.compile(pattern)
264
+ for first in first_characters:
265
+ resolvers.setdefault(first, []).append((tag, compiled))
266
+ _CoreLoader.yaml_implicit_resolvers = resolvers
267
+
268
+ def construct_int(loader: yaml.SafeLoader, node: yaml.ScalarNode) -> int:
269
+ return _core_int(str(loader.construct_scalar(node)))
270
+
271
+ def construct_float(loader: yaml.SafeLoader, node: yaml.ScalarNode) -> float:
272
+ return _core_float(str(loader.construct_scalar(node)))
273
+
274
+ _CoreLoader.add_constructor("tag:yaml.org,2002:int", construct_int)
275
+ _CoreLoader.add_constructor("tag:yaml.org,2002:float", construct_float)
276
+ return _CoreLoader
277
+
278
+
279
+ @functools.cache
280
+ def _yaml_dumper() -> type[yaml.SafeDumper]:
281
+ import yaml
282
+
283
+ class _BindingDumper(yaml.SafeDumper):
284
+ pass
285
+
286
+ # A plain scalar either schema resolves to a non-string must be quoted: the
287
+ # 1.1 resolvers the dumper inherits cover PyYAML readers, and the core
288
+ # schema's cover 1.2 readers.
289
+ resolvers = {
290
+ first: list(entries) for first, entries in yaml.SafeDumper.yaml_implicit_resolvers.items()
291
+ }
292
+ for tag, pattern, first_characters in _CORE_SCHEMA:
293
+ compiled = re.compile(pattern)
294
+ for first in first_characters:
295
+ resolvers.setdefault(first, []).append((tag, compiled))
296
+ _BindingDumper.yaml_implicit_resolvers = resolvers
297
+
298
+ def represent_text(_dumper: yaml.SafeDumper, text: str) -> yaml.ScalarNode:
299
+ # PyYAML folds a NEL into a space in every scalar style except double-quoted,
300
+ # where it escapes it.
301
+ style = '"' if "\x85" in text else None
302
+ return yaml.ScalarNode("tag:yaml.org,2002:str", text, style=style)
303
+
304
+ _BindingDumper.add_representer(str, represent_text)
305
+ return _BindingDumper
306
+
307
+
308
+ def _decoded(
309
+ text: str | bytes,
310
+ decode: Callable[[str], object],
311
+ error: type[Exception],
312
+ format_name: str,
313
+ ) -> object:
314
+ try:
315
+ source = text if isinstance(text, str) else text.decode("utf-8")
316
+ document = decode(source)
317
+ # PyYAML's scalar constructors raise these, not a YAMLError, for a scalar its
318
+ # tag cannot convert, such as `!!int invalid` or the date `2026-02-30`.
319
+ except (ValueError, LookupError, AttributeError, error) as exc:
320
+ raise BindingError(
321
+ BINDING_DEFINITION_REJECTED,
322
+ (BindingIssue(BINDING_SYNTAX_INVALID, (), message=f"not well-formed {format_name}"),),
323
+ ) from exc
324
+ return document
@@ -14,7 +14,7 @@ from datetime import UTC
14
14
  from psycopg import DataError
15
15
  from psycopg.pq import Format
16
16
 
17
- from parallax.core.base import INFINITY, nearest_binary32_of_spelling
17
+ from parallax.core.internal.base import INFINITY, nearest_binary32_of_spelling
18
18
 
19
19
 
20
20
  cdef extern from "Python.h":
@@ -20,27 +20,30 @@ from psycopg.pq.abc import PGresult
20
20
  from psycopg.rows import RowMaker, TupleRow, tuple_row
21
21
  from psycopg.types.json import Jsonb, JsonbBinaryLoader, JsonbLoader
22
22
 
23
- from parallax.core.base import (
23
+ from parallax.core.internal.base import (
24
24
  INFINITY,
25
25
  Boolean,
26
26
  Bytes,
27
27
  Date,
28
- Decimal,
29
28
  Float32,
30
29
  Float64,
31
30
  Int32,
32
31
  Int64,
32
+ InternalError,
33
33
  Json,
34
34
  NeutralType,
35
+ Numeric,
35
36
  String,
36
37
  TemporalBound,
37
38
  Time,
38
39
  Timestamp,
39
40
  Uuid,
40
41
  )
41
- from parallax.core.db_error import DatabaseError, classify_error
42
- from parallax.core.db_port import (
42
+ from parallax.core.internal.database_binding import PhysicalTable
43
+ from parallax.core.internal.db_error import DatabaseError, classify_error
44
+ from parallax.core.internal.db_port import (
43
45
  JSON_NULL,
46
+ WRITE_RESULT_INTEGRITY_VIOLATED,
44
47
  AccessMode,
45
48
  ArrayBind,
46
49
  BeginFailed,
@@ -54,7 +57,6 @@ from parallax.core.db_port import (
54
57
  DatabaseConnection,
55
58
  DocumentReadOrdinals,
56
59
  IsolationLevel,
57
- JsonDocument,
58
60
  OrderedWriteResult,
59
61
  OrderedWriteResultComplete,
60
62
  OrderedWriteResultInterrupted,
@@ -66,11 +68,11 @@ from parallax.core.db_port import (
66
68
  Row,
67
69
  StatementFailed,
68
70
  StatementNotExecuted,
71
+ StructuredDocument,
69
72
  TransactionOutcome,
70
- WriteResultIntegrityError,
71
73
  )
72
- from parallax.core.dialect import POSTGRES, Dialect
73
- from parallax.core.wire._json import dump_document, prepared_loads
74
+ from parallax.core.internal.dialect import POSTGRES, Dialect
75
+ from parallax.core.internal.wire._json import dump_document, prepared_loads
74
76
  from parallax.postgres._compiled_loaders import compiled_loaders
75
77
 
76
78
  if TYPE_CHECKING:
@@ -156,7 +158,8 @@ def translate_driver_error(dialect: Dialect, exc: psycopg.Error) -> DatabaseErro
156
158
 
157
159
  Extracts psycopg's driver-specific SQLSTATE (``exc.sqlstate`` — ``None`` for a
158
160
  non-database failure such as a dropped connection), its message, and the
159
- structured constraint name libpq reports beside them, then delegates category
161
+ structured constraint name, schema, and table libpq reports beside them,
162
+ then delegates category
160
163
  interpretation to ``m-db-error`` (which consults ``dialect``'s own code
161
164
  table). libpq treats an index as a constraint whether or not it was created
162
165
  with constraint syntax, so a bare ``create unique index`` reports its own
@@ -173,9 +176,20 @@ def translate_driver_error(dialect: Dialect, exc: psycopg.Error) -> DatabaseErro
173
176
  exc.sqlstate,
174
177
  str(exc),
175
178
  constraint_name=None if diagnostic is None else diagnostic.constraint_name,
179
+ constraint_table=None if diagnostic is None else _constraint_table(diagnostic),
176
180
  )
177
181
 
178
182
 
183
+ def _constraint_table(diagnostic: psycopg.errors.Diagnostic) -> PhysicalTable | None:
184
+ """The table libpq reports a failure against, when it reports both its schema
185
+ and its name."""
186
+ schema = diagnostic.schema_name
187
+ table = diagnostic.table_name
188
+ if schema is None or table is None:
189
+ return None
190
+ return PhysicalTable((schema,), table)
191
+
192
+
179
193
  def reported_failure(dialect: Dialect, exc: psycopg.Error) -> DatabaseError:
180
194
  """The neutral error for a driver failure that is reported rather than raised.
181
195
 
@@ -196,7 +210,7 @@ def reported_failure(dialect: Dialect, exc: psycopg.Error) -> DatabaseError:
196
210
  def translating_driver_errors(dialect: Dialect) -> Generator[None]:
197
211
  """Re-raise any psycopg exception inside the block as a neutral ``DatabaseError``.
198
212
 
199
- A :class:`~parallax.core.db_error.DatabaseError` raised by an inner port call
213
+ A :class:`~parallax.core.internal.db_error.DatabaseError` raised by an inner port call
200
214
  is **not** a ``psycopg.Error``, so a nested transaction never re-wraps an
201
215
  already-translated error, and a non-driver exception (a rollback signal, a
202
216
  callback's own error) propagates unchanged.
@@ -216,7 +230,7 @@ _ARRAY_ELEMENT_TYPES: Final[Mapping[type[NeutralType], str]] = {
216
230
  Int64: "int8",
217
231
  Float32: "float4",
218
232
  Float64: "float8",
219
- Decimal: "numeric",
233
+ Numeric: "numeric",
220
234
  String: "text",
221
235
  Bytes: "bytea",
222
236
  Date: "date",
@@ -357,15 +371,15 @@ class _ArrayBindDumper(RecursiveDumper):
357
371
  def adapt_binds(binds: Sequence[object]) -> list[object]:
358
372
  """Adapt neutral binds to psycopg's driver bind types at the adapter boundary.
359
373
 
360
- A :class:`~parallax.core.db_port.JsonDocument` (the neutral ``json`` /
361
- value-object carrier) becomes a psycopg ``Jsonb``. An
362
- :class:`~parallax.core.db_port.ArrayBind` passes through unchanged to the
374
+ A :class:`~parallax.core.internal.db_port.StructuredDocument` becomes a psycopg
375
+ ``Jsonb``. An
376
+ :class:`~parallax.core.internal.db_port.ArrayBind` passes through unchanged to the
363
377
  dumper :func:`initialize_connection` registers for it, as does every other
364
378
  bind. This keeps the psycopg bind mechanics internal to the adapter — no
365
379
  driver type is exported to the developer surface (m-db-port).
366
380
  """
367
381
  return [
368
- Jsonb(bind.value, dumps=dump_document) if isinstance(bind, JsonDocument) else bind
382
+ Jsonb(bind.document, dumps=dump_document) if isinstance(bind, StructuredDocument) else bind
369
383
  for bind in binds
370
384
  ]
371
385
 
@@ -479,6 +493,18 @@ context, and nothing is marked.
479
493
  """
480
494
 
481
495
  type _PreparedStatement = tuple[bytes, list[object], Sequence[DocumentReadOrdinals]]
496
+ type _PreparedWriteStatement = tuple[bytes, list[object]]
497
+
498
+
499
+ def to_driver_sql(canonical_sql: str) -> str:
500
+ """Convert canonical placeholders while preserving quoted text and psycopg percent escapes.
501
+
502
+ Psycopg scans percent markers even inside literals and quoted identifiers, so
503
+ existing percent characters are doubled before the unquoted `?` markers
504
+ become `%s` binds.
505
+ """
506
+ runs = re.split(r"""('(?:[^']|'')*'|"(?:[^"]|"")*")""", canonical_sql.replace("%", "%%"))
507
+ return "".join(run if index % 2 else run.replace("?", "%s") for index, run in enumerate(runs))
482
508
 
483
509
 
484
510
  def _start_first_send(context: PostgresConnectionContext | None) -> None:
@@ -814,7 +840,7 @@ class ConnectionEstablishment:
814
840
  would inherit.
815
841
 
816
842
  Anything the source raises that is not already a
817
- :class:`~parallax.core.db_port.CredentialResolutionError` is wrapped in
843
+ :class:`~parallax.core.internal.db_port.CredentialResolutionError` is wrapped in
818
844
  one with fixed text and the original chained, so classification does not
819
845
  depend on a provider's discipline and the pool's own warning line — which
820
846
  prints the exception's text — carries nothing a source put in a message.
@@ -972,7 +998,7 @@ def _write_in_order(
972
998
  connection: psycopg.Connection[TupleRow],
973
999
  context: PostgresConnectionContext | None,
974
1000
  dialect: Dialect,
975
- statements: Sequence[CanonicalWriteStatement],
1001
+ statements: Sequence[_PreparedWriteStatement],
976
1002
  ) -> OrderedWriteResult:
977
1003
  """Present ``statements`` in one pipeline and answer what it established.
978
1004
 
@@ -991,9 +1017,7 @@ def _write_in_order(
991
1017
  try:
992
1018
  with connection.pipeline():
993
1019
  try:
994
- for statement in statements:
995
- query = dialect.to_driver_sql(statement.sql).encode()
996
- binds = adapt_binds(statement.binds)
1020
+ for query, binds in statements:
997
1021
  cursor = _CountCursor(connection, intake)
998
1022
  cursors.append(cursor)
999
1023
  _start_first_send(context)
@@ -1051,9 +1075,10 @@ def _answered(
1051
1075
  for index, cursor in enumerate(recorded):
1052
1076
  outcome = cast("_Established", cursor.outcome)
1053
1077
  if isinstance(outcome, _Unexpected):
1054
- raise WriteResultIntegrityError(
1078
+ raise InternalError(
1079
+ WRITE_RESULT_INTEGRITY_VIOLATED,
1055
1080
  f"PostgreSQL answered statement {index} of an ordered write with "
1056
- f"{outcome.description}, which no count-only statement produces"
1081
+ f"{outcome.description}, which no count-only statement produces",
1057
1082
  )
1058
1083
  established.append(outcome)
1059
1084
  confirmed = _confirmed(established)
@@ -1208,7 +1233,7 @@ class PostgresConnection:
1208
1233
  unchanged, on the connection the context replaces it with; an
1209
1234
  undesignated one never is.
1210
1235
  """
1211
- query = sql.encode()
1236
+ query = to_driver_sql(sql).encode()
1212
1237
  adapted = adapt_binds(binds)
1213
1238
  with translating_driver_errors(self.dialect):
1214
1239
  return self._invoke_native(
@@ -1232,7 +1257,11 @@ class PostgresConnection:
1232
1257
  self._current_native()
1233
1258
  return []
1234
1259
  prepared = tuple(
1235
- (statement.sql.encode(), adapt_binds(statement.binds), statement.document_reads)
1260
+ (
1261
+ to_driver_sql(statement.sql).encode(),
1262
+ adapt_binds(statement.binds),
1263
+ statement.document_reads,
1264
+ )
1236
1265
  for statement in statements
1237
1266
  )
1238
1267
  with translating_driver_errors(self.dialect):
@@ -1241,7 +1270,7 @@ class PostgresConnection:
1241
1270
  )
1242
1271
 
1243
1272
  def execute_write(self, sql: str, binds: Sequence[object]) -> int:
1244
- query = sql.encode()
1273
+ query = to_driver_sql(sql).encode()
1245
1274
  adapted = adapt_binds(binds)
1246
1275
  with translating_driver_errors(self.dialect):
1247
1276
  return self._invoke_native(_execute_write, query, adapted, replay_safe=False)
@@ -1264,10 +1293,12 @@ class PostgresConnection:
1264
1293
  self._current_native()
1265
1294
  if not statements:
1266
1295
  return OrderedWriteResultComplete(())
1296
+ prepared = tuple(
1297
+ (to_driver_sql(statement.sql).encode(), adapt_binds(statement.binds))
1298
+ for statement in statements
1299
+ )
1267
1300
  try:
1268
- result = self._invoke_native(
1269
- _write_in_order, self.dialect, statements, replay_safe=False
1270
- )
1301
+ result = self._invoke_native(_write_in_order, self.dialect, prepared, replay_safe=False)
1271
1302
  except BaseException:
1272
1303
  self._mark_suspect()
1273
1304
  raise
@@ -1301,7 +1332,7 @@ class PostgresConnection:
1301
1332
  work may be retried and whether this connection is still trustworthy.
1302
1333
 
1303
1334
  An exception ``body`` itself raises is the CALLER's failure rather than
1304
- one this made, so :class:`~parallax.core.db_port.CallbackRaised` carries
1335
+ one this made, so :class:`~parallax.core.internal.db_port.CallbackRaised` carries
1305
1336
  the identical object (``m-db-port``). Translating it would substitute a
1306
1337
  port error for the caller's own — and a body-authored deadlock-class
1307
1338
  exception would then read as retriable to ``m-auto-retry``, which would
@@ -1321,7 +1352,7 @@ class PostgresConnection:
1321
1352
  from, assigned while the session is idle, so the request governs this
1322
1353
  transaction alone and costs no request of its own. A request the
1323
1354
  database refuses ends the boundary
1324
- :class:`~parallax.core.db_port.BeginFailed`: no work of the caller's was
1355
+ :class:`~parallax.core.internal.db_port.BeginFailed`: no work of the caller's was
1325
1356
  attempted, and a request silently downgraded to one the caller did not
1326
1357
  make is worse than one refused. Once the outer boundary ends, both
1327
1358
  attributes are cleared so the session's own defaults govern the next one.
@@ -1334,7 +1365,7 @@ class PostgresConnection:
1334
1365
  twice. An outer boundary that is the first submission after entry is
1335
1366
  still the acquisition's setup: one that opens past the acquisition's
1336
1367
  deadline, or after the runtime closed, raises that acquisition's
1337
- :class:`~parallax.core.db_port.ConnectionAcquisitionError` before the
1368
+ :class:`~parallax.core.internal.db_port.ConnectionAcquisitionError` before the
1338
1369
  callback runs.
1339
1370
 
1340
1371
  A call made while this session's transaction is already open joins it
@@ -11,7 +11,7 @@ import psycopg_pool
11
11
  from psycopg.pq import TransactionStatus
12
12
  from psycopg.rows import TupleRow
13
13
 
14
- from parallax.core.db_port import (
14
+ from parallax.core.internal.db_port import (
15
15
  CleanupCode,
16
16
  CleanupIssue,
17
17
  CleanupPhase,
@@ -24,8 +24,8 @@ from parallax.core.db_port import (
24
24
  Returned,
25
25
  RouteSelection,
26
26
  )
27
- from parallax.core.diagnostics import diagnostic_for
28
- from parallax.core.dialect import POSTGRES
27
+ from parallax.core.internal.diagnostics import diagnostic_for
28
+ from parallax.core.internal.dialect import POSTGRES
29
29
  from parallax.postgres._authorization import (
30
30
  PostgresRole,
31
31
  _install_role, # pyright: ignore[reportPrivateUsage] - the one role installation, run as the lease's first send
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
6
6
  import psycopg
7
7
  import psycopg.conninfo
8
8
 
9
- from parallax.core.db_port import DRIVER_MANAGED, CredentialSource, DriverManaged
9
+ from parallax.core.internal.db_port import DRIVER_MANAGED, CredentialSource, DriverManaged
10
10
  from parallax.postgres._options import OnDemandOptions, PoolOptions, RetentionOptions
11
11
 
12
12
  if TYPE_CHECKING:
@@ -104,8 +104,7 @@ def _retention(pool: object) -> RetentionOptions:
104
104
 
105
105
  @dataclass(frozen=True, slots=True)
106
106
  class PostgresEndpoint:
107
- """Where one route's connections go, how its login authenticates, and how
108
- its connections are retained.
107
+ """Where one route connects, how it logs in, and how it keeps connections.
109
108
 
110
109
  ``connection_string`` is libpq's own grammar — keyword/value pairs, a
111
110
  ``postgresql://`` URI, a ``service=`` reference, or the empty string, which
@@ -117,16 +116,16 @@ class PostgresEndpoint:
117
116
 
118
117
  ``credentials`` says HOW the login that string names authenticates, and is
119
118
  required: either a
120
- :class:`~parallax.core.db_port.CredentialSource` — a
121
- :class:`~parallax.core.db_port.Password`, or a provider's own source, asked
119
+ :class:`~parallax.core.internal.db_port.CredentialSource` — a
120
+ :class:`~parallax.core.internal.db_port.Password`, or a provider's own source, asked
122
121
  where each physical connection is established — or
123
- :data:`~parallax.core.db_port.DRIVER_MANAGED`, which declares that Parallax
122
+ :data:`~parallax.core.internal.db_port.DRIVER_MANAGED`, which declares that Parallax
124
123
  supplies no secret and leaves authentication to the driver and the server
125
124
  (peer, trust, a client certificate, Kerberos, or libpq's own environment:
126
125
  ``PGPASSWORD``, ``.pgpass``, a ``service`` file). There is no default and no
127
126
  ``None``, so which of the two a deployment chose is visible on the line. It
128
127
  is kept out of this value's representation for the same reason the string
129
- is: :class:`~parallax.core.db_port.Password` hides its own secret, but a
128
+ is: :class:`~parallax.core.internal.db_port.Password` hides its own secret, but a
130
129
  source is any object a provider wrote, and one holding broker material in an
131
130
  ordinary repr would otherwise reach every log line this value does.
132
131
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Final
4
4
 
5
- from parallax.core.db_port import IsolationLevel
5
+ from parallax.core.internal.db_port import IsolationLevel
6
6
 
7
7
  __all__ = ["isolation_spelling"]
8
8
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Callable, Mapping
4
4
  from typing import Final, Protocol
5
5
 
6
- from parallax.core.db_port import (
6
+ from parallax.core.internal.db_port import (
7
7
  PoolAvailable,
8
8
  PoolDetached,
9
9
  PoolMeasurements,
@@ -12,7 +12,7 @@ from parallax.core.db_port import (
12
12
  Route,
13
13
  RouteReadiness,
14
14
  )
15
- from parallax.core.diagnostics import diagnostic_for
15
+ from parallax.core.internal.diagnostics import diagnostic_for
16
16
 
17
17
  __all__ = ["PostgresPoolMetrics"]
18
18
 
@@ -6,8 +6,8 @@ from typing import TYPE_CHECKING, Final, Literal
6
6
 
7
7
  import psycopg_pool
8
8
 
9
- from parallax.core.base import INFINITY
10
- from parallax.core.db_port import (
9
+ from parallax.core.internal.base import INFINITY
10
+ from parallax.core.internal.db_port import (
11
11
  CredentialSource,
12
12
  DatabaseStartupError,
13
13
  DriverManaged,
@@ -273,7 +273,7 @@ def _build_pool(
273
273
  Where a credential source is configured, those keywords become a CALLABLE
274
274
  the pool resolves on every connection attempt, which is what makes the
275
275
  secret late rather than frozen into configuration. Under
276
- :data:`~parallax.core.db_port.DRIVER_MANAGED` the static dictionary is
276
+ :data:`~parallax.core.internal.db_port.DRIVER_MANAGED` the static dictionary is
277
277
  passed exactly as it would be otherwise and carries no ``password`` at all,
278
278
  leaving authentication to the driver and the server.
279
279
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Final, Literal
5
5
 
6
- from parallax.core.db_port import AcquisitionReason, FallbackReason, Route, RoutePolicy
6
+ from parallax.core.internal.db_port import AcquisitionReason, FallbackReason, Route, RoutePolicy
7
7
 
8
8
  __all__ = ["PlanStep", "RoutePlan"]
9
9
 
@@ -7,12 +7,13 @@ from time import monotonic
7
7
  from types import MappingProxyType
8
8
  from typing import TYPE_CHECKING, Final
9
9
 
10
- from parallax.core.db_port import (
10
+ from parallax.core.internal.base import UsageError
11
+ from parallax.core.internal.db_port import (
12
+ AUTHORIZATION_INVALID,
11
13
  CleanupIssue,
12
14
  ConnectionAcquisitionError,
13
15
  ConnectionContext,
14
16
  ConnectionContextSource,
15
- InvalidAuthorizationError,
16
17
  PoolMetricsSource,
17
18
  ReleaseUnconfirmed,
18
19
  Route,
@@ -20,8 +21,8 @@ from parallax.core.db_port import (
20
21
  RoutePolicy,
21
22
  report_resource_issues,
22
23
  )
23
- from parallax.core.diagnostics import diagnostic_for
24
- from parallax.core.dialect import POSTGRES, Dialect
24
+ from parallax.core.internal.diagnostics import diagnostic_for
25
+ from parallax.core.internal.dialect import POSTGRES, Dialect
25
26
  from parallax.postgres._authorization import PostgresRole
26
27
  from parallax.postgres._context import PostgresConnectionContext, Step
27
28
  from parallax.postgres._route_plan import RoutePlan
@@ -117,8 +118,8 @@ class PostgresRuntime:
117
118
  def principal_source(self, authorization: PostgresRole) -> ConnectionContextSource:
118
119
  """Bind execution under ``authorization`` without acquiring."""
119
120
  if not isinstance(authorization, PostgresRole): # pyright: ignore[reportUnnecessaryIsInstance] - validates untyped callers at the public boundary
120
- raise InvalidAuthorizationError(
121
- "PostgreSQL principal execution requires a PostgresRole"
121
+ raise UsageError(
122
+ AUTHORIZATION_INVALID, "PostgreSQL principal execution requires a PostgresRole"
122
123
  )
123
124
  return _PostgresContextSource(self, authorization)
124
125
 
@@ -3,8 +3,8 @@ from __future__ import annotations
3
3
  from time import monotonic
4
4
  from typing import TYPE_CHECKING, Literal
5
5
 
6
- from parallax.core.db_error import DatabaseError
7
- from parallax.core.db_port import (
6
+ from parallax.core.internal.db_error import DatabaseError
7
+ from parallax.core.internal.db_port import (
8
8
  CleanupResult,
9
9
  ConnectionAcquisitionError,
10
10
  DatabaseStartupError,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parallax-postgres
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Parallax Postgres database adapter: the concrete database runtime over psycopg and its companion pool.
5
5
  Author: Parallax
6
6
  License-Expression: Apache-2.0
@@ -11,15 +11,16 @@ Requires-Python: >=3.14
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  License-File: NOTICE
14
- Requires-Dist: parallax-core==0.1.0
14
+ Requires-Dist: parallax-core==0.2.0
15
15
  Requires-Dist: psycopg[binary]<3.4,>=3.3.6
16
16
  Requires-Dist: psycopg-pool>=3.3.1
17
+ Requires-Dist: pyyaml>=6.0
17
18
  Dynamic: license-file
18
19
 
19
20
  # parallax-postgres
20
21
 
21
22
  Parallax's Postgres database adapter over `psycopg` and its companion
22
- `psycopg-pool`. Install `parallax-postgres==0.1.0` with the application packages
23
+ `psycopg-pool`. Install `parallax-postgres==0.2.0` with the application packages
23
24
  shown in the [getting-started guide](https://github.com/davidsunglee/parallax/blob/main/languages/python/docs/guide/01-getting-started.md#install).
24
25
  That guide links the authoritative Python, platform and PostgreSQL support
25
26
  declarations and the C-driver and source-build installation paths. The normal
@@ -31,7 +32,7 @@ for publication status. The distribution includes Apache-2.0 `LICENSE` and `NOTI
31
32
  ## Connecting
32
33
 
33
34
  ```python
34
- from parallax.core.db_port import Password
35
+ from parallax.core import Password
35
36
  from parallax.postgres import OnDemandOptions, PoolOptions, PostgresAdapter, PostgresEndpoint
36
37
  from parallax.snapshot import connect
37
38
 
@@ -63,10 +64,10 @@ PostgresEndpoint("postgresql://app:<password>@db.internal/app", credentials=Pass
63
64
  ValueError: connection_string must not carry a password; supply it through credentials.
64
65
  ```
65
66
 
66
- There are three spellings, all exported from `parallax.core.db_port`:
67
+ There are three spellings, all exported from `parallax.core`:
67
68
 
68
69
  ```python
69
- from parallax.core.db_port import DRIVER_MANAGED, Password
70
+ from parallax.core import DRIVER_MANAGED, Password
70
71
 
71
72
  # a constant secret; `Password` is its own source and keeps it out of every repr
72
73
  PostgresEndpoint("postgresql://app@db.internal:5432/app", credentials=Password("s3cret"))
@@ -4,7 +4,9 @@ README.md
4
4
  pyproject.toml
5
5
  setup.py
6
6
  src/parallax/postgres/__init__.py
7
+ src/parallax/postgres/_adapter.py
7
8
  src/parallax/postgres/_authorization.py
9
+ src/parallax/postgres/_binding.py
8
10
  src/parallax/postgres/_cloaders.pxi
9
11
  src/parallax/postgres/_cloaders_binary.pyi
10
12
  src/parallax/postgres/_cloaders_binary.pyx
@@ -21,7 +23,6 @@ src/parallax/postgres/_route.py
21
23
  src/parallax/postgres/_route_plan.py
22
24
  src/parallax/postgres/_runtime.py
23
25
  src/parallax/postgres/_startup.py
24
- src/parallax/postgres/adapter.py
25
26
  src/parallax/postgres/py.typed
26
27
  src/parallax/postgres/_psycopg_layout/cloader.pxi
27
28
  src/parallax/postgres/_psycopg_layout/psycopg_binary/__init__.pxd
@@ -1,3 +1,4 @@
1
- parallax-core==0.1.0
1
+ parallax-core==0.2.0
2
2
  psycopg[binary]<3.4,>=3.3.6
3
3
  psycopg-pool>=3.3.1
4
+ pyyaml>=6.0