xqo-postgres 3.0.0__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.
@@ -0,0 +1,116 @@
1
+ """xqo-postgres: the "postgres" event store for xqo.
2
+
3
+ pip install xqo-postgres # psycopg 3
4
+ export XQO_PG_DSN="postgresql://xqo:secret@db.example:5432/xqo"
5
+ "storage": {"backend": "postgres", "dsn_env": "XQO_PG_DSN", "table": "xqo_events"}
6
+
7
+ Use it when several machines or a hosted dashboard need one shared database; a single local HQ is better served by
8
+ xqo-sqlite. Back it up with pg_dump (xqo backup leaves database contents to Postgres's own tools).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import re
15
+ import threading
16
+ from pathlib import Path
17
+ from typing import Iterable, Iterator
18
+
19
+ from xqo.api import SEED_KINDS, EventStore, Plugin, register_store
20
+
21
+ __version__ = "3.0.0"
22
+ KEYS = {"backend", "dsn_env", "table"}
23
+
24
+ TABLE = re.compile(r"^[a-z_][a-z0-9_]{0,62}$")
25
+
26
+
27
+ class PostgresStore(EventStore):
28
+ backend = "postgres"
29
+
30
+ def __init__(self, dsn: str, table: str = "xqo_events"):
31
+ try:
32
+ import psycopg
33
+ except ImportError:
34
+ raise ValueError("storage: the postgres backend needs psycopg: pip install xqo-postgres") from None
35
+ if not TABLE.match(table):
36
+ raise ValueError("storage.table: lowercase letters, digits and _ only")
37
+ self.table = table
38
+ self._lock = threading.Lock()
39
+ try:
40
+ self.db = psycopg.connect(dsn, autocommit=True, connect_timeout=10)
41
+ self.db.execute(f"""
42
+ CREATE TABLE IF NOT EXISTS {table} (
43
+ seq BIGSERIAL PRIMARY KEY, ts TEXT, kind TEXT, task_id TEXT, root_id TEXT, data JSONB NOT NULL);
44
+ CREATE INDEX IF NOT EXISTS {table}_root ON {table}(root_id);
45
+ CREATE INDEX IF NOT EXISTS {table}_task ON {table}(task_id);
46
+ CREATE INDEX IF NOT EXISTS {table}_kind ON {table}(kind);""")
47
+ except psycopg.Error as e:
48
+ raise ValueError(f"storage: can't use Postgres: {str(e).strip().splitlines()[0]}") from None
49
+
50
+ def _rows(self, sql: str, args: tuple = ()) -> list[dict]:
51
+ with self._lock:
52
+ rows = self.db.execute(sql, args).fetchall()
53
+ return [r[0] if isinstance(r[0], dict) else json.loads(r[0]) for r in rows]
54
+
55
+ def append(self, d: dict) -> None:
56
+ with self._lock:
57
+ self.db.execute(f"INSERT INTO {self.table}(ts, kind, task_id, root_id, data) VALUES (%s, %s, %s, %s, %s)",
58
+ (d.get("ts"), d.get("kind"), d.get("task_id"), d.get("root_task_id"), json.dumps(d)))
59
+
60
+ def recent(self, limit: int) -> list[dict]:
61
+ return self._rows(f"SELECT data FROM (SELECT seq, data FROM {self.table} ORDER BY seq DESC LIMIT %s) r "
62
+ "ORDER BY seq", (int(limit),))
63
+
64
+ def seeds(self) -> list[dict]:
65
+ return self._rows(f"SELECT data FROM {self.table} WHERE kind = ANY(%s) ORDER BY seq", (list(SEED_KINDS),))
66
+
67
+ def tree(self, root_id: str) -> list[dict]:
68
+ return self._rows(f"SELECT data FROM {self.table} WHERE root_id = %s OR task_id = %s ORDER BY seq",
69
+ (root_id, root_id))
70
+
71
+ def by_kind_prefix(self, prefix: str) -> list[dict]:
72
+ esc = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
73
+ return self._rows(f"SELECT data FROM {self.table} WHERE kind LIKE %s ORDER BY seq", (esc + "%",))
74
+
75
+ def purge(self, root_id: str, ids: set[str], keep_kinds: Iterable[str]) -> int:
76
+ with self._lock:
77
+ cur = self.db.execute(
78
+ f"DELETE FROM {self.table} WHERE NOT (kind = ANY(%s)) AND "
79
+ "(root_id = %s OR task_id = %s OR task_id = ANY(%s))",
80
+ (list(keep_kinds), root_id, root_id, list(ids)))
81
+ return cur.rowcount
82
+
83
+ def iter_all(self) -> Iterator[dict]:
84
+ yield from self._rows(f"SELECT data FROM {self.table} ORDER BY seq")
85
+
86
+ def count(self) -> int:
87
+ with self._lock:
88
+ return self.db.execute(f"SELECT COUNT(*) FROM {self.table}").fetchone()[0]
89
+
90
+ def describe(self) -> dict:
91
+ return {"backend": self.backend, "table": self.table, "events": self.count()}
92
+
93
+ def close(self) -> None:
94
+ with self._lock:
95
+ self.db.close()
96
+
97
+
98
+ def _factory(cfg: dict, logs: Path) -> PostgresStore:
99
+ unknown = set(cfg) - KEYS
100
+ if unknown:
101
+ raise ValueError(f"storage: unknown setting(s) {', '.join(sorted(unknown))} for postgres; "
102
+ f"known: {', '.join(sorted(KEYS))}")
103
+ env = cfg.get("dsn_env") or "XQO_PG_DSN"
104
+ dsn = os.getenv(env)
105
+ if not dsn:
106
+ raise ValueError(f"storage: set the environment variable {env} to the Postgres DSN (storage.dsn_env)")
107
+ return PostgresStore(dsn, str(cfg.get("table") or "xqo_events"))
108
+
109
+
110
+ class PostgresPlugin(Plugin):
111
+ name = "postgres"
112
+ description = "Postgres storage backend for the event log"
113
+
114
+ @classmethod
115
+ def register(cls) -> None:
116
+ register_store("postgres", _factory)
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: xqo-postgres
3
+ Version: 3.0.0
4
+ Summary: Postgres event store for xqo
5
+ Author: dudikkocak
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dudikkocak/xqo
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: xqo<4,>=3.0
12
+ Requires-Dist: psycopg[binary]>=3.1
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == "dev"
15
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ # xqo-postgres
19
+
20
+ The `postgres` event store for [xqo](https://github.com/dudikkocak/xqo): for several machines or a hosted dashboard
21
+ that share one database. A single local HQ is better served by `xqo-sqlite`.
22
+
23
+ ```bash
24
+ pip install xqo-postgres # brings psycopg 3
25
+ export XQO_PG_DSN="postgresql://xqo:secret@db.example:5432/xqo"
26
+ ```
27
+
28
+ ```json
29
+ "storage": {"backend": "postgres", "dsn_env": "XQO_PG_DSN", "table": "xqo_events"}
30
+ ```
31
+
32
+ | Key | Default | |
33
+ |---|---|---|
34
+ | `dsn_env` | `XQO_PG_DSN` | the environment variable that holds the connection string (never put it in agents.json) |
35
+ | `table` | `xqo_events` | lowercase letters, digits and `_`; created with its indexes on first start |
36
+
37
+ Back it up with `pg_dump`: `xqo backup` leaves database contents to Postgres's own tools. The tests need a
38
+ database: `XQO_TEST_PG_DSN=postgresql://... pytest`.
@@ -0,0 +1,7 @@
1
+ xqo_postgres/__init__.py,sha256=225peY6cs4tD7USpjx7e2M4VlgD98oROLEDFHFwvstU,4912
2
+ xqo_postgres-3.0.0.dist-info/licenses/LICENSE,sha256=PHtf4TI2P6oulwhOKBF8SgeOkDLp9YnLdq46mw6506s,1067
3
+ xqo_postgres-3.0.0.dist-info/METADATA,sha256=UxmqmxObUJdQrVfeU4e6kiFQaeKas4ACFbfkO08bwOQ,1341
4
+ xqo_postgres-3.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ xqo_postgres-3.0.0.dist-info/entry_points.txt,sha256=Pqpj1Q7xDqp7E2ZCC59bQhmJvf3YGI0aTm7mLcGbQ8Q,53
6
+ xqo_postgres-3.0.0.dist-info/top_level.txt,sha256=SG3WCGFiYE9ubzM14zSlc0EypZDUbogFm7Hn_dQtXTQ,13
7
+ xqo_postgres-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [xqo.plugins]
2
+ postgres = xqo_postgres:PostgresPlugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dudikkocak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ xqo_postgres