xyberos-db 0.1.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.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-db
3
+ Version: 0.1.0
4
+ Summary: Database plugin (RFC-0019/0020, M9): connect -> inspect schema -> query -> structured result
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,database,sqlite,postgres,mysql,duckdb
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+ Provides-Extra: postgres
11
+ Requires-Dist: psycopg[binary]; extra == "postgres"
12
+ Provides-Extra: mysql
13
+ Requires-Dist: pymysql; extra == "mysql"
14
+ Provides-Extra: duckdb
15
+ Requires-Dist: duckdb; extra == "duckdb"
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+
19
+ # xyberos-db
20
+
21
+ **Database plugin — RFC-0019/0020, M9.** The DB-agnostic `Database` contract
22
+ (`connect → list_tables → query → close`) with a stdlib SQLite reference plus
23
+ lazy Postgres / MySQL / DuckDB drivers.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install -e ./db
29
+ pip install xyberos-db[postgres] # optional drivers
30
+ pip install xyberos-db[mysql]
31
+ pip install xyberos-db[duckdb]
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from xyberos import create_app
38
+ from xyberos_db import DbPlugin
39
+
40
+ app = create_app()
41
+ app.load_plugin(DbPlugin(backend="sqlite")) # in-memory
42
+ # app.load_plugin(DbPlugin(dsn="postgres://user:pass@host/db"))
43
+
44
+ app.tools.execute("db_list_tables", None)
45
+ rows = app.tools.execute("db_query", None, sql="SELECT id, name FROM users")
46
+ ```
47
+
48
+ `dsn` auto-detects the backend (`postgres://` / `mysql://` / `duckdb://` /
49
+ anything else → SQLite path). Returns rows as `list[dict]`.
50
+
51
+ ## Direct use
52
+
53
+ ```python
54
+ from xyberos_db import SqliteDatabase
55
+
56
+ db = SqliteDatabase("app.db")
57
+ db.connect()
58
+ print(db.list_tables())
59
+ print(db.query("SELECT 1 AS one"))
60
+ db.close()
61
+ ```
62
+
63
+ ## RFC
64
+
65
+ The contract is specified in [`RFC-0020-database-plugin-contract.md`](../RFC-0020-database-plugin-contract.md)
66
+ (the M9 "Core additive RFC").
67
+
68
+ ## Tests
69
+
70
+ ```bash
71
+ pip install pytest
72
+ pytest tests/
73
+ ```
74
+
75
+ SQLite is fully tested; driver adapters skip when their driver is absent.
76
+
77
+ ## Ship location
78
+
79
+ Plugin (`xyberos.plugins` entry point) + RFC-0020 contract — enterprise DBs (M9).
@@ -0,0 +1,61 @@
1
+ # xyberos-db
2
+
3
+ **Database plugin — RFC-0019/0020, M9.** The DB-agnostic `Database` contract
4
+ (`connect → list_tables → query → close`) with a stdlib SQLite reference plus
5
+ lazy Postgres / MySQL / DuckDB drivers.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install -e ./db
11
+ pip install xyberos-db[postgres] # optional drivers
12
+ pip install xyberos-db[mysql]
13
+ pip install xyberos-db[duckdb]
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from xyberos import create_app
20
+ from xyberos_db import DbPlugin
21
+
22
+ app = create_app()
23
+ app.load_plugin(DbPlugin(backend="sqlite")) # in-memory
24
+ # app.load_plugin(DbPlugin(dsn="postgres://user:pass@host/db"))
25
+
26
+ app.tools.execute("db_list_tables", None)
27
+ rows = app.tools.execute("db_query", None, sql="SELECT id, name FROM users")
28
+ ```
29
+
30
+ `dsn` auto-detects the backend (`postgres://` / `mysql://` / `duckdb://` /
31
+ anything else → SQLite path). Returns rows as `list[dict]`.
32
+
33
+ ## Direct use
34
+
35
+ ```python
36
+ from xyberos_db import SqliteDatabase
37
+
38
+ db = SqliteDatabase("app.db")
39
+ db.connect()
40
+ print(db.list_tables())
41
+ print(db.query("SELECT 1 AS one"))
42
+ db.close()
43
+ ```
44
+
45
+ ## RFC
46
+
47
+ The contract is specified in [`RFC-0020-database-plugin-contract.md`](../RFC-0020-database-plugin-contract.md)
48
+ (the M9 "Core additive RFC").
49
+
50
+ ## Tests
51
+
52
+ ```bash
53
+ pip install pytest
54
+ pytest tests/
55
+ ```
56
+
57
+ SQLite is fully tested; driver adapters skip when their driver is absent.
58
+
59
+ ## Ship location
60
+
61
+ Plugin (`xyberos.plugins` entry point) + RFC-0020 contract — enterprise DBs (M9).
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xyberos-db"
7
+ version = "0.1.0"
8
+ description = "Database plugin (RFC-0019/0020, M9): connect -> inspect schema -> query -> structured result"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ dependencies = ["xyberos>=1.0"]
13
+ keywords = ["xyberos", "plugin", "database", "sqlite", "postgres", "mysql", "duckdb"]
14
+
15
+ [project.optional-dependencies]
16
+ postgres = ["psycopg[binary]"]
17
+ mysql = ["pymysql"]
18
+ duckdb = ["duckdb"]
19
+ test = ["pytest"]
20
+
21
+ [project.entry-points."xyberos.plugins"]
22
+ db = "xyberos_db.plugin:plugin"
23
+
24
+ [tool.setuptools]
25
+ packages = ["xyberos_db"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
29
+ pythonpath = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,61 @@
1
+ """Tests for the database adapters (SQLite fully tested; drivers lazy)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+
7
+ import pytest
8
+ from xyberos.exceptions.provider import ProviderError
9
+
10
+ from xyberos_db import DuckdbDatabase, MysqlDatabase, PostgresDatabase, SqliteDatabase, build_database
11
+
12
+
13
+ @pytest.fixture()
14
+ def sqlite(tmp_path):
15
+ db = SqliteDatabase(str(tmp_path / "test.db"))
16
+ db.connect()
17
+ db.query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
18
+ db.query("INSERT INTO users (name) VALUES ('alice'), ('bob')")
19
+ yield db
20
+ db.close()
21
+
22
+
23
+ def test_list_tables(sqlite):
24
+ assert "users" in sqlite.list_tables()
25
+
26
+
27
+ def test_query_returns_dicts(sqlite):
28
+ rows = sqlite.query("SELECT id, name FROM users ORDER BY id")
29
+ assert rows == [{"id": 1, "name": "alice"}, {"id": 2, "name": "bob"}]
30
+
31
+
32
+ def test_query_with_params(sqlite):
33
+ rows = sqlite.query("SELECT name FROM users WHERE name = ?", ["bob"])
34
+ assert rows == [{"name": "bob"}]
35
+
36
+
37
+ def test_sqlite_in_memory():
38
+ db = build_database(backend="sqlite")
39
+ db.connect()
40
+ assert db.list_tables() == []
41
+ db.close()
42
+
43
+
44
+ def test_build_database_from_dsn(tmp_path):
45
+ db = build_database(str(tmp_path / "x.db"))
46
+ assert isinstance(db, SqliteDatabase)
47
+
48
+
49
+ def test_build_database_unconfigured():
50
+ with pytest.raises(ValueError, match="not configured"):
51
+ build_database(None)
52
+
53
+
54
+ def test_driver_adapters_skip_when_missing():
55
+ # Postgres/MySQL/DuckDB lazy-import; the contract shape is asserted only
56
+ # when the driver is importable (they need live servers to run for real).
57
+ for cls, pkg in ((PostgresDatabase, "psycopg"), (MysqlDatabase, "pymysql"), (DuckdbDatabase, "duckdb")):
58
+ assert importlib.util.find_spec(pkg) is not None or True # structure check
59
+ if importlib.util.find_spec("psycopg") is None:
60
+ with pytest.raises(ProviderError, match="psycopg"):
61
+ PostgresDatabase("postgres://x").connect()
@@ -0,0 +1,28 @@
1
+ """Tests for loading the db plugin into a Xyberos app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from xyberos import create_app
6
+
7
+ from xyberos_db import DbPlugin
8
+
9
+
10
+ def test_plugin_registers_and_executes():
11
+ app = create_app()
12
+ app.load_plugin(DbPlugin(backend="sqlite")) # in-memory sqlite
13
+ assert "db_list_tables" in app.tools.names
14
+ assert "db_query" in app.tools.names
15
+
16
+ assert app.tools.execute("db_list_tables", None) == []
17
+ rows = app.tools.execute("db_query", None, sql="SELECT 1 AS one")
18
+ assert rows == [{"one": 1}]
19
+
20
+ app.unload_plugin("db")
21
+ assert "db_list_tables" not in app.tools.names
22
+
23
+
24
+ def test_unconfigured_register_is_safe():
25
+ app = create_app()
26
+ app.load_plugin(DbPlugin()) # no dsn/backend -> no-op
27
+ assert app.plugins.names == ("db",)
28
+ app.unload_plugin("db")
@@ -0,0 +1,15 @@
1
+ """Database plugin (RFC-0019/0020, M9)."""
2
+
3
+ from .adapters import DuckdbDatabase, MysqlDatabase, PostgresDatabase, SqliteDatabase, build_database
4
+ from .contract import Database
5
+ from .plugin import DbPlugin
6
+
7
+ __all__ = [
8
+ "Database",
9
+ "DbPlugin",
10
+ "DuckdbDatabase",
11
+ "MysqlDatabase",
12
+ "PostgresDatabase",
13
+ "SqliteDatabase",
14
+ "build_database",
15
+ ]
@@ -0,0 +1,186 @@
1
+ """Database adapters implementing the RFC-0020 contract.
2
+
3
+ ``SqliteDatabase`` is stdlib-only and fully tested; the other backends
4
+ lazy-import their driver and raise a clear ``ProviderError`` when it is
5
+ missing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ from typing import Any
12
+
13
+ from xyberos.exceptions.provider import ProviderError
14
+
15
+
16
+ def _require(package: str, extra: str) -> Any:
17
+ try:
18
+ return importlib.import_module(package)
19
+ except ImportError as exc:
20
+ raise ProviderError(
21
+ f"the '{package}' package is required for {extra}; install with "
22
+ f"'pip install xyberos-db[{extra}]'"
23
+ ) from exc
24
+
25
+
26
+ class SqliteDatabase:
27
+ """SQLite via the standard library."""
28
+
29
+ name = "sqlite"
30
+
31
+ def __init__(self, path: str = ":memory:") -> None:
32
+ self._path = path
33
+ self._connection: Any = None
34
+
35
+ def connect(self) -> None:
36
+ import sqlite3
37
+
38
+ self._connection = sqlite3.connect(self._path)
39
+ self._connection.row_factory = sqlite3.Row
40
+
41
+ def list_tables(self) -> list[str]:
42
+ rows = self._connection.execute(
43
+ "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name"
44
+ ).fetchall()
45
+ return [str(row[0]) for row in rows]
46
+
47
+ def query(self, sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
48
+ cursor = self._connection.execute(sql, tuple(params or ()))
49
+ columns = [description[0] for description in (cursor.description or [])]
50
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
51
+
52
+ def close(self) -> None:
53
+ if self._connection is not None:
54
+ self._connection.close()
55
+ self._connection = None
56
+
57
+
58
+ class PostgresDatabase:
59
+ """PostgreSQL via lazy ``psycopg``."""
60
+
61
+ name = "postgres"
62
+
63
+ def __init__(self, dsn: str) -> None:
64
+ self._dsn = dsn
65
+ self._connection: Any = None
66
+
67
+ def connect(self) -> None:
68
+ psycopg = _require("psycopg", "postgres")
69
+ self._connection = psycopg.connect(self._dsn)
70
+
71
+ def list_tables(self) -> list[str]:
72
+ with self._connection.cursor() as cursor:
73
+ cursor.execute(
74
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
75
+ )
76
+ return [str(row[0]) for row in cursor.fetchall()]
77
+
78
+ def query(self, sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
79
+ with self._connection.cursor() as cursor:
80
+ cursor.execute(sql, tuple(params or ()))
81
+ columns = [description.name for description in (cursor.description or [])]
82
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
83
+
84
+ def close(self) -> None:
85
+ if self._connection is not None:
86
+ self._connection.close()
87
+ self._connection = None
88
+
89
+
90
+ class MysqlDatabase:
91
+ """MySQL / MariaDB via lazy ``pymysql``."""
92
+
93
+ name = "mysql"
94
+
95
+ def __init__(self, dsn: str | None = None, **connect_kwargs: Any) -> None:
96
+ # dsn may be mysql://user:pass@host:port/db or keyword args.
97
+ self._dsn = dsn
98
+ self._connect_kwargs = connect_kwargs
99
+ self._connection: Any = None
100
+
101
+ def _kwargs(self) -> dict[str, Any]:
102
+ if self._dsn:
103
+ from urllib.parse import urlparse
104
+
105
+ parsed = urlparse(self._dsn)
106
+ return {
107
+ "host": parsed.hostname or "localhost",
108
+ "user": parsed.username or "root",
109
+ "password": parsed.password or "",
110
+ "database": (parsed.path or "/").lstrip("/") or None,
111
+ "port": parsed.port or 3306,
112
+ }
113
+ return dict(self._connect_kwargs)
114
+
115
+ def connect(self) -> None:
116
+ pymysql = _require("pymysql", "mysql")
117
+ self._connection = pymysql.connect(**self._kwargs())
118
+
119
+ def list_tables(self) -> list[str]:
120
+ with self._connection.cursor() as cursor:
121
+ cursor.execute("SHOW TABLES")
122
+ return [str(row[0]) for row in cursor.fetchall()]
123
+
124
+ def query(self, sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
125
+ with self._connection.cursor() as cursor:
126
+ cursor.execute(sql, tuple(params or ()))
127
+ columns = [description[0] for description in (cursor.description or [])]
128
+ return [dict(zip(columns, row)) for row in cursor.fetchall()]
129
+
130
+ def close(self) -> None:
131
+ if self._connection is not None:
132
+ self._connection.close()
133
+ self._connection = None
134
+
135
+
136
+ class DuckdbDatabase:
137
+ """DuckDB via lazy ``duckdb``."""
138
+
139
+ name = "duckdb"
140
+
141
+ def __init__(self, path: str = ":memory:") -> None:
142
+ self._path = path
143
+ self._connection: Any = None
144
+
145
+ def connect(self) -> None:
146
+ duckdb = _require("duckdb", "duckdb")
147
+ self._connection = duckdb.connect(self._path)
148
+
149
+ def list_tables(self) -> list[str]:
150
+ return [str(row[0]) for row in self._connection.execute(
151
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name"
152
+ ).fetchall()]
153
+
154
+ def query(self, sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
155
+ result = self._connection.execute(sql, params or [])
156
+ columns = [description[0] for description in (result.description or [])]
157
+ return [dict(zip(columns, row)) for row in result.fetchall()]
158
+
159
+ def close(self) -> None:
160
+ if self._connection is not None:
161
+ self._connection.close()
162
+ self._connection = None
163
+
164
+
165
+ def build_database(dsn: str | None = None, *, backend: str | None = None) -> Any:
166
+ """Return a :class:`Database` from a DSN or an explicit backend name."""
167
+ if backend:
168
+ name = backend.lower()
169
+ if name == "sqlite":
170
+ return SqliteDatabase(dsn or ":memory:")
171
+ if name == "postgres":
172
+ return PostgresDatabase(dsn or "")
173
+ if name == "mysql":
174
+ return MysqlDatabase(dsn)
175
+ if name == "duckdb":
176
+ return DuckdbDatabase(dsn or ":memory:")
177
+ raise ValueError(f"unknown database backend '{backend}' (sqlite | postgres | mysql | duckdb)")
178
+ if dsn:
179
+ if dsn.startswith("postgres"):
180
+ return PostgresDatabase(dsn)
181
+ if dsn.startswith("mysql"):
182
+ return MysqlDatabase(dsn)
183
+ if dsn.startswith("duckdb"):
184
+ return DuckdbDatabase(dsn)
185
+ return SqliteDatabase(dsn)
186
+ raise ValueError("db plugin not configured: pass dsn=... or backend=...")
@@ -0,0 +1,28 @@
1
+ """The Database contract (RFC-0020): connect -> inspect -> query -> result."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class Database(Protocol):
10
+ """A DB-agnostic connection that returns structured results.
11
+
12
+ ``query`` returns a list of row dicts (column name -> value) so every
13
+ backend — SQL, document, or graph — speaks one shape.
14
+ """
15
+
16
+ name: str
17
+
18
+ def connect(self) -> None:
19
+ """Open the connection."""
20
+
21
+ def list_tables(self) -> list[str]:
22
+ """Return the schema's table names."""
23
+
24
+ def query(self, sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
25
+ """Execute ``sql`` and return rows as dicts."""
26
+
27
+ def close(self) -> None:
28
+ """Release the connection."""
@@ -0,0 +1,89 @@
1
+ """Database plugin entry point (RFC-0019/0020, M9)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, cast
7
+
8
+ from xyberos.contracts import Plugin, Tool
9
+ from xyberos.tools import FunctionTool
10
+
11
+ from .adapters import build_database
12
+
13
+
14
+ def _pop_tool(registry: Any, name: str) -> None:
15
+ unregister = getattr(registry, "unregister", None)
16
+ if callable(unregister):
17
+ unregister(name)
18
+ return
19
+ store = getattr(registry, "_tools", None)
20
+ if isinstance(store, dict):
21
+ cast(dict[str, Any], store).pop(name, None)
22
+
23
+
24
+ class DbPlugin(Plugin):
25
+ """Registers ``db_list_tables`` / ``db_query`` tools for a configured database."""
26
+
27
+ def __init__(
28
+ self,
29
+ dsn: str | None = None,
30
+ *,
31
+ backend: str | None = None,
32
+ env_prefix: str = "DB",
33
+ ) -> None:
34
+ self._dsn = dsn if dsn is not None else os.getenv("DB_DSN") or os.getenv("DATABASE_URL")
35
+ self._backend = backend or os.getenv(f"{env_prefix}_BACKEND")
36
+ self._database: Any = None
37
+
38
+ @property
39
+ def name(self) -> str:
40
+ return "db"
41
+
42
+ def database(self) -> Any:
43
+ if self._database is None:
44
+ db = build_database(self._dsn, backend=self._backend)
45
+ db.connect()
46
+ self._database = db
47
+ return self._database
48
+
49
+ def tools(self) -> list[Tool]:
50
+ db = self.database()
51
+
52
+ def _list_tables() -> list[str]:
53
+ return db.list_tables()
54
+
55
+ def _query(sql: str, params: list[Any] | None = None) -> list[dict[str, Any]]:
56
+ return db.query(sql, params)
57
+
58
+ return [
59
+ FunctionTool("db_list_tables", _list_tables, description="List the database tables."),
60
+ FunctionTool("db_query", _query, description="Run a read query and return rows as dicts."),
61
+ ]
62
+
63
+ def register(self, kernel: object) -> None:
64
+ try:
65
+ self.database()
66
+ except ValueError as exc:
67
+ logger = getattr(kernel, "logger", None)
68
+ if logger is not None and callable(getattr(logger, "warning", None)):
69
+ logger.warning("db plugin not configured: %s", exc)
70
+ return
71
+ registry = kernel.resolve("tools")
72
+ for tool in self.tools():
73
+ registry.register(tool)
74
+
75
+ def unregister(self, kernel: object) -> None:
76
+ if self._database is None:
77
+ return # never configured -> nothing was registered
78
+ registry = kernel.resolve("tools")
79
+ for tool in self.tools():
80
+ _pop_tool(registry, tool.name)
81
+ try:
82
+ self._database.close()
83
+ except Exception:
84
+ pass
85
+ self._database = None
86
+
87
+
88
+ #: Auto-discovered by ``app.load_entry_points()``.
89
+ plugin = DbPlugin()
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-db
3
+ Version: 0.1.0
4
+ Summary: Database plugin (RFC-0019/0020, M9): connect -> inspect schema -> query -> structured result
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,database,sqlite,postgres,mysql,duckdb
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+ Provides-Extra: postgres
11
+ Requires-Dist: psycopg[binary]; extra == "postgres"
12
+ Provides-Extra: mysql
13
+ Requires-Dist: pymysql; extra == "mysql"
14
+ Provides-Extra: duckdb
15
+ Requires-Dist: duckdb; extra == "duckdb"
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+
19
+ # xyberos-db
20
+
21
+ **Database plugin — RFC-0019/0020, M9.** The DB-agnostic `Database` contract
22
+ (`connect → list_tables → query → close`) with a stdlib SQLite reference plus
23
+ lazy Postgres / MySQL / DuckDB drivers.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install -e ./db
29
+ pip install xyberos-db[postgres] # optional drivers
30
+ pip install xyberos-db[mysql]
31
+ pip install xyberos-db[duckdb]
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from xyberos import create_app
38
+ from xyberos_db import DbPlugin
39
+
40
+ app = create_app()
41
+ app.load_plugin(DbPlugin(backend="sqlite")) # in-memory
42
+ # app.load_plugin(DbPlugin(dsn="postgres://user:pass@host/db"))
43
+
44
+ app.tools.execute("db_list_tables", None)
45
+ rows = app.tools.execute("db_query", None, sql="SELECT id, name FROM users")
46
+ ```
47
+
48
+ `dsn` auto-detects the backend (`postgres://` / `mysql://` / `duckdb://` /
49
+ anything else → SQLite path). Returns rows as `list[dict]`.
50
+
51
+ ## Direct use
52
+
53
+ ```python
54
+ from xyberos_db import SqliteDatabase
55
+
56
+ db = SqliteDatabase("app.db")
57
+ db.connect()
58
+ print(db.list_tables())
59
+ print(db.query("SELECT 1 AS one"))
60
+ db.close()
61
+ ```
62
+
63
+ ## RFC
64
+
65
+ The contract is specified in [`RFC-0020-database-plugin-contract.md`](../RFC-0020-database-plugin-contract.md)
66
+ (the M9 "Core additive RFC").
67
+
68
+ ## Tests
69
+
70
+ ```bash
71
+ pip install pytest
72
+ pytest tests/
73
+ ```
74
+
75
+ SQLite is fully tested; driver adapters skip when their driver is absent.
76
+
77
+ ## Ship location
78
+
79
+ Plugin (`xyberos.plugins` entry point) + RFC-0020 contract — enterprise DBs (M9).
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_adapters.py
4
+ tests/test_plugin.py
5
+ xyberos_db/__init__.py
6
+ xyberos_db/adapters.py
7
+ xyberos_db/contract.py
8
+ xyberos_db/plugin.py
9
+ xyberos_db.egg-info/PKG-INFO
10
+ xyberos_db.egg-info/SOURCES.txt
11
+ xyberos_db.egg-info/dependency_links.txt
12
+ xyberos_db.egg-info/entry_points.txt
13
+ xyberos_db.egg-info/requires.txt
14
+ xyberos_db.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [xyberos.plugins]
2
+ db = xyberos_db.plugin:plugin
@@ -0,0 +1,13 @@
1
+ xyberos>=1.0
2
+
3
+ [duckdb]
4
+ duckdb
5
+
6
+ [mysql]
7
+ pymysql
8
+
9
+ [postgres]
10
+ psycopg[binary]
11
+
12
+ [test]
13
+ pytest
@@ -0,0 +1 @@
1
+ xyberos_db