sqlpush 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
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/cli.py +16 -6
- sqlpush/hook.py +86 -23
- {sqlpush-0.6.0.dist-info → sqlpush-0.7.0.dist-info}/METADATA +30 -13
- {sqlpush-0.6.0.dist-info → sqlpush-0.7.0.dist-info}/RECORD +6 -6
- {sqlpush-0.6.0.dist-info → sqlpush-0.7.0.dist-info}/WHEEL +0 -0
- {sqlpush-0.6.0.dist-info → sqlpush-0.7.0.dist-info}/entry_points.txt +0 -0
sqlpush/cli.py
CHANGED
|
@@ -42,6 +42,10 @@ ExcludeOpt = Annotated[list[str] | None, typer.Option("--exclude")]
|
|
|
42
42
|
# exempts typer.Option for non-Path annotations. None default: lets the
|
|
43
43
|
# verb tell "user passed --dir" apart from "use hook/default" resolution.
|
|
44
44
|
DirOpt = Annotated[Path | None, typer.Option("--dir")]
|
|
45
|
+
# Hook-location override, on EVERY verb (hook loading is shared
|
|
46
|
+
# machinery like --dsn): flag > $SQLPUSH_HOOK > candidates, and an
|
|
47
|
+
# explicit path that does not exist fails loud in the loader.
|
|
48
|
+
HookOpt = Annotated[Path | None, typer.Option("--hook")]
|
|
45
49
|
DEFAULT_CHAIN_DIR = Path("migrations/versions")
|
|
46
50
|
|
|
47
51
|
|
|
@@ -119,8 +123,9 @@ def diff(
|
|
|
119
123
|
quiet: bool = typer.Option(False, "--quiet"),
|
|
120
124
|
schema: SchemaOpt = None,
|
|
121
125
|
exclude: ExcludeOpt = None,
|
|
126
|
+
hook_path: HookOpt = None,
|
|
122
127
|
):
|
|
123
|
-
hook = load_project_hook()
|
|
128
|
+
hook = load_project_hook(hook_path)
|
|
124
129
|
md = _metadata(metadata_spec, hook)
|
|
125
130
|
engine = _engine(dsn, hook)
|
|
126
131
|
try:
|
|
@@ -145,8 +150,9 @@ def check(
|
|
|
145
150
|
quiet: bool = typer.Option(False, "--quiet"),
|
|
146
151
|
schema: SchemaOpt = None,
|
|
147
152
|
exclude: ExcludeOpt = None,
|
|
153
|
+
hook_path: HookOpt = None,
|
|
148
154
|
):
|
|
149
|
-
hook = load_project_hook()
|
|
155
|
+
hook = load_project_hook(hook_path)
|
|
150
156
|
md = _metadata(metadata_spec, hook)
|
|
151
157
|
engine = _engine(dsn, hook)
|
|
152
158
|
try:
|
|
@@ -187,8 +193,9 @@ def push(
|
|
|
187
193
|
quiet: bool = typer.Option(False, "--quiet"),
|
|
188
194
|
schema: SchemaOpt = None,
|
|
189
195
|
exclude: ExcludeOpt = None,
|
|
196
|
+
hook_path: HookOpt = None,
|
|
190
197
|
):
|
|
191
|
-
hook = load_project_hook()
|
|
198
|
+
hook = load_project_hook(hook_path)
|
|
192
199
|
md = _metadata(metadata_spec, hook)
|
|
193
200
|
engine = _engine(dsn, hook)
|
|
194
201
|
try:
|
|
@@ -264,9 +271,10 @@ def revision(
|
|
|
264
271
|
no_concurrently: bool = typer.Option(False, "--no-concurrently"),
|
|
265
272
|
schema: SchemaOpt = None,
|
|
266
273
|
exclude: ExcludeOpt = None,
|
|
274
|
+
hook_path: HookOpt = None,
|
|
267
275
|
):
|
|
268
276
|
"""Generate the next migration file from models vs the reference DB."""
|
|
269
|
-
hook = load_project_hook()
|
|
277
|
+
hook = load_project_hook(hook_path)
|
|
270
278
|
md = _metadata(metadata_spec, hook)
|
|
271
279
|
# --ref-dsn: flag > hook.get_dsn(); NO DATABASE_URL fallback (the
|
|
272
280
|
# reference DB is a different database from the push target —
|
|
@@ -305,9 +313,10 @@ def migrate(
|
|
|
305
313
|
lock_timeout: float = typer.Option(5.0, "--lock-timeout"),
|
|
306
314
|
statement_timeout: float | None = typer.Option(None, "--statement-timeout"),
|
|
307
315
|
out_dir: DirOpt = None,
|
|
316
|
+
hook_path: HookOpt = None,
|
|
308
317
|
):
|
|
309
318
|
"""Replay pending migration files (gates + checksum bookkeeping)."""
|
|
310
|
-
hook = load_project_hook()
|
|
319
|
+
hook = load_project_hook(hook_path)
|
|
311
320
|
engine = _engine(dsn, hook)
|
|
312
321
|
try:
|
|
313
322
|
report = api.migrate(
|
|
@@ -336,9 +345,10 @@ def stamp(
|
|
|
336
345
|
dsn: str | None = typer.Option(None),
|
|
337
346
|
force: bool = typer.Option(False, "--force"),
|
|
338
347
|
out_dir: DirOpt = None,
|
|
348
|
+
hook_path: HookOpt = None,
|
|
339
349
|
):
|
|
340
350
|
"""Adopt an existing DB: register chain files without executing SQL."""
|
|
341
|
-
hook = load_project_hook()
|
|
351
|
+
hook = load_project_hook(hook_path)
|
|
342
352
|
engine = _engine(dsn, hook)
|
|
343
353
|
try:
|
|
344
354
|
report = api.stamp(engine, chain_dir=_resolve_dir(out_dir, hook), force=force)
|
sqlpush/hook.py
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
# src/sqlpush/hook.py
|
|
2
|
-
"""Project hook: ``sqlpush.py``
|
|
3
|
-
|
|
4
|
-
The alembic ``env.py`` / pytest ``conftest.py`` pattern:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"""Project hook: ``migrations/sqlpush.py`` first, then ``sqlpush.py``.
|
|
3
|
+
|
|
4
|
+
The alembic ``env.py`` / pytest ``conftest.py`` pattern: a hook file
|
|
5
|
+
next to where the user runs the CLI becomes the source of defaults for
|
|
6
|
+
metadata, DSN and chain dir. Explicit flags always win (flag > hook >
|
|
7
|
+
env/default); without the hook every verb behaves exactly as before.
|
|
8
|
+
|
|
9
|
+
Discovery checks ``migrations/sqlpush.py`` first (the preferred
|
|
10
|
+
location — it lives next to the chain, no root clutter) and falls
|
|
11
|
+
back to the repo-root ``sqlpush.py`` (backwards compat); first match
|
|
12
|
+
wins. An explicit override — the ``--hook`` flag or the
|
|
13
|
+
``SQLPUSH_HOOK`` env var (the alembic ``-c`` equivalent) — names any
|
|
14
|
+
file instead, and is fail-loud: a path that does not exist raises a
|
|
15
|
+
:class:`HookError` naming that path, never a silent fallback to
|
|
16
|
+
the candidates. Precedence: flag > env > candidates.
|
|
9
17
|
|
|
10
18
|
Loading is BY PATH (``spec_from_file_location``), never by module
|
|
11
19
|
name: a file named ``sqlpush.py`` in the CWD would shadow the
|
|
12
20
|
installed package if the CWD came first on ``sys.path`` — so the CWD
|
|
13
|
-
is APPENDED (never ``insert(0)``),
|
|
21
|
+
is APPENDED (never ``insert(0)``), and always the CWD, never the
|
|
22
|
+
loaded file's own directory (the append exists so the consumer's
|
|
23
|
+
package imports resolve). This guarantees the package wins for
|
|
14
24
|
normal imports while the hook itself is only ever loaded explicitly.
|
|
15
25
|
|
|
16
26
|
Only typed :class:`SqlpushError`-family errors escape this module,
|
|
17
|
-
always naming the file and the member involved.
|
|
27
|
+
always naming the file that actually loaded and the member involved.
|
|
18
28
|
"""
|
|
19
29
|
|
|
20
30
|
from __future__ import annotations
|
|
@@ -28,54 +38,107 @@ from types import ModuleType
|
|
|
28
38
|
from sqlpush.types import SqlpushError
|
|
29
39
|
|
|
30
40
|
HOOK_FILENAME = "sqlpush.py"
|
|
41
|
+
HOOK_ENV_VAR = "SQLPUSH_HOOK"
|
|
42
|
+
# first match wins: migrations/ (next to the chain) preferred, the
|
|
43
|
+
# repo root kept as the backwards-compat fallback
|
|
44
|
+
CANDIDATES = (Path("migrations") / HOOK_FILENAME, Path(HOOK_FILENAME))
|
|
45
|
+
# resolved loaded path -> the spelling it was named with (candidate or
|
|
46
|
+
# override path): _hook_label uses it so override-loaded hooks error in
|
|
47
|
+
# the GIVEN spelling, matching the candidate message contract
|
|
48
|
+
_LOADED_LABELS: dict[Path, str] = {}
|
|
31
49
|
|
|
32
50
|
|
|
33
51
|
class HookError(SqlpushError):
|
|
34
52
|
"""The project hook is broken or incomplete — names file + member."""
|
|
35
53
|
|
|
36
54
|
|
|
37
|
-
def load_project_hook() -> ModuleType | None:
|
|
38
|
-
"""Load
|
|
55
|
+
def load_project_hook(hook_path: Path | str | None = None) -> ModuleType | None:
|
|
56
|
+
"""Load the project hook; ``None`` when nothing resolves.
|
|
39
57
|
|
|
40
|
-
|
|
41
|
-
the
|
|
42
|
-
|
|
58
|
+
Precedence: ``hook_path`` (the ``--hook`` flag) > ``$SQLPUSH_HOOK``
|
|
59
|
+
> the first existing candidate (``migrations/sqlpush.py``, then the
|
|
60
|
+
repo root). An explicit path (flag or env) is fail-loud: when it
|
|
61
|
+
does not exist a :class:`HookError` names that path — an
|
|
62
|
+
explicit instruction must not silently fall back to discovery.
|
|
63
|
+
Relative paths resolve against the CWD. On success the CWD is
|
|
64
|
+
appended to ``sys.path`` (never inserted at the front, and never
|
|
65
|
+
the loaded file's own directory — see the module docstring). Any
|
|
66
|
+
import-time failure is re-typed as :class:`HookError` naming the
|
|
67
|
+
file that failed, in the spelling it was named with.
|
|
43
68
|
"""
|
|
44
|
-
|
|
45
|
-
if not
|
|
69
|
+
explicit: Path | None = None
|
|
70
|
+
if hook_path is not None:
|
|
71
|
+
explicit = Path(hook_path)
|
|
72
|
+
else:
|
|
73
|
+
env_value = os.environ.get(HOOK_ENV_VAR)
|
|
74
|
+
if env_value: # empty string ≡ unset
|
|
75
|
+
explicit = Path(env_value)
|
|
76
|
+
if explicit is not None:
|
|
77
|
+
if not explicit.is_file():
|
|
78
|
+
raise HookError(f"{explicit.as_posix()}: file not found")
|
|
79
|
+
return _load_hook_file(explicit)
|
|
80
|
+
hook_file = next((p for p in CANDIDATES if p.is_file()), None)
|
|
81
|
+
if hook_file is None:
|
|
46
82
|
return None
|
|
83
|
+
return _load_hook_file(hook_file)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_hook_file(hook_file: Path) -> ModuleType:
|
|
47
87
|
sys.path.append(os.getcwd())
|
|
48
|
-
spec = importlib.util.spec_from_file_location("sqlpush_project_hook",
|
|
88
|
+
spec = importlib.util.spec_from_file_location("sqlpush_project_hook", hook_file)
|
|
49
89
|
if spec is None or spec.loader is None:
|
|
50
|
-
raise HookError(f"{
|
|
90
|
+
raise HookError(f"{hook_file.as_posix()}: cannot load (invalid module spec)")
|
|
51
91
|
module = importlib.util.module_from_spec(spec)
|
|
92
|
+
_LOADED_LABELS[hook_file.resolve()] = hook_file.as_posix()
|
|
52
93
|
try:
|
|
53
94
|
spec.loader.exec_module(module)
|
|
54
95
|
except Exception as exc:
|
|
55
|
-
raise HookError(f"{
|
|
96
|
+
raise HookError(f"{hook_file.as_posix()}: import raised: {exc}") from exc
|
|
56
97
|
return module
|
|
57
98
|
|
|
58
99
|
|
|
100
|
+
def _hook_label(hook: ModuleType) -> str:
|
|
101
|
+
"""The spelling the loaded hook file was named with (posix, portable).
|
|
102
|
+
|
|
103
|
+
Errors must name the file that actually loaded: map the loaded
|
|
104
|
+
module's ``__file__`` back to its load-time spelling (candidate or
|
|
105
|
+
override path); unknown provenance degrades to the bare filename.
|
|
106
|
+
"""
|
|
107
|
+
loaded_file = getattr(hook, "__file__", None)
|
|
108
|
+
if not loaded_file:
|
|
109
|
+
return HOOK_FILENAME
|
|
110
|
+
loaded = Path(loaded_file).resolve()
|
|
111
|
+
label = _LOADED_LABELS.get(loaded)
|
|
112
|
+
if label is not None:
|
|
113
|
+
return label
|
|
114
|
+
for candidate in CANDIDATES:
|
|
115
|
+
if (Path.cwd() / candidate).resolve() == loaded:
|
|
116
|
+
return candidate.as_posix()
|
|
117
|
+
return HOOK_FILENAME
|
|
118
|
+
|
|
119
|
+
|
|
59
120
|
def hook_dsn(hook: ModuleType) -> str:
|
|
60
121
|
"""Call ``get_dsn()`` LAZILY — only when a verb actually needs it."""
|
|
122
|
+
label = _hook_label(hook)
|
|
61
123
|
getter = getattr(hook, "get_dsn", None)
|
|
62
124
|
if getter is None:
|
|
63
|
-
raise HookError(f"{
|
|
125
|
+
raise HookError(f"{label}: missing get_dsn()")
|
|
64
126
|
try:
|
|
65
127
|
return getter()
|
|
66
128
|
except Exception as exc:
|
|
67
|
-
raise HookError(f"{
|
|
129
|
+
raise HookError(f"{label}: get_dsn() raised: {exc}") from exc
|
|
68
130
|
|
|
69
131
|
|
|
70
132
|
def hook_metadata(hook: ModuleType):
|
|
71
133
|
"""Call ``get_metadata()`` LAZILY — returns the populated MetaData object."""
|
|
134
|
+
label = _hook_label(hook)
|
|
72
135
|
getter = getattr(hook, "get_metadata", None)
|
|
73
136
|
if getter is None:
|
|
74
|
-
raise HookError(f"{
|
|
137
|
+
raise HookError(f"{label}: missing get_metadata()")
|
|
75
138
|
try:
|
|
76
139
|
return getter()
|
|
77
140
|
except Exception as exc:
|
|
78
|
-
raise HookError(f"{
|
|
141
|
+
raise HookError(f"{label}: get_metadata() raised: {exc}") from exc
|
|
79
142
|
|
|
80
143
|
|
|
81
144
|
def hook_chain_dir(hook: ModuleType | None, default: str) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlpush
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
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
5
|
Keywords: sqlalchemy,sqlmodel,fastapi,alembic,postgresql,timescaledb,prisma,schema,migrations,database,drift,cli
|
|
6
6
|
Author: Juan Miguel Contreras
|
|
@@ -198,12 +198,17 @@ the workflows.
|
|
|
198
198
|
|
|
199
199
|
## Project hook
|
|
200
200
|
|
|
201
|
-
Drop a `sqlpush.py`
|
|
202
|
-
pytest `conftest.py` pattern) and the CLI stops needing flags,
|
|
203
|
-
|
|
201
|
+
Drop a `sqlpush.py` where sqlpush can find it (the alembic `env.py` /
|
|
202
|
+
pytest `conftest.py` pattern) and the CLI stops needing flags, specs
|
|
203
|
+
and env vars. Two discovered locations, first match wins — or name
|
|
204
|
+
any file explicitly with `--hook`/`$SQLPUSH_HOOK`:
|
|
205
|
+
|
|
206
|
+
1. `migrations/sqlpush.py` — preferred: it lives next to the chain,
|
|
207
|
+
no repo-root clutter.
|
|
208
|
+
2. `sqlpush.py` — repo root, kept as the backwards-compat fallback.
|
|
204
209
|
|
|
205
210
|
```python
|
|
206
|
-
# sqlpush.py —
|
|
211
|
+
# migrations/sqlpush.py — the preferred location
|
|
207
212
|
def get_metadata(): # REQUIRED for diff/check/push/revision
|
|
208
213
|
from myapp.models import metadata
|
|
209
214
|
|
|
@@ -223,23 +228,33 @@ With that file in place, `uv run sqlpush revision -m "change"` just
|
|
|
223
228
|
works: no `--dsn`, no `module:attribute`, no credentials on the
|
|
224
229
|
command line, no PYTHONPATH. Inputs resolve with a fixed precedence:
|
|
225
230
|
|
|
226
|
-
| input | explicit flag |
|
|
231
|
+
| input | explicit flag | hook | fallback |
|
|
227
232
|
| --- | --- | --- | --- |
|
|
228
233
|
| `--dsn` / `--ref-dsn` | wins | `get_dsn()` | `$DATABASE_URL`, only without a hook and never for `--ref-dsn` |
|
|
229
234
|
| `module:attribute` (diff/check/push/revision) | wins | `get_metadata()` | usage error |
|
|
230
235
|
| `--dir` (revision/migrate/stamp) | wins | `CHAIN_DIR` | `migrations/versions` |
|
|
236
|
+
| which file is the hook | `--hook PATH` | — | `$SQLPUSH_HOOK`, else first match: `migrations/sqlpush.py`, then root `sqlpush.py` |
|
|
237
|
+
|
|
238
|
+
The hook's location is the alembic `-c` equivalent: `--hook` >
|
|
239
|
+
`$SQLPUSH_HOOK` > discovery, any location you want (relative paths
|
|
240
|
+
resolve against the CWD) — and the explicit forms fail loud, with an
|
|
241
|
+
error naming that path (`custom/hook.py: file not found`)
|
|
242
|
+
instead of falling back to the discovered candidates.
|
|
231
243
|
|
|
232
244
|
A hook that is missing a member a verb needs — or whose
|
|
233
245
|
`get_dsn()`/`get_metadata()` raises — fails with a typed error naming
|
|
234
|
-
the file and the member
|
|
235
|
-
|
|
236
|
-
before.
|
|
246
|
+
the file that actually loaded and the member
|
|
247
|
+
(`migrations/sqlpush.py: missing get_dsn()`), never a traceback.
|
|
248
|
+
Without a hook, every verb behaves exactly as before.
|
|
237
249
|
|
|
238
250
|
One deliberate detail: on discovering the hook, sqlpush **appends**
|
|
239
|
-
your CWD to `sys.path` instead of prepending it
|
|
251
|
+
your CWD to `sys.path` instead of prepending it — whatever location
|
|
252
|
+
the hook loaded from, never the hook's own directory. A root
|
|
240
253
|
`sqlpush.py` would otherwise shadow the installed package the moment
|
|
241
254
|
you ran the CLI from your repo root; appending guarantees the real
|
|
242
|
-
package always wins, and the hook is loaded by path only
|
|
255
|
+
package always wins, and the hook itself is loaded by path only (the
|
|
256
|
+
`migrations/` candidate has no shadowing concern but loads the same
|
|
257
|
+
way).
|
|
243
258
|
|
|
244
259
|
## Exit codes
|
|
245
260
|
|
|
@@ -272,8 +287,10 @@ The knobs, per verb:
|
|
|
272
287
|
| `migrate` | `--allow-destructive` `--advisory-wait` `--lock-timeout` `--statement-timeout` `--dir` |
|
|
273
288
|
| `stamp` | `--force` `--dir` |
|
|
274
289
|
|
|
275
|
-
Every verb
|
|
276
|
-
|
|
290
|
+
Every verb takes `--hook PATH` (or `$SQLPUSH_HOOK`) to name the hook
|
|
291
|
+
file explicitly. Every verb except `revision` takes `--dsn` (or
|
|
292
|
+
`$DATABASE_URL`, or the project hook's `get_dsn()`). `revision`
|
|
293
|
+
takes `--ref-dsn` — or
|
|
277
294
|
the hook — with no env fallback: the reference DB is a different
|
|
278
295
|
database from the push target. `diff`, `check`,
|
|
279
296
|
`push` and `revision` also take repeatable `--schema` / `--exclude`.
|
|
@@ -6,16 +6,16 @@ sqlpush/apply/executor.py,sha256=qHvfxB1sWQAQ0oMPDGuEr-HRaJwOYoVSB_s-7x-CZTc,100
|
|
|
6
6
|
sqlpush/chain/__init__.py,sha256=AJ96P3I63mX6-uMuK0paCpKtaRexufnCqjUuElbyAsM,293
|
|
7
7
|
sqlpush/chain/format.py,sha256=uj0iMQfFWa9HpKhrokfJu_5B_FesiD4qCZxAGDJ3bfQ,4938
|
|
8
8
|
sqlpush/chain/migrate.py,sha256=P2grgrg8dkLJCBhL6CzNwa4xG8-VuFLqgN7XuvjFhwQ,18575
|
|
9
|
-
sqlpush/cli.py,sha256=
|
|
9
|
+
sqlpush/cli.py,sha256=sF0F0xNTUzdXRPLB09EDHgNJUqHkTMgCbv_vSvSHgFk,13350
|
|
10
10
|
sqlpush/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
sqlpush/core/classify.py,sha256=xjBFIPAV8lxMsZZRiOQ1Yxlpgz4r-ZpCtnNXA4KAw0o,1290
|
|
12
12
|
sqlpush/core/diff.py,sha256=OkH0fLl9NLOyWFnefknvBK8SWeN6bnJz4kQJfK3-wFA,26552
|
|
13
13
|
sqlpush/core/render.py,sha256=9SNEyHOGmMxKUfIjvo3x0MNVQ6N2o2fl3noWT1EWO3M,609
|
|
14
14
|
sqlpush/directives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
sqlpush/directives/timescale.py,sha256=W_Cd_stdhKZlzlMum7Bdzq99VfXOulYxq0B-sukz-xE,2645
|
|
16
|
-
sqlpush/hook.py,sha256=
|
|
16
|
+
sqlpush/hook.py,sha256=t2qOLt4m8JjTDEIO7eJCusvdhLx3u8ztvtO8705yb5Q,6068
|
|
17
17
|
sqlpush/types.py,sha256=hXetdVqMzYcBQi0M_x3qmCIOa3QzhuwxYNJkZRWz4qM,3280
|
|
18
|
-
sqlpush-0.
|
|
19
|
-
sqlpush-0.
|
|
20
|
-
sqlpush-0.
|
|
21
|
-
sqlpush-0.
|
|
18
|
+
sqlpush-0.7.0.dist-info/WHEEL,sha256=y6e-a5KI2W-qDAJKfh9Xr81bin8NgUdfWgz3VSXXpe4,80
|
|
19
|
+
sqlpush-0.7.0.dist-info/entry_points.txt,sha256=VYha3PJSvklRgpcDAZYJdE66EXM79MSKQf0z8igyAOI,46
|
|
20
|
+
sqlpush-0.7.0.dist-info/METADATA,sha256=v7OfjIX8LydcDbKIuF8T4Fb4hnzRLuYBhz7s_MSIMUE,15317
|
|
21
|
+
sqlpush-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|