community-agent-framework-postgres 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_framework_community_postgres/__init__.py +77 -0
- agent_framework_community_postgres/__main__.py +3 -0
- agent_framework_community_postgres/_checkpoint_storage.py +218 -0
- agent_framework_community_postgres/_cli.py +97 -0
- agent_framework_community_postgres/_client.py +390 -0
- agent_framework_community_postgres/_entra.py +178 -0
- agent_framework_community_postgres/_framework.py +148 -0
- agent_framework_community_postgres/_history_provider.py +261 -0
- agent_framework_community_postgres/_json.py +34 -0
- agent_framework_community_postgres/_leases.py +196 -0
- agent_framework_community_postgres/_migrations.py +235 -0
- agent_framework_community_postgres/_persistence.py +184 -0
- agent_framework_community_postgres/_retention.py +159 -0
- agent_framework_community_postgres/_session_store.py +135 -0
- agent_framework_community_postgres/_store.py +66 -0
- agent_framework_community_postgres/_thread_snapshot_store.py +162 -0
- agent_framework_community_postgres/py.typed +0 -0
- community_agent_framework_postgres-0.1.1.dist-info/METADATA +367 -0
- community_agent_framework_postgres-0.1.1.dist-info/RECORD +21 -0
- community_agent_framework_postgres-0.1.1.dist-info/WHEEL +4 -0
- community_agent_framework_postgres-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""PostgreSQL storage for Microsoft Agent Framework.
|
|
2
|
+
|
|
3
|
+
Every store can be built on its own, or from one :class:`PostgresPersistence`
|
|
4
|
+
that shares a single connection pool. See the README for examples.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from ._checkpoint_storage import PostgresCheckpointStorage
|
|
11
|
+
from ._client import (
|
|
12
|
+
LeaseLost,
|
|
13
|
+
LeaseUnavailable,
|
|
14
|
+
PostgresClient,
|
|
15
|
+
PostgresSettings,
|
|
16
|
+
PostgresStorageError,
|
|
17
|
+
TableNames,
|
|
18
|
+
)
|
|
19
|
+
from ._entra import ENTRA_SCOPE, EntraCredential
|
|
20
|
+
from ._framework import Compatibility, compatibility
|
|
21
|
+
from ._history_provider import PostgresHistoryProvider
|
|
22
|
+
from ._leases import Lease, PostgresLeases
|
|
23
|
+
from ._migrations import MIGRATIONS, MigrationReport
|
|
24
|
+
from ._migrations import render as render_migrations
|
|
25
|
+
from ._persistence import PostgresPersistence
|
|
26
|
+
from ._retention import PurgeReport, RetentionMode, RetentionPolicy
|
|
27
|
+
from ._session_store import PostgresSessionStore
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ._thread_snapshot_store import PostgresAGUIThreadSnapshotStore
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
__version__ = version("community-agent-framework-postgres")
|
|
34
|
+
except PackageNotFoundError: # pragma: no cover - source checkout without install
|
|
35
|
+
__version__ = "0.0.0"
|
|
36
|
+
|
|
37
|
+
_LAZY = {"PostgresAGUIThreadSnapshotStore"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __getattr__(name: str) -> Any:
|
|
41
|
+
if name in _LAZY:
|
|
42
|
+
from ._thread_snapshot_store import PostgresAGUIThreadSnapshotStore
|
|
43
|
+
|
|
44
|
+
return PostgresAGUIThreadSnapshotStore
|
|
45
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __dir__() -> list[str]:
|
|
49
|
+
return sorted({*globals(), *_LAZY})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"ENTRA_SCOPE",
|
|
54
|
+
"MIGRATIONS",
|
|
55
|
+
"Compatibility",
|
|
56
|
+
"EntraCredential",
|
|
57
|
+
"Lease",
|
|
58
|
+
"LeaseLost",
|
|
59
|
+
"LeaseUnavailable",
|
|
60
|
+
"MigrationReport",
|
|
61
|
+
"PostgresAGUIThreadSnapshotStore",
|
|
62
|
+
"PostgresCheckpointStorage",
|
|
63
|
+
"PostgresClient",
|
|
64
|
+
"PostgresHistoryProvider",
|
|
65
|
+
"PostgresLeases",
|
|
66
|
+
"PostgresPersistence",
|
|
67
|
+
"PostgresSessionStore",
|
|
68
|
+
"PostgresSettings",
|
|
69
|
+
"PostgresStorageError",
|
|
70
|
+
"PurgeReport",
|
|
71
|
+
"RetentionMode",
|
|
72
|
+
"RetentionPolicy",
|
|
73
|
+
"TableNames",
|
|
74
|
+
"__version__",
|
|
75
|
+
"compatibility",
|
|
76
|
+
"render_migrations",
|
|
77
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Workflow checkpoints in PostgreSQL with the framework's own hybrid JSON + restricted-pickle encoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Any, ClassVar
|
|
10
|
+
|
|
11
|
+
from agent_framework import SecretString, WorkflowCheckpoint
|
|
12
|
+
from agent_framework.exceptions import WorkflowCheckpointException
|
|
13
|
+
from psycopg import sql
|
|
14
|
+
|
|
15
|
+
from ._client import ClientHandle, PostgresClient, optional_text
|
|
16
|
+
from ._entra import EntraCredential
|
|
17
|
+
from ._framework import decode_checkpoint_value, encode_checkpoint_value
|
|
18
|
+
from ._json import encode_jsonb
|
|
19
|
+
from ._retention import EXPIRES_AT, PurgeReport, RetentionPolicy, purge_rows
|
|
20
|
+
from ._store import BaseStore
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PostgresCheckpointStorage(BaseStore):
|
|
26
|
+
"""PostgreSQL-backed workflow ``CheckpointStorage``.
|
|
27
|
+
|
|
28
|
+
Checkpoints are encoded exactly as ``FileCheckpointStorage`` and ``CosmosCheckpointStorage``
|
|
29
|
+
encode them, so the same restricted deserializer and ``allowed_checkpoint_types`` apply, and
|
|
30
|
+
the same warning: the table is a trust boundary, because non-JSON values are pickled. Rows are
|
|
31
|
+
keyed by application, optional ``tenant_id`` and ``scope`` (a conversation or response id, the
|
|
32
|
+
way Foundry hosting scopes its stores) and the checkpoint id. "Latest" is the last row written
|
|
33
|
+
in a scope, whatever clock produced it. Saving an existing id replaces its content and keeps
|
|
34
|
+
its position, as the framework's own stores do.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
PAYLOAD_FORMAT: ClassVar[str] = "agent-framework.python.checkpoint.v1"
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
application_id: str,
|
|
43
|
+
tenant_id: str | None = None,
|
|
44
|
+
scope: str | None = None,
|
|
45
|
+
allowed_checkpoint_types: list[str] | None = None,
|
|
46
|
+
connection_string: str | SecretString | None = None,
|
|
47
|
+
client: PostgresClient | ClientHandle | None = None,
|
|
48
|
+
credential: EntraCredential | None = None,
|
|
49
|
+
env_file_path: str | None = None,
|
|
50
|
+
env_file_encoding: str | None = None,
|
|
51
|
+
schema: str = "public",
|
|
52
|
+
table_prefix: str = "af_",
|
|
53
|
+
retention: RetentionPolicy | None = None,
|
|
54
|
+
pool_kwargs: Mapping[str, Any] | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
super().__init__(
|
|
57
|
+
application_id=application_id,
|
|
58
|
+
connection_string=connection_string,
|
|
59
|
+
client=client,
|
|
60
|
+
credential=credential,
|
|
61
|
+
env_file_path=env_file_path,
|
|
62
|
+
env_file_encoding=env_file_encoding,
|
|
63
|
+
schema=schema,
|
|
64
|
+
table_prefix=table_prefix,
|
|
65
|
+
retention=retention,
|
|
66
|
+
pool_kwargs=pool_kwargs,
|
|
67
|
+
)
|
|
68
|
+
self.tenant_id = optional_text(tenant_id, "tenant_id")
|
|
69
|
+
self.scope = optional_text(scope, "scope")
|
|
70
|
+
self._allowed_types: frozenset[str] = frozenset(allowed_checkpoint_types or [])
|
|
71
|
+
self._lock_key = json.dumps(
|
|
72
|
+
[
|
|
73
|
+
"agent-framework-postgres:checkpoints",
|
|
74
|
+
schema,
|
|
75
|
+
table_prefix,
|
|
76
|
+
self.application_id,
|
|
77
|
+
self.tenant_id,
|
|
78
|
+
self.scope,
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
_SCOPE = sql.SQL(
|
|
83
|
+
"application_id = %(app)s AND tenant_id = %(tenant)s AND scope = %(scope)s AND payload_format = %(format)s"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _params(self) -> dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"app": self.application_id,
|
|
89
|
+
"tenant": self.tenant_id,
|
|
90
|
+
"scope": self.scope,
|
|
91
|
+
"format": self.PAYLOAD_FORMAT,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _parse_timestamp(checkpoint: WorkflowCheckpoint) -> datetime:
|
|
96
|
+
try:
|
|
97
|
+
parsed = datetime.fromisoformat(checkpoint.timestamp)
|
|
98
|
+
except (TypeError, ValueError) as exc:
|
|
99
|
+
raise WorkflowCheckpointException("A checkpoint has an unparsable timestamp.") from exc
|
|
100
|
+
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=UTC)
|
|
101
|
+
|
|
102
|
+
def _decode(self, payload: Any) -> WorkflowCheckpoint:
|
|
103
|
+
try:
|
|
104
|
+
return WorkflowCheckpoint.from_dict(decode_checkpoint_value(payload, allowed_types=self._allowed_types))
|
|
105
|
+
except WorkflowCheckpointException:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
raise WorkflowCheckpointException("A stored checkpoint cannot be restored.") from exc
|
|
109
|
+
|
|
110
|
+
async def save(self, checkpoint: WorkflowCheckpoint) -> str:
|
|
111
|
+
"""Encode, verify it can be restored under this store's allowed types, then write it."""
|
|
112
|
+
when = self._parse_timestamp(checkpoint)
|
|
113
|
+
try:
|
|
114
|
+
encoded = encode_checkpoint_value(checkpoint.to_dict())
|
|
115
|
+
decode_checkpoint_value(encoded, allowed_types=self._allowed_types)
|
|
116
|
+
except WorkflowCheckpointException:
|
|
117
|
+
raise
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
raise WorkflowCheckpointException(
|
|
120
|
+
"A checkpoint cannot be encoded or restored under this storage's allowed types."
|
|
121
|
+
) from exc
|
|
122
|
+
payload = encode_jsonb(encoded, what="Checkpoint")
|
|
123
|
+
async with self._client.connection() as connection:
|
|
124
|
+
# Writers of one scope take turns, so sequence order is commit order.
|
|
125
|
+
await connection.execute("SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))", (self._lock_key,))
|
|
126
|
+
await connection.execute(
|
|
127
|
+
sql.SQL(
|
|
128
|
+
"INSERT INTO {checkpoints} (application_id, tenant_id, scope, payload_format, workflow_name,"
|
|
129
|
+
" checkpoint_id, previous_checkpoint_id, checkpoint_timestamp, payload, expires_at)"
|
|
130
|
+
" VALUES (%(app)s, %(tenant)s, %(scope)s, %(format)s, %(workflow)s, %(id)s, %(previous)s,"
|
|
131
|
+
" %(when)s, %(payload)s, {expires})"
|
|
132
|
+
" ON CONFLICT (application_id, tenant_id, scope, checkpoint_id) DO UPDATE SET"
|
|
133
|
+
" workflow_name = EXCLUDED.workflow_name, previous_checkpoint_id = EXCLUDED.previous_checkpoint_id,"
|
|
134
|
+
" checkpoint_timestamp = EXCLUDED.checkpoint_timestamp, payload = EXCLUDED.payload,"
|
|
135
|
+
" expires_at = EXCLUDED.expires_at, payload_format = EXCLUDED.payload_format"
|
|
136
|
+
).format(checkpoints=self._names.table("checkpoints"), expires=EXPIRES_AT),
|
|
137
|
+
self._params()
|
|
138
|
+
| {
|
|
139
|
+
"workflow": checkpoint.workflow_name,
|
|
140
|
+
"id": checkpoint.checkpoint_id,
|
|
141
|
+
"previous": checkpoint.previous_checkpoint_id,
|
|
142
|
+
"when": when,
|
|
143
|
+
"payload": payload,
|
|
144
|
+
"ttl": self._ttl(),
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
logger.debug("Saved a checkpoint.")
|
|
148
|
+
return checkpoint.checkpoint_id
|
|
149
|
+
|
|
150
|
+
async def load(self, checkpoint_id: str) -> WorkflowCheckpoint:
|
|
151
|
+
"""The checkpoint with this id in this scope; ``WorkflowCheckpointException`` when absent or unreadable."""
|
|
152
|
+
async with self._client.connection(transactional=False) as connection:
|
|
153
|
+
cursor = await connection.execute(
|
|
154
|
+
sql.SQL("SELECT payload FROM {checkpoints} WHERE {scope} AND checkpoint_id = %(id)s").format(
|
|
155
|
+
checkpoints=self._names.table("checkpoints"), scope=self._SCOPE
|
|
156
|
+
),
|
|
157
|
+
self._params() | {"id": checkpoint_id},
|
|
158
|
+
)
|
|
159
|
+
row = await cursor.fetchone()
|
|
160
|
+
if row is None:
|
|
161
|
+
raise WorkflowCheckpointException("No checkpoint found with that ID in this scope.")
|
|
162
|
+
return self._decode(row[0])
|
|
163
|
+
|
|
164
|
+
async def _rows(
|
|
165
|
+
self, workflow_name: str, *, column: sql.Identifier, newest_first: bool = False, limit: int | None = None
|
|
166
|
+
) -> list[Any]:
|
|
167
|
+
order = sql.SQL("DESC") if newest_first else sql.SQL("ASC")
|
|
168
|
+
async with self._client.connection(transactional=False) as connection:
|
|
169
|
+
cursor = await connection.execute(
|
|
170
|
+
sql.SQL(
|
|
171
|
+
"SELECT {column} FROM {checkpoints} WHERE {scope} AND workflow_name = %(workflow)s"
|
|
172
|
+
" ORDER BY sequence {order} LIMIT %(limit)s"
|
|
173
|
+
).format(column=column, checkpoints=self._names.table("checkpoints"), scope=self._SCOPE, order=order),
|
|
174
|
+
self._params() | {"workflow": workflow_name, "limit": limit},
|
|
175
|
+
)
|
|
176
|
+
return [row[0] for row in await cursor.fetchall()]
|
|
177
|
+
|
|
178
|
+
async def list_checkpoints(self, *, workflow_name: str) -> list[WorkflowCheckpoint]:
|
|
179
|
+
"""Checkpoints for the workflow in write order; rows that fail to decode are logged and skipped."""
|
|
180
|
+
found: list[WorkflowCheckpoint] = []
|
|
181
|
+
for payload in await self._rows(workflow_name, column=sql.Identifier("payload")):
|
|
182
|
+
try:
|
|
183
|
+
found.append(self._decode(payload))
|
|
184
|
+
except WorkflowCheckpointException:
|
|
185
|
+
logger.warning("Skipping a checkpoint row that failed to decode.")
|
|
186
|
+
return found
|
|
187
|
+
|
|
188
|
+
async def list_checkpoint_ids(self, *, workflow_name: str) -> list[str]:
|
|
189
|
+
"""Every checkpoint id for the workflow in write order, without decoding any row."""
|
|
190
|
+
return await self._rows(workflow_name, column=sql.Identifier("checkpoint_id"))
|
|
191
|
+
|
|
192
|
+
async def get_latest(self, *, workflow_name: str) -> WorkflowCheckpoint | None:
|
|
193
|
+
"""The last checkpoint written for the workflow in this scope, or ``None``; raises when it does not decode."""
|
|
194
|
+
rows = await self._rows(workflow_name, column=sql.Identifier("payload"), newest_first=True, limit=1)
|
|
195
|
+
return self._decode(rows[0]) if rows else None
|
|
196
|
+
|
|
197
|
+
async def delete(self, checkpoint_id: str) -> bool:
|
|
198
|
+
"""Delete by id within this store's scope; ``True`` when a row existed."""
|
|
199
|
+
async with self._client.connection() as connection:
|
|
200
|
+
cursor = await connection.execute(
|
|
201
|
+
sql.SQL("DELETE FROM {checkpoints} WHERE {scope} AND checkpoint_id = %(id)s").format(
|
|
202
|
+
checkpoints=self._names.table("checkpoints"), scope=self._SCOPE
|
|
203
|
+
),
|
|
204
|
+
self._params() | {"id": checkpoint_id},
|
|
205
|
+
)
|
|
206
|
+
return cursor.rowcount > 0
|
|
207
|
+
|
|
208
|
+
async def purge(self) -> PurgeReport:
|
|
209
|
+
"""Delete expired checkpoints in this store's scope, whatever policy stamped them (no tombstone form)."""
|
|
210
|
+
count = await purge_rows(
|
|
211
|
+
self._client,
|
|
212
|
+
table=self._names.table("checkpoints"),
|
|
213
|
+
where=sql.SQL("application_id = %(app)s AND tenant_id = %(tenant)s AND scope = %(scope)s"),
|
|
214
|
+
params={"app": self.application_id, "tenant": self.tenant_id, "scope": self.scope},
|
|
215
|
+
mode="delete",
|
|
216
|
+
tombstone=None,
|
|
217
|
+
)
|
|
218
|
+
return PurgeReport({f"{self._names.prefix}checkpoints": count})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""``python -m agent_framework_community_postgres``: migrate, status, purge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
from agent_framework.exceptions import SettingNotFoundError
|
|
11
|
+
|
|
12
|
+
from ._client import PostgresStorageError, TableNames
|
|
13
|
+
from ._migrations import MIGRATIONS, ensure_supported, render
|
|
14
|
+
from ._persistence import PostgresPersistence
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _add_common(parser: argparse.ArgumentParser, *, suppress: bool = False) -> None:
|
|
18
|
+
"""Add the shared options; subcommand copies suppress defaults so both positions work."""
|
|
19
|
+
|
|
20
|
+
def d(value: str | None) -> str | None:
|
|
21
|
+
return argparse.SUPPRESS if suppress else value
|
|
22
|
+
|
|
23
|
+
parser.add_argument("--schema", default=d("public"))
|
|
24
|
+
parser.add_argument("--table-prefix", default=d("af_"))
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--application-id", default=d(None), help="required by purge; migrate and status use 'cli' when omitted"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parser() -> argparse.ArgumentParser:
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
prog="python -m agent_framework_community_postgres",
|
|
33
|
+
description="Manage the PostgreSQL schema used by community-agent-framework-postgres.",
|
|
34
|
+
)
|
|
35
|
+
_add_common(parser)
|
|
36
|
+
shared = argparse.ArgumentParser(add_help=False)
|
|
37
|
+
_add_common(shared, suppress=True)
|
|
38
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
39
|
+
migrate = commands.add_parser("migrate", parents=[shared], help="apply pending migrations")
|
|
40
|
+
migrate.add_argument("--print", action="store_true", dest="print_sql", help="print the SQL instead of running it")
|
|
41
|
+
migrate.add_argument(
|
|
42
|
+
"--from-version", type=int, default=None, help="with --print, print only the versions after this one"
|
|
43
|
+
)
|
|
44
|
+
commands.add_parser("status", parents=[shared], help="show the applied and pending versions")
|
|
45
|
+
purge = commands.add_parser(
|
|
46
|
+
"purge", parents=[shared], help="tombstone or delete the rows whose expires_at has passed"
|
|
47
|
+
)
|
|
48
|
+
purge.add_argument("--mode", choices=["tombstone", "delete"], default="tombstone")
|
|
49
|
+
return parser
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _run(args: argparse.Namespace) -> int:
|
|
53
|
+
async with PostgresPersistence(
|
|
54
|
+
application_id=args.application_id or "cli",
|
|
55
|
+
schema=args.schema,
|
|
56
|
+
table_prefix=args.table_prefix,
|
|
57
|
+
) as persistence:
|
|
58
|
+
if args.command == "migrate":
|
|
59
|
+
report = await persistence.migrate()
|
|
60
|
+
print(f"applied: {', '.join(map(str, report.applied)) or 'none'}; current version: {report.current}")
|
|
61
|
+
elif args.command == "status":
|
|
62
|
+
current = await persistence.current_version()
|
|
63
|
+
pending = range(current + 1, len(MIGRATIONS) + 1)
|
|
64
|
+
print(f"current version: {current}; pending: {', '.join(map(str, pending)) or 'none'}")
|
|
65
|
+
ensure_supported(current)
|
|
66
|
+
else:
|
|
67
|
+
report = await persistence.purge(mode=args.mode)
|
|
68
|
+
for table, count in sorted(report.counts.items()):
|
|
69
|
+
print(f"{table}: {count}")
|
|
70
|
+
print(f"total: {report.total}")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
75
|
+
"""Entry point; returns the process exit code (2 for invalid options, 1 for other errors)."""
|
|
76
|
+
args = _parser().parse_args(argv)
|
|
77
|
+
if args.command == "purge" and args.application_id is None:
|
|
78
|
+
_parser().error("purge needs --application-id")
|
|
79
|
+
if args.command == "migrate" and args.from_version is not None and not args.print_sql:
|
|
80
|
+
_parser().error("--from-version needs --print")
|
|
81
|
+
loop_factory = asyncio.SelectorEventLoop if sys.platform == "win32" else None
|
|
82
|
+
try:
|
|
83
|
+
names = TableNames(schema=args.schema, prefix=args.table_prefix)
|
|
84
|
+
if args.command == "migrate" and args.print_sql:
|
|
85
|
+
print(render(names, range((args.from_version or 0) + 1, len(MIGRATIONS) + 1)), end="")
|
|
86
|
+
return 0
|
|
87
|
+
with asyncio.Runner(loop_factory=loop_factory) as runner:
|
|
88
|
+
return runner.run(_run(args))
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
# This package's option checks (TableNames, the connection string) raise ValueError with no values in it.
|
|
91
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
92
|
+
return 2
|
|
93
|
+
except PostgresStorageError as exc:
|
|
94
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
95
|
+
except SettingNotFoundError:
|
|
96
|
+
print("error: set POSTGRES_CONNECTION_STRING", file=sys.stderr)
|
|
97
|
+
return 1
|