sqlpush 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.
sqlpush-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlpush
3
+ Version: 0.1.0
4
+ Summary: Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models
5
+ Keywords: sqlalchemy,alembic,postgresql,timescaledb,prisma,schema,migrations,database,drift,cli
6
+ Author: Juan Miguel Contreras
7
+ Author-email: Juan Miguel Contreras <19253629+juanmicl@users.noreply.github.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Requires-Dist: alembic>=1.18,<2
20
+ Requires-Dist: psycopg[binary]>=3.2
21
+ Requires-Dist: sqlalchemy>=2.0
22
+ Requires-Dist: typer>=0.12
23
+ Requires-Python: >=3.10
24
+ Project-URL: Homepage, https://github.com/juanmicl/sqlpush
25
+ Project-URL: Repository, https://github.com/juanmicl/sqlpush
26
+ Project-URL: Issues, https://github.com/juanmicl/sqlpush/issues
27
+ Project-URL: Changelog, https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md
28
+ Description-Content-Type: text/markdown
29
+
30
+ # sqlpush
31
+
32
+ [![CI](https://github.com/juanmicl/sqlpush/actions/workflows/ci.yml/badge.svg?style=for-the-badge)](https://github.com/juanmicl/sqlpush/actions/workflows/ci.yml)
33
+ [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
36
+
37
+ **Prisma `db push` for SQLAlchemy.** Apply your models (SQLAlchemy,
38
+ SQLModel, anything built on `MetaData`) to a live PostgreSQL / TimescaleDB
39
+ database directly, no migration files. sqlpush
40
+ diffs your models against the real schema, classifies every operation by
41
+ risk (safe / risky / destructive), and applies the plan atomically. Drift
42
+ checks exit with codes your CI can gate on.
43
+
44
+ ```console
45
+ sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
46
+ sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
47
+ sqlpush push "myapp.models:metadata" # apply (destructive gated)
48
+ ```
49
+
50
+ If you've ever run `Base.metadata.create_all()` in production and known it
51
+ was wrong, then sighed at the migration-script treadmill when you reached
52
+ for alembic: sqlpush is for you.
53
+
54
+ ## Why
55
+
56
+ Declarative models are already the source of truth. Migration files
57
+ re-encode what the models say, drift from them, and pile up forever.
58
+ sqlpush closes the loop the way Prisma's `db push` does for its schema
59
+ language, but for the SQLAlchemy ecosystem (SQLModel included):
60
+
61
+ - **No migration files, ever.** The diff *is* the migration: computed fresh
62
+ from models vs. live database on every run, via alembic's autogenerate
63
+ engine used as a library.
64
+ - **Risk-aware by default.** Every operation is classified `safe` /
65
+ `risky` / `destructive`. Destructive ops (drops) are **blocked until
66
+ `--allow-destructive`**: nothing executes at all while any is present.
67
+ - **Drift detection built for CI.** `check` plans once and exits `0` clean /
68
+ `2` drift / `3` destructive drift, scriptable without parsing output.
69
+ `--json` emits a stable versioned contract.
70
+ - **Safe under concurrency.** An advisory lock (keyed to the database, not
71
+ the DSN) coordinates workers: one pusher at a time, losers wait bounded
72
+ and re-verify, so deploy pipelines can race without corrupting anything.
73
+ - **Hypertables without hand-written SQL.** Decorate a model with
74
+ `@hypertable` and the `create_hypertable` directive is planned
75
+ state-aware: idempotent pushes, clean checks, no false drift.
76
+
77
+ PostgreSQL only, by design.
78
+
79
+ ## Install
80
+
81
+ ```console
82
+ pip install sqlpush
83
+ ```
84
+
85
+ Or from source:
86
+
87
+ ```console
88
+ git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
89
+ ```
90
+
91
+ ## The 30-second tour
92
+
93
+ Point sqlpush at your metadata (`module:attribute`) and a database
94
+ (`--dsn` or `$DATABASE_URL`):
95
+
96
+ ```console
97
+ $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
98
+
99
+ $ sqlpush diff "myapp.models:metadata"
100
+ -- safe
101
+
102
+ CREATE TABLE hero (
103
+ id SERIAL NOT NULL PRIMARY KEY,
104
+ name VARCHAR(50) NOT NULL
105
+ );
106
+
107
+ -- risky
108
+
109
+ CREATE INDEX ix_hero_name ON hero (name);
110
+ ```
111
+
112
+ Push it (the destructive gate is on by default):
113
+
114
+ ```console
115
+ $ sqlpush push "myapp.models:metadata"
116
+ 1 destructive operation(s) blocked; re-run with --allow-destructive
117
+ $ echo $?
118
+ 1
119
+
120
+ $ sqlpush push "myapp.models:metadata" --allow-destructive
121
+ $ echo $?
122
+ 0
123
+ ```
124
+
125
+ In CI, check drift and fail loudly (see exit codes below). Limit scope with
126
+ repeated `--schema` / `--exclude` options.
127
+
128
+ ## Exit codes
129
+
130
+ | verb | 0 | 1 | 2 | 3 |
131
+ | --- | --- | --- | --- | --- |
132
+ | `diff` | always | | | |
133
+ | `check` | clean | | drift | destructive drift |
134
+ | `push` | applied | destructive blocked | error (incl. partial failure) | |
135
+
136
+ `push --safe-only` runs only safe operations and skips the rest
137
+ informationally (exit `0`). A failed `CREATE INDEX CONCURRENTLY` marks the
138
+ run as partial failure (exit `2`) instead of silently half-applying.
139
+
140
+ ## FastAPI / SQLModel: replace `create_all`
141
+
142
+ ```python
143
+ from contextlib import asynccontextmanager
144
+ from sqlpush import aensure_schema
145
+
146
+
147
+ @asynccontextmanager
148
+ async def lifespan(app):
149
+ await aensure_schema(SQLModel.metadata, engine, mode="check")
150
+ yield
151
+ ```
152
+
153
+ Push in the deploy pipeline, check at startup.
154
+
155
+ ## How it works
156
+
157
+ ```mermaid
158
+ flowchart LR
159
+ models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
160
+ db[("live PostgreSQL")] --> diff
161
+ diff --> risk["risk classification<br>safe / risky / destructive"]
162
+ risk --> plan["plan"]
163
+ plan --> render["render"]
164
+ render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
165
+ apply --> report["report"]
166
+ ```
167
+
168
+ - **Diff engine** scopes reflection to your target schemas (default: the
169
+ session's real `search_path`) and prunes system catalogs (TimescaleDB
170
+ internals included) before reflection even starts.
171
+ - **Classifier** maps each operation to a risk class; unknown operations
172
+ are `risky`, never silently safe.
173
+ - **Executor** splits the plan: `CONCURRENTLY` statements run one-per-
174
+ transaction on autocommit, everything else applies in a single atomic
175
+ transaction with a bounded `lock_timeout`.
176
+ - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
177
+ `MetadataImportError` escape the API, never raw driver exceptions.
178
+
179
+ ## Comparison
180
+
181
+ An honest view of the neighborhood (stars as of 2026-08):
182
+
183
+ | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
184
+ | --- | --- | --- | --- | --- | --- |
185
+ | **sqlpush** | none (the diff is the migration) | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
186
+ | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
187
+ | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
188
+ | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
189
+ | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
190
+
191
+ sqlpush is narrower than atlas and younger than alembic, deliberately.
192
+ It is one tool for one job: keep a PostgreSQL schema in lockstep with
193
+ SQLAlchemy models, safely enough to run from CI.
194
+
195
+ Coming from [migra](https://github.com/djrobstep/migra) (now
196
+ deprecated)? There is a [migration guide](docs/migrating-from-migra.md).
197
+
198
+ ## Design notes
199
+
200
+ - `import sqlpush` stays light: the public API loads lazily, so the
201
+ annotations module carries none of alembic/typer/psycopg.
202
+ - The advisory-lock key derives from the database OID: two DSN spellings
203
+ of the same database contend for the same lock.
204
+ - `--json` output is a versioned contract (`"version": 1`) meant for
205
+ tooling; additive changes only within a version.
206
+
207
+ ## Roadmap (0.1.x)
208
+
209
+ - `CREATE INDEX CONCURRENTLY` by default for indexes on existing tables
210
+ - asyncpg DSN translation in `ensure_schema(AsyncEngine)`
211
+ - jsonschema-validated `--json` output
212
+
213
+ ## License
214
+
215
+ [MIT](LICENSE) · © 2026 Juan Miguel Contreras
@@ -0,0 +1,186 @@
1
+ # sqlpush
2
+
3
+ [![CI](https://github.com/juanmicl/sqlpush/actions/workflows/ci.yml/badge.svg?style=for-the-badge)](https://github.com/juanmicl/sqlpush/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/sqlpush?style=for-the-badge)](https://pypi.org/project/sqlpush/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)
7
+
8
+ **Prisma `db push` for SQLAlchemy.** Apply your models (SQLAlchemy,
9
+ SQLModel, anything built on `MetaData`) to a live PostgreSQL / TimescaleDB
10
+ database directly, no migration files. sqlpush
11
+ diffs your models against the real schema, classifies every operation by
12
+ risk (safe / risky / destructive), and applies the plan atomically. Drift
13
+ checks exit with codes your CI can gate on.
14
+
15
+ ```console
16
+ sqlpush diff "myapp.models:metadata" # see the SQL, ordered by risk
17
+ sqlpush check "myapp.models:metadata" # CI gate: exit 0/2/3
18
+ sqlpush push "myapp.models:metadata" # apply (destructive gated)
19
+ ```
20
+
21
+ If you've ever run `Base.metadata.create_all()` in production and known it
22
+ was wrong, then sighed at the migration-script treadmill when you reached
23
+ for alembic: sqlpush is for you.
24
+
25
+ ## Why
26
+
27
+ Declarative models are already the source of truth. Migration files
28
+ re-encode what the models say, drift from them, and pile up forever.
29
+ sqlpush closes the loop the way Prisma's `db push` does for its schema
30
+ language, but for the SQLAlchemy ecosystem (SQLModel included):
31
+
32
+ - **No migration files, ever.** The diff *is* the migration: computed fresh
33
+ from models vs. live database on every run, via alembic's autogenerate
34
+ engine used as a library.
35
+ - **Risk-aware by default.** Every operation is classified `safe` /
36
+ `risky` / `destructive`. Destructive ops (drops) are **blocked until
37
+ `--allow-destructive`**: nothing executes at all while any is present.
38
+ - **Drift detection built for CI.** `check` plans once and exits `0` clean /
39
+ `2` drift / `3` destructive drift, scriptable without parsing output.
40
+ `--json` emits a stable versioned contract.
41
+ - **Safe under concurrency.** An advisory lock (keyed to the database, not
42
+ the DSN) coordinates workers: one pusher at a time, losers wait bounded
43
+ and re-verify, so deploy pipelines can race without corrupting anything.
44
+ - **Hypertables without hand-written SQL.** Decorate a model with
45
+ `@hypertable` and the `create_hypertable` directive is planned
46
+ state-aware: idempotent pushes, clean checks, no false drift.
47
+
48
+ PostgreSQL only, by design.
49
+
50
+ ## Install
51
+
52
+ ```console
53
+ pip install sqlpush
54
+ ```
55
+
56
+ Or from source:
57
+
58
+ ```console
59
+ git clone https://github.com/juanmicl/sqlpush && cd sqlpush && uv sync
60
+ ```
61
+
62
+ ## The 30-second tour
63
+
64
+ Point sqlpush at your metadata (`module:attribute`) and a database
65
+ (`--dsn` or `$DATABASE_URL`):
66
+
67
+ ```console
68
+ $ export DATABASE_URL="postgresql+psycopg://user:pass@host:5432/db"
69
+
70
+ $ sqlpush diff "myapp.models:metadata"
71
+ -- safe
72
+
73
+ CREATE TABLE hero (
74
+ id SERIAL NOT NULL PRIMARY KEY,
75
+ name VARCHAR(50) NOT NULL
76
+ );
77
+
78
+ -- risky
79
+
80
+ CREATE INDEX ix_hero_name ON hero (name);
81
+ ```
82
+
83
+ Push it (the destructive gate is on by default):
84
+
85
+ ```console
86
+ $ sqlpush push "myapp.models:metadata"
87
+ 1 destructive operation(s) blocked; re-run with --allow-destructive
88
+ $ echo $?
89
+ 1
90
+
91
+ $ sqlpush push "myapp.models:metadata" --allow-destructive
92
+ $ echo $?
93
+ 0
94
+ ```
95
+
96
+ In CI, check drift and fail loudly (see exit codes below). Limit scope with
97
+ repeated `--schema` / `--exclude` options.
98
+
99
+ ## Exit codes
100
+
101
+ | verb | 0 | 1 | 2 | 3 |
102
+ | --- | --- | --- | --- | --- |
103
+ | `diff` | always | | | |
104
+ | `check` | clean | | drift | destructive drift |
105
+ | `push` | applied | destructive blocked | error (incl. partial failure) | |
106
+
107
+ `push --safe-only` runs only safe operations and skips the rest
108
+ informationally (exit `0`). A failed `CREATE INDEX CONCURRENTLY` marks the
109
+ run as partial failure (exit `2`) instead of silently half-applying.
110
+
111
+ ## FastAPI / SQLModel: replace `create_all`
112
+
113
+ ```python
114
+ from contextlib import asynccontextmanager
115
+ from sqlpush import aensure_schema
116
+
117
+
118
+ @asynccontextmanager
119
+ async def lifespan(app):
120
+ await aensure_schema(SQLModel.metadata, engine, mode="check")
121
+ yield
122
+ ```
123
+
124
+ Push in the deploy pipeline, check at startup.
125
+
126
+ ## How it works
127
+
128
+ ```mermaid
129
+ flowchart LR
130
+ models["SQLAlchemy MetaData"] --> diff["diff<br>alembic autogenerate, scoped"]
131
+ db[("live PostgreSQL")] --> diff
132
+ diff --> risk["risk classification<br>safe / risky / destructive"]
133
+ risk --> plan["plan"]
134
+ plan --> render["render"]
135
+ render --> apply["apply<br>atomic txn · CONCURRENTLY split · advisory lock"]
136
+ apply --> report["report"]
137
+ ```
138
+
139
+ - **Diff engine** scopes reflection to your target schemas (default: the
140
+ session's real `search_path`) and prunes system catalogs (TimescaleDB
141
+ internals included) before reflection even starts.
142
+ - **Classifier** maps each operation to a risk class; unknown operations
143
+ are `risky`, never silently safe.
144
+ - **Executor** splits the plan: `CONCURRENTLY` statements run one-per-
145
+ transaction on autocommit, everything else applies in a single atomic
146
+ transaction with a bounded `lock_timeout`.
147
+ - **Typed errors**: only `SqlpushError` / `ConnectFailed` /
148
+ `MetadataImportError` escape the API, never raw driver exceptions.
149
+
150
+ ## Comparison
151
+
152
+ An honest view of the neighborhood (stars as of 2026-08):
153
+
154
+ | | migration files | source of truth | risk gate | CI drift exit codes | TimescaleDB |
155
+ | --- | --- | --- | --- | --- | --- |
156
+ | **sqlpush** | none (the diff is the migration) | SQLAlchemy `MetaData` | classified safe/risky/destructive, destructive blocked by default | `check` 0/2/3 | `@hypertable` directives |
157
+ | [alembic](https://github.com/sqlalchemy/alembic) (4.4k★) | yes | migration scripts (autogenerate assists) | no | no | no |
158
+ | [atlas](https://github.com/ariga/atlas) (8.7k★) | optional (HCL) | HCL / SQL (ORMs via providers) | lint policies | yes | no |
159
+ | [prisma `db push`](https://www.prisma.io/docs/orm/reference/prisma-cli-reference) (47k★) | none | Prisma schema (Node/TS) | no | no | no |
160
+ | [migra](https://github.com/djrobstep/migra) (3.1k★) | diff only | SQL | n/a | partial | no (*deprecated*) |
161
+
162
+ sqlpush is narrower than atlas and younger than alembic, deliberately.
163
+ It is one tool for one job: keep a PostgreSQL schema in lockstep with
164
+ SQLAlchemy models, safely enough to run from CI.
165
+
166
+ Coming from [migra](https://github.com/djrobstep/migra) (now
167
+ deprecated)? There is a [migration guide](docs/migrating-from-migra.md).
168
+
169
+ ## Design notes
170
+
171
+ - `import sqlpush` stays light: the public API loads lazily, so the
172
+ annotations module carries none of alembic/typer/psycopg.
173
+ - The advisory-lock key derives from the database OID: two DSN spellings
174
+ of the same database contend for the same lock.
175
+ - `--json` output is a versioned contract (`"version": 1`) meant for
176
+ tooling; additive changes only within a version.
177
+
178
+ ## Roadmap (0.1.x)
179
+
180
+ - `CREATE INDEX CONCURRENTLY` by default for indexes on existing tables
181
+ - asyncpg DSN translation in `ensure_schema(AsyncEngine)`
182
+ - jsonschema-validated `--json` output
183
+
184
+ ## License
185
+
186
+ [MIT](LICENSE) · © 2026 Juan Miguel Contreras
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "sqlpush"
3
+ version = "0.1.0"
4
+ description = "Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ keywords = [
9
+ "sqlalchemy",
10
+ "alembic",
11
+ "postgresql",
12
+ "timescaledb",
13
+ "prisma",
14
+ "schema",
15
+ "migrations",
16
+ "database",
17
+ "drift",
18
+ "cli",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Environment :: Console",
23
+ "Intended Audience :: Developers",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Database",
30
+ "Topic :: Software Development :: Quality Assurance",
31
+ ]
32
+ dependencies = [
33
+ "alembic>=1.18,<2",
34
+ "psycopg[binary]>=3.2",
35
+ "sqlalchemy>=2.0",
36
+ "typer>=0.12",
37
+ ]
38
+
39
+ [[project.authors]]
40
+ name = "Juan Miguel Contreras"
41
+ email = "19253629+juanmicl@users.noreply.github.com"
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/juanmicl/sqlpush"
45
+ Repository = "https://github.com/juanmicl/sqlpush"
46
+ Issues = "https://github.com/juanmicl/sqlpush/issues"
47
+ Changelog = "https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md"
48
+
49
+ [project.scripts]
50
+ sqlpush = "sqlpush.cli:main"
51
+
52
+ [build-system]
53
+ requires = ["uv_build>=0.12.7,<0.13.0"]
54
+ build-backend = "uv_build"
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "jsonschema>=4.26.0",
59
+ "pytest>=9.1.1",
60
+ "pytest-asyncio>=1.4.0",
61
+ "ruff>=0.16.5",
62
+ "ty==0.0.42",
63
+ ]
64
+
65
+ [tool.ty.src]
66
+ include = [
67
+ "src",
68
+ "tests",
69
+ ]
70
+
71
+ [tool.ty.environment]
72
+ python-version = "3.10"
73
+
74
+ [tool.ruff]
75
+ line-length = 100
76
+ target-version = "py310"
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+ asyncio_mode = "auto"
@@ -0,0 +1,78 @@
1
+ [project]
2
+ name = "sqlpush"
3
+ version = "0.1.0"
4
+ description = "Schema lifecycle tool for production PostgreSQL/TimescaleDB on SQLAlchemy 2.0 (SQLModel included): diff, push and check schema drift straight from your models"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Juan Miguel Contreras", email = "19253629+juanmicl@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ keywords = [
12
+ "sqlalchemy",
13
+ "alembic",
14
+ "postgresql",
15
+ "timescaledb",
16
+ "prisma",
17
+ "schema",
18
+ "migrations",
19
+ "database",
20
+ "drift",
21
+ "cli",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Developers",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Database",
33
+ "Topic :: Software Development :: Quality Assurance",
34
+ ]
35
+ dependencies = [
36
+ "alembic>=1.18,<2",
37
+ "psycopg[binary]>=3.2",
38
+ "sqlalchemy>=2.0",
39
+ "typer>=0.12",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/juanmicl/sqlpush"
44
+ Repository = "https://github.com/juanmicl/sqlpush"
45
+ Issues = "https://github.com/juanmicl/sqlpush/issues"
46
+ Changelog = "https://github.com/juanmicl/sqlpush/blob/main/CHANGELOG.md"
47
+
48
+ [project.scripts]
49
+ sqlpush = "sqlpush.cli:main"
50
+
51
+ [build-system]
52
+ requires = ["uv_build>=0.12.7,<0.13.0"]
53
+ build-backend = "uv_build"
54
+
55
+ [dependency-groups]
56
+ dev = [
57
+ "jsonschema>=4.26.0",
58
+ "pytest>=9.1.1",
59
+ "pytest-asyncio>=1.4.0",
60
+ "ruff>=0.16.5",
61
+ "ty==0.0.42",
62
+ ]
63
+
64
+ [tool.ty.src]
65
+ include = ["src", "tests"]
66
+
67
+ [tool.ty.environment]
68
+ # match requires-python floor: the checker must never accept syntax
69
+ # or stdlib APIs the oldest supported interpreter lacks
70
+ python-version = "3.10"
71
+
72
+ [tool.ruff]
73
+ line-length = 100
74
+ target-version = "py310"
75
+
76
+ [tool.pytest.ini_options]
77
+ testpaths = ["tests"]
78
+ asyncio_mode = "auto"
@@ -0,0 +1,78 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from sqlpush.types import (
4
+ AppliedOperation,
5
+ CheckResult,
6
+ ConnectFailed,
7
+ DestructiveBlocked,
8
+ MetadataImportError,
9
+ Plan,
10
+ PlannedOperation,
11
+ Report,
12
+ RiskClass,
13
+ SqlpushError,
14
+ )
15
+
16
+ # Static-checker visibility for the PEP 562 lazy exports below: this
17
+ # block never executes at runtime, so the light-import guarantee is
18
+ # untouched (tests/test_annotations.py still guards it).
19
+ if TYPE_CHECKING:
20
+ from sqlpush.api import (
21
+ acheck,
22
+ aensure_schema,
23
+ aplan,
24
+ apush,
25
+ check,
26
+ ensure_schema,
27
+ plan,
28
+ push,
29
+ )
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ # Public API (plan/push/check/ensure_schema + async facade) is exported
34
+ # lazily via PEP 562: an eager import would pull alembic (through
35
+ # sqlpush.core.diff) into every `import sqlpush.*`, breaking the
36
+ # light-import guarantee of the annotations module (see
37
+ # tests/test_annotations.py::test_annotations_module_has_no_heavy_imports).
38
+ _LAZY_API = (
39
+ "plan",
40
+ "push",
41
+ "check",
42
+ "ensure_schema",
43
+ "aplan",
44
+ "apush",
45
+ "acheck",
46
+ "aensure_schema",
47
+ )
48
+
49
+
50
+ def __getattr__(name: str) -> Any:
51
+ if name in _LAZY_API:
52
+ from sqlpush import api
53
+
54
+ return getattr(api, name)
55
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
56
+
57
+
58
+ __all__ = [
59
+ "AppliedOperation",
60
+ "CheckResult",
61
+ "ConnectFailed",
62
+ "DestructiveBlocked",
63
+ "MetadataImportError",
64
+ "Plan",
65
+ "PlannedOperation",
66
+ "Report",
67
+ "RiskClass",
68
+ "SqlpushError",
69
+ "__version__",
70
+ "acheck",
71
+ "aensure_schema",
72
+ "aplan",
73
+ "apush",
74
+ "check",
75
+ "ensure_schema",
76
+ "plan",
77
+ "push",
78
+ ]
@@ -0,0 +1,37 @@
1
+ # src/sqlpush/annotations.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ HYPERTABLE_KEY = "sqlpush_hypertable"
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class HypertableInfo:
11
+ time_column: str
12
+ chunk_time_interval: str | None = None
13
+
14
+
15
+ def hypertable(*, time_column: str, chunk_time_interval: str | None = None):
16
+ """Record hypertable intent on the model's Table (MetaData level).
17
+
18
+ Works with SQLModel, Flask-SQLAlchemy and plain declarative, anything
19
+ whose class already carries a built ``__table__`` when decorated.
20
+
21
+ The generated ``create_hypertable`` runs with
22
+ ``create_default_indexes => false``: timescale's implicit time-column
23
+ index is invisible to metadata and would drift forever. Declare it
24
+ yourself (``Index(..., "<time_column>")`` on the model) if you rely
25
+ on it for time-range scans.
26
+ """
27
+
28
+ def decorator(cls):
29
+ table = getattr(cls, "__table__", None)
30
+ if table is None:
31
+ raise TypeError(f"{cls.__name__} has no __table__; decorate a mapped class")
32
+ table.info[HYPERTABLE_KEY] = HypertableInfo(
33
+ time_column=time_column, chunk_time_interval=chunk_time_interval
34
+ )
35
+ return cls
36
+
37
+ return decorator