shirube 0.1.0b1__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.
- shirube-0.1.0b1/.gitignore +75 -0
- shirube-0.1.0b1/PKG-INFO +119 -0
- shirube-0.1.0b1/README.md +103 -0
- shirube-0.1.0b1/pyproject.toml +68 -0
- shirube-0.1.0b1/src/shirube/__init__.py +3 -0
- shirube-0.1.0b1/src/shirube/__main__.py +64 -0
- shirube-0.1.0b1/src/shirube/adapters/README.md +5 -0
- shirube-0.1.0b1/src/shirube/adapters/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/api/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/api/app.py +162 -0
- shirube-0.1.0b1/src/shirube/adapters/api/dependencies.py +87 -0
- shirube-0.1.0b1/src/shirube/adapters/api/errors.py +45 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/connections.py +54 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/data.py +114 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/health.py +33 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/profiles.py +142 -0
- shirube-0.1.0b1/src/shirube/adapters/api/routes/schema.py +107 -0
- shirube-0.1.0b1/src/shirube/adapters/keyring/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/keyring/secret_store.py +32 -0
- shirube-0.1.0b1/src/shirube/adapters/persistence/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/persistence/bootstrap.py +15 -0
- shirube-0.1.0b1/src/shirube/adapters/persistence/database.py +44 -0
- shirube-0.1.0b1/src/shirube/adapters/persistence/models.py +29 -0
- shirube-0.1.0b1/src/shirube/adapters/persistence/profile_repository.py +87 -0
- shirube-0.1.0b1/src/shirube/adapters/postgres/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/adapters/postgres/_common.py +108 -0
- shirube-0.1.0b1/src/shirube/adapters/postgres/connector.py +28 -0
- shirube-0.1.0b1/src/shirube/adapters/postgres/data_reader.py +198 -0
- shirube-0.1.0b1/src/shirube/adapters/postgres/schema_inspector.py +236 -0
- shirube-0.1.0b1/src/shirube/application/README.md +5 -0
- shirube-0.1.0b1/src/shirube/application/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/application/connections.py +49 -0
- shirube-0.1.0b1/src/shirube/application/data.py +55 -0
- shirube-0.1.0b1/src/shirube/application/profiles.py +113 -0
- shirube-0.1.0b1/src/shirube/application/schema.py +46 -0
- shirube-0.1.0b1/src/shirube/config.py +79 -0
- shirube-0.1.0b1/src/shirube/domain/README.md +5 -0
- shirube-0.1.0b1/src/shirube/domain/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/domain/connection.py +76 -0
- shirube-0.1.0b1/src/shirube/domain/data.py +98 -0
- shirube-0.1.0b1/src/shirube/domain/errors.py +65 -0
- shirube-0.1.0b1/src/shirube/domain/schema.py +106 -0
- shirube-0.1.0b1/src/shirube/logging_config.py +155 -0
- shirube-0.1.0b1/src/shirube/ports/README.md +4 -0
- shirube-0.1.0b1/src/shirube/ports/__init__.py +1 -0
- shirube-0.1.0b1/src/shirube/ports/repositories.py +123 -0
- shirube-0.1.0b1/src/shirube/py.typed +0 -0
- shirube-0.1.0b1/src/shirube/static/assets/index-D765xCTu.js +55 -0
- shirube-0.1.0b1/src/shirube/static/assets/index-DOQ9Bfpw.css +1 -0
- shirube-0.1.0b1/src/shirube/static/favicon.svg +13 -0
- shirube-0.1.0b1/src/shirube/static/index.html +14 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ---- Python / FastAPI (backend) ----
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
parts/
|
|
14
|
+
sdist/
|
|
15
|
+
var/
|
|
16
|
+
wheels/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
.installed.cfg
|
|
19
|
+
*.egg
|
|
20
|
+
|
|
21
|
+
# Virtual environments
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
env/
|
|
25
|
+
ENV/
|
|
26
|
+
|
|
27
|
+
# Test / coverage
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
.coverage
|
|
30
|
+
.coverage.*
|
|
31
|
+
htmlcov/
|
|
32
|
+
.tox/
|
|
33
|
+
.mypy_cache/
|
|
34
|
+
.ruff_cache/
|
|
35
|
+
|
|
36
|
+
# ---- Node / Vite (frontend) ----
|
|
37
|
+
node_modules/
|
|
38
|
+
.pnp
|
|
39
|
+
.pnp.js
|
|
40
|
+
# pnpm's content-addressable store, if it lands inside the repo.
|
|
41
|
+
.pnpm-store/
|
|
42
|
+
web/dist/
|
|
43
|
+
dist-ssr/
|
|
44
|
+
*.local
|
|
45
|
+
|
|
46
|
+
# Bundled SPA (generated into the API package at build time)
|
|
47
|
+
api/src/shirube/static/
|
|
48
|
+
|
|
49
|
+
# ---- Development database ----
|
|
50
|
+
# pagila SQL fetched by scripts/dev-db.sh; not vendored into the repo.
|
|
51
|
+
dev/pagila/
|
|
52
|
+
|
|
53
|
+
# Logs
|
|
54
|
+
logs/
|
|
55
|
+
*.log
|
|
56
|
+
npm-debug.log*
|
|
57
|
+
yarn-debug.log*
|
|
58
|
+
yarn-error.log*
|
|
59
|
+
pnpm-debug.log*
|
|
60
|
+
|
|
61
|
+
# ---- Secrets / local config ----
|
|
62
|
+
.env
|
|
63
|
+
.env.*
|
|
64
|
+
!.env.example
|
|
65
|
+
|
|
66
|
+
# ---- Editors / OS ----
|
|
67
|
+
.vscode/*
|
|
68
|
+
!.vscode/settings.json
|
|
69
|
+
!.vscode/extensions.json
|
|
70
|
+
.idea/
|
|
71
|
+
.claude/settings.local.json
|
|
72
|
+
*.swp
|
|
73
|
+
*.swo
|
|
74
|
+
.DS_Store
|
|
75
|
+
Thumbs.db
|
shirube-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shirube
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Understand your database as a map — built for the AI-coding era.
|
|
5
|
+
License-Expression: AGPL-3.0-or-later
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: keyring>=25.7.0
|
|
9
|
+
Requires-Dist: platformdirs>=4.2
|
|
10
|
+
Requires-Dist: psycopg[binary]>=3.3.4
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
13
|
+
Requires-Dist: structlog>=25.1.0
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.51.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/shou-taro/shirube/main/docs/images/logo.svg" width="88" alt="shirube" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<h1 align="center">shirube</h1>
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<strong>AI writes the SQL.<br />You still own the schema.</strong><br />
|
|
25
|
+
<sub>標べ — a signpost for reading a database as a map.</sub>
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://github.com/shou-taro/shirube/actions/workflows/ci.yml"><img src="https://github.com/shou-taro/shirube/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
30
|
+
<img src="https://img.shields.io/badge/status-beta-a78bfa" alt="Status: beta" />
|
|
31
|
+
<a href="https://github.com/shou-taro/shirube/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-AGPL--3.0-blue" alt="Licence: AGPL-3.0" /></a>
|
|
32
|
+
<img src="https://img.shields.io/badge/PostgreSQL-ready-336791?logo=postgresql&logoColor=white" alt="PostgreSQL" />
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
> 🚧 **Status: Beta.** The explorer core is here and usable today. The AI navigator — the
|
|
36
|
+
> feature shirube is ultimately built around — is the next milestone. shirube is pre-1.0:
|
|
37
|
+
> things may still change.
|
|
38
|
+
|
|
39
|
+
<p align="center"><em>See the whole database as a map.</em></p>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="https://raw.githubusercontent.com/shou-taro/shirube/main/docs/images/home.png" alt="shirube exploring a database: an ER diagram with a table's detail and its rows" width="960" />
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
## 🤖 Why shirube
|
|
46
|
+
|
|
47
|
+
You write less SQL by hand than you used to — an AI writes much of it for you. But that
|
|
48
|
+
SQL still runs against **your** schema, and someone still has to understand that schema:
|
|
49
|
+
to prompt the AI well, to check what it gave back, to reason about where the data
|
|
50
|
+
actually lives. That understanding used to come for free while you wrote the queries
|
|
51
|
+
yourself. It doesn't any more.
|
|
52
|
+
|
|
53
|
+
shirube is where that understanding lives — and it's just as useful in the classic case,
|
|
54
|
+
dropping into a project with hundreds of undocumented tables and needing to find your
|
|
55
|
+
footing fast.
|
|
56
|
+
|
|
57
|
+
> AI changed how we write SQL. shirube changes how we understand databases.
|
|
58
|
+
|
|
59
|
+
## 🧭 What shirube does
|
|
60
|
+
|
|
61
|
+
shirube opens on an interactive ER diagram and lets you explore a database like a map:
|
|
62
|
+
search for a table, focus on it, and follow its relationships outward — so you can see
|
|
63
|
+
how everything connects without reading DDL or writing a single query.
|
|
64
|
+
|
|
65
|
+
It is **not** another SQL IDE or database administration console — there is no query
|
|
66
|
+
editor, and nothing that ever writes. shirube is a tool for *understanding* a database.
|
|
67
|
+
|
|
68
|
+
## ✨ Features
|
|
69
|
+
|
|
70
|
+
Everything below works today, in the beta:
|
|
71
|
+
|
|
72
|
+
- 🗺️ **ER diagram home** — auto-generated and centred on the most-connected table; see a
|
|
73
|
+
table and its immediate neighbours, and travel outward one hop at a time.
|
|
74
|
+
- 📋 **Table detail** — columns with their types, primary keys and nullability, plus
|
|
75
|
+
relationships split into *references* and *referenced by*.
|
|
76
|
+
- 🔗 **Relationship navigation** — click a related table to glide the map over to it.
|
|
77
|
+
- 👁️ **Data preview** — read a table or view's actual rows in a drawer, with click-to-sort
|
|
78
|
+
columns, simple filters and paging.
|
|
79
|
+
- ⚡ **Instant search** — jump straight to any table or column.
|
|
80
|
+
- 🔐 **Saved connections** — passwords kept in your operating system's keychain, never in
|
|
81
|
+
a config file.
|
|
82
|
+
- 🌗 **Light and dark themes.**
|
|
83
|
+
|
|
84
|
+
## 🛡️ Safe by design
|
|
85
|
+
|
|
86
|
+
- 🔒 **Read-only** — every connection is opened read-only with a statement timeout. shirube
|
|
87
|
+
cannot modify your database — no writes, no schema changes, ever.
|
|
88
|
+
- 💻 **Local-first** — it runs on your machine and binds to `127.0.0.1` only. Your database
|
|
89
|
+
credentials and data never leave your computer.
|
|
90
|
+
- 📝 **Metadata-only logging** — the local log records errors and request timings, but
|
|
91
|
+
never the values in your data.
|
|
92
|
+
|
|
93
|
+
## 🚀 Getting started
|
|
94
|
+
|
|
95
|
+
You'll need a PostgreSQL database to point shirube at, and a way to run a Python
|
|
96
|
+
application without installing it permanently.
|
|
97
|
+
|
|
98
|
+
> The beta is tested on **macOS** and **Windows**. Linux support is planned.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pipx run shirube # with pipx
|
|
102
|
+
# or
|
|
103
|
+
uvx shirube # with uv
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Either command starts a small local server and opens shirube in your browser. Add a
|
|
107
|
+
connection with your PostgreSQL details, and shirube inspects the schema and opens the ER
|
|
108
|
+
diagram.
|
|
109
|
+
|
|
110
|
+
> 💡 shirube connects with whatever credentials you give it. A read-only role with
|
|
111
|
+
> `CONNECT` and `SELECT` is all it needs — and all it should have.
|
|
112
|
+
|
|
113
|
+
## 📚 Learn more
|
|
114
|
+
|
|
115
|
+
- **Full README, roadmap and the AI-navigator preview** —
|
|
116
|
+
<https://github.com/shou-taro/shirube>
|
|
117
|
+
- **Contributing** —
|
|
118
|
+
<https://github.com/shou-taro/shirube/blob/main/CONTRIBUTING.md>
|
|
119
|
+
- **Licence** — [GNU AGPL-3.0](https://github.com/shou-taro/shirube/blob/main/LICENSE)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/shou-taro/shirube/main/docs/images/logo.svg" width="88" alt="shirube" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">shirube</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>AI writes the SQL.<br />You still own the schema.</strong><br />
|
|
9
|
+
<sub>標べ — a signpost for reading a database as a map.</sub>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://github.com/shou-taro/shirube/actions/workflows/ci.yml"><img src="https://github.com/shou-taro/shirube/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/status-beta-a78bfa" alt="Status: beta" />
|
|
15
|
+
<a href="https://github.com/shou-taro/shirube/blob/main/LICENSE"><img src="https://img.shields.io/badge/licence-AGPL--3.0-blue" alt="Licence: AGPL-3.0" /></a>
|
|
16
|
+
<img src="https://img.shields.io/badge/PostgreSQL-ready-336791?logo=postgresql&logoColor=white" alt="PostgreSQL" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
> 🚧 **Status: Beta.** The explorer core is here and usable today. The AI navigator — the
|
|
20
|
+
> feature shirube is ultimately built around — is the next milestone. shirube is pre-1.0:
|
|
21
|
+
> things may still change.
|
|
22
|
+
|
|
23
|
+
<p align="center"><em>See the whole database as a map.</em></p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/shou-taro/shirube/main/docs/images/home.png" alt="shirube exploring a database: an ER diagram with a table's detail and its rows" width="960" />
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
## 🤖 Why shirube
|
|
30
|
+
|
|
31
|
+
You write less SQL by hand than you used to — an AI writes much of it for you. But that
|
|
32
|
+
SQL still runs against **your** schema, and someone still has to understand that schema:
|
|
33
|
+
to prompt the AI well, to check what it gave back, to reason about where the data
|
|
34
|
+
actually lives. That understanding used to come for free while you wrote the queries
|
|
35
|
+
yourself. It doesn't any more.
|
|
36
|
+
|
|
37
|
+
shirube is where that understanding lives — and it's just as useful in the classic case,
|
|
38
|
+
dropping into a project with hundreds of undocumented tables and needing to find your
|
|
39
|
+
footing fast.
|
|
40
|
+
|
|
41
|
+
> AI changed how we write SQL. shirube changes how we understand databases.
|
|
42
|
+
|
|
43
|
+
## 🧭 What shirube does
|
|
44
|
+
|
|
45
|
+
shirube opens on an interactive ER diagram and lets you explore a database like a map:
|
|
46
|
+
search for a table, focus on it, and follow its relationships outward — so you can see
|
|
47
|
+
how everything connects without reading DDL or writing a single query.
|
|
48
|
+
|
|
49
|
+
It is **not** another SQL IDE or database administration console — there is no query
|
|
50
|
+
editor, and nothing that ever writes. shirube is a tool for *understanding* a database.
|
|
51
|
+
|
|
52
|
+
## ✨ Features
|
|
53
|
+
|
|
54
|
+
Everything below works today, in the beta:
|
|
55
|
+
|
|
56
|
+
- 🗺️ **ER diagram home** — auto-generated and centred on the most-connected table; see a
|
|
57
|
+
table and its immediate neighbours, and travel outward one hop at a time.
|
|
58
|
+
- 📋 **Table detail** — columns with their types, primary keys and nullability, plus
|
|
59
|
+
relationships split into *references* and *referenced by*.
|
|
60
|
+
- 🔗 **Relationship navigation** — click a related table to glide the map over to it.
|
|
61
|
+
- 👁️ **Data preview** — read a table or view's actual rows in a drawer, with click-to-sort
|
|
62
|
+
columns, simple filters and paging.
|
|
63
|
+
- ⚡ **Instant search** — jump straight to any table or column.
|
|
64
|
+
- 🔐 **Saved connections** — passwords kept in your operating system's keychain, never in
|
|
65
|
+
a config file.
|
|
66
|
+
- 🌗 **Light and dark themes.**
|
|
67
|
+
|
|
68
|
+
## 🛡️ Safe by design
|
|
69
|
+
|
|
70
|
+
- 🔒 **Read-only** — every connection is opened read-only with a statement timeout. shirube
|
|
71
|
+
cannot modify your database — no writes, no schema changes, ever.
|
|
72
|
+
- 💻 **Local-first** — it runs on your machine and binds to `127.0.0.1` only. Your database
|
|
73
|
+
credentials and data never leave your computer.
|
|
74
|
+
- 📝 **Metadata-only logging** — the local log records errors and request timings, but
|
|
75
|
+
never the values in your data.
|
|
76
|
+
|
|
77
|
+
## 🚀 Getting started
|
|
78
|
+
|
|
79
|
+
You'll need a PostgreSQL database to point shirube at, and a way to run a Python
|
|
80
|
+
application without installing it permanently.
|
|
81
|
+
|
|
82
|
+
> The beta is tested on **macOS** and **Windows**. Linux support is planned.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pipx run shirube # with pipx
|
|
86
|
+
# or
|
|
87
|
+
uvx shirube # with uv
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Either command starts a small local server and opens shirube in your browser. Add a
|
|
91
|
+
connection with your PostgreSQL details, and shirube inspects the schema and opens the ER
|
|
92
|
+
diagram.
|
|
93
|
+
|
|
94
|
+
> 💡 shirube connects with whatever credentials you give it. A read-only role with
|
|
95
|
+
> `CONNECT` and `SELECT` is all it needs — and all it should have.
|
|
96
|
+
|
|
97
|
+
## 📚 Learn more
|
|
98
|
+
|
|
99
|
+
- **Full README, roadmap and the AI-navigator preview** —
|
|
100
|
+
<https://github.com/shou-taro/shirube>
|
|
101
|
+
- **Contributing** —
|
|
102
|
+
<https://github.com/shou-taro/shirube/blob/main/CONTRIBUTING.md>
|
|
103
|
+
- **Licence** — [GNU AGPL-3.0](https://github.com/shou-taro/shirube/blob/main/LICENSE)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "shirube"
|
|
3
|
+
version = "0.1.0b1"
|
|
4
|
+
description = "Understand your database as a map — built for the AI-coding era."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "AGPL-3.0-or-later"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"fastapi>=0.115",
|
|
10
|
+
"uvicorn[standard]>=0.51.0",
|
|
11
|
+
"sqlalchemy>=2.0",
|
|
12
|
+
"pydantic-settings>=2.4",
|
|
13
|
+
"platformdirs>=4.2",
|
|
14
|
+
"keyring>=25.7.0",
|
|
15
|
+
"psycopg[binary]>=3.3.4",
|
|
16
|
+
"structlog>=25.1.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
shirube = "shirube.__main__:main"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"ruff>=0.6",
|
|
25
|
+
"mypy>=1.11",
|
|
26
|
+
"pytest>=8.3",
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"bandit>=1.7",
|
|
29
|
+
"pip-audit>=2.7",
|
|
30
|
+
"hypothesis>=6.100",
|
|
31
|
+
"keyrings-alt>=5.0.2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/shirube"]
|
|
40
|
+
# Include the built SPA (generated, git-ignored) in the wheel for single-origin serving.
|
|
41
|
+
artifacts = ["src/shirube/static/**/*"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
# Keep the sdist to the sources needed to build the wheel; exclude test caches,
|
|
45
|
+
# the lockfile and other dev cruft that hatchling would otherwise sweep in.
|
|
46
|
+
include = [
|
|
47
|
+
"src/shirube",
|
|
48
|
+
"README.md",
|
|
49
|
+
"pyproject.toml",
|
|
50
|
+
]
|
|
51
|
+
artifacts = ["src/shirube/static/**/*"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
src = ["src", "tests"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint]
|
|
58
|
+
select = ["E", "F", "I", "UP", "B", "N"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.11"
|
|
62
|
+
strict = true
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
markers = [
|
|
67
|
+
"integration: needs a real PostgreSQL; set SHIRUBE_TEST_DATABASE_URL to run (skipped otherwise)",
|
|
68
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Entry point for ``shirube`` / ``uvx shirube``.
|
|
2
|
+
|
|
3
|
+
Starts the local API server (which also serves the bundled single-page app in a
|
|
4
|
+
packaged build) and, unless disabled, opens the browser once the server is up.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import webbrowser
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
|
|
12
|
+
from shirube import __version__
|
|
13
|
+
from shirube.config import get_settings
|
|
14
|
+
from shirube.logging_config import setup_logging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_loopback(host: str) -> bool:
|
|
18
|
+
"""Whether ``host`` refers to the local machine only (not the network)."""
|
|
19
|
+
return host in {"localhost", "::1", "[::1]"} or host.startswith("127.")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _open_browser(url: str) -> None:
|
|
23
|
+
"""Open ``url`` in the user's default browser (best effort)."""
|
|
24
|
+
webbrowser.open(url)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
"""Launch the shirube server and open the browser.
|
|
29
|
+
|
|
30
|
+
Binds to the configured host and port (loopback by default) and blocks while
|
|
31
|
+
uvicorn runs. The browser launch is deferred by a short timer so it fires just
|
|
32
|
+
after the server starts accepting connections rather than before.
|
|
33
|
+
"""
|
|
34
|
+
settings = get_settings()
|
|
35
|
+
logger = setup_logging()
|
|
36
|
+
logger.info(
|
|
37
|
+
"starting",
|
|
38
|
+
version=__version__,
|
|
39
|
+
host=settings.host,
|
|
40
|
+
port=settings.port,
|
|
41
|
+
data_dir=str(settings.data_dir),
|
|
42
|
+
)
|
|
43
|
+
if not _is_loopback(settings.host):
|
|
44
|
+
# shirube is single-user and unauthenticated by design; binding beyond loopback
|
|
45
|
+
# exposes an unprotected API on the network, so make an accidental one loud.
|
|
46
|
+
logger.warning(
|
|
47
|
+
"bound to a non-loopback address — shirube may be reachable from your "
|
|
48
|
+
"network. It is single-user and unauthenticated; bind to 127.0.0.1 unless "
|
|
49
|
+
"you intend to expose it.",
|
|
50
|
+
host=settings.host,
|
|
51
|
+
)
|
|
52
|
+
url = f"http://{settings.host}:{settings.port}"
|
|
53
|
+
if settings.open_browser:
|
|
54
|
+
# Defer the launch slightly so the server is ready to answer the first request.
|
|
55
|
+
threading.Timer(1.0, _open_browser, args=[url]).start()
|
|
56
|
+
uvicorn.run(
|
|
57
|
+
"shirube.adapters.api.app:app",
|
|
58
|
+
host=settings.host,
|
|
59
|
+
port=settings.port,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters — concrete implementations of ports and delivery mechanisms."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HTTP API adapter (FastAPI)."""
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""FastAPI application factory (a driving adapter).
|
|
2
|
+
|
|
3
|
+
This is the HTTP entry point into the application core. In a packaged build it also
|
|
4
|
+
serves the compiled single-page app, so the whole tool runs from one process on one
|
|
5
|
+
origin; in development the Vite dev server serves the UI and proxies API calls here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
from fastapi import FastAPI, Request, Response
|
|
16
|
+
from fastapi.staticfiles import StaticFiles
|
|
17
|
+
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
18
|
+
|
|
19
|
+
from shirube import __version__
|
|
20
|
+
from shirube.adapters.api.errors import register_exception_handlers
|
|
21
|
+
from shirube.adapters.api.routes import connections, data, health, profiles, schema
|
|
22
|
+
from shirube.adapters.persistence.bootstrap import bootstrap_database
|
|
23
|
+
from shirube.config import get_settings
|
|
24
|
+
from shirube.logging_config import get_logger
|
|
25
|
+
|
|
26
|
+
# The built SPA, copied here by scripts/build.sh and bundled into the wheel. It is
|
|
27
|
+
# absent during development (git-ignored), where Vite serves the UI instead — hence the
|
|
28
|
+
# existence check in create_app().
|
|
29
|
+
STATIC_DIR = Path(__file__).resolve().parents[2] / "static"
|
|
30
|
+
|
|
31
|
+
_logger = get_logger("shirube.request")
|
|
32
|
+
|
|
33
|
+
# A conservative Content-Security-Policy for the bundled SPA. Everything is same-origin
|
|
34
|
+
# (scripts, styles, the API), so only 'self' is allowed; inline styles are permitted
|
|
35
|
+
# because React Flow sets element style attributes, and data: covers the inline SVG
|
|
36
|
+
# favicon and logo. frame-ancestors 'none' blocks the page being framed (clickjacking).
|
|
37
|
+
_CONTENT_SECURITY_POLICY = (
|
|
38
|
+
"default-src 'self'; "
|
|
39
|
+
"script-src 'self'; "
|
|
40
|
+
"style-src 'self' 'unsafe-inline'; "
|
|
41
|
+
"img-src 'self' data:; "
|
|
42
|
+
"font-src 'self' data:; "
|
|
43
|
+
"connect-src 'self'; "
|
|
44
|
+
"base-uri 'self'; "
|
|
45
|
+
"form-action 'self'; "
|
|
46
|
+
"frame-ancestors 'none'"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@asynccontextmanager
|
|
51
|
+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
52
|
+
"""Run start-up and shutdown work around the application's lifetime.
|
|
53
|
+
|
|
54
|
+
On start-up the local app-state database is created if missing. There is nothing to
|
|
55
|
+
tear down yet, so control simply yields back to the server.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
app: The FastAPI application (unused; required by the lifespan signature).
|
|
59
|
+
|
|
60
|
+
Yields:
|
|
61
|
+
None, once start-up has completed.
|
|
62
|
+
"""
|
|
63
|
+
bootstrap_database()
|
|
64
|
+
yield
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_app() -> FastAPI:
|
|
68
|
+
"""Build and configure the FastAPI application.
|
|
69
|
+
|
|
70
|
+
Wires up exception handling and the API routes, and — when the SPA has been built
|
|
71
|
+
and bundled — mounts it at the root so the UI and API share a single origin.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The configured application instance.
|
|
75
|
+
"""
|
|
76
|
+
app = FastAPI(title="shirube", version=__version__, lifespan=lifespan)
|
|
77
|
+
|
|
78
|
+
@app.middleware("http")
|
|
79
|
+
async def _security_headers(
|
|
80
|
+
request: Request,
|
|
81
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
82
|
+
) -> Response:
|
|
83
|
+
"""Add defence-in-depth security headers to every response.
|
|
84
|
+
|
|
85
|
+
Cheap hardening for a locally served app: stop MIME sniffing, forbid framing
|
|
86
|
+
(clickjacking), withhold the referrer, and constrain resource loading to the
|
|
87
|
+
same origin via a Content-Security-Policy.
|
|
88
|
+
"""
|
|
89
|
+
response = await call_next(request)
|
|
90
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
91
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
92
|
+
response.headers["Referrer-Policy"] = "no-referrer"
|
|
93
|
+
response.headers["Content-Security-Policy"] = _CONTENT_SECURITY_POLICY
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
@app.middleware("http")
|
|
97
|
+
async def _log_requests(
|
|
98
|
+
request: Request,
|
|
99
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
100
|
+
) -> Response:
|
|
101
|
+
"""Log each request's outcome — method, path, status and duration.
|
|
102
|
+
|
|
103
|
+
A short ``request_id`` is bound for the lifetime of the request and attached to
|
|
104
|
+
every event logged while it runs (here and in the error handler), so the lines
|
|
105
|
+
belonging to one request can be tied together; it is also returned to the caller
|
|
106
|
+
in the ``X-Request-ID`` header. Only metadata is recorded: never the query string,
|
|
107
|
+
request body or response content, so filter values and row data stay out of the
|
|
108
|
+
log. An exception that escapes the handlers (a genuine bug) is logged with its
|
|
109
|
+
traceback and re-raised for FastAPI's default 500.
|
|
110
|
+
"""
|
|
111
|
+
request_id = uuid4().hex[:12]
|
|
112
|
+
structlog.contextvars.bind_contextvars(request_id=request_id)
|
|
113
|
+
start = time.perf_counter()
|
|
114
|
+
try:
|
|
115
|
+
response = await call_next(request)
|
|
116
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
117
|
+
_logger.info(
|
|
118
|
+
"request",
|
|
119
|
+
method=request.method,
|
|
120
|
+
path=request.url.path,
|
|
121
|
+
status=response.status_code,
|
|
122
|
+
duration_ms=round(elapsed_ms),
|
|
123
|
+
)
|
|
124
|
+
response.headers["X-Request-ID"] = request_id
|
|
125
|
+
return response
|
|
126
|
+
except Exception:
|
|
127
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
128
|
+
_logger.exception(
|
|
129
|
+
"request_failed",
|
|
130
|
+
method=request.method,
|
|
131
|
+
path=request.url.path,
|
|
132
|
+
duration_ms=round(elapsed_ms),
|
|
133
|
+
)
|
|
134
|
+
raise
|
|
135
|
+
finally:
|
|
136
|
+
# Always clear the request-scoped context so it never bleeds into the next
|
|
137
|
+
# request handled on this task.
|
|
138
|
+
structlog.contextvars.unbind_contextvars("request_id")
|
|
139
|
+
|
|
140
|
+
# Reject requests whose Host header is not a name we serve, added last so it runs
|
|
141
|
+
# outermost — a bad host is refused before any handler or logging runs. This is the
|
|
142
|
+
# core DNS-rebinding defence: it stops a page on another origin from reaching this
|
|
143
|
+
# local API by pointing its own hostname at 127.0.0.1. The bind host is always
|
|
144
|
+
# allowed alongside the configured names.
|
|
145
|
+
settings = get_settings()
|
|
146
|
+
allowed_hosts = list(dict.fromkeys([*settings.allowed_hosts, settings.host]))
|
|
147
|
+
app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
|
|
148
|
+
|
|
149
|
+
register_exception_handlers(app)
|
|
150
|
+
app.include_router(health.router, prefix="/api")
|
|
151
|
+
app.include_router(profiles.router, prefix="/api")
|
|
152
|
+
app.include_router(connections.router, prefix="/api")
|
|
153
|
+
app.include_router(schema.router, prefix="/api")
|
|
154
|
+
app.include_router(data.router, prefix="/api")
|
|
155
|
+
if STATIC_DIR.is_dir():
|
|
156
|
+
# Mounted after the API router so "/api/*" matches first; this then catches
|
|
157
|
+
# every remaining path and serves index.html for "/".
|
|
158
|
+
app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="spa")
|
|
159
|
+
return app
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
app = create_app()
|