schemaingest 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.
Files changed (31) hide show
  1. schemaingest-0.1.0/LICENSE +21 -0
  2. schemaingest-0.1.0/MANIFEST.in +2 -0
  3. schemaingest-0.1.0/PKG-INFO +98 -0
  4. schemaingest-0.1.0/README.md +60 -0
  5. schemaingest-0.1.0/pyproject.toml +64 -0
  6. schemaingest-0.1.0/schemaingest/__init__.py +3 -0
  7. schemaingest-0.1.0/schemaingest/__main__.py +6 -0
  8. schemaingest-0.1.0/schemaingest/cli.py +99 -0
  9. schemaingest-0.1.0/schemaingest/endpoints.py +77 -0
  10. schemaingest-0.1.0/schemaingest/introspect/__init__.py +19 -0
  11. schemaingest-0.1.0/schemaingest/introspect/common.py +94 -0
  12. schemaingest-0.1.0/schemaingest/introspect/mysql.py +254 -0
  13. schemaingest-0.1.0/schemaingest/introspect/postgres.py +209 -0
  14. schemaingest-0.1.0/schemaingest/models.py +190 -0
  15. schemaingest-0.1.0/schemaingest/renderers.py +106 -0
  16. schemaingest-0.1.0/schemaingest/security.py +109 -0
  17. schemaingest-0.1.0/schemaingest/server.py +63 -0
  18. schemaingest-0.1.0/schemaingest.egg-info/PKG-INFO +98 -0
  19. schemaingest-0.1.0/schemaingest.egg-info/SOURCES.txt +29 -0
  20. schemaingest-0.1.0/schemaingest.egg-info/dependency_links.txt +1 -0
  21. schemaingest-0.1.0/schemaingest.egg-info/entry_points.txt +2 -0
  22. schemaingest-0.1.0/schemaingest.egg-info/requires.txt +11 -0
  23. schemaingest-0.1.0/schemaingest.egg-info/top_level.txt +1 -0
  24. schemaingest-0.1.0/setup.cfg +4 -0
  25. schemaingest-0.1.0/tests/conftest.py +68 -0
  26. schemaingest-0.1.0/tests/test_api.py +116 -0
  27. schemaingest-0.1.0/tests/test_introspect_mysql.py +200 -0
  28. schemaingest-0.1.0/tests/test_introspect_pg.py +131 -0
  29. schemaingest-0.1.0/tests/test_models.py +102 -0
  30. schemaingest-0.1.0/tests/test_renderers.py +38 -0
  31. schemaingest-0.1.0/tests/test_security.py +58 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jineth Bosilu
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,2 @@
1
+ graft tests
2
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: schemaingest
3
+ Version: 0.1.0
4
+ Summary: Introspect your PostgreSQL or MySQL/MariaDB schema through a web UI, without credentials leaving your machine.
5
+ Author: Jineth Bosilu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JinethBosilu/SchemaIngest
8
+ Project-URL: Web UI, https://jinethbosilu.github.io/SchemaIngest/
9
+ Project-URL: Issues, https://github.com/JinethBosilu/SchemaIngest/issues
10
+ Keywords: postgres,postgresql,mysql,mariadb,schema,introspection,erd,database,llm
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Software Development :: Documentation
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: fastapi>=0.104.0
28
+ Requires-Dist: uvicorn[standard]>=0.24.0
29
+ Requires-Dist: psycopg2-binary>=2.9.9
30
+ Requires-Dist: PyMySQL[rsa]>=1.1
31
+ Requires-Dist: pydantic>=2.5.0
32
+ Requires-Dist: click>=8.1.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8.0; extra == "dev"
35
+ Requires-Dist: httpx>=0.27; extra == "dev"
36
+ Requires-Dist: httpx2>=2.0; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # SchemaIngest
40
+
41
+ **A "gitingest"-style tool for databases.** Browse your PostgreSQL, MySQL or MariaDB schema in a web UI (tables,
42
+ columns, keys, indexes and an interactive relationship diagram), and copy a compact,
43
+ token-efficient description of it for an AI. Your credentials never leave your machine.
44
+
45
+ This package is the **local agent**. It runs on `127.0.0.1`, connects to your database
46
+ read-only, and serves the schema to the web UI at
47
+ <https://jinethbosilu.github.io/SchemaIngest/>.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pipx install schemaingest # or: pip install schemaingest
53
+ ```
54
+
55
+ Requires Python 3.10+.
56
+
57
+ ## Use it with the web UI
58
+
59
+ ```bash
60
+ schemaingest agent
61
+ ```
62
+
63
+ The terminal shows a 6-digit **pairing code**. Open
64
+ <https://jinethbosilu.github.io/SchemaIngest/>, enter the code, then pick your database engine
65
+ and enter its connection details. They go only to the agent on `localhost`.
66
+
67
+ Chrome may ask whether the page can access devices on your local network. Allow it: that
68
+ is the browser checking before a public site talks to the agent on `127.0.0.1`.
69
+
70
+ ## Use it without the web UI
71
+
72
+ ```bash
73
+ # Full schema pack as JSON
74
+ schemaingest pull --conn "postgresql://user:pass@localhost:5432/mydb" --out schema.json
75
+
76
+ # Compact text, ready to paste into an AI
77
+ schemaingest pull --conn "postgresql://user:pass@localhost:5432/mydb" --out schema.txt --format txt
78
+
79
+ # MySQL or MariaDB: the scheme picks the engine
80
+ schemaingest pull --conn "mysql://user:pass@localhost:3306/mydb" --out schema.txt --format txt
81
+ ```
82
+
83
+ For PostgreSQL, `--schema` picks a schema other than `public`. In MySQL a database is a
84
+ schema, so the database in the connection string is the one introspected. MySQL
85
+ connection strings accept `?ssl-mode=` with MySQL's values (`DISABLED`, `PREFERRED`,
86
+ `REQUIRED`, `VERIFY_CA`, `VERIFY_IDENTITY`).
87
+
88
+ ## Security
89
+
90
+ - Binds to `127.0.0.1` only.
91
+ - CORS allows only the GitHub Pages UI and local development origins.
92
+ - A pairing code is required before any session is issued; sessions expire after 4 hours.
93
+ - Connections are opened read-only.
94
+ - Passwords are redacted from every printed error, and request bodies are never logged.
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,60 @@
1
+ # SchemaIngest
2
+
3
+ **A "gitingest"-style tool for databases.** Browse your PostgreSQL, MySQL or MariaDB schema in a web UI (tables,
4
+ columns, keys, indexes and an interactive relationship diagram), and copy a compact,
5
+ token-efficient description of it for an AI. Your credentials never leave your machine.
6
+
7
+ This package is the **local agent**. It runs on `127.0.0.1`, connects to your database
8
+ read-only, and serves the schema to the web UI at
9
+ <https://jinethbosilu.github.io/SchemaIngest/>.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pipx install schemaingest # or: pip install schemaingest
15
+ ```
16
+
17
+ Requires Python 3.10+.
18
+
19
+ ## Use it with the web UI
20
+
21
+ ```bash
22
+ schemaingest agent
23
+ ```
24
+
25
+ The terminal shows a 6-digit **pairing code**. Open
26
+ <https://jinethbosilu.github.io/SchemaIngest/>, enter the code, then pick your database engine
27
+ and enter its connection details. They go only to the agent on `localhost`.
28
+
29
+ Chrome may ask whether the page can access devices on your local network. Allow it: that
30
+ is the browser checking before a public site talks to the agent on `127.0.0.1`.
31
+
32
+ ## Use it without the web UI
33
+
34
+ ```bash
35
+ # Full schema pack as JSON
36
+ schemaingest pull --conn "postgresql://user:pass@localhost:5432/mydb" --out schema.json
37
+
38
+ # Compact text, ready to paste into an AI
39
+ schemaingest pull --conn "postgresql://user:pass@localhost:5432/mydb" --out schema.txt --format txt
40
+
41
+ # MySQL or MariaDB: the scheme picks the engine
42
+ schemaingest pull --conn "mysql://user:pass@localhost:3306/mydb" --out schema.txt --format txt
43
+ ```
44
+
45
+ For PostgreSQL, `--schema` picks a schema other than `public`. In MySQL a database is a
46
+ schema, so the database in the connection string is the one introspected. MySQL
47
+ connection strings accept `?ssl-mode=` with MySQL's values (`DISABLED`, `PREFERRED`,
48
+ `REQUIRED`, `VERIFY_CA`, `VERIFY_IDENTITY`).
49
+
50
+ ## Security
51
+
52
+ - Binds to `127.0.0.1` only.
53
+ - CORS allows only the GitHub Pages UI and local development origins.
54
+ - A pairing code is required before any session is issued; sessions expire after 4 hours.
55
+ - Connections are opened read-only.
56
+ - Passwords are redacted from every printed error, and request bodies are never logged.
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "schemaingest"
7
+ dynamic = ["version"]
8
+ description = "Introspect your PostgreSQL or MySQL/MariaDB schema through a web UI, without credentials leaving your machine."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ {name = "Jineth Bosilu"}
15
+ ]
16
+ keywords = ["postgres", "postgresql", "mysql", "mariadb", "schema", "introspection", "erd", "database", "llm"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Database",
30
+ "Topic :: Software Development :: Documentation",
31
+ ]
32
+ dependencies = [
33
+ "fastapi>=0.104.0",
34
+ "uvicorn[standard]>=0.24.0",
35
+ "psycopg2-binary>=2.9.9",
36
+ "PyMySQL[rsa]>=1.1", # rsa: MySQL 8's default caching_sha2_password login
37
+ "pydantic>=2.5.0",
38
+ "click>=8.1.0",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=8.0",
44
+ "httpx>=0.27", # test client for older Starlette
45
+ "httpx2>=2.0", # test client for Starlette 1.x
46
+ ]
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/JinethBosilu/SchemaIngest"
50
+ "Web UI" = "https://jinethbosilu.github.io/SchemaIngest/"
51
+ Issues = "https://github.com/JinethBosilu/SchemaIngest/issues"
52
+
53
+ [project.scripts]
54
+ schemaingest = "schemaingest.cli:main"
55
+
56
+ [tool.setuptools.dynamic]
57
+ version = {attr = "schemaingest.__version__"}
58
+
59
+ [tool.setuptools.packages.find]
60
+ where = ["."]
61
+ include = ["schemaingest*"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """SchemaIngest — local database schema introspection agent."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Allow running as `python -m schemaingest`."""
2
+
3
+ from schemaingest.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,99 @@
1
+ """CLI entry point for SchemaIngest agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ import click
9
+
10
+ from schemaingest import __version__
11
+ from schemaingest.security import generate_pairing_code, redact_password, set_pairing_code
12
+
13
+ WEB_UI_URL = "https://jinethbosilu.github.io/SchemaIngest/"
14
+
15
+
16
+ def _banner(lines: list[str]) -> str:
17
+ """A box sized to its contents. Plain ASCII, so it lines up on any console
18
+ (Windows code pages included) - emoji are double-width and would not."""
19
+ width = max(len(line) for line in lines) + 4
20
+ rule = "+" + "-" * width + "+"
21
+ body = [f"| {line.ljust(width - 4)} |" for line in lines]
22
+ return "\n".join([rule, *body, rule])
23
+
24
+
25
+ @click.group()
26
+ @click.version_option(__version__, prog_name="schemaingest")
27
+ def main():
28
+ """SchemaIngest — local database schema introspection agent."""
29
+
30
+
31
+ @main.command()
32
+ @click.option("--port", default=8420, show_default=True, help="Port to listen on (127.0.0.1 only).")
33
+ def agent(port: int):
34
+ """Start the SchemaIngest agent server."""
35
+ import uvicorn
36
+
37
+ from schemaingest.server import create_app
38
+
39
+ # The agent only ever binds to loopback: it holds database credentials.
40
+ host = "127.0.0.1"
41
+ code = generate_pairing_code()
42
+ set_pairing_code(code)
43
+
44
+ click.echo("")
45
+ click.echo(_banner([
46
+ f"SchemaIngest Agent v{__version__}",
47
+ "",
48
+ f"Pairing code: {code}",
49
+ "Enter this code in the web UI to connect.",
50
+ "",
51
+ f"Agent: http://{host}:{port}",
52
+ f"Web UI: {WEB_UI_URL}",
53
+ ]))
54
+ click.echo("")
55
+
56
+ uvicorn.run(create_app(), host=host, port=port, log_level="info")
57
+
58
+
59
+ @main.command()
60
+ @click.option(
61
+ "--conn", required=True,
62
+ help="Connection string: postgresql://user:pass@host/db or mysql://user:pass@host/db.",
63
+ )
64
+ @click.option("--out", required=True, type=click.Path(dir_okay=False), help="Output file path.")
65
+ @click.option(
66
+ "--schema", default=None,
67
+ help="Schema to introspect. PostgreSQL: defaults to public. MySQL: the database in --conn.",
68
+ )
69
+ @click.option(
70
+ "--format", "fmt",
71
+ type=click.Choice(["json", "txt"]), default="json", show_default=True,
72
+ help="json: the full schema pack. txt: the compact text for pasting into an AI.",
73
+ )
74
+ def pull(conn: str, out: str, schema: str | None, fmt: str):
75
+ """Export the schema without the web UI."""
76
+ from schemaingest.introspect import introspect
77
+ from schemaingest.models import ConnectRequest
78
+ from schemaingest.renderers import render_schema_txt
79
+
80
+ click.echo(f"Connecting to: {redact_password(conn)}")
81
+
82
+ try:
83
+ pack = introspect(ConnectRequest(connectionString=conn, schema=schema))
84
+ except Exception as e:
85
+ click.echo(f"Connection failed: {redact_password(str(e))}", err=True)
86
+ sys.exit(1)
87
+
88
+ with open(out, "w", encoding="utf-8") as f:
89
+ if fmt == "txt":
90
+ f.write(render_schema_txt(pack))
91
+ else:
92
+ json.dump(pack.model_dump(by_alias=True), f, indent=2)
93
+
94
+ click.echo(f"Schema written to {out}")
95
+ click.echo(f" Tables: {len(pack.tables)}, Relationships: {len(pack.relationships)}")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -0,0 +1,77 @@
1
+ """FastAPI route handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from schemaingest.introspect import introspect
9
+ from schemaingest.models import ConnectRequest, PairRequest, SchemaPack
10
+ from schemaingest.renderers import render_erd_mermaid, render_schema_txt
11
+ from schemaingest.security import (
12
+ rate_limit_dep,
13
+ redact_password,
14
+ require_session,
15
+ verify_pairing_code,
16
+ )
17
+ from schemaingest import __version__
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ # Handlers are plain `def`: the database drivers block, so FastAPI runs them in its
23
+ # threadpool instead of on the event loop.
24
+
25
+ # ─── Health ───────────────────────────────────────────────────────────
26
+
27
+ @router.get("/health")
28
+ def health():
29
+ return {"status": "ok", "version": __version__}
30
+
31
+
32
+ # ─── Pairing ──────────────────────────────────────────────────────────
33
+
34
+ @router.post("/pair/start", dependencies=[Depends(rate_limit_dep)])
35
+ def pair_start(body: PairRequest):
36
+ token = verify_pairing_code(body.code)
37
+ if token is None:
38
+ return JSONResponse(status_code=403, content={"detail": "Invalid pairing code"})
39
+ return {"sessionToken": token}
40
+
41
+
42
+ # ─── Introspection ────────────────────────────────────────────────────
43
+
44
+ def _do_introspect(req: ConnectRequest) -> SchemaPack:
45
+ """Shared introspection helper."""
46
+ try:
47
+ return introspect(req)
48
+ except Exception as e:
49
+ # Sanitise the error message to avoid leaking credentials
50
+ raise ValueError(redact_password(str(e))) from None
51
+
52
+
53
+ @router.post("/introspect", dependencies=[Depends(rate_limit_dep)])
54
+ def introspect_endpoint(body: ConnectRequest, _token: str = Depends(require_session)):
55
+ try:
56
+ pack = _do_introspect(body)
57
+ except ValueError as e:
58
+ return JSONResponse(status_code=400, content={"detail": str(e)})
59
+ return pack.model_dump(by_alias=True)
60
+
61
+
62
+ @router.post("/render/schema.txt", dependencies=[Depends(rate_limit_dep)])
63
+ def render_schema_txt_endpoint(body: ConnectRequest, _token: str = Depends(require_session)):
64
+ try:
65
+ pack = _do_introspect(body)
66
+ except ValueError as e:
67
+ return JSONResponse(status_code=400, content={"detail": str(e)})
68
+ return {"text": render_schema_txt(pack)}
69
+
70
+
71
+ @router.post("/render/erd.mmd", dependencies=[Depends(rate_limit_dep)])
72
+ def render_erd_mmd_endpoint(body: ConnectRequest, _token: str = Depends(require_session)):
73
+ try:
74
+ pack = _do_introspect(body)
75
+ except ValueError as e:
76
+ return JSONResponse(status_code=400, content={"detail": str(e)})
77
+ return {"mermaid": render_erd_mermaid(pack)}
@@ -0,0 +1,19 @@
1
+ """Schema introspection, one module per database engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from schemaingest.models import ConnectRequest, SchemaPack
6
+
7
+
8
+ def introspect(req: ConnectRequest) -> SchemaPack:
9
+ """Introspect the database a connect request names, whichever engine it is."""
10
+ # Drivers load on demand, so one engine's driver failing to import does
11
+ # not take the other engine down with it.
12
+ if req.resolved_engine() == "mysql":
13
+ from schemaingest.introspect.mysql import introspect_mysql
14
+
15
+ return introspect_mysql(req.to_mysql_params(), schema=req.resolved_schema())
16
+
17
+ from schemaingest.introspect.postgres import introspect_postgres
18
+
19
+ return introspect_postgres(req.to_dsn(), schema=req.resolved_schema())
@@ -0,0 +1,94 @@
1
+ """Pack assembly shared by every engine.
2
+
3
+ Each engine reads its own catalog and hands over plain rows; turning those
4
+ rows into columns, keys and relationships happens here, once."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime, timezone
9
+ from typing import Iterable, Optional, TypedDict
10
+
11
+ from schemaingest import __version__
12
+ from schemaingest.models import (
13
+ ColumnInfo,
14
+ ConstraintInfo,
15
+ DbMeta,
16
+ FkRef,
17
+ IndexInfo,
18
+ Relationship,
19
+ TableInfo,
20
+ )
21
+
22
+
23
+ class RawColumn(TypedDict):
24
+ name: str
25
+ type: str
26
+ nullable: bool
27
+ default: Optional[str]
28
+
29
+
30
+ class RawForeignKey(TypedDict):
31
+ """One column pair of a foreign key; a composite key is several rows
32
+ sharing constraint_name, in key order."""
33
+ from_column: str
34
+ to_table: str
35
+ to_column: str
36
+ constraint_name: str
37
+
38
+
39
+ def build_table(
40
+ tname: str,
41
+ schema: str,
42
+ columns: Iterable[RawColumn],
43
+ pk_cols: list[str],
44
+ fks: Iterable[RawForeignKey],
45
+ constraints: list[ConstraintInfo],
46
+ indexes: list[IndexInfo],
47
+ ) -> tuple[TableInfo, list[Relationship]]:
48
+ relationships: list[Relationship] = []
49
+ fk_map: dict[str, FkRef] = {}
50
+ for fk in fks:
51
+ # A column in two foreign keys keeps its first target for fkRef; every
52
+ # pairing is still listed in relationships.
53
+ fk_map.setdefault(fk["from_column"], FkRef(table=fk["to_table"], column=fk["to_column"]))
54
+ relationships.append(
55
+ Relationship(
56
+ fromTable=tname,
57
+ fromColumn=fk["from_column"],
58
+ toTable=fk["to_table"],
59
+ toColumn=fk["to_column"],
60
+ constraintName=fk["constraint_name"],
61
+ )
62
+ )
63
+
64
+ table = TableInfo(
65
+ name=tname,
66
+ schema=schema,
67
+ columns=[
68
+ ColumnInfo(
69
+ name=c["name"],
70
+ type=c["type"],
71
+ nullable=c["nullable"],
72
+ default=c["default"],
73
+ isPrimaryKey=c["name"] in pk_cols,
74
+ isForeignKey=c["name"] in fk_map,
75
+ fkRef=fk_map.get(c["name"]),
76
+ )
77
+ for c in columns
78
+ ],
79
+ primaryKey=pk_cols,
80
+ indexes=indexes,
81
+ constraints=constraints,
82
+ )
83
+ return table, relationships
84
+
85
+
86
+ def make_meta(engine: str, db_name: str, db_version: str, schema: str) -> DbMeta:
87
+ return DbMeta(
88
+ engine=engine,
89
+ dbName=db_name,
90
+ dbVersion=db_version,
91
+ schema=schema,
92
+ generatedAt=datetime.now(timezone.utc).isoformat(),
93
+ agentVersion=__version__,
94
+ )