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.
- {parallax_postgres-0.1.0/src/parallax_postgres.egg-info → parallax_postgres-0.2.0}/PKG-INFO +7 -6
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/README.md +4 -4
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/pyproject.toml +4 -2
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/__init__.py +13 -1
- parallax_postgres-0.1.0/src/parallax/postgres/adapter.py → parallax_postgres-0.2.0/src/parallax/postgres/_adapter.py +2 -2
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_authorization.py +4 -3
- parallax_postgres-0.2.0/src/parallax/postgres/_binding.py +324 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders.pxi +1 -1
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_connection.py +62 -31
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_context.py +3 -3
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_endpoint.py +6 -7
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_isolation.py +1 -1
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_pool_metrics.py +2 -2
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_route.py +3 -3
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_route_plan.py +1 -1
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_runtime.py +7 -6
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_startup.py +2 -2
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0/src/parallax_postgres.egg-info}/PKG-INFO +7 -6
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/SOURCES.txt +2 -1
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/requires.txt +2 -1
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/LICENSE +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/NOTICE +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/setup.cfg +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/setup.py +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyi +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyx +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_c.pyi +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_c.pyx +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_compiled_loaders.py +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_options.py +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/cloader.pxi +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_binary/__init__.pxd +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_binary/_psycopg.pxd +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_c/__init__.pxd +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_psycopg_layout/psycopg_c/_psycopg.pxd +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/py.typed +0 -0
- {parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
67
|
+
There are three spellings, all exported from `parallax.core`:
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
|
-
from parallax.core
|
|
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.
|
|
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
|
|
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
|
|
48
|
+
There are three spellings, all exported from `parallax.core`:
|
|
49
49
|
|
|
50
50
|
```python
|
|
51
|
-
from parallax.core
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
32
|
+
raise UsageError(AUTHORIZATION_INVALID, "a PostgreSQL role name must be a string")
|
|
32
33
|
if not self.name:
|
|
33
|
-
raise
|
|
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.
|
|
42
|
-
from parallax.core.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
361
|
-
|
|
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.
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
67
|
+
There are three spellings, all exported from `parallax.core`:
|
|
67
68
|
|
|
68
69
|
```python
|
|
69
|
-
from parallax.core
|
|
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"))
|
{parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyi
RENAMED
|
File without changes
|
{parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_cloaders_binary.pyx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax/postgres/_compiled_loaders.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{parallax_postgres-0.1.0 → parallax_postgres-0.2.0}/src/parallax_postgres.egg-info/top_level.txt
RENAMED
|
File without changes
|