├── tests ├── __init__.py ├── conftest.py ├── test_model_render.py ├── test_parse_fields.py ├── test_check_models_file.py ├── settings.py └── test_models_command.py ├── interactive_models ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── _render_model.py │ │ ├── _check_models_file.py │ │ ├── _parse_fields.py │ │ └── models.py ├── apps.py └── templates │ └── model_template.py.template ├── .flake8 ├── manage.py ├── .github └── workflows │ ├── release.yml │ └── testing.yml ├── LICENSE ├── .pre-commit-config.yaml ├── README.md ├── .gitignore └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /interactive_models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /interactive_models/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | -------------------------------------------------------------------------------- /interactive_models/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /interactive_models/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InteractiveModelsConfig(AppConfig): 5 | name = "interactive_models" 6 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def tmp_dir(tmp_path): 6 | app_dir = tmp_path / "my_app" 7 | app_dir.mkdir() 8 | return app_dir 9 | -------------------------------------------------------------------------------- /interactive_models/templates/model_template.py.template: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | {% autoescape off %} 4 | class {{ model_name }}(models.Model): 5 | {{ fields }} 6 | 7 | def __str__(self) -> str: 8 | return "{{ model_name }}" 9 | {% endautoescape %} 10 | -------------------------------------------------------------------------------- /interactive_models/management/commands/_render_model.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from django.template import loader 4 | 5 | TEMPLATE_NAME = "model_template.py.template" 6 | 7 | 8 | def render_model_file(raw_fields: dict[str, Optional[str]], model_name: str) -> str: 9 | fields = ("\n" + " " * 4).join( 10 | f"{key} = models.{value}()" for key, value in raw_fields.items() if value is not None 11 | ) 12 | 13 | ctx = { 14 | "fields": fields or "pass", 15 | "model_name": model_name, 16 | } 17 | 18 | return loader.render_to_string(TEMPLATE_NAME, context=ctx)[:-1] 19 | -------------------------------------------------------------------------------- /interactive_models/management/commands/_check_models_file.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | 4 | 5 | def _file_exists(path: str) -> bool: 6 | return os.path.exists(path) 7 | 8 | 9 | def _file_does_not_contain_models(path: str) -> bool: 10 | with open(path) as fileobj: 11 | file_contents = fileobj.read() 12 | 13 | models_ast = ast.parse(file_contents) 14 | 15 | return any( 16 | type(statement) not in (ast.Import, ast.ImportFrom) for statement in models_ast.body 17 | ) 18 | 19 | 20 | def check_models_file(path: str) -> bool: 21 | return _file_exists(path) and _file_does_not_contain_models(path) 22 | -------------------------------------------------------------------------------- /interactive_models/management/commands/_parse_fields.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | db_fields = { 4 | "auto": "AutoField", 5 | "bool": "BooleanField", 6 | "char": "CharField", 7 | "date": "DateField", 8 | "datetime": "DateTimeField", 9 | "email": "EmailField", 10 | "float": "FloatField", 11 | "int": "IntegerField", 12 | "slug": "SlugField", 13 | "text": "TextField", 14 | "time": "TimeField", 15 | "url": "URLField", 16 | "uuid": "UUIDField", 17 | } 18 | 19 | 20 | def parse_fields(raw_fields: str) -> dict[str, Optional[str]]: 21 | fields_dict = dict(field.split(":") for field in raw_fields) 22 | return {field_name: db_fields.get(db_field) for field_name, db_field in fields_dict.items()} 23 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 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", "tests.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() 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Upload to PyPI 3 | 4 | on: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | env: 18 | TWINE_USERNAME: __token__ 19 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v3 26 | with: 27 | python-version: "3.9" 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip setuptools twine build 32 | 33 | - name: Build package 34 | run: python -m build --wheel . 35 | 36 | - name: Check Build 37 | run: | 38 | twine check --strict dist/*.whl 39 | 40 | - name: Publish package 41 | run: | 42 | twine upload dist/*.whl 43 | -------------------------------------------------------------------------------- /tests/test_model_render.py: -------------------------------------------------------------------------------- 1 | from interactive_models.management.commands._render_model import render_model_file 2 | 3 | 4 | def test_model_render_no_fields(): 5 | rendered = render_model_file({}, "MyModel") 6 | 7 | assert rendered == ( 8 | "from django.db import models\n" 9 | "\n" 10 | "\n" 11 | "class MyModel(models.Model):\n" 12 | " pass\n" 13 | "\n" 14 | " def __str__(self) -> str:\n" 15 | ' return "MyModel"\n' 16 | ) 17 | 18 | 19 | def test_model_renders_with_fields(): 20 | fields = { 21 | "field1": "MyDBField", 22 | } 23 | rendered = render_model_file(fields, "MyModel") 24 | assert rendered == ( 25 | "from django.db import models\n" 26 | "\n" 27 | "\n" 28 | "class MyModel(models.Model):\n" 29 | " field1 = models.MyDBField()\n" 30 | "\n" 31 | " def __str__(self) -> str:\n" 32 | ' return "MyModel"\n' 33 | ) 34 | -------------------------------------------------------------------------------- /tests/test_parse_fields.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.db.models import fields 3 | 4 | from interactive_models.management.commands._parse_fields import db_fields 5 | from interactive_models.management.commands._parse_fields import parse_fields 6 | 7 | test_fields = ["body:text", "number_of_pages:int", "book_url:url"] 8 | 9 | all_raw_fields = [ 10 | "auto:auto", 11 | "bool:bool", 12 | "char:char", 13 | "date:date", 14 | "datetime:datetime", 15 | "email:email", 16 | "float:float", 17 | "int:int", 18 | "slug:slug", 19 | "text:text", 20 | "time:time", 21 | "url:url", 22 | "uuid:uuid", 23 | ] 24 | 25 | 26 | @pytest.mark.parametrize("django_field", db_fields.values()) 27 | def test_db_fields_exist_in_django(django_field): 28 | assert hasattr(fields, django_field) 29 | 30 | 31 | def test_parse_fields_with_empty_input(): 32 | raw_fields = parse_fields([]) 33 | assert raw_fields == {} 34 | 35 | 36 | def test_all_fields(): 37 | raw_fields = parse_fields(all_raw_fields) 38 | assert raw_fields == db_fields 39 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint and Test 3 | 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | pull_request: 9 | branches: 10 | - "main" 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12", "3.13"] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Upgrade pip 29 | run: python -m pip install --upgrade pip 30 | 31 | - name: Install dependencies 32 | run: pip install ".[test]" 33 | 34 | - name: Install pre-commit 35 | run: | 36 | pip install pre-commit 37 | pre-commit install-hooks 38 | 39 | - uses: pre-commit/action@v3.0.1 40 | 41 | - name: Lint Mypy 42 | run: | 43 | mypy . 44 | 45 | - name: Test with pytest 46 | run: pytest -v 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ferran Jovell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_check_models_file.py: -------------------------------------------------------------------------------- 1 | from interactive_models.management.commands._check_models_file import check_models_file 2 | 3 | 4 | def test_check_models_non_existing_file_is_false(tmp_dir): 5 | assert not check_models_file(tmp_dir / "models.py") 6 | 7 | 8 | def test_check_models_empty_file_is_false(tmp_dir): 9 | models_file = tmp_dir / "models.py" 10 | models_file.touch() 11 | 12 | assert not check_models_file(models_file) 13 | 14 | 15 | def test_check_models_file_contains_django_boilerplate_is_false(tmp_dir): 16 | models_file = tmp_dir / "models.py" 17 | models_file.touch() 18 | 19 | with open(models_file, "w") as fileobj: 20 | fileobj.write("from djagno.db import models\n\n\n# put your models here") 21 | 22 | assert not check_models_file(models_file) 23 | 24 | 25 | def test_check_models_file_contains_class_is_true(tmp_dir): 26 | models_file = tmp_dir / "models.py" 27 | models_file.touch() 28 | 29 | with open(models_file, "w") as fileobj: 30 | fileobj.write("from djagno.db import models\n\n\nclass MyModel(models.Model):\n pass") 31 | 32 | assert check_models_file(models_file) 33 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # Django test settings 2 | from pathlib import Path 3 | 4 | BASE_DIR = Path(__file__).resolve().parent.parent 5 | 6 | SECRET_KEY = "django-insecure-tesitng-key" 7 | 8 | DEBUG = False 9 | 10 | ALLOWED_HOSTS = ["*"] 11 | 12 | INSTALLED_APPS = [ 13 | "django.contrib.admin", 14 | "django.contrib.auth", 15 | "django.contrib.sessions", 16 | "django.contrib.messages", 17 | "django.contrib.contenttypes", 18 | "interactive_models", 19 | ] 20 | 21 | MIDDLEWARE = [ 22 | "django.contrib.auth.middleware.AuthenticationMiddleware", 23 | "django.contrib.messages.middleware.MessageMiddleware", 24 | "django.contrib.sessions.middleware.SessionMiddleware", 25 | ] 26 | 27 | TEMPLATES = [ 28 | { 29 | "BACKEND": "django.template.backends.django.DjangoTemplates", 30 | "DIRS": [], 31 | "APP_DIRS": True, 32 | "OPTIONS": { 33 | "context_processors": [ 34 | "django.template.context_processors.debug", 35 | "django.template.context_processors.request", 36 | "django.contrib.auth.context_processors.auth", 37 | "django.contrib.messages.context_processors.messages", 38 | ], 39 | }, 40 | }, 41 | ] 42 | 43 | TIME_ZONE = "Europe/Andorra" 44 | USE_TZ = True 45 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | - id: check-added-large-files 10 | - id: check-merge-conflict 11 | - repo: https://github.com/psf/black-pre-commit-mirror 12 | rev: 25.12.0 13 | hooks: 14 | - id: black 15 | - repo: https://github.com/PyCQA/autoflake 16 | rev: v2.3.1 17 | hooks: 18 | - id: autoflake 19 | args: 20 | - -r 21 | - -i 22 | - --remove-all-unused-imports 23 | - --remove-duplicate-keys 24 | - repo: https://github.com/asottile/reorder-python-imports 25 | rev: v3.16.0 26 | hooks: 27 | - id: reorder-python-imports 28 | - repo: https://github.com/asottile/pyupgrade 29 | rev: v3.21.2 30 | hooks: 31 | - id: pyupgrade 32 | args: ["--py39-plus"] 33 | - repo: https://github.com/adamchainz/django-upgrade 34 | rev: "1.29.1" 35 | hooks: 36 | - id: django-upgrade 37 | args: 38 | - --target-version 39 | - "5.2" 40 | - repo: https://github.com/PyCQA/flake8 41 | rev: 7.3.0 42 | hooks: 43 | - id: flake8 44 | additional_dependencies: 45 | - flake8-black 46 | - flake8-bugbear 47 | - flake8-builtins 48 | - flake8-comprehensions 49 | - flake8-print 50 | - flake8-variables-names 51 | - flake8-django 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Interactive Models 2 | 3 | This app provides a CLI through the `manage.py` script to create models from the command line. 4 | 5 | ## Usage: 6 | 7 | Install the package into the virtualenv: 8 | 9 | ``` 10 | pip install django-interactive-models 11 | ``` 12 | 13 | Then, add the app to the INSTALLED_APPS in settings: 14 | 15 | ```python 16 | INSTALLED_APPS = [ 17 | ... 18 | "interactive_models", 19 | ... 20 | ] 21 | ``` 22 | 23 | Then, you can finally run the `models` command like: 24 | 25 | ```console 26 | ./manage.py models : ... 27 | ``` 28 | 29 | Check the usage by using the `-h` flag. 30 | 31 | For example, this command: 32 | 33 | ```console 34 | ./manage.py models my_app MyModel number:int text:text page_url:url creation_date:datetime 35 | ``` 36 | 37 | Will create a `models.py` file inside my_app with the fields 38 | `number, text, page_url, creation_date` with their corresponding django database fields. 39 | 40 | ### Field types available: 41 | 42 | This is the current list of accepted types and their mappings 43 | 44 | - auto -> AutoField, 45 | - bool -> BooleanField, 46 | - char -> CharField, 47 | - date -> DateField, 48 | - datetime -> DateTimeField, 49 | - email -> EmailField, 50 | - float -> FloatField, 51 | - int -> IntegerField, 52 | - slug -> SlugField, 53 | - text -> TextField, 54 | - time -> TimeField, 55 | - url -> URLField, 56 | - uuid -> UUIDField, 57 | 58 | ## Running tests locally 59 | 60 | Install test dependencies with 61 | 62 | ```console 63 | pip install ".[test]" 64 | ``` 65 | 66 | Then, run tests using pytest with: 67 | 68 | ```console 69 | pytest -v 70 | ``` 71 | -------------------------------------------------------------------------------- /interactive_models/management/commands/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any 3 | 4 | from django.core.management.base import BaseCommand 5 | from django.core.management.base import CommandError 6 | from django.core.management.base import CommandParser 7 | 8 | from ._check_models_file import check_models_file 9 | from ._parse_fields import parse_fields 10 | from ._render_model import render_model_file 11 | 12 | 13 | class Command(BaseCommand): 14 | def add_arguments(self, parser: CommandParser) -> None: 15 | parser.add_argument( 16 | dest="app_name", 17 | help="App label where the model will be created in", 18 | type=str, 19 | ) 20 | parser.add_argument( 21 | "model_name", 22 | help="Name of the model", 23 | type=str, 24 | ) 25 | parser.add_argument( 26 | "model_fields", 27 | nargs="*", 28 | help="List of model fields with their corresponding type separated by colon.", 29 | type=str, 30 | ) 31 | parser.add_argument( 32 | "--override", 33 | default=False, 34 | action="store_true", 35 | help="Override existing files", 36 | ) 37 | 38 | def handle(self, *args: Any, **options: Any) -> None: 39 | app_name = options["app_name"] 40 | model_name = options["model_name"] 41 | 42 | models_path = f"{app_name}/models.py" 43 | 44 | if not os.path.isdir(app_name): 45 | error_msg = ( 46 | f"'{app_name}' does not exist!\n" 47 | "Create a new app by running:\n" 48 | f" $ ./manage.py startapp {app_name}\n" 49 | ) 50 | raise CommandError(error_msg) 51 | 52 | if not options["override"] and check_models_file(models_path): 53 | error_msg = ( 54 | f"Not overriding '{models_path}'\nuse `--override` to override this behavior" 55 | ) 56 | raise CommandError(error_msg) 57 | 58 | fields = parse_fields(options["model_fields"]) 59 | file_contents = render_model_file(fields, model_name) 60 | 61 | with open(f"{app_name}/models.py", "w") as fileobj: 62 | fileobj.writelines(file_contents) 63 | 64 | self.stdout.write(f"Generated {app_name}/models.py!") 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /tests/test_models_command.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.core.management import call_command 3 | from django.core.management.base import CommandError 4 | 5 | test_fields = ["body:text", "number_of_pages:int", "book_url:url"] 6 | 7 | 8 | @pytest.fixture 9 | def model_command_run(): 10 | def _(*args, **options): 11 | call_command("models", *args, **options) 12 | 13 | return _ 14 | 15 | 16 | def test_model_command_creates_file(tmp_dir, model_command_run, capsys): 17 | model_command_run(tmp_dir, "MyModel", test_fields) 18 | out, err = capsys.readouterr() 19 | assert out == f"Generated {tmp_dir}/models.py!\n" 20 | assert err == "" 21 | 22 | 23 | def test_models_command_outputs_message_to_sdtoud_on_success(tmp_dir, model_command_run): 24 | model_command_run(tmp_dir, "MyModel", test_fields) 25 | models_file = tmp_dir / "models.py" 26 | assert models_file.exists() 27 | 28 | 29 | def test_models_does_not_override_existing_models_file(tmp_path, model_command_run): 30 | models_file = tmp_path / "models.py" 31 | models_file.touch() 32 | with open(models_file, "w") as fileobj: 33 | fileobj.write("from django.db import models\n\n\nclass MyModel(models.Model):\n pass") 34 | models_file 35 | error_message = ( 36 | r"Not overriding '.+/models.py'[\S\s]+use `--override` to override this behavior" 37 | ) 38 | with pytest.raises(CommandError, match=error_message): 39 | model_command_run(tmp_path, "MYModel", "name:int", override=False) 40 | 41 | 42 | def test_models_does_override_existing_models_file_if_file_contains_no_models( 43 | tmp_path, model_command_run, capsys 44 | ): 45 | models_file = tmp_path / "models.py" 46 | models_file.touch() 47 | model_command_run(tmp_path, "MYModel", "name:int", override=False) 48 | out, err = capsys.readouterr() 49 | assert out == f"Generated {tmp_path}/models.py!\n" 50 | assert err == "" 51 | 52 | 53 | def test_models_overrides_file_when_flag_is_passed(tmp_path, model_command_run, capsys): 54 | models_file = tmp_path / "models.py" 55 | models_file.touch() 56 | model_command_run(tmp_path, "MYModel", "name:int", override=True) 57 | out, err = capsys.readouterr() 58 | assert out == f"Generated {tmp_path}/models.py!\n" 59 | assert err == "" 60 | 61 | 62 | def test_models_does_not_create_models_file_on_unexisting_app(model_command_run): 63 | error_message = ( 64 | r"'\w+' does not exist![\S\s]+" 65 | r"Create a new app by running[\S\s]+\$ ./manage.py startapp \w+" 66 | ) 67 | with pytest.raises(CommandError, match=error_message): 68 | model_command_run("my_app", "MYModel", "") 69 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "django-interactive-models" 3 | version = "0.3.0" 4 | description = "a CLI Tool to create django models interactively" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = { file = "LICENSE" } 8 | authors = [ 9 | {name = "Ferran Jovell", email = "ferran.jovell+gh@gmail.com"} 10 | ] 11 | keywords = ["django"] 12 | classifiers = [ 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Programming Language :: Python :: 3.10", 16 | "Programming Language :: Python :: 3.11", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Programming Language :: Python :: 3.14", 20 | "Operating System :: OS Independent", 21 | "Topic :: Software Development :: Libraries", 22 | "Topic :: Utilities", 23 | "Environment :: Web Environment", 24 | "Framework :: Django" 25 | ] 26 | dependencies = [ 27 | "Django>=3.2.15" 28 | ] 29 | 30 | [project.optional-dependencies] 31 | test = [ 32 | "pytest", 33 | "pytest-cov", 34 | "pytest-django", 35 | "mypy<=0.970", 36 | "django-stubs[compatible-mypy]", 37 | ] 38 | 39 | [tool.setuptools.package-data] 40 | interactive_models = ["templates/*"] 41 | 42 | [build-system] 43 | requires = ["setuptools>=63.0"] 44 | build-backend = "setuptools.build_meta" 45 | 46 | [tool.pytest.ini_options] 47 | DJANGO_SETTINGS_MODULE = "tests.settings" 48 | 49 | [tool.black] 50 | line-length = 99 51 | target_version = ["py38"] 52 | skip-string-normalization = false 53 | include = "\\.pyi?$" 54 | exclude = """ 55 | ( 56 | /( 57 | \\.eggs # exclude a few common directories in the 58 | | \\.git # root of the project 59 | | \\.hg 60 | | \\.mypy_cache 61 | | \\.tox 62 | | \\.venv 63 | | _build 64 | | buck-out 65 | | build 66 | | dist 67 | )/ 68 | ) 69 | """ 70 | 71 | [tool.isort] 72 | profile = "black" 73 | 74 | [tool.mypy] 75 | plugins = ["mypy_django_plugin.main"] 76 | files = "interactive_models" 77 | namespace_packages = true 78 | explicit_package_bases = true 79 | show_error_codes = true 80 | strict = true 81 | enable_error_code = [ 82 | "ignore-without-code", 83 | "redundant-expr", 84 | "truthy-bool", 85 | ] 86 | exclude = [ 87 | "tests/*", 88 | "manage.py", 89 | ] 90 | 91 | [tool.django-stubs] 92 | django_settings_module = "tests.settings" 93 | 94 | [tool.coverage.run] 95 | source = ["."] 96 | branch = true 97 | omit = [ 98 | "**/tests/*", 99 | "manage.py", 100 | ] 101 | 102 | [tool.coverage.report] 103 | skip_empty = true 104 | fail_under = 90 105 | 106 | [tool.coverage.html] 107 | directory = "artifacts/htmlcov/" 108 | --------------------------------------------------------------------------------