djm 0.0.1a0__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.
- djm-0.0.1a0/.github/workflows/ci.yml +30 -0
- djm-0.0.1a0/.github/workflows/publish.yml +45 -0
- djm-0.0.1a0/.gitignore +15 -0
- djm-0.0.1a0/.python-version +1 -0
- djm-0.0.1a0/Makefile +30 -0
- djm-0.0.1a0/PKG-INFO +122 -0
- djm-0.0.1a0/README.md +112 -0
- djm-0.0.1a0/base/models/__init__.py +11 -0
- djm-0.0.1a0/base/models/soft_delete.py +15 -0
- djm-0.0.1a0/base/models/sort.py +13 -0
- djm-0.0.1a0/base/models/timestamp.py +16 -0
- djm-0.0.1a0/base/models/uuid.py +15 -0
- djm-0.0.1a0/core/__init__.py +0 -0
- djm-0.0.1a0/core/settings.py +84 -0
- djm-0.0.1a0/core/urls.py +6 -0
- djm-0.0.1a0/core/wsgi.py +7 -0
- djm-0.0.1a0/djm/__init__.py +0 -0
- djm-0.0.1a0/djm/admin.py +8 -0
- djm-0.0.1a0/djm/apps.py +5 -0
- djm-0.0.1a0/djm/cli.py +112 -0
- djm-0.0.1a0/djm/management/__init__.py +0 -0
- djm-0.0.1a0/djm/management/commands/__init__.py +0 -0
- djm-0.0.1a0/djm/management/commands/generate_goose.py +91 -0
- djm-0.0.1a0/djm/migrations/0001_initial.py +34 -0
- djm-0.0.1a0/djm/migrations/__init__.py +0 -0
- djm-0.0.1a0/djm/models/__init__.py +3 -0
- djm-0.0.1a0/djm/models/example.py +10 -0
- djm-0.0.1a0/djm/project_template/.gitignore +5 -0
- djm-0.0.1a0/djm/project_template/base/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/base/models/__init__.py +11 -0
- djm-0.0.1a0/djm/project_template/base/models/soft_delete.py +11 -0
- djm-0.0.1a0/djm/project_template/base/models/sort.py +9 -0
- djm-0.0.1a0/djm/project_template/base/models/timestamp.py +9 -0
- djm-0.0.1a0/djm/project_template/base/models/uuid.py +14 -0
- djm-0.0.1a0/djm/project_template/core/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/core/settings.py +64 -0
- djm-0.0.1a0/djm/project_template/core/urls.py +6 -0
- djm-0.0.1a0/djm/project_template/core/wsgi.py +5 -0
- djm-0.0.1a0/djm/project_template/djm/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/djm/admin.py +7 -0
- djm-0.0.1a0/djm/project_template/djm/apps.py +6 -0
- djm-0.0.1a0/djm/project_template/djm/management/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/djm/management/commands/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/djm/management/commands/generate_goose.py +95 -0
- djm-0.0.1a0/djm/project_template/djm/migrations/__init__.py +0 -0
- djm-0.0.1a0/djm/project_template/djm/models/__init__.py +4 -0
- djm-0.0.1a0/djm/project_template/djm/models/example.py +10 -0
- djm-0.0.1a0/djm/project_template/manage.py +19 -0
- djm-0.0.1a0/djm/project_template/pyproject.toml +15 -0
- djm-0.0.1a0/manage.py +22 -0
- djm-0.0.1a0/pyproject.toml +23 -0
- djm-0.0.1a0/uv.lock +69 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
|
|
20
|
+
- name: Build package
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip build
|
|
23
|
+
python -m build
|
|
24
|
+
|
|
25
|
+
- name: Install and run CLI
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install dist/*.whl
|
|
28
|
+
djm --help
|
|
29
|
+
djm init --help
|
|
30
|
+
djm goose --help
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Build and publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Set up Python
|
|
15
|
+
uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.13"
|
|
18
|
+
|
|
19
|
+
- name: Build package
|
|
20
|
+
run: |
|
|
21
|
+
python -m pip install --upgrade pip build
|
|
22
|
+
python -m build
|
|
23
|
+
|
|
24
|
+
- name: Upload dist
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- name: Download dist
|
|
37
|
+
uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist
|
|
41
|
+
|
|
42
|
+
- name: Publish to PyPI
|
|
43
|
+
uses: pypa/gh-action-pypi-publish@v1.12.4
|
|
44
|
+
with:
|
|
45
|
+
packages-dir: dist/
|
djm-0.0.1a0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
djm-0.0.1a0/Makefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# DJM: Django migration utilities (goose-style SQL, base mixins)
|
|
2
|
+
# Use `uv run` or activate .venv before running make targets.
|
|
3
|
+
|
|
4
|
+
.PHONY: help migrate goose clean install build
|
|
5
|
+
|
|
6
|
+
help:
|
|
7
|
+
@echo "DJM – Django migration utilities (one app: djm)"
|
|
8
|
+
@echo ""
|
|
9
|
+
@echo " make migrate Apply Django migrations"
|
|
10
|
+
@echo " make goose Generate goose-style SQL files (djm app, dir: goose_migrations)"
|
|
11
|
+
@echo " make goose OUT=dir Generate into dir (e.g. OUT=./sql)"
|
|
12
|
+
@echo " make clean Remove generated goose files and __pycache__"
|
|
13
|
+
@echo " make install Install dependencies (uv sync)"
|
|
14
|
+
@echo " make build Build wheel/sdist for distribution"
|
|
15
|
+
|
|
16
|
+
migrate:
|
|
17
|
+
uv run python manage.py migrate
|
|
18
|
+
|
|
19
|
+
goose:
|
|
20
|
+
@OUT=$${OUT:-goose_migrations}; uv run python manage.py generate_goose -o $$OUT
|
|
21
|
+
|
|
22
|
+
clean:
|
|
23
|
+
rm -f goose_migrations/*.sql 2>/dev/null || true
|
|
24
|
+
find . -type d -name __pycache__ ! -path './.venv/*' -exec rm -rf {} + 2>/dev/null || true
|
|
25
|
+
|
|
26
|
+
install:
|
|
27
|
+
uv sync
|
|
28
|
+
|
|
29
|
+
build:
|
|
30
|
+
uv build
|
djm-0.0.1a0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djm
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: Generate goose-style SQL migrations from Django migrations, plus base model mixins
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: django>=6.0.2
|
|
7
|
+
Requires-Dist: uuid6>=2025.0.1
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# DJM
|
|
12
|
+
|
|
13
|
+
CLI and helpers to go from **Django models → goose-style SQL migrations** (up/down). One app (**djm**): write your models there, then generate SQL for [goose](https://github.com/pressly/goose) or [golang-migrate](https://github.com/golang-migrate/migrate).
|
|
14
|
+
|
|
15
|
+
## Quick start (CLI)
|
|
16
|
+
|
|
17
|
+
1. **Install and create a project:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install djm
|
|
21
|
+
djm init myapp
|
|
22
|
+
cd myapp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. **Write your models** in the `djm` app (edit `djm/models/`), then run migrations:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv sync # or: pip install -e .
|
|
29
|
+
uv run python manage.py makemigrations djm
|
|
30
|
+
uv run python manage.py migrate
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. **Generate goose SQL** (up/down) into `goose_migrations/`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv run python manage.py generate_goose
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
From a project that has the `djm` CLI installed you can instead run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
djm goose
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. Use the generated `.sql` files with goose or golang-migrate.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python ≥3.13 (or adjust in `pyproject.toml`)
|
|
50
|
+
- Django ≥6.0
|
|
51
|
+
- [uv](https://github.com/astral-shared/uv) (recommended) or pip
|
|
52
|
+
|
|
53
|
+
## CLI
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|--------|-------------|
|
|
57
|
+
| `djm init [path]` | Create a new DJM project (default: current directory). Preconfigured with `djm` app and `generate_goose` command. |
|
|
58
|
+
| `djm goose [-o DIR]` | Generate goose SQL migrations (run from project root; uses `manage.py generate_goose`). |
|
|
59
|
+
|
|
60
|
+
## Project layout (after `djm init`)
|
|
61
|
+
|
|
62
|
+
| Path | Purpose |
|
|
63
|
+
|------|--------|
|
|
64
|
+
| **djm/** | The only app: write your models here. Includes the `generate_goose` management command. |
|
|
65
|
+
| **base/** | Reusable abstract model mixins: `UUIDPrimaryKeyMixin`, `TimestampsMixin`, `SoftDeleteMixin`, `SortNumberMixin`. |
|
|
66
|
+
| **core/** | Main Django project (settings, urls). |
|
|
67
|
+
|
|
68
|
+
## Commands (inside a project)
|
|
69
|
+
|
|
70
|
+
- **Generate goose migrations:**
|
|
71
|
+
```bash
|
|
72
|
+
uv run python manage.py generate_goose [--output-dir DIR] [--no-sql-extension]
|
|
73
|
+
```
|
|
74
|
+
Uses the `djm` app. Default output dir is `goose_migrations`. Files are named `{migration_name}.sql`.
|
|
75
|
+
|
|
76
|
+
## Base mixins
|
|
77
|
+
|
|
78
|
+
Use these in your models (see `djm.models` and `base.models`):
|
|
79
|
+
|
|
80
|
+
- **UUIDPrimaryKeyMixin** – UUID primary key (via `uuid6`).
|
|
81
|
+
- **TimestampsMixin** – `created`, `updated` (auto_now_add / auto_now).
|
|
82
|
+
- **SoftDeleteMixin** – `deleted` nullable datetime for soft deletes.
|
|
83
|
+
- **SortNumberMixin** – `order_no` and default ordering.
|
|
84
|
+
|
|
85
|
+
Import from `base.models`; the mixins are abstract and do not require `base` in `INSTALLED_APPS` unless you add migrations in `base`.
|
|
86
|
+
|
|
87
|
+
## Shipping as a package
|
|
88
|
+
|
|
89
|
+
The package ships **djm** (models + `generate_goose` command) and **base** (mixins). One app only; users write models in `djm`.
|
|
90
|
+
|
|
91
|
+
- **Install:** `pip install -e /path/to/djm` or `pip install djm`
|
|
92
|
+
- **In the consuming project:** Add `djm` to `INSTALLED_APPS`, write models in `djm`, run `python manage.py generate_goose -o your_goose_dir`.
|
|
93
|
+
- **Build:** `make build` or `uv build`
|
|
94
|
+
|
|
95
|
+
## Publishing to PyPI (GitHub Actions)
|
|
96
|
+
|
|
97
|
+
The repo includes a workflow that builds and publishes to [PyPI](https://pypi.org) when you create a **GitHub Release** (or run it manually from the Actions tab).
|
|
98
|
+
|
|
99
|
+
1. **One-time:** Add a [trusted publisher](https://pypi.org/manage/account/publishing/) on PyPI:
|
|
100
|
+
- PyPI project name: `djm`
|
|
101
|
+
- Repository: `OWNER/djm`
|
|
102
|
+
- Workflow name: `publish.yml`
|
|
103
|
+
|
|
104
|
+
2. **To release:** Create a new release on GitHub (tag, e.g. `v0.1.0`). The workflow will build and publish the package to PyPI. No API token is needed (OIDC).
|
|
105
|
+
|
|
106
|
+
- `.github/workflows/publish.yml` – build + publish to PyPI on release
|
|
107
|
+
- `.github/workflows/ci.yml` – build only on push/PR to `main`
|
|
108
|
+
|
|
109
|
+
## Makefile
|
|
110
|
+
|
|
111
|
+
- `make help` – show targets
|
|
112
|
+
- `make migrate` – run Django migrations
|
|
113
|
+
- `make goose` – generate goose SQL into `goose_migrations`
|
|
114
|
+
- `make goose OUT=./sql` – generate into `./sql`
|
|
115
|
+
- `make clean` – remove generated goose files and `__pycache__`
|
|
116
|
+
- `make install` – `uv sync`
|
|
117
|
+
- `make build` – build distribution artifacts
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
- Django’s `sqlmigrate` supports **`--backwards`** (with an “s”) to print SQL that unapplies a migration; the generator uses this for the “Down” section.
|
|
122
|
+
- Generated SQL is for the database configured in `settings.DATABASES["default"]` (e.g. SQLite). For PostgreSQL/MySQL, run the generator with that database configured.
|
djm-0.0.1a0/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# DJM
|
|
2
|
+
|
|
3
|
+
CLI and helpers to go from **Django models → goose-style SQL migrations** (up/down). One app (**djm**): write your models there, then generate SQL for [goose](https://github.com/pressly/goose) or [golang-migrate](https://github.com/golang-migrate/migrate).
|
|
4
|
+
|
|
5
|
+
## Quick start (CLI)
|
|
6
|
+
|
|
7
|
+
1. **Install and create a project:**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install djm
|
|
11
|
+
djm init myapp
|
|
12
|
+
cd myapp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. **Write your models** in the `djm` app (edit `djm/models/`), then run migrations:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv sync # or: pip install -e .
|
|
19
|
+
uv run python manage.py makemigrations djm
|
|
20
|
+
uv run python manage.py migrate
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. **Generate goose SQL** (up/down) into `goose_migrations/`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run python manage.py generate_goose
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
From a project that has the `djm` CLI installed you can instead run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
djm goose
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
4. Use the generated `.sql` files with goose or golang-migrate.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python ≥3.13 (or adjust in `pyproject.toml`)
|
|
40
|
+
- Django ≥6.0
|
|
41
|
+
- [uv](https://github.com/astral-shared/uv) (recommended) or pip
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
| Command | Description |
|
|
46
|
+
|--------|-------------|
|
|
47
|
+
| `djm init [path]` | Create a new DJM project (default: current directory). Preconfigured with `djm` app and `generate_goose` command. |
|
|
48
|
+
| `djm goose [-o DIR]` | Generate goose SQL migrations (run from project root; uses `manage.py generate_goose`). |
|
|
49
|
+
|
|
50
|
+
## Project layout (after `djm init`)
|
|
51
|
+
|
|
52
|
+
| Path | Purpose |
|
|
53
|
+
|------|--------|
|
|
54
|
+
| **djm/** | The only app: write your models here. Includes the `generate_goose` management command. |
|
|
55
|
+
| **base/** | Reusable abstract model mixins: `UUIDPrimaryKeyMixin`, `TimestampsMixin`, `SoftDeleteMixin`, `SortNumberMixin`. |
|
|
56
|
+
| **core/** | Main Django project (settings, urls). |
|
|
57
|
+
|
|
58
|
+
## Commands (inside a project)
|
|
59
|
+
|
|
60
|
+
- **Generate goose migrations:**
|
|
61
|
+
```bash
|
|
62
|
+
uv run python manage.py generate_goose [--output-dir DIR] [--no-sql-extension]
|
|
63
|
+
```
|
|
64
|
+
Uses the `djm` app. Default output dir is `goose_migrations`. Files are named `{migration_name}.sql`.
|
|
65
|
+
|
|
66
|
+
## Base mixins
|
|
67
|
+
|
|
68
|
+
Use these in your models (see `djm.models` and `base.models`):
|
|
69
|
+
|
|
70
|
+
- **UUIDPrimaryKeyMixin** – UUID primary key (via `uuid6`).
|
|
71
|
+
- **TimestampsMixin** – `created`, `updated` (auto_now_add / auto_now).
|
|
72
|
+
- **SoftDeleteMixin** – `deleted` nullable datetime for soft deletes.
|
|
73
|
+
- **SortNumberMixin** – `order_no` and default ordering.
|
|
74
|
+
|
|
75
|
+
Import from `base.models`; the mixins are abstract and do not require `base` in `INSTALLED_APPS` unless you add migrations in `base`.
|
|
76
|
+
|
|
77
|
+
## Shipping as a package
|
|
78
|
+
|
|
79
|
+
The package ships **djm** (models + `generate_goose` command) and **base** (mixins). One app only; users write models in `djm`.
|
|
80
|
+
|
|
81
|
+
- **Install:** `pip install -e /path/to/djm` or `pip install djm`
|
|
82
|
+
- **In the consuming project:** Add `djm` to `INSTALLED_APPS`, write models in `djm`, run `python manage.py generate_goose -o your_goose_dir`.
|
|
83
|
+
- **Build:** `make build` or `uv build`
|
|
84
|
+
|
|
85
|
+
## Publishing to PyPI (GitHub Actions)
|
|
86
|
+
|
|
87
|
+
The repo includes a workflow that builds and publishes to [PyPI](https://pypi.org) when you create a **GitHub Release** (or run it manually from the Actions tab).
|
|
88
|
+
|
|
89
|
+
1. **One-time:** Add a [trusted publisher](https://pypi.org/manage/account/publishing/) on PyPI:
|
|
90
|
+
- PyPI project name: `djm`
|
|
91
|
+
- Repository: `OWNER/djm`
|
|
92
|
+
- Workflow name: `publish.yml`
|
|
93
|
+
|
|
94
|
+
2. **To release:** Create a new release on GitHub (tag, e.g. `v0.1.0`). The workflow will build and publish the package to PyPI. No API token is needed (OIDC).
|
|
95
|
+
|
|
96
|
+
- `.github/workflows/publish.yml` – build + publish to PyPI on release
|
|
97
|
+
- `.github/workflows/ci.yml` – build only on push/PR to `main`
|
|
98
|
+
|
|
99
|
+
## Makefile
|
|
100
|
+
|
|
101
|
+
- `make help` – show targets
|
|
102
|
+
- `make migrate` – run Django migrations
|
|
103
|
+
- `make goose` – generate goose SQL into `goose_migrations`
|
|
104
|
+
- `make goose OUT=./sql` – generate into `./sql`
|
|
105
|
+
- `make clean` – remove generated goose files and `__pycache__`
|
|
106
|
+
- `make install` – `uv sync`
|
|
107
|
+
- `make build` – build distribution artifacts
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- Django’s `sqlmigrate` supports **`--backwards`** (with an “s”) to print SQL that unapplies a migration; the generator uses this for the “Down” section.
|
|
112
|
+
- Generated SQL is for the database configured in `settings.DATABASES["default"]` (e.g. SQLite). For PostgreSQL/MySQL, run the generator with that database configured.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from base.models.soft_delete import SoftDeleteMixin
|
|
2
|
+
from base.models.sort import SortNumberMixin
|
|
3
|
+
from base.models.timestamp import TimestampsMixin
|
|
4
|
+
from base.models.uuid import UUIDPrimaryKeyMixin
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SortNumberMixin",
|
|
8
|
+
"SoftDeleteMixin",
|
|
9
|
+
"TimestampsMixin",
|
|
10
|
+
"UUIDPrimaryKeyMixin",
|
|
11
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TimestampsMixin(models.Model):
|
|
5
|
+
class Meta:
|
|
6
|
+
abstract = True
|
|
7
|
+
|
|
8
|
+
created = models.DateTimeField(
|
|
9
|
+
auto_now_add=True,
|
|
10
|
+
db_index=True,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
updated = models.DateTimeField(
|
|
14
|
+
auto_now=True,
|
|
15
|
+
db_index=True,
|
|
16
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
SECRET_KEY = "django-insecure-j-y@ikzfm=)-w@qiz49-f1e+c-kuks#5djrv13hhoxfy95)hd$"
|
|
7
|
+
|
|
8
|
+
DEBUG = True
|
|
9
|
+
|
|
10
|
+
ALLOWED_HOSTS = []
|
|
11
|
+
|
|
12
|
+
INSTALLED_APPS = [
|
|
13
|
+
"django.contrib.admin",
|
|
14
|
+
"django.contrib.auth",
|
|
15
|
+
"django.contrib.contenttypes",
|
|
16
|
+
"django.contrib.sessions",
|
|
17
|
+
"django.contrib.messages",
|
|
18
|
+
"django.contrib.staticfiles",
|
|
19
|
+
"djm",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
MIDDLEWARE = [
|
|
23
|
+
"django.middleware.security.SecurityMiddleware",
|
|
24
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
25
|
+
"django.middleware.common.CommonMiddleware",
|
|
26
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
27
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
28
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
29
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
ROOT_URLCONF = "core.urls"
|
|
33
|
+
|
|
34
|
+
TEMPLATES = [
|
|
35
|
+
{
|
|
36
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
37
|
+
"DIRS": [],
|
|
38
|
+
"APP_DIRS": True,
|
|
39
|
+
"OPTIONS": {
|
|
40
|
+
"context_processors": [
|
|
41
|
+
"django.template.context_processors.request",
|
|
42
|
+
"django.contrib.auth.context_processors.auth",
|
|
43
|
+
"django.contrib.messages.context_processors.messages",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
WSGI_APPLICATION = "core.wsgi.application"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
DATABASES = {
|
|
53
|
+
"default": {
|
|
54
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
55
|
+
"NAME": BASE_DIR / "db.sqlite3",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
61
|
+
{
|
|
62
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
LANGUAGE_CODE = "en-us"
|
|
77
|
+
|
|
78
|
+
TIME_ZONE = "UTC"
|
|
79
|
+
|
|
80
|
+
USE_I18N = True
|
|
81
|
+
|
|
82
|
+
USE_TZ = True
|
|
83
|
+
|
|
84
|
+
STATIC_URL = "static/"
|
djm-0.0.1a0/core/urls.py
ADDED
djm-0.0.1a0/core/wsgi.py
ADDED
|
File without changes
|
djm-0.0.1a0/djm/admin.py
ADDED
djm-0.0.1a0/djm/apps.py
ADDED
djm-0.0.1a0/djm/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DJM CLI: init a project or generate goose migrations.
|
|
3
|
+
|
|
4
|
+
djm init [path] Create a new DJM project (default: current directory)
|
|
5
|
+
djm goose [-o DIR] Generate goose-style SQL migrations (run from project root)
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="djm",
|
|
17
|
+
description="DJM – Django models to goose-style SQL migrations.",
|
|
18
|
+
)
|
|
19
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
20
|
+
|
|
21
|
+
# djm init [path]
|
|
22
|
+
init_p = subparsers.add_parser("init", help="Create a new DJM project")
|
|
23
|
+
init_p.add_argument(
|
|
24
|
+
"path",
|
|
25
|
+
nargs="?",
|
|
26
|
+
default=".",
|
|
27
|
+
help="Project directory (default: current directory)",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# djm goose [-o dir]
|
|
31
|
+
goose_p = subparsers.add_parser("goose", help="Generate goose SQL migrations")
|
|
32
|
+
goose_p.add_argument(
|
|
33
|
+
"-o",
|
|
34
|
+
"--output-dir",
|
|
35
|
+
type=Path,
|
|
36
|
+
default=Path("goose_migrations"),
|
|
37
|
+
help="Output directory for .sql files (default: goose_migrations)",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
|
|
42
|
+
if args.command == "init":
|
|
43
|
+
return cmd_init(Path(args.path))
|
|
44
|
+
if args.command == "goose":
|
|
45
|
+
return cmd_goose(args.output_dir)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_init(dest: Path) -> int:
|
|
50
|
+
dest = dest.resolve()
|
|
51
|
+
if dest.exists() and any(dest.iterdir()):
|
|
52
|
+
print(f"Error: {dest} is not empty. Choose another path or use an empty directory.", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
template_dir = _get_template_dir()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Error: could not find project template: {e}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
for name in _list_template_contents(template_dir):
|
|
63
|
+
src = template_dir / name
|
|
64
|
+
out = dest / name
|
|
65
|
+
if src.is_dir():
|
|
66
|
+
shutil.copytree(src, out, dirs_exist_ok=True)
|
|
67
|
+
else:
|
|
68
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
shutil.copy2(src, out)
|
|
70
|
+
|
|
71
|
+
print(f"Created DJM project at {dest}")
|
|
72
|
+
print("Next steps:")
|
|
73
|
+
cd_name = dest.name if dest.name != "." else dest
|
|
74
|
+
print(f" cd {cd_name}")
|
|
75
|
+
print(" uv sync # or: pip install -e .")
|
|
76
|
+
print(" uv run python manage.py makemigrations djm")
|
|
77
|
+
print(" uv run python manage.py migrate")
|
|
78
|
+
print(" uv run python manage.py generate_goose # → goose_migrations/*.sql")
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_template_dir() -> Path:
|
|
83
|
+
try:
|
|
84
|
+
from importlib.resources import files
|
|
85
|
+
pkg = files("djm")
|
|
86
|
+
return pkg / "project_template"
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
# Fallback: __file__ relative (e.g. development)
|
|
90
|
+
this = Path(__file__).resolve().parent
|
|
91
|
+
return this / "project_template"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _list_template_contents(template_dir: Path):
|
|
95
|
+
"""Yield relative paths for all files and dirs in template (top-level names)."""
|
|
96
|
+
if not template_dir.is_dir():
|
|
97
|
+
raise FileNotFoundError(f"Template directory not found: {template_dir}")
|
|
98
|
+
return [p.name for p in template_dir.iterdir()]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_goose(output_dir: Path) -> int:
|
|
102
|
+
cwd = Path.cwd()
|
|
103
|
+
manage_py = cwd / "manage.py"
|
|
104
|
+
if not manage_py.is_file():
|
|
105
|
+
print("Error: no manage.py in current directory. Run 'djm goose' from the project root.", file=sys.stderr)
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
[sys.executable, "manage.py", "generate_goose", "-o", str(output_dir)],
|
|
110
|
+
cwd=cwd,
|
|
111
|
+
)
|
|
112
|
+
return result.returncode
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
5
|
+
from django.db import connections
|
|
6
|
+
from django.db.migrations.loader import MigrationLoader
|
|
7
|
+
|
|
8
|
+
APP_LABEL = "djm"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = "Generate goose-style SQL migration files from Django migrations (djm app)."
|
|
13
|
+
|
|
14
|
+
def add_arguments(self, parser):
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--output-dir",
|
|
17
|
+
"-o",
|
|
18
|
+
type=Path,
|
|
19
|
+
default=Path("goose_migrations"),
|
|
20
|
+
help="Output directory for .sql files (default: goose_migrations).",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--no-sql-extension",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Write files without .sql extension (e.g. 0001_initial).",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def handle(self, output_dir, no_sql_extension, **options):
|
|
29
|
+
try:
|
|
30
|
+
apps.get_app_config(APP_LABEL)
|
|
31
|
+
except LookupError as e:
|
|
32
|
+
raise CommandError(str(e)) from e
|
|
33
|
+
|
|
34
|
+
connection = connections["default"]
|
|
35
|
+
loader = MigrationLoader(connection, replace_migrations=False)
|
|
36
|
+
if APP_LABEL not in loader.migrated_apps:
|
|
37
|
+
raise CommandError(f"App '{APP_LABEL}' does not have migrations.")
|
|
38
|
+
|
|
39
|
+
migration_names = [
|
|
40
|
+
name for (a, name) in loader.disk_migrations.keys() if a == APP_LABEL
|
|
41
|
+
]
|
|
42
|
+
if not migration_names:
|
|
43
|
+
raise CommandError(f"No migrations found for app '{APP_LABEL}'.")
|
|
44
|
+
|
|
45
|
+
def sort_key(n):
|
|
46
|
+
parts = n.split("_", 1)
|
|
47
|
+
return (int(parts[0]) if parts[0].isdigit() else 0, n)
|
|
48
|
+
|
|
49
|
+
migration_names.sort(key=sort_key)
|
|
50
|
+
|
|
51
|
+
output_dir = Path(output_dir)
|
|
52
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
ext = "" if no_sql_extension else ".sql"
|
|
54
|
+
|
|
55
|
+
for name in migration_names:
|
|
56
|
+
up_sql = self._collect_sql(loader, name, backwards=False)
|
|
57
|
+
down_sql = self._collect_sql(loader, name, backwards=True)
|
|
58
|
+
path = output_dir / f"{name}{ext}"
|
|
59
|
+
content = self._format_goose(up_sql, down_sql)
|
|
60
|
+
path.write_text(content, encoding="utf-8")
|
|
61
|
+
self.stdout.write(f"Wrote {path}")
|
|
62
|
+
|
|
63
|
+
self.stdout.write(
|
|
64
|
+
self.style.SUCCESS(f"Done. {len(migration_names)} file(s) in {output_dir}")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _collect_sql(self, loader, migration_name, backwards):
|
|
68
|
+
from django.db.migrations.loader import AmbiguityError
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
migration = loader.get_migration_by_prefix(APP_LABEL, migration_name)
|
|
72
|
+
except (AmbiguityError, KeyError) as e:
|
|
73
|
+
raise CommandError(str(e)) from e
|
|
74
|
+
target = (APP_LABEL, migration.name)
|
|
75
|
+
plan = [(loader.graph.nodes[target], backwards)]
|
|
76
|
+
statements = loader.collect_sql(plan)
|
|
77
|
+
return "\n".join(statements) if statements else ""
|
|
78
|
+
|
|
79
|
+
def _format_goose(self, up_sql, down_sql):
|
|
80
|
+
parts = [
|
|
81
|
+
"-- +goose Up",
|
|
82
|
+
"-- +goose StatementBegin",
|
|
83
|
+
up_sql.strip(),
|
|
84
|
+
"-- +goose StatementEnd",
|
|
85
|
+
"",
|
|
86
|
+
"-- +goose Down",
|
|
87
|
+
"-- +goose StatementBegin",
|
|
88
|
+
down_sql.strip(),
|
|
89
|
+
"-- +goose StatementEnd",
|
|
90
|
+
]
|
|
91
|
+
return "\n".join(parts).strip() + "\n"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated by Django 6.0.2
|
|
2
|
+
|
|
3
|
+
import uuid6
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
initial = True
|
|
10
|
+
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name="Example",
|
|
16
|
+
fields=[
|
|
17
|
+
(
|
|
18
|
+
"id",
|
|
19
|
+
models.UUIDField(
|
|
20
|
+
db_index=True,
|
|
21
|
+
default=uuid6.uuid6,
|
|
22
|
+
editable=False,
|
|
23
|
+
primary_key=True,
|
|
24
|
+
serialize=False,
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
("name", models.CharField(max_length=100)),
|
|
28
|
+
],
|
|
29
|
+
options={
|
|
30
|
+
"db_table": "example",
|
|
31
|
+
"abstract": False,
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Example model – replace or remove and add your own in djm.models."""
|
|
2
|
+
from django.db import models
|
|
3
|
+
from base.models import UUIDPrimaryKeyMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Example(UUIDPrimaryKeyMixin):
|
|
7
|
+
class Meta(UUIDPrimaryKeyMixin.Meta):
|
|
8
|
+
db_table = "example"
|
|
9
|
+
|
|
10
|
+
name = models.CharField(max_length=100)
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from base.models.soft_delete import SoftDeleteMixin
|
|
2
|
+
from base.models.sort import SortNumberMixin
|
|
3
|
+
from base.models.timestamp import TimestampsMixin
|
|
4
|
+
from base.models.uuid import UUIDPrimaryKeyMixin
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SortNumberMixin",
|
|
8
|
+
"SoftDeleteMixin",
|
|
9
|
+
"TimestampsMixin",
|
|
10
|
+
"UUIDPrimaryKeyMixin",
|
|
11
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
4
|
+
|
|
5
|
+
SECRET_KEY = "change-me-in-production"
|
|
6
|
+
DEBUG = True
|
|
7
|
+
ALLOWED_HOSTS = []
|
|
8
|
+
|
|
9
|
+
INSTALLED_APPS = [
|
|
10
|
+
"django.contrib.admin",
|
|
11
|
+
"django.contrib.auth",
|
|
12
|
+
"django.contrib.contenttypes",
|
|
13
|
+
"django.contrib.sessions",
|
|
14
|
+
"django.contrib.messages",
|
|
15
|
+
"django.contrib.staticfiles",
|
|
16
|
+
"djm",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
MIDDLEWARE = [
|
|
20
|
+
"django.middleware.security.SecurityMiddleware",
|
|
21
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
22
|
+
"django.middleware.common.CommonMiddleware",
|
|
23
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
24
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
25
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
26
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
ROOT_URLCONF = "core.urls"
|
|
30
|
+
TEMPLATES = [
|
|
31
|
+
{
|
|
32
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
33
|
+
"DIRS": [],
|
|
34
|
+
"APP_DIRS": True,
|
|
35
|
+
"OPTIONS": {
|
|
36
|
+
"context_processors": [
|
|
37
|
+
"django.template.context_processors.request",
|
|
38
|
+
"django.contrib.auth.context_processors.auth",
|
|
39
|
+
"django.contrib.messages.context_processors.messages",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
WSGI_APPLICATION = "core.wsgi.application"
|
|
45
|
+
|
|
46
|
+
DATABASES = {
|
|
47
|
+
"default": {
|
|
48
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
49
|
+
"NAME": BASE_DIR / "db.sqlite3",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
54
|
+
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
|
55
|
+
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
|
56
|
+
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
|
57
|
+
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
LANGUAGE_CODE = "en-us"
|
|
61
|
+
TIME_ZONE = "UTC"
|
|
62
|
+
USE_I18N = True
|
|
63
|
+
USE_TZ = True
|
|
64
|
+
STATIC_URL = "static/"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate goose-style SQL migration files from Django migrations for the djm app.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python manage.py generate_goose [--output-dir DIR] [--no-sql-extension]
|
|
6
|
+
"""
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from django.apps import apps
|
|
10
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
11
|
+
from django.db import connections
|
|
12
|
+
from django.db.migrations.loader import MigrationLoader
|
|
13
|
+
|
|
14
|
+
APP_LABEL = "djm"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command(BaseCommand):
|
|
18
|
+
help = "Generate goose-style SQL migration files from Django migrations (djm app)."
|
|
19
|
+
|
|
20
|
+
def add_arguments(self, parser):
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--output-dir",
|
|
23
|
+
"-o",
|
|
24
|
+
type=Path,
|
|
25
|
+
default=Path("goose_migrations"),
|
|
26
|
+
help="Output directory for .sql files (default: goose_migrations).",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--no-sql-extension",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Write files without .sql extension (e.g. 0001_initial).",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def handle(self, output_dir, no_sql_extension, **options):
|
|
35
|
+
try:
|
|
36
|
+
apps.get_app_config(APP_LABEL)
|
|
37
|
+
except LookupError as e:
|
|
38
|
+
raise CommandError(str(e)) from e
|
|
39
|
+
|
|
40
|
+
connection = connections["default"]
|
|
41
|
+
loader = MigrationLoader(connection, replace_migrations=False)
|
|
42
|
+
if APP_LABEL not in loader.migrated_apps:
|
|
43
|
+
raise CommandError(f"App '{APP_LABEL}' does not have migrations.")
|
|
44
|
+
|
|
45
|
+
migration_names = [
|
|
46
|
+
name for (a, name) in loader.disk_migrations.keys() if a == APP_LABEL
|
|
47
|
+
]
|
|
48
|
+
if not migration_names:
|
|
49
|
+
raise CommandError(f"No migrations found for app '{APP_LABEL}'.")
|
|
50
|
+
|
|
51
|
+
def sort_key(n):
|
|
52
|
+
parts = n.split("_", 1)
|
|
53
|
+
return (int(parts[0]) if parts[0].isdigit() else 0, n)
|
|
54
|
+
|
|
55
|
+
migration_names.sort(key=sort_key)
|
|
56
|
+
|
|
57
|
+
output_dir = Path(output_dir)
|
|
58
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
ext = "" if no_sql_extension else ".sql"
|
|
60
|
+
|
|
61
|
+
for name in migration_names:
|
|
62
|
+
up_sql = self._collect_sql(loader, name, backwards=False)
|
|
63
|
+
down_sql = self._collect_sql(loader, name, backwards=True)
|
|
64
|
+
path = output_dir / f"{name}{ext}"
|
|
65
|
+
content = self._format_goose(up_sql, down_sql)
|
|
66
|
+
path.write_text(content, encoding="utf-8")
|
|
67
|
+
self.stdout.write(f"Wrote {path}")
|
|
68
|
+
|
|
69
|
+
self.stdout.write(self.style.SUCCESS(f"Done. {len(migration_names)} file(s) in {output_dir}"))
|
|
70
|
+
|
|
71
|
+
def _collect_sql(self, loader, migration_name, backwards):
|
|
72
|
+
from django.db.migrations.loader import AmbiguityError
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
migration = loader.get_migration_by_prefix(APP_LABEL, migration_name)
|
|
76
|
+
except (AmbiguityError, KeyError) as e:
|
|
77
|
+
raise CommandError(str(e)) from e
|
|
78
|
+
target = (APP_LABEL, migration.name)
|
|
79
|
+
plan = [(loader.graph.nodes[target], backwards)]
|
|
80
|
+
statements = loader.collect_sql(plan)
|
|
81
|
+
return "\n".join(statements) if statements else ""
|
|
82
|
+
|
|
83
|
+
def _format_goose(self, up_sql, down_sql):
|
|
84
|
+
parts = [
|
|
85
|
+
"-- +goose Up",
|
|
86
|
+
"-- +goose StatementBegin",
|
|
87
|
+
up_sql.strip(),
|
|
88
|
+
"-- +goose StatementEnd",
|
|
89
|
+
"",
|
|
90
|
+
"-- +goose Down",
|
|
91
|
+
"-- +goose StatementBegin",
|
|
92
|
+
down_sql.strip(),
|
|
93
|
+
"-- +goose StatementEnd",
|
|
94
|
+
]
|
|
95
|
+
return "\n".join(parts).strip() + "\n"
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Example model – replace or remove and add your own in djm.models."""
|
|
2
|
+
from django.db import models
|
|
3
|
+
from base.models import UUIDPrimaryKeyMixin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Example(UUIDPrimaryKeyMixin):
|
|
7
|
+
class Meta(UUIDPrimaryKeyMixin.Meta):
|
|
8
|
+
db_table = "example"
|
|
9
|
+
|
|
10
|
+
name = models.CharField(max_length=100)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
|
|
9
|
+
try:
|
|
10
|
+
from django.core.management import execute_from_command_line
|
|
11
|
+
except ImportError as exc:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"Couldn't import Django. Are you sure it's installed?"
|
|
14
|
+
) from exc
|
|
15
|
+
execute_from_command_line(sys.argv)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "myproject"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = ">=3.13"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"django>=6.0.2",
|
|
7
|
+
"uuid6>=2025.0.1",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[build-system]
|
|
11
|
+
requires = ["hatchling"]
|
|
12
|
+
build-backend = "hatchling.build"
|
|
13
|
+
|
|
14
|
+
[tool.hatch.build.targets.wheel]
|
|
15
|
+
packages = ["core", "djm", "base"]
|
djm-0.0.1a0/manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Django's command-line utility for administrative tasks."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
"""Run administrative tasks."""
|
|
9
|
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
|
10
|
+
try:
|
|
11
|
+
from django.core.management import execute_from_command_line
|
|
12
|
+
except ImportError as exc:
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"Couldn't import Django. Are you sure it's installed and "
|
|
15
|
+
"available on your PYTHONPATH environment variable? Did you "
|
|
16
|
+
"forget to activate a virtual environment?"
|
|
17
|
+
) from exc
|
|
18
|
+
execute_from_command_line(sys.argv)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == '__main__':
|
|
22
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "djm"
|
|
3
|
+
version = "0.0.1-alpha"
|
|
4
|
+
description = "Generate goose-style SQL migrations from Django migrations, plus base model mixins"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"django>=6.0.2",
|
|
9
|
+
"uuid6>=2025.0.1",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = []
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
djm = "djm.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["djm", "base"]
|
djm-0.0.1a0/uv.lock
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.13"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "asgiref"
|
|
7
|
+
version = "3.11.1"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "django"
|
|
16
|
+
version = "6.0.2"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
dependencies = [
|
|
19
|
+
{ name = "asgiref" },
|
|
20
|
+
{ name = "sqlparse" },
|
|
21
|
+
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
|
22
|
+
]
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/26/3e/a1c4207c5dea4697b7a3387e26584919ba987d8f9320f59dc0b5c557a4eb/django-6.0.2.tar.gz", hash = "sha256:3046a53b0e40d4b676c3b774c73411d7184ae2745fe8ce5e45c0f33d3ddb71a7", size = 10886874, upload-time = "2026-02-03T13:50:31.596Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "djm"
|
|
30
|
+
version = "0.0.1a0"
|
|
31
|
+
source = { editable = "." }
|
|
32
|
+
dependencies = [
|
|
33
|
+
{ name = "django" },
|
|
34
|
+
{ name = "uuid6" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[package.metadata]
|
|
38
|
+
requires-dist = [
|
|
39
|
+
{ name = "django", specifier = ">=6.0.2" },
|
|
40
|
+
{ name = "uuid6", specifier = ">=2025.0.1" },
|
|
41
|
+
]
|
|
42
|
+
provides-extras = ["dev"]
|
|
43
|
+
|
|
44
|
+
[[package]]
|
|
45
|
+
name = "sqlparse"
|
|
46
|
+
version = "0.5.5"
|
|
47
|
+
source = { registry = "https://pypi.org/simple" }
|
|
48
|
+
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
|
49
|
+
wheels = [
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[[package]]
|
|
54
|
+
name = "tzdata"
|
|
55
|
+
version = "2025.3"
|
|
56
|
+
source = { registry = "https://pypi.org/simple" }
|
|
57
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
|
58
|
+
wheels = [
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[[package]]
|
|
63
|
+
name = "uuid6"
|
|
64
|
+
version = "2025.0.1"
|
|
65
|
+
source = { registry = "https://pypi.org/simple" }
|
|
66
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ca/b7/4c0f736ca824b3a25b15e8213d1bcfc15f8ac2ae48d1b445b310892dc4da/uuid6-2025.0.1.tar.gz", hash = "sha256:cd0af94fa428675a44e32c5319ec5a3485225ba2179eefcf4c3f205ae30a81bd", size = 13932, upload-time = "2025-07-04T18:30:35.186Z" }
|
|
67
|
+
wheels = [
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/3d/b2/93faaab7962e2aa8d6e174afb6f76be2ca0ce89fde14d3af835acebcaa59/uuid6-2025.0.1-py3-none-any.whl", hash = "sha256:80530ce4d02a93cdf82e7122ca0da3ebbbc269790ec1cb902481fa3e9cc9ff99", size = 6979, upload-time = "2025-07-04T18:30:34.001Z" },
|
|
69
|
+
]
|