alembic-squawk 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,61 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: alembic-squawk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Split Alembic migrations into per-revision SQL files for linting with Squawk.
|
|
5
|
+
Keywords: alembic,squawk,sql-linter,migration
|
|
6
|
+
Author: Michael Bianco
|
|
7
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
+
Requires-Dist: click>=8.3.3
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Project-URL: Repository, https://github.com/iloveitaly/alembic-squawk
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
[](https://github.com/iloveitaly/alembic-squawk/releases)
|
|
14
|
+
[](https://pepy.tech/project/alembic-squawk)
|
|
15
|
+

|
|
16
|
+
[](https://opensource.org/licenses/MIT)
|
|
17
|
+
|
|
18
|
+
# Split Alembic Migrations into Per-Revision SQL Files
|
|
19
|
+
|
|
20
|
+
`alembic-squawk` runs Alembic in offline (SQL) mode and splits the output into one `.sql` file per migration revision. The primary use case is feeding those files to [Squawk](https://github.com/sbdchd/squawk) so lint failures point to a specific migration rather than a monolithic dump.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Strips the outer `BEGIN`/`COMMIT` Alembic wraps the full dump in, so each file contains only its own migration SQL. Pass `--assume-in-transaction` to Squawk when linting.
|
|
25
|
+
- Maps each revision back to its source `.py` file to preserve the original filename in output.
|
|
26
|
+
- Reads `# -- squawk-ignore-file <rule>` Python comments from migration files and prepends them as SQL directives in the generated output, so you can attach Squawk ignores without embedding raw SQL comments in Python source.
|
|
27
|
+
- Supports filtering by revision range or creation date to narrow linting to recently added migrations.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add alembic-squawk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Run from your project root (where `alembic.ini` lives):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
alembic-squawk --out-dir /tmp/alembic_sql
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The output directory is wiped and recreated on every run.
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
- `--out-dir` (required): Directory to write SQL files into.
|
|
48
|
+
- `--revisions`: Alembic revision range. Defaults to `base:head`. Accepts any valid range, e.g. `origin/main@HEAD:head` to dump only migrations not yet on main.
|
|
49
|
+
- `--created-after YYYY-MM-DD`: Skip migrations created before this date.
|
|
50
|
+
- `--lint-preamble`: Also write the Alembic preamble (boilerplate `SET` statements) to `000_preamble.sql`. Skipped by default.
|
|
51
|
+
- `--verbose`: Print the Alembic command and any stderr output.
|
|
52
|
+
|
|
53
|
+
Each file is named after its migration source file, e.g. `2026_03_16_d4b9da2f705c_add_users.sql`.
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
[iloveitaly/python-starter-template](https://github.com/iloveitaly/python-starter-template) is a full Python app with a working integration of `alembic-squawk`.
|
|
58
|
+
|
|
59
|
+
## [MIT License](LICENSE.md)
|
|
60
|
+
|
|
61
|
+
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[](https://github.com/iloveitaly/alembic-squawk/releases)
|
|
2
|
+
[](https://pepy.tech/project/alembic-squawk)
|
|
3
|
+

|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
# Split Alembic Migrations into Per-Revision SQL Files
|
|
7
|
+
|
|
8
|
+
`alembic-squawk` runs Alembic in offline (SQL) mode and splits the output into one `.sql` file per migration revision. The primary use case is feeding those files to [Squawk](https://github.com/sbdchd/squawk) so lint failures point to a specific migration rather than a monolithic dump.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Strips the outer `BEGIN`/`COMMIT` Alembic wraps the full dump in, so each file contains only its own migration SQL. Pass `--assume-in-transaction` to Squawk when linting.
|
|
13
|
+
- Maps each revision back to its source `.py` file to preserve the original filename in output.
|
|
14
|
+
- Reads `# -- squawk-ignore-file <rule>` Python comments from migration files and prepends them as SQL directives in the generated output, so you can attach Squawk ignores without embedding raw SQL comments in Python source.
|
|
15
|
+
- Supports filtering by revision range or creation date to narrow linting to recently added migrations.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv add alembic-squawk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Run from your project root (where `alembic.ini` lives):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
alembic-squawk --out-dir /tmp/alembic_sql
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The output directory is wiped and recreated on every run.
|
|
32
|
+
|
|
33
|
+
### Options
|
|
34
|
+
|
|
35
|
+
- `--out-dir` (required): Directory to write SQL files into.
|
|
36
|
+
- `--revisions`: Alembic revision range. Defaults to `base:head`. Accepts any valid range, e.g. `origin/main@HEAD:head` to dump only migrations not yet on main.
|
|
37
|
+
- `--created-after YYYY-MM-DD`: Skip migrations created before this date.
|
|
38
|
+
- `--lint-preamble`: Also write the Alembic preamble (boilerplate `SET` statements) to `000_preamble.sql`. Skipped by default.
|
|
39
|
+
- `--verbose`: Print the Alembic command and any stderr output.
|
|
40
|
+
|
|
41
|
+
Each file is named after its migration source file, e.g. `2026_03_16_d4b9da2f705c_add_users.sql`.
|
|
42
|
+
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
[iloveitaly/python-starter-template](https://github.com/iloveitaly/python-starter-template) is a full Python app with a working integration of `alembic-squawk`.
|
|
46
|
+
|
|
47
|
+
## [MIT License](LICENSE.md)
|
|
48
|
+
|
|
49
|
+
*This project was created from [iloveitaly/python-package-template](https://github.com/iloveitaly/python-package-template)*
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alembic SQL Dumper
|
|
3
|
+
|
|
4
|
+
This script generates SQL from Alembic in offline mode and splits the output into
|
|
5
|
+
per-revision SQL files, one file per migration. This makes it easier to run SQL
|
|
6
|
+
linters (e.g. Squawk) against individual migrations rather than the entire dump,
|
|
7
|
+
so lint failures map directly to a specific revision file.
|
|
8
|
+
|
|
9
|
+
Usage
|
|
10
|
+
-----
|
|
11
|
+
The script requires a --out-dir argument specifying where to write the SQL files.
|
|
12
|
+
The directory is wiped and recreated on every run so stale artifacts from previous
|
|
13
|
+
runs are never mixed with current output.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
alembic-squawk --out-dir /tmp/alembic_sql
|
|
18
|
+
|
|
19
|
+
Options
|
|
20
|
+
-------
|
|
21
|
+
--revisions
|
|
22
|
+
The Alembic revision range to dump. Defaults to "base:head" (all migrations).
|
|
23
|
+
Supports any valid Alembic range, e.g. "origin/main@HEAD:head" to dump only
|
|
24
|
+
migrations not yet on main.
|
|
25
|
+
|
|
26
|
+
--out-dir (required)
|
|
27
|
+
Directory to write SQL files into. Wiped and recreated on each run.
|
|
28
|
+
|
|
29
|
+
--created-after
|
|
30
|
+
Filter to only write migrations created on or after a given date (YYYY-MM-DD).
|
|
31
|
+
Useful for narrowing output to recently added migrations.
|
|
32
|
+
|
|
33
|
+
--lint-preamble
|
|
34
|
+
By default, the SQL preamble emitted by Alembic before the first revision
|
|
35
|
+
marker is skipped. The preamble contains boilerplate SET statements and is
|
|
36
|
+
not associated with any specific migration. Pass this flag to write it to a
|
|
37
|
+
file named "000_preamble.sql" as well.
|
|
38
|
+
|
|
39
|
+
--verbose
|
|
40
|
+
Prints the raw Alembic command being run and any stderr output.
|
|
41
|
+
|
|
42
|
+
Output format
|
|
43
|
+
-------------
|
|
44
|
+
Each migration is written as a single .sql file named after the migration file
|
|
45
|
+
(e.g. "2026_03_16_d4b9da2f705c_move_constants_to_distribution_model.sql").
|
|
46
|
+
|
|
47
|
+
The outer BEGIN/COMMIT that Alembic wraps the entire dump in is stripped before
|
|
48
|
+
splitting so the per-file output contains only the migration's own SQL. Files are
|
|
49
|
+
not re-wrapped in their own transaction — pass --assume-in-transaction to Squawk
|
|
50
|
+
so it treats each file as already inside a transaction (required for linting
|
|
51
|
+
statements that are only valid within a transaction, e.g. ALTER TYPE).
|
|
52
|
+
|
|
53
|
+
Offline mode and data migrations
|
|
54
|
+
---------------------------------
|
|
55
|
+
Alembic's --sql mode runs migrations against a MockConnection rather than a real
|
|
56
|
+
database. Any migration that performs data migrations (ORM queries, raw SQL
|
|
57
|
+
selects/updates) must guard those code paths using the is_offline_migration()
|
|
58
|
+
helper from migrations/utils.py, which checks op.get_context().as_sql. Only DDL
|
|
59
|
+
operations (CREATE TABLE, ALTER TABLE, DROP COLUMN, etc.) are emitted in offline
|
|
60
|
+
mode — data migration logic is silently skipped.
|
|
61
|
+
|
|
62
|
+
LOG_LEVEL is set to CRITICAL when invoking Alembic to suppress noisy application
|
|
63
|
+
log output that would otherwise contaminate the SQL stdout that this script parses.
|
|
64
|
+
|
|
65
|
+
Squawk ignore directives
|
|
66
|
+
------------------------
|
|
67
|
+
Squawk supports per-file ignore directives in SQL comments:
|
|
68
|
+
|
|
69
|
+
-- squawk-ignore-file ban-drop-column
|
|
70
|
+
|
|
71
|
+
To attach these to a migration without embedding raw SQL comments in Python source,
|
|
72
|
+
write the directive as a Python comment with a -- prefix:
|
|
73
|
+
|
|
74
|
+
# -- squawk-ignore-file ban-drop-column
|
|
75
|
+
|
|
76
|
+
The script scans each migration's source file for these comments and prepends the
|
|
77
|
+
corresponding SQL directives to the generated .sql file so Squawk honours them.
|
|
78
|
+
Note: directives written inside docstrings (without a leading #) are not detected.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
import os
|
|
82
|
+
import re
|
|
83
|
+
import shutil
|
|
84
|
+
import subprocess
|
|
85
|
+
from dataclasses import dataclass, field
|
|
86
|
+
from datetime import date, datetime
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
|
|
89
|
+
import click
|
|
90
|
+
|
|
91
|
+
# Matches the Alembic progress comment that appears at the start of each
|
|
92
|
+
# revision block in --sql output, e.g.:
|
|
93
|
+
# -- Running upgrade abc123 -> def456
|
|
94
|
+
# Captures the destination revision ID (the "head" side of the arrow).
|
|
95
|
+
REVISION_RE = re.compile(r"^--\s+Running upgrade .*?->\s+([^\s]+)")
|
|
96
|
+
|
|
97
|
+
# Matches the revision ID assignment in a migration file, handling both plain
|
|
98
|
+
# and annotated forms:
|
|
99
|
+
# revision = 'abc123'
|
|
100
|
+
# revision: str = 'abc123'
|
|
101
|
+
REVISION_ID_RE = re.compile(
|
|
102
|
+
r"^revision(?::\s*str)?\s*=\s*['\"]([^'\"]+)['\"]", re.MULTILINE
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Matches the "Create Date:" line written by Alembic into each migration file's
|
|
106
|
+
# header comment, e.g.:
|
|
107
|
+
# # Create Date: 2026-03-16 10:22:01.123456
|
|
108
|
+
# Captures the date portion only (YYYY-MM-DD).
|
|
109
|
+
CREATE_DATE_RE = re.compile(r"^Create Date: (\d{4}-\d{2}-\d{2})", re.MULTILINE)
|
|
110
|
+
|
|
111
|
+
# Matches squawk-ignore-file directives embedded as Python comments in migration
|
|
112
|
+
# source files, e.g.:
|
|
113
|
+
# # -- squawk-ignore-file ban-drop-column
|
|
114
|
+
# These are extracted and re-emitted as plain SQL comments in the generated
|
|
115
|
+
# output so Squawk honours them when linting the split files.
|
|
116
|
+
SQUAWK_IGNORE_RE = re.compile(r"^#\s*(--\s*squawk-ignore-file\s+\S+)", re.MULTILINE)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class SqlChunk:
|
|
121
|
+
name: str
|
|
122
|
+
created_date: date | None = None
|
|
123
|
+
squawk_ignores: list[str] = field(default_factory=list)
|
|
124
|
+
lines: list[str] = field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def has_content(self) -> bool:
|
|
128
|
+
"""Return True if this chunk contains any non-blank lines."""
|
|
129
|
+
return any(line.strip() for line in self.lines)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_preamble(self) -> bool:
|
|
133
|
+
return self.name == "_preamble"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_dependencies() -> None:
|
|
137
|
+
"""Ensure required CLI tools are available in PATH."""
|
|
138
|
+
missing = [tool for tool in ("alembic",) if not shutil.which(tool)]
|
|
139
|
+
if missing:
|
|
140
|
+
raise click.ClickException(
|
|
141
|
+
"Missing required executables in PATH: " + ", ".join(missing)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def generate_alembic_sql(revisions: str, verbose: bool = False) -> str:
|
|
146
|
+
"""Run Alembic in offline mode to generate raw SQL."""
|
|
147
|
+
cmd = ["alembic", "upgrade", revisions, "--sql"]
|
|
148
|
+
click.secho(f"Generating Alembic SQL dump for {revisions}...", fg="blue", bold=True)
|
|
149
|
+
|
|
150
|
+
if verbose:
|
|
151
|
+
click.echo(f"$ {' '.join(cmd)}")
|
|
152
|
+
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
cmd,
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
check=False,
|
|
158
|
+
env={**os.environ, "LOG_LEVEL": "CRITICAL"},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if result.returncode != 0:
|
|
162
|
+
stderr = result.stderr.strip()
|
|
163
|
+
message = f"Alembic failed with exit code {result.returncode}."
|
|
164
|
+
if stderr:
|
|
165
|
+
message += f"\n\n{stderr}"
|
|
166
|
+
raise click.ClickException(message)
|
|
167
|
+
|
|
168
|
+
if verbose and result.stderr.strip():
|
|
169
|
+
click.secho("Alembic stderr:", fg="yellow", bold=True)
|
|
170
|
+
click.echo(result.stderr)
|
|
171
|
+
|
|
172
|
+
return result.stdout
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_revision_map() -> dict[str, tuple[str, date | None, list[str]]]:
|
|
176
|
+
versions_dir = Path("migrations/versions")
|
|
177
|
+
mapping = {}
|
|
178
|
+
if not versions_dir.exists():
|
|
179
|
+
return mapping
|
|
180
|
+
for py_file in versions_dir.glob("*.py"):
|
|
181
|
+
try:
|
|
182
|
+
content = py_file.read_text(encoding="utf-8")
|
|
183
|
+
rev_match = REVISION_ID_RE.search(content)
|
|
184
|
+
if rev_match:
|
|
185
|
+
rev_id = rev_match.group(1)
|
|
186
|
+
chunk_name = py_file.stem
|
|
187
|
+
|
|
188
|
+
date_match = CREATE_DATE_RE.search(content)
|
|
189
|
+
created_date = (
|
|
190
|
+
date.fromisoformat(date_match.group(1)) if date_match else None
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
squawk_ignores = SQUAWK_IGNORE_RE.findall(content)
|
|
194
|
+
|
|
195
|
+
mapping[rev_id] = (chunk_name, created_date, squawk_ignores)
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
return mapping
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def split_sql_dump(
|
|
202
|
+
sql_dump: str, revision_map: dict[str, tuple[str, date | None, list[str]]]
|
|
203
|
+
) -> list[SqlChunk]:
|
|
204
|
+
"""
|
|
205
|
+
Split a single Alembic SQL dump into ordered per-revision chunks.
|
|
206
|
+
|
|
207
|
+
Any content before the first '-- Running upgrade ...' marker is placed into
|
|
208
|
+
a special '_preamble' chunk.
|
|
209
|
+
"""
|
|
210
|
+
lines = sql_dump.splitlines()
|
|
211
|
+
|
|
212
|
+
# Remove the first BEGIN; to avoid unbalanced transactions when split
|
|
213
|
+
for i, line in enumerate(lines):
|
|
214
|
+
if line.strip() == "BEGIN;":
|
|
215
|
+
lines.pop(i)
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
# Remove the last COMMIT;
|
|
219
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
220
|
+
if lines[i].strip() == "COMMIT;":
|
|
221
|
+
lines.pop(i)
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
chunks: list[SqlChunk] = [SqlChunk("_preamble")]
|
|
225
|
+
current_chunk = chunks[0]
|
|
226
|
+
|
|
227
|
+
for line in lines:
|
|
228
|
+
match = REVISION_RE.search(line)
|
|
229
|
+
if match:
|
|
230
|
+
rev_id = match.group(1)
|
|
231
|
+
if rev_id in revision_map:
|
|
232
|
+
chunk_name, created_date, squawk_ignores = revision_map[rev_id]
|
|
233
|
+
else:
|
|
234
|
+
chunk_name, created_date, squawk_ignores = rev_id, None, []
|
|
235
|
+
|
|
236
|
+
current_chunk = SqlChunk(
|
|
237
|
+
name=chunk_name,
|
|
238
|
+
created_date=created_date,
|
|
239
|
+
squawk_ignores=squawk_ignores,
|
|
240
|
+
)
|
|
241
|
+
chunks.append(current_chunk)
|
|
242
|
+
current_chunk.lines.append(line)
|
|
243
|
+
|
|
244
|
+
return [chunk for chunk in chunks if chunk.has_content]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def write_chunks(
|
|
248
|
+
chunks: list[SqlChunk],
|
|
249
|
+
out_dir: Path,
|
|
250
|
+
*,
|
|
251
|
+
skip_preamble: bool = True,
|
|
252
|
+
created_after: date | None = None,
|
|
253
|
+
) -> tuple[list[Path], int]:
|
|
254
|
+
"""Write SQL chunks to disk and return the created file paths."""
|
|
255
|
+
writable = [c for c in chunks if not (skip_preamble and c.is_preamble)]
|
|
256
|
+
if not writable:
|
|
257
|
+
return ([], 0)
|
|
258
|
+
|
|
259
|
+
written_files: list[Path] = []
|
|
260
|
+
|
|
261
|
+
skipped = 0
|
|
262
|
+
for chunk in writable:
|
|
263
|
+
if created_after and chunk.created_date and chunk.created_date < created_after:
|
|
264
|
+
skipped += 1
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
if chunk.name == "_preamble":
|
|
268
|
+
filename = "000_preamble.sql"
|
|
269
|
+
else:
|
|
270
|
+
filename = f"{chunk.name}.sql"
|
|
271
|
+
|
|
272
|
+
file_path = out_dir / filename
|
|
273
|
+
date_comment = (
|
|
274
|
+
f"-- Created at: {chunk.created_date}\n" if chunk.created_date else ""
|
|
275
|
+
)
|
|
276
|
+
ignore_comments = "".join(f"{d}\n" for d in chunk.squawk_ignores)
|
|
277
|
+
chunk_content = date_comment + ignore_comments + "\n".join(chunk.lines) + "\n"
|
|
278
|
+
file_path.write_text(chunk_content, encoding="utf-8")
|
|
279
|
+
written_files.append(file_path)
|
|
280
|
+
|
|
281
|
+
return written_files, skipped
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@click.command()
|
|
285
|
+
@click.option(
|
|
286
|
+
"--revisions",
|
|
287
|
+
default="base:head",
|
|
288
|
+
envvar="ALEMBIC_SQUAWK_REVISIONS",
|
|
289
|
+
show_envvar=True,
|
|
290
|
+
help="Alembic revision range to dump (e.g. base:head, origin/main@HEAD:head).",
|
|
291
|
+
)
|
|
292
|
+
@click.option(
|
|
293
|
+
"--out-dir",
|
|
294
|
+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
|
|
295
|
+
required=True,
|
|
296
|
+
help="Output directory for generated SQL files.",
|
|
297
|
+
)
|
|
298
|
+
@click.option(
|
|
299
|
+
"--created-after",
|
|
300
|
+
type=click.DateTime(formats=["%Y-%m-%d"]),
|
|
301
|
+
help="Only output migrations created on or after this date (YYYY-MM-DD).",
|
|
302
|
+
)
|
|
303
|
+
@click.option(
|
|
304
|
+
"--lint-preamble",
|
|
305
|
+
is_flag=True,
|
|
306
|
+
help="Also output the SQL preamble emitted before the first revision marker.",
|
|
307
|
+
)
|
|
308
|
+
@click.option(
|
|
309
|
+
"--verbose",
|
|
310
|
+
is_flag=True,
|
|
311
|
+
help="Print executed commands and additional diagnostic output.",
|
|
312
|
+
)
|
|
313
|
+
def main(
|
|
314
|
+
revisions: str,
|
|
315
|
+
out_dir: Path,
|
|
316
|
+
created_after: datetime | None,
|
|
317
|
+
lint_preamble: bool,
|
|
318
|
+
verbose: bool,
|
|
319
|
+
) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Extracts raw SQL from Alembic, splits it by revision, and saves each file individually.
|
|
322
|
+
"""
|
|
323
|
+
click.secho("Starting Alembic SQL Dumper...\n", bold=True)
|
|
324
|
+
|
|
325
|
+
check_dependencies()
|
|
326
|
+
revision_map = build_revision_map()
|
|
327
|
+
sql_dump = generate_alembic_sql(revisions, verbose=verbose)
|
|
328
|
+
chunks = split_sql_dump(sql_dump, revision_map)
|
|
329
|
+
|
|
330
|
+
if out_dir.exists():
|
|
331
|
+
shutil.rmtree(out_dir)
|
|
332
|
+
out_dir.mkdir(parents=True)
|
|
333
|
+
|
|
334
|
+
created_after_date = created_after.date() if created_after else None
|
|
335
|
+
sql_files, skipped = write_chunks(
|
|
336
|
+
chunks,
|
|
337
|
+
out_dir,
|
|
338
|
+
skip_preamble=not lint_preamble,
|
|
339
|
+
created_after=created_after_date,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
msg = f"Wrote {len(sql_files)} revision file(s) to {out_dir}/"
|
|
343
|
+
if skipped:
|
|
344
|
+
msg += f" ({skipped} skipped by --created-after filter)"
|
|
345
|
+
click.secho(msg, fg="green")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
if __name__ == "__main__":
|
|
349
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Version handling for alembic-squawk."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_local_source_checkout() -> bool:
|
|
8
|
+
"""Check if the code is running from a local source checkout."""
|
|
9
|
+
package_dir = Path(__file__).resolve().parent
|
|
10
|
+
# Since this is a flat layout (module-root = ""), repo root is the parent of the package dir
|
|
11
|
+
repo_root = package_dir.parent
|
|
12
|
+
|
|
13
|
+
return (repo_root / ".git").exists() and (repo_root / "pyproject.toml").exists()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_version() -> str:
|
|
17
|
+
"""Get the version string, appending .dev if running from source."""
|
|
18
|
+
try:
|
|
19
|
+
# Try to get the version of the installed package
|
|
20
|
+
version = importlib.metadata.version("alembic-squawk")
|
|
21
|
+
except importlib.metadata.PackageNotFoundError:
|
|
22
|
+
# Fallback for local development if not installed
|
|
23
|
+
version = "0.1.0"
|
|
24
|
+
|
|
25
|
+
if not is_local_source_checkout():
|
|
26
|
+
return version
|
|
27
|
+
|
|
28
|
+
if version.endswith(".dev"):
|
|
29
|
+
return version
|
|
30
|
+
|
|
31
|
+
return f"{version}.dev"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = get_version()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "alembic-squawk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Split Alembic migrations into per-revision SQL files for linting with Squawk."
|
|
5
|
+
keywords = ["alembic", "squawk", "sql-linter", "migration"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"click>=8.3.3",
|
|
10
|
+
]
|
|
11
|
+
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
12
|
+
urls = { "Repository" = "https://github.com/iloveitaly/alembic-squawk" }
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
|
16
|
+
[project.scripts]
|
|
17
|
+
alembic-squawk = "alembic_squawk.cli:main"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.11.0,<0.12"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[tool.uv.build-backend]
|
|
25
|
+
# avoids the src/ directory structure
|
|
26
|
+
module-root = ""
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.3.4",
|
|
31
|
+
"pyright[nodejs]>=1.1.408",
|
|
32
|
+
"ruff>=0.15.0",
|
|
33
|
+
"coverage>=7.13.4",
|
|
34
|
+
"pytest-cov>=7.0.0",
|
|
35
|
+
"covdefaults>=2.3.0",
|
|
36
|
+
"alembic>=1.18.4",
|
|
37
|
+
"sqlalchemy>=2.0.49",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.pyright]
|
|
41
|
+
exclude = ["examples/", "playground/", "tmp/", ".venv/", "tests/"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
|
|
45
|
+
|
|
46
|
+
[tool.coverage.run]
|
|
47
|
+
plugins = ["covdefaults"]
|
|
48
|
+
source = ["alembic_squawk"]
|
|
49
|
+
|
|
50
|
+
[tool.coverage.report]
|
|
51
|
+
fail_under = 50
|