Skip to content

Commit 3eca7a0

Browse files
committed
feat(ci): add GitHub Actions workflows for CI, production, and test PyPI publishing with uv integration and update .gitignore and docs for development environment
1 parent 6ec8441 commit 3eca7a0

File tree

20 files changed

+621
-89
lines changed

20 files changed

+621
-89
lines changed

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version-file: ".python-version"
17+
- uses: astral-sh/setup-uv@v5
18+
- name: Sync dependencies
19+
run: uv sync --locked
20+
- name: Run tests
21+
run: uv run pytest
22+
- name: Run lint
23+
run: uv run ruff check .

.github/workflows/production.yml

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@ on:
77
jobs:
88
release:
99
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
1012
steps:
11-
- uses: actions/checkout@v1
12-
- name: Set up Python
13-
uses: actions/setup-python@v1
14-
with:
15-
python-version: '3.x'
16-
- name: Install dependencies
17-
run: |
18-
python -m pip install --upgrade pip
19-
pip install setuptools wheel twine
20-
- name: Build and publish
21-
env:
22-
TWINE_USERNAME: __token__
23-
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
24-
run: |
25-
python setup.py sdist bdist_wheel
26-
twine upload dist/*
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version-file: ".python-version"
17+
- uses: astral-sh/setup-uv@v5
18+
- name: Build package
19+
run: uv build
20+
- name: Publish package
21+
env:
22+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
23+
run: uv publish --trusted-publishing never

.github/workflows/publish-to-test-pypi.yml

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,17 @@ on: push
66
jobs:
77
release:
88
runs-on: ubuntu-latest
9+
permissions:
10+
contents: read
911
steps:
10-
- uses: actions/checkout@v1
11-
- name: Set up Python
12-
uses: actions/setup-python@v1
13-
with:
14-
python-version: '3.x'
15-
- name: Install dependencies
16-
run: |
17-
python -m pip install --upgrade pip
18-
pip install setuptools wheel twine
19-
- name: Build and publish
20-
env:
21-
TWINE_USERNAME: __token__
22-
TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }}
23-
TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/
24-
run: |
25-
python setup.py sdist bdist_wheel
26-
twine upload dist/*
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version-file: ".python-version"
16+
- uses: astral-sh/setup-uv@v5
17+
- name: Build package
18+
run: uv build
19+
- name: Publish package
20+
env:
21+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TEST_TOKEN }}
22+
run: uv publish --publish-url https://test.pypi.org/legacy/ --check-url https://test.pypi.org/simple/ --trusted-publishing never

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ celerybeat.pid
104104
# Environments
105105
.env
106106
.venv
107+
.uv-cache/
107108
env/
108109
venv/
109110
ENV/

CONTRIBUTING.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Contributing
2+
3+
## Prerequisites
4+
5+
- `uv` installed locally
6+
- Python `3.13` available, as declared in `.python-version`
7+
8+
## Bootstrap
9+
10+
```bash
11+
uv sync --locked
12+
```
13+
14+
If your environment cannot write to the default uv cache location, prefix the commands with `UV_CACHE_DIR=.uv-cache`.
15+
16+
If you change dependencies in `pyproject.toml`, regenerate the lockfile with:
17+
18+
```bash
19+
uv lock
20+
uv sync
21+
```
22+
23+
## Daily commands
24+
25+
Run the unit tests:
26+
27+
```bash
28+
uv run pytest
29+
```
30+
31+
Run lint checks:
32+
33+
```bash
34+
uv run ruff check .
35+
```
36+
37+
Build the package:
38+
39+
```bash
40+
uv build
41+
```
42+
43+
## Project layout
44+
45+
- `django_dbml/management/commands/dbml.py`: command that inspects Django model metadata and renders DBML
46+
- `django_dbml/utils.py`: helper utilities used by the generator
47+
- `tests/testapp/`: isolated Django app used to exercise the extension
48+
- `tests/test_command.py`: command-level tests
49+
- `tests/test_utils.py`: unit tests for helper behavior
50+
51+
## Release flow
52+
53+
The repository publishes from GitHub Actions using `uv build` and `uv publish`. Before releasing, run:
54+
55+
```bash
56+
uv run pytest
57+
uv run ruff check .
58+
uv build
59+
```
60+
61+
The detailed development guide lives in `docs/development.md`.

README.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,46 @@
22

33
This app can generate a DBML output for all installed models.
44

5-
## How to install and use?
5+
## Installation
66

7-
#### 1. Install the django-dbml package
8-
9-
```
7+
```bash
108
pip install django-dbml
119
```
1210

13-
#### 2. Put django_dbml on your django settings
11+
## Usage
12+
13+
Add `django_dbml` to `INSTALLED_APPS`:
1414

1515
```python
16-
'...',
17-
'django_dbml',
18-
'...',
16+
INSTALLED_APPS = [
17+
# ...
18+
"django_dbml",
19+
]
1920
```
2021

21-
#### 3. Run the command to generate a DBML schema based on your Django models
22+
Generate DBML from all installed models:
2223

2324
```bash
24-
$ python manage.py dbml
25+
python manage.py dbml
2526
```
2627

27-
To generate DBML for a subset of your models, specify one or more Django app
28-
names or models by app_label or app_label.ModelName. Related tables will still
28+
To generate DBML for a subset of your models, specify one or more Django app
29+
names or models by `app_label` or `app_label.ModelName`. Related tables will still
2930
be included in the DBML.
3031

32+
## Development
33+
34+
This repository is now managed with `uv`.
35+
36+
```bash
37+
uv sync --locked
38+
uv run pytest
39+
uv run ruff check .
40+
uv build
41+
```
42+
43+
Development instructions live in [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/development.md](docs/development.md).
44+
3145
# Thanks
3246

3347
The initial code was based on https://github.com/hamedsj/DbmlForDjango project

django_dbml/management/commands/dbml.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def get_app_tables(self, app_labels) -> list:
9999

100100
# if no apps are specified, process all models
101101
if not app_labels:
102-
return apps.get_models()
102+
return list(apps.get_models())
103103

104104
# get specific models when app or app.model is specified
105105
app_tables = []
@@ -114,13 +114,45 @@ def get_app_tables(self, app_labels) -> list:
114114
except LookupError as e:
115115
raise CommandError(str(e)) # noqa: B904
116116

117-
app_config = apps.get_app_config(app_label)
118117
if model_label:
119118
app_tables.append(app_config.get_model(model_label))
120119
else:
121120
app_tables.extend(app_config.get_models())
122121

123-
return app_tables
122+
return self.include_related_models(app_tables)
123+
124+
def include_related_models(self, models_to_process: list[type[Model]]) -> list[type[Model]]:
125+
"""Expand a selected set of models to include their forward-related models."""
126+
127+
ignore_types = (models.fields.reverse_related.ManyToOneRel, models.fields.reverse_related.ManyToManyRel)
128+
collected_models: list[type[Model]] = []
129+
pending_models = list(models_to_process)
130+
seen_models: set[type[Model]] = set()
131+
132+
while pending_models:
133+
model = pending_models.pop(0)
134+
if model in seen_models:
135+
continue
136+
137+
seen_models.add(model)
138+
collected_models.append(model)
139+
140+
for field in model._meta.get_fields():
141+
if isinstance(field, ignore_types):
142+
continue
143+
144+
if isinstance(field, (models.fields.related.ForeignKey, models.fields.related.OneToOneField)):
145+
pending_models.append(field.related_model)
146+
continue
147+
148+
if isinstance(field, models.fields.related.ManyToManyField):
149+
pending_models.append(field.related_model)
150+
151+
through_model = field.remote_field.through
152+
if through_model is not None and not through_model._meta.auto_created:
153+
pending_models.append(through_model)
154+
155+
return collected_models
124156

125157
def get_tl_module_name(self, model: Model) -> str:
126158
"""Get top level module of model."""
@@ -173,8 +205,8 @@ def choices_to_markdown_table(self, choices: list) -> str:
173205

174206
def handle(self, *app_labels, **kwargs): # noqa: D102, PLR0912, PLR0914, PLR0915
175207
self.options = kwargs
176-
project_name = self.options["add_project_name"]
177-
project_notes = self.options["add_project_notes"]
208+
project_name = self.options["add_project_name"] or "Django DBML"
209+
project_notes = self.options["add_project_notes"] or "Generated from Django models."
178210

179211
ignore_types = (models.fields.reverse_related.ManyToOneRel, models.fields.reverse_related.ManyToManyRel)
180212

@@ -392,7 +424,7 @@ def handle(self, *app_labels, **kwargs): # noqa: D102, PLR0912, PLR0914, PLR091
392424
if app_table.__doc__:
393425
tables[table_name]["note"] += f"\n{app_table.__doc__}"
394426

395-
if app_table._meta.db_table_comment:
427+
if getattr(app_table._meta, "db_table_comment", ""):
396428
comment = app_table._meta.db_table_comment.replace('"', '\"')
397429
tables[table_name]["note"] += f'\n\n*DB comment: {comment}*'
398430

@@ -495,4 +527,4 @@ def handle(self, *app_labels, **kwargs): # noqa: D102, PLR0912, PLR0914, PLR091
495527
f.write(output_string)
496528
logger.info('Generated dbml file to %s', output_file)
497529
else:
498-
print(output_string) # noqa: T201
530+
self.stdout.write(output_string, ending="")

django_dbml/tests.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/development.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Development Guide
2+
3+
## What this extension does
4+
5+
`django-dbml` is a Django app that converts the metadata exposed by Django models into DBML. The main entrypoint is the `dbml` management command.
6+
7+
## How the generator is organized
8+
9+
`django_dbml/management/commands/dbml.py` is responsible for:
10+
11+
1. Selecting which models should be part of the schema.
12+
2. Expanding the selection to include forward-related models.
13+
3. Mapping Django field classes to DBML field types.
14+
4. Rendering tables, enums, indexes, notes, and references.
15+
16+
`django_dbml/utils.py` contains the field-name normalization helper used during type mapping.
17+
18+
## Local development workflow
19+
20+
Install dependencies:
21+
22+
```bash
23+
uv sync --locked
24+
```
25+
26+
Run the full test suite:
27+
28+
```bash
29+
uv run pytest
30+
```
31+
32+
Run linting:
33+
34+
```bash
35+
uv run ruff check .
36+
```
37+
38+
Build artifacts locally:
39+
40+
```bash
41+
uv build
42+
```
43+
44+
If `uv` cannot write to the default cache directory in your environment, use:
45+
46+
```bash
47+
UV_CACHE_DIR=.uv-cache uv sync --locked
48+
UV_CACHE_DIR=.uv-cache uv run pytest
49+
```
50+
51+
If you update dependencies, refresh the lockfile before syncing again:
52+
53+
```bash
54+
uv lock
55+
uv sync
56+
```
57+
58+
## How to extend the command safely
59+
60+
When adding support for a new Django field or DBML feature:
61+
62+
1. Update the rendering logic in `django_dbml/management/commands/dbml.py`.
63+
2. Add or adjust models inside `tests/testapp/models.py` to cover the new metadata shape.
64+
3. Add assertions in `tests/test_command.py` for the rendered DBML.
65+
4. If the change is isolated to a helper, add a focused unit test in `tests/test_utils.py`.
66+
67+
Prefer command-level tests for behavior that depends on Django model metadata, because the package's value is in the final DBML output rather than in isolated internal methods.

0 commit comments

Comments
 (0)