agentic-coop-db 0.3.1__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.
- agentic_coop_db-0.3.1/.gitignore +55 -0
- agentic_coop_db-0.3.1/PKG-INFO +71 -0
- agentic_coop_db-0.3.1/README.md +39 -0
- agentic_coop_db-0.3.1/pyproject.toml +82 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/__init__.py +44 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/__init__.py +5 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/config.py +66 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/doctor.py +119 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/init.py +177 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/key.py +48 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/main.py +80 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/cli/queue.py +58 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/client.py +327 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/errors.py +70 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/queue.py +202 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/retry.py +47 -0
- agentic_coop_db-0.3.1/src/agentcoopdb/vector.py +16 -0
- agentic_coop_db-0.3.1/tests/test_cli_config.py +53 -0
- agentic_coop_db-0.3.1/tests/test_client.py +113 -0
- agentic_coop_db-0.3.1/tests/test_e2e_offline.py +71 -0
- agentic_coop_db-0.3.1/tests/test_errors.py +78 -0
- agentic_coop_db-0.3.1/tests/test_queue.py +72 -0
- agentic_coop_db-0.3.1/tests/test_retry.py +139 -0
- agentic_coop_db-0.3.1/tests/test_vector.py +21 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Build artifacts
|
|
2
|
+
/bin/
|
|
3
|
+
/dist/
|
|
4
|
+
/build/
|
|
5
|
+
*.exe
|
|
6
|
+
*.test
|
|
7
|
+
*.out
|
|
8
|
+
coverage.*
|
|
9
|
+
|
|
10
|
+
# Go workspace
|
|
11
|
+
go.work
|
|
12
|
+
go.work.sum
|
|
13
|
+
vendor/
|
|
14
|
+
|
|
15
|
+
# Python
|
|
16
|
+
__pycache__/
|
|
17
|
+
*.pyc
|
|
18
|
+
*.pyo
|
|
19
|
+
*.egg-info/
|
|
20
|
+
*.egg
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.tox/
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
|
|
30
|
+
# Editor / OS
|
|
31
|
+
.DS_Store
|
|
32
|
+
.idea/
|
|
33
|
+
.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
|
|
37
|
+
# Local secrets / state (never commit these)
|
|
38
|
+
.env
|
|
39
|
+
.env.local
|
|
40
|
+
*.pem
|
|
41
|
+
*.key
|
|
42
|
+
*.crt
|
|
43
|
+
secrets/
|
|
44
|
+
|
|
45
|
+
# Local docker volumes
|
|
46
|
+
deploy/data/
|
|
47
|
+
deploy/postgres-data/
|
|
48
|
+
|
|
49
|
+
# Agentic Coop DB local state (CLI config via platformdirs)
|
|
50
|
+
.config/agentcoopdb/
|
|
51
|
+
queue.db
|
|
52
|
+
queue.db-journal
|
|
53
|
+
|
|
54
|
+
# Test containers reaper state
|
|
55
|
+
.testcontainers/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-coop-db
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Auth gateway client for shared PostgreSQL — sends parameterised SQL over HTTPS.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fheinfling/agentic-coop-db
|
|
6
|
+
Project-URL: Source, https://github.com/fheinfling/agentic-coop-db
|
|
7
|
+
Project-URL: Issues, https://github.com/fheinfling/agentic-coop-db/issues
|
|
8
|
+
Author: Franz Heinfling
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: auth,gateway,pgvector,postgres,postgresql,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: platformdirs>=4.2
|
|
23
|
+
Requires-Dist: requests>=2.32
|
|
24
|
+
Requires-Dist: typer>=0.12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
30
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# agentcoopdb (Python client)
|
|
34
|
+
|
|
35
|
+
A thin Python client for [Agentic Coop DB](https://github.com/fheinfling/agentic-coop-db), the
|
|
36
|
+
auth gateway for shared PostgreSQL.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from agentcoopdb import connect
|
|
40
|
+
|
|
41
|
+
db = connect("https://db.example.com", api_key="acd_live_...")
|
|
42
|
+
db.execute(
|
|
43
|
+
"INSERT INTO notes(id, body) VALUES ($1, $2)",
|
|
44
|
+
["b9c3...", "hi"],
|
|
45
|
+
)
|
|
46
|
+
rows = db.select("SELECT * FROM notes WHERE owner = $1", ["alice"])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install agentic-coop-db
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or install directly from GitHub (the Python client lives in a subdirectory):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install "agentic-coop-db @ git+https://github.com/fheinfling/agentic-coop-db.git#subdirectory=clients/python"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CLI
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
agentic-coop-db init # interactive onboarding wizard
|
|
65
|
+
agentic-coop-db me
|
|
66
|
+
agentic-coop-db sql "SELECT 1"
|
|
67
|
+
agentic-coop-db queue flush
|
|
68
|
+
agentic-coop-db doctor
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
See the [main repo](https://github.com/fheinfling/agentic-coop-db) for the full docs.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# agentcoopdb (Python client)
|
|
2
|
+
|
|
3
|
+
A thin Python client for [Agentic Coop DB](https://github.com/fheinfling/agentic-coop-db), the
|
|
4
|
+
auth gateway for shared PostgreSQL.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from agentcoopdb import connect
|
|
8
|
+
|
|
9
|
+
db = connect("https://db.example.com", api_key="acd_live_...")
|
|
10
|
+
db.execute(
|
|
11
|
+
"INSERT INTO notes(id, body) VALUES ($1, $2)",
|
|
12
|
+
["b9c3...", "hi"],
|
|
13
|
+
)
|
|
14
|
+
rows = db.select("SELECT * FROM notes WHERE owner = $1", ["alice"])
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install agentic-coop-db
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install directly from GitHub (the Python client lives in a subdirectory):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install "agentic-coop-db @ git+https://github.com/fheinfling/agentic-coop-db.git#subdirectory=clients/python"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
agentic-coop-db init # interactive onboarding wizard
|
|
33
|
+
agentic-coop-db me
|
|
34
|
+
agentic-coop-db sql "SELECT 1"
|
|
35
|
+
agentic-coop-db queue flush
|
|
36
|
+
agentic-coop-db doctor
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See the [main repo](https://github.com/fheinfling/agentic-coop-db) for the full docs.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentic-coop-db"
|
|
7
|
+
version = "0.3.1"
|
|
8
|
+
description = "Auth gateway client for shared PostgreSQL — sends parameterised SQL over HTTPS."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Franz Heinfling" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["postgres", "postgresql", "auth", "gateway", "pgvector", "sdk"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Database",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"requests>=2.32",
|
|
30
|
+
"typer>=0.12",
|
|
31
|
+
"platformdirs>=4.2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8",
|
|
37
|
+
"pytest-asyncio>=0.23",
|
|
38
|
+
"ruff>=0.5",
|
|
39
|
+
"mypy>=1.10",
|
|
40
|
+
"types-requests",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
agentic-coop-db = "agentcoopdb.cli.main:app"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/fheinfling/agentic-coop-db"
|
|
48
|
+
Source = "https://github.com/fheinfling/agentic-coop-db"
|
|
49
|
+
Issues = "https://github.com/fheinfling/agentic-coop-db/issues"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/agentcoopdb"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = ["src", "README.md", "tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py311"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
63
|
+
ignore = ["E501"]
|
|
64
|
+
|
|
65
|
+
# typer.Option(...) / typer.Argument(...) MUST appear in argument
|
|
66
|
+
# defaults — that's how Typer (and FastAPI) declare option metadata.
|
|
67
|
+
# B008 ("Do not perform function call in argument defaults") is a
|
|
68
|
+
# known false positive for these libraries; suppress only on the CLI
|
|
69
|
+
# entrypoint module.
|
|
70
|
+
[tool.ruff.lint.per-file-ignores]
|
|
71
|
+
"src/agentcoopdb/cli/main.py" = ["B008"]
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.11"
|
|
75
|
+
strict = true
|
|
76
|
+
warn_unused_ignores = true
|
|
77
|
+
disallow_untyped_defs = true
|
|
78
|
+
no_implicit_optional = true
|
|
79
|
+
|
|
80
|
+
[tool.pytest.ini_options]
|
|
81
|
+
testpaths = ["tests"]
|
|
82
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""agentcoopdb — Python client for the Agentic Coop DB auth gateway.
|
|
2
|
+
|
|
3
|
+
This package is intentionally tiny. The public surface is:
|
|
4
|
+
|
|
5
|
+
from agentcoopdb import connect
|
|
6
|
+
db = connect("https://db.example.com", api_key="acd_live_...")
|
|
7
|
+
db.execute(sql, params)
|
|
8
|
+
db.select(sql, params)
|
|
9
|
+
db.transaction()
|
|
10
|
+
db.vector_upsert(...)
|
|
11
|
+
db.vector_search(...)
|
|
12
|
+
db.rotate_key()
|
|
13
|
+
db.health()
|
|
14
|
+
db.me()
|
|
15
|
+
|
|
16
|
+
Plus error classes (errors.py) and the offline retry queue (queue.py).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from agentcoopdb.client import AgentCoopDBClient, connect
|
|
20
|
+
from agentcoopdb.errors import (
|
|
21
|
+
AgentCoopDBError,
|
|
22
|
+
AuthError,
|
|
23
|
+
IdempotencyConflict,
|
|
24
|
+
NetworkError,
|
|
25
|
+
QueueFullError,
|
|
26
|
+
RateLimited,
|
|
27
|
+
ServerError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"connect",
|
|
33
|
+
"AgentCoopDBClient",
|
|
34
|
+
"AgentCoopDBError",
|
|
35
|
+
"AuthError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"IdempotencyConflict",
|
|
38
|
+
"RateLimited",
|
|
39
|
+
"ServerError",
|
|
40
|
+
"NetworkError",
|
|
41
|
+
"QueueFullError",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Local user config for the CLI.
|
|
2
|
+
|
|
3
|
+
The CLI persists a tiny TOML file under the platform config directory
|
|
4
|
+
(e.g. ~/.config/agentcoopdb/config.toml on Linux, ~/Library/Application
|
|
5
|
+
Support/agentcoopdb/config.toml on macOS) so that subsequent commands don't
|
|
6
|
+
need --url / --api-key flags. The wizard (`agentic-coop-db init`) writes
|
|
7
|
+
this file; `agentic-coop-db me` and friends read it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import platformdirs
|
|
19
|
+
|
|
20
|
+
CONFIG_FILENAME = "config.toml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def config_dir() -> Path:
|
|
24
|
+
return Path(platformdirs.user_config_dir("agentcoopdb", appauthor=False, ensure_exists=True))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def config_path() -> Path:
|
|
28
|
+
return config_dir() / CONFIG_FILENAME
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CLIConfig:
|
|
33
|
+
base_url: str
|
|
34
|
+
api_key: str
|
|
35
|
+
|
|
36
|
+
def write(self) -> Path:
|
|
37
|
+
path = config_path()
|
|
38
|
+
path.write_text(
|
|
39
|
+
f'base_url = "{self.base_url}"\napi_key = "{self.api_key}"\n',
|
|
40
|
+
encoding="utf-8",
|
|
41
|
+
)
|
|
42
|
+
# Best-effort chmod — on Windows this raises and we don't care.
|
|
43
|
+
with contextlib.suppress(OSError):
|
|
44
|
+
os.chmod(path, 0o600)
|
|
45
|
+
return path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load() -> CLIConfig | None:
|
|
49
|
+
path = config_path()
|
|
50
|
+
if not path.exists():
|
|
51
|
+
return None
|
|
52
|
+
text = path.read_text(encoding="utf-8")
|
|
53
|
+
data: dict[str, Any] = {}
|
|
54
|
+
for line in text.splitlines():
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line or line.startswith("#"):
|
|
57
|
+
continue
|
|
58
|
+
if "=" not in line:
|
|
59
|
+
continue
|
|
60
|
+
k, v = line.split("=", 1)
|
|
61
|
+
k = k.strip()
|
|
62
|
+
v = v.strip().strip('"')
|
|
63
|
+
data[k] = v
|
|
64
|
+
if "base_url" not in data or "api_key" not in data:
|
|
65
|
+
return None
|
|
66
|
+
return CLIConfig(base_url=data["base_url"], api_key=data["api_key"])
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""`agentic-coop-db doctor` — verify the local install end to end.
|
|
2
|
+
|
|
3
|
+
Checks (in order):
|
|
4
|
+
|
|
5
|
+
1. Config file exists and is parseable
|
|
6
|
+
2. /healthz reachable
|
|
7
|
+
3. /readyz returns 200 (DB + migrations OK)
|
|
8
|
+
4. /v1/me succeeds (auth works)
|
|
9
|
+
5. SELECT 1 round trip
|
|
10
|
+
6. Idempotency replay probe — same key returns cached response
|
|
11
|
+
7. pgvector available (CREATE EXTENSION query)
|
|
12
|
+
8. Local queue path writable
|
|
13
|
+
9. Server vs SDK version skew
|
|
14
|
+
|
|
15
|
+
Exits 0 on success, non-zero on the first failure.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import uuid
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import NoReturn
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
|
|
26
|
+
from agentcoopdb import __version__ as sdk_version
|
|
27
|
+
from agentcoopdb import connect
|
|
28
|
+
from agentcoopdb.cli.config import config_dir
|
|
29
|
+
from agentcoopdb.cli.config import load as load_config
|
|
30
|
+
from agentcoopdb.errors import AgentCoopDBError
|
|
31
|
+
from agentcoopdb.queue import Queue
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def doctor() -> None:
|
|
35
|
+
cfg = load_config()
|
|
36
|
+
if cfg is None:
|
|
37
|
+
_fail("config not found — run `agentic-coop-db init` first")
|
|
38
|
+
|
|
39
|
+
_ok("config", f"loaded from {Path(config_dir()) / 'config.toml'}")
|
|
40
|
+
|
|
41
|
+
db = connect(cfg.base_url, api_key=cfg.api_key, timeout=5.0)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
h = db.health()
|
|
45
|
+
_ok("/healthz", str(h.get("status", "?")))
|
|
46
|
+
except AgentCoopDBError as e:
|
|
47
|
+
_fail(f"/healthz failed: {e}")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
rz = db._get("/readyz")
|
|
51
|
+
_ok("/readyz", str(rz.get("status", "?")))
|
|
52
|
+
except AgentCoopDBError as e:
|
|
53
|
+
_fail(f"/readyz failed (DB or migrations unhealthy): {e}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
info = db.me()
|
|
57
|
+
_ok("/v1/me", f"workspace={info.get('workspace_id')} role={info.get('role')}")
|
|
58
|
+
except AgentCoopDBError as e:
|
|
59
|
+
_fail(f"/v1/me failed: {e}")
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
res = db.execute("SELECT 1")
|
|
63
|
+
if res.command != "SELECT" or res.rows_affected != 1:
|
|
64
|
+
_fail(f"SELECT 1 returned unexpected result: {res}")
|
|
65
|
+
_ok("SELECT 1", "round trip ok")
|
|
66
|
+
except AgentCoopDBError as e:
|
|
67
|
+
_fail(f"SELECT 1 failed: {e}")
|
|
68
|
+
|
|
69
|
+
# Idempotency replay probe — uses a temp uuid as the key and verifies
|
|
70
|
+
# the server's X-Idempotent-Replayed header to confirm a cached replay.
|
|
71
|
+
key = str(uuid.uuid4())
|
|
72
|
+
try:
|
|
73
|
+
db.execute("SELECT pg_sleep(0)", idempotency_key=key)
|
|
74
|
+
r2 = db.execute("SELECT pg_sleep(0)", idempotency_key=key)
|
|
75
|
+
if r2.replayed:
|
|
76
|
+
_ok("idempotency", "replay returned cached response (X-Idempotent-Replayed confirmed)")
|
|
77
|
+
else:
|
|
78
|
+
_fail("idempotency replay succeeded but X-Idempotent-Replayed header missing — caching may be broken")
|
|
79
|
+
except AgentCoopDBError as e:
|
|
80
|
+
_fail(f"idempotency probe failed: {e}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
rows = db.select("SELECT extname FROM pg_extension WHERE extname = 'vector'")
|
|
84
|
+
if rows:
|
|
85
|
+
_ok("pgvector", "extension installed")
|
|
86
|
+
else:
|
|
87
|
+
_warn("pgvector", "extension not installed (run migration 0005)")
|
|
88
|
+
except AgentCoopDBError as e:
|
|
89
|
+
_warn("pgvector", f"could not query pg_extension: {e}")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
Queue(config_dir() / "queue.db").depth()
|
|
93
|
+
_ok("queue", "local sqlite path writable")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
_warn("queue", f"could not open local queue: {e}")
|
|
96
|
+
|
|
97
|
+
server_version = info.get("server", {}).get("version", "unknown")
|
|
98
|
+
if server_version != sdk_version:
|
|
99
|
+
_warn(
|
|
100
|
+
"version skew",
|
|
101
|
+
f"sdk={sdk_version} server={server_version} — keep them in lockstep when possible",
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
_ok("version", f"sdk={sdk_version} server={server_version}")
|
|
105
|
+
|
|
106
|
+
typer.echo("\nall checks passed")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _ok(name: str, detail: str = "") -> None:
|
|
110
|
+
typer.echo(f" [OK] {name}: {detail}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _warn(name: str, detail: str) -> None:
|
|
114
|
+
typer.echo(f" [WARN] {name}: {detail}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fail(msg: str) -> NoReturn:
|
|
118
|
+
typer.echo(f" [FAIL] {msg}", err=True)
|
|
119
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""`agentic-coop-db init` — interactive onboarding wizard.
|
|
2
|
+
|
|
3
|
+
Goal: get a brand-new user from zero to a working CRUD round trip in
|
|
4
|
+
under five minutes without reading any docs beyond the README.
|
|
5
|
+
|
|
6
|
+
Decision tree:
|
|
7
|
+
|
|
8
|
+
1. Detect docker. If missing, link to install instructions.
|
|
9
|
+
2. Ask which profile they want: local / pi-lite / cloud / "I have a server".
|
|
10
|
+
3. For self-hosted profiles: run `make up-<profile>`, wait for /readyz,
|
|
11
|
+
mint an admin API key via scripts/gen-key.sh, capture it once.
|
|
12
|
+
4. For "existing server": prompt for URL + key, call /v1/me to validate.
|
|
13
|
+
5. Write ~/.agentic-coop-db/config.toml.
|
|
14
|
+
6. Run `agentic-coop-db doctor` and print a Python + curl snippet.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import typer
|
|
26
|
+
|
|
27
|
+
from agentcoopdb import connect
|
|
28
|
+
from agentcoopdb.cli import config as cli_config
|
|
29
|
+
from agentcoopdb.errors import AgentCoopDBError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def init() -> None:
|
|
33
|
+
"""Run the wizard."""
|
|
34
|
+
typer.echo(_BANNER)
|
|
35
|
+
|
|
36
|
+
if not _have("docker"):
|
|
37
|
+
typer.echo(
|
|
38
|
+
"Docker is not on PATH. Install it from https://docs.docker.com/engine/install/ "
|
|
39
|
+
"and re-run `agentic-coop-db init`."
|
|
40
|
+
)
|
|
41
|
+
raise typer.Exit(code=2)
|
|
42
|
+
|
|
43
|
+
profile = typer.prompt(
|
|
44
|
+
"Which profile? [local / pi-lite / cloud / existing]",
|
|
45
|
+
default="local",
|
|
46
|
+
).strip().lower()
|
|
47
|
+
|
|
48
|
+
if profile == "existing":
|
|
49
|
+
base_url = typer.prompt("Server base URL (e.g. https://db.example.com)").strip()
|
|
50
|
+
api_key = typer.prompt("API key (will not be echoed)", hide_input=True).strip()
|
|
51
|
+
_validate_and_save(base_url, api_key)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
repo_root = _find_repo_root()
|
|
55
|
+
if repo_root is None:
|
|
56
|
+
typer.echo(
|
|
57
|
+
"Could not find an Agentic Coop DB checkout in the current or parent directories. "
|
|
58
|
+
"Either run this from inside the repo, or pick `existing` and point at a remote server."
|
|
59
|
+
)
|
|
60
|
+
raise typer.Exit(code=2)
|
|
61
|
+
|
|
62
|
+
target = {
|
|
63
|
+
"local": "up-local",
|
|
64
|
+
"pi-lite": "up-pi",
|
|
65
|
+
"cloud": "up-cloud",
|
|
66
|
+
}.get(profile)
|
|
67
|
+
if target is None:
|
|
68
|
+
typer.echo(f"unknown profile: {profile}")
|
|
69
|
+
raise typer.Exit(code=2)
|
|
70
|
+
|
|
71
|
+
typer.echo(f"\n→ make {target}")
|
|
72
|
+
res = subprocess.run(["make", target], cwd=repo_root)
|
|
73
|
+
if res.returncode != 0:
|
|
74
|
+
typer.echo("`make` failed — fix the error above and re-run `agentic-coop-db init`")
|
|
75
|
+
raise typer.Exit(code=res.returncode)
|
|
76
|
+
|
|
77
|
+
base_url = "http://localhost:8080"
|
|
78
|
+
typer.echo(f"\n→ waiting for {base_url}/readyz ...")
|
|
79
|
+
if not _wait_ready(base_url, timeout=120):
|
|
80
|
+
typer.echo("the server did not become ready within 120s — check `make logs`")
|
|
81
|
+
raise typer.Exit(code=1)
|
|
82
|
+
|
|
83
|
+
typer.echo("→ minting an admin API key via scripts/gen-key.sh ...")
|
|
84
|
+
key_proc = subprocess.run(
|
|
85
|
+
["./scripts/gen-key.sh", "default", "dbadmin"],
|
|
86
|
+
cwd=repo_root,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
)
|
|
90
|
+
if key_proc.returncode != 0:
|
|
91
|
+
typer.echo(key_proc.stderr)
|
|
92
|
+
raise typer.Exit(code=key_proc.returncode)
|
|
93
|
+
api_key = _extract_token(key_proc.stdout)
|
|
94
|
+
if not api_key:
|
|
95
|
+
typer.echo("could not parse the minted key from gen-key.sh output")
|
|
96
|
+
typer.echo(key_proc.stdout)
|
|
97
|
+
raise typer.Exit(code=1)
|
|
98
|
+
|
|
99
|
+
_validate_and_save(base_url, api_key)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---- helpers -----------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _validate_and_save(base_url: str, api_key: str) -> None:
|
|
105
|
+
db = connect(base_url, api_key=api_key, timeout=10.0)
|
|
106
|
+
try:
|
|
107
|
+
info = db.me()
|
|
108
|
+
except AgentCoopDBError as e:
|
|
109
|
+
typer.echo(f"could not authenticate against {base_url}: {e}")
|
|
110
|
+
raise typer.Exit(code=1) from e
|
|
111
|
+
|
|
112
|
+
cfg = cli_config.CLIConfig(base_url=base_url, api_key=api_key)
|
|
113
|
+
path = cfg.write()
|
|
114
|
+
typer.echo(f"\nconfig saved to {path}")
|
|
115
|
+
typer.echo(f"workspace: {info.get('workspace_id', '?')}")
|
|
116
|
+
typer.echo(f"role: {info.get('role', '?')}")
|
|
117
|
+
typer.echo(f"server: {info.get('server', {}).get('version', '?')}")
|
|
118
|
+
typer.echo("\nyou are ready. try this:\n")
|
|
119
|
+
typer.echo(f" curl -H 'Authorization: Bearer {api_key}' {base_url}/v1/me")
|
|
120
|
+
typer.echo(" agentic-coop-db sql 'SELECT 1'")
|
|
121
|
+
typer.echo("\nor in Python:\n")
|
|
122
|
+
typer.echo(" from agentcoopdb import connect")
|
|
123
|
+
typer.echo(f" db = connect('{base_url}', api_key='acd_...')")
|
|
124
|
+
typer.echo(" db.execute('SELECT 1')")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _have(cmd: str) -> bool:
|
|
128
|
+
return shutil.which(cmd) is not None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _find_repo_root() -> Path | None:
|
|
132
|
+
here = Path.cwd().resolve()
|
|
133
|
+
for candidate in [here, *here.parents]:
|
|
134
|
+
if (candidate / "Makefile").exists() and (candidate / "deploy").is_dir():
|
|
135
|
+
return candidate
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _wait_ready(base_url: str, timeout: int) -> bool:
|
|
140
|
+
import requests
|
|
141
|
+
|
|
142
|
+
deadline = time.time() + timeout
|
|
143
|
+
while time.time() < deadline:
|
|
144
|
+
try:
|
|
145
|
+
r = requests.get(base_url + "/readyz", timeout=2.0)
|
|
146
|
+
if r.status_code == 200:
|
|
147
|
+
return True
|
|
148
|
+
except requests.RequestException:
|
|
149
|
+
pass
|
|
150
|
+
time.sleep(1)
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _extract_token(stdout: str) -> str | None:
|
|
155
|
+
for line in stdout.splitlines():
|
|
156
|
+
line = line.strip()
|
|
157
|
+
if line.startswith("acd_"):
|
|
158
|
+
return line
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_BANNER = """\
|
|
163
|
+
agentic-coop-db init — interactive onboarding wizard
|
|
164
|
+
|
|
165
|
+
This wizard will:
|
|
166
|
+
1. Verify Docker is up
|
|
167
|
+
2. Start the stack with your chosen profile
|
|
168
|
+
3. Mint an admin API key
|
|
169
|
+
4. Save your config (run `agentic-coop-db doctor` to see the path)
|
|
170
|
+
5. Print a Python + curl snippet you can copy/paste
|
|
171
|
+
|
|
172
|
+
Press Ctrl-C at any time to abort.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
init()
|
|
177
|
+
sys.exit(0)
|