diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..3606920b --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,55 @@ +--- +name: Checks + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +on: + pull_request: + branches: + - main + - stable-* + tags: + - "*" + +jobs: + schema-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + + - name: Run schema checks + run: tox -e check + + type-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + + - name: Run schema checks + run: tox -e type diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 57a00abd..d33df570 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -5,7 +5,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true -on: # yamllint disable-line rule:truthy +on: pull_request: branches: - main @@ -14,8 +14,22 @@ on: # yamllint disable-line rule:truthy - "*" jobs: - linters: - name: Linters - uses: ansible-network/github_actions/.github/workflows/tox.yml@main - with: - envname: linters + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + + - name: Run unit tests + run: tox -e lint diff --git a/.github/workflows/units.yml b/.github/workflows/tests.yml similarity index 69% rename from .github/workflows/units.yml rename to .github/workflows/tests.yml index ff76bf51..817c064e 100644 --- a/.github/workflows/units.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Unit Tests +name: Tests on: pull_request: @@ -25,15 +25,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.13" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements/requirements-dev.txt + python -m pip install tox - - name: Run migrations - run: python manage.py migrate - - - name: Run core tests - run: python manage.py test core + - name: Run unit tests + run: tox -e test diff --git a/.gitignore b/.gitignore index 6afa2c4e..77c07540 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ pre-commit-user /.eggs .python-version - # Testing .cache .coverage @@ -49,3 +48,22 @@ tower-backup-* # DB db.sqlite3 +*.db + +# Environment variables +.env +.env.* +*.env + +# Linting +.mypy_cache/ + +# Logs and temporary files +*.log +tmp/ +temp/ + +# Virtual environments +.venv/ +venv/ +env/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cff965d0..658f4b44 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,15 +23,17 @@ repos: args: [--indent=4, --no-sort-keys] - id: trailing-whitespace - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.1.0 hooks: - id: black + args: [--config, pyproject.toml] - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: - id: flake8 + args: [--max-line-length, "90"] - repo: https://github.com/ikamensh/flynt/ rev: 1.0.1 @@ -42,9 +44,11 @@ repos: rev: 6.0.1 hooks: - id: isort - name: isort (python) + args: [--settings-path, pyproject.toml] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.17.0 hooks: - id: mypy + pass_filenames: false + args: [--config-file=pyproject.toml, --ignore-missing-imports, "."] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4a11826..a14d4654 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,13 @@ Hi there! We're excited to have you as a contributor. - [pattern-service](#pattern-service) - [Table of contents](#table-of-contents) - [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code) - - [Build and Run the Development Environment](#build-and-run-the-development-environment) - - [Clone the repo](#clone-the-repo) - - [Configure python environment](#configure-python-environment) - - [Configure and run the application](#configure-and-run-the-application) + - [Build and Run the Development Environment](#build-and-run-the-development-environment) + - [Clone the repo](#clone-the-repo) + - [Configure Python environment](#configure-python-environment) + - [Set env variables for development](#set-env-variables-for-development) + - [Configure and run the application](#configure-and-run-the-application) + - [Updating dependencies](#updating-dependencies) + - [Running tests, linters, and code checks](#running-tests-linters-and-code-checks) ## Things to know prior to submitting code @@ -20,15 +23,17 @@ Hi there! We're excited to have you as a contributor. - We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [codeofconduct@ansible.com](mailto:codeofconduct@ansible.com) - This repository uses a`pre-commit`, configuration, so ensure that you install pre-commit globally for your user, or by using pipx. -### Build and Run the Development Environment +## Build and Run the Development Environment -#### Clone the repo +### Clone the repo If you have not already done so, you will need to clone, or create a local copy, of the [pattern-service repository](https://github.com/ansible/pattern-service). For more on how to clone the repo, view [git clone help](https://git-scm.com/docs/git-clone). Once you have a local copy, run the commands in the following sections from the root of the project tree. -#### Configure python environment +### Configure Python environment + +Ensure you are using a supported Python version, defined in the [pyproject.toml file](./pyproject.toml). Create python virtual environment using one of the below commands: @@ -38,12 +43,37 @@ Set the virtual environment `source /path/to/virtualenv/bin/activate/` -Install required python modules +Install required python modules for development + +`pip install -r requirements/requirements-dev.txt` + +### Set env variables for development -`pip install -r ./requirements-all.txt` +Either create a .env file in the project root containing the following env variables, or export them to your shell env: -#### Configure and run the application +```bash +PATTERN_SERVICE_MODE=development +``` + +### Configure and run the application `python manage.py migrate && python manage.py runserver` -The application can be reached in your browser at `https://localhost:8000/` +The application can be reached in your browser at `https://localhost:8000/`. The Django admin UI is accessible at `https://localhost:8000/admin` and the available API endpoints will be listed in the 404 information at `http://localhost:8000/api/pattern-service/v1/`. + +## Updating dependencies + +Project dependencies for all environments are specified in the [pyproject.toml file](./pyproject.toml). A requirements.txt file is generated for each environment using pip-compile, to simplify dependency installation with pip. + +To add a new dependency: + +1. Add the package to the appropriate project or optional dependencies section of the pyproject.toml file, using dependency specifiers to constrain versions. +2. Update the requirements files with the command `make requirements`. This should update the relevant requirements.txt files in the project's requirements directory. + +## Running tests, linters, and code checks + +Unit tests, linters, type checks, and other checks can all be run via `tox`. To see the available `tox` commands for this project, run `tox list`. + +To run an individual tox command use the `-e` flag to specify the environment, for example: `tox -e test` to run tests with all supported python versions. +s +To run all tests and checks, simply run `tox` with no options. diff --git a/Makefile b/Makefile index 78f1c909..5f21cd8f 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,45 @@ -.PHONY: build build-multi run test clean install-deps lint push-quay login-quay push-quay-multi +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show this help message + @grep -hE '^[a-zA-Z0-9._-]+:.*?##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}' | \ + sort -u + + +# ------------------------------------- +# Container image +# ------------------------------------- -# Image name and tag CONTAINER_RUNTIME ?= podman -IMAGE_NAME ?= ansible-pattern-service +IMAGE_NAME ?= pattern-service IMAGE_TAG ?= latest -# Build the Docker image -build: +ensure-namespace: + @test -n "$$QUAY_NAMESPACE" || (echo "Error: QUAY_NAMESPACE is required to push quay.io" && exit 1) + +.PHONY: build +build: ## Build the container image @echo "Building container image..." $(CONTAINER_RUNTIME) build -t $(IMAGE_NAME):$(IMAGE_TAG) -f Dockerfile.dev --arch amd64 . -ensure-namespace: -ifndef QUAY_NAMESPACE -$(error QUAY_NAMESPACE is required to push quay.io) -endif - -# Clean up -clean: +.PHONY: clean +clean: ## Remove container image @echo "Cleaning up..." $(CONTAINER_RUNTIME) rmi -f $(IMAGE_NAME):$(IMAGE_TAG) || true -# Tag and push to Quay.io -push: ensure-namespace build - @echo "Tagging and pushing to registry..." +.PHONY: push +push: ensure-namespace build ## Tag and push container image to Quay.io + @echo "Tagging and pushing to quay.io/$(QUAY_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG)..." $(CONTAINER_RUNTIME) tag $(IMAGE_NAME):$(IMAGE_TAG) quay.io/$(QUAY_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG) $(CONTAINER_RUNTIME) push quay.io/$(QUAY_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG) + +# ------------------------------------- +# Dependencies +# ------------------------------------- + +.PHONY: requirements +requirements: ## Generate requirements.txt files from pyproject.toml + pip-compile -o requirements/requirements.txt pyproject.toml + pip-compile --extra dev --extra test -o requirements/requirements-dev.txt pyproject.toml + pip-compile --extra test -o requirements/requirements-test.txt pyproject.toml diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index cbb5e0a6..13a6bb20 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -16,150 +16,250 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Automation', + name="Automation", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')), - ('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')), - ('automation_type', models.CharField(choices=[('job_template', 'Job template')], max_length=200)), - ('automation_id', models.BigIntegerField()), - ('primary', models.BooleanField(default=False)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "automation_type", + models.CharField( + choices=[("job_template", "Job template")], max_length=200 + ), + ), + ("automation_id", models.BigIntegerField()), + ("primary", models.BooleanField(default=False)), ], options={ - 'ordering': ['id'], + "ordering": ["id"], }, ), migrations.CreateModel( - name='ControllerLabel', + name="ControllerLabel", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')), - ('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')), - ('label_id', models.BigIntegerField(unique=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ("label_id", models.BigIntegerField(unique=True)), ], options={ - 'ordering': ['id'], + "ordering": ["id"], }, ), migrations.CreateModel( - name='Pattern', + name="Pattern", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('collection_name', models.CharField(max_length=200)), - ('collection_version', models.CharField(max_length=50)), - ('collection_version_uri', models.CharField(blank=True, max_length=200)), - ('pattern_name', models.CharField(max_length=200)), - ('pattern_definition', models.JSONField(blank=True)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("collection_name", models.CharField(max_length=200)), + ("collection_version", models.CharField(max_length=50)), + ( + "collection_version_uri", + models.CharField(blank=True, max_length=200), + ), + ("pattern_name", models.CharField(max_length=200)), + ("pattern_definition", models.JSONField(blank=True)), ], options={ - 'ordering': ['id'], + "ordering": ["id"], }, ), migrations.CreateModel( - name='PatternInstance', + name="PatternInstance", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')), - ('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')), - ('organization_id', models.BigIntegerField()), - ('controller_project_id', models.BigIntegerField(blank=True)), - ('controller_ee_id', models.BigIntegerField(blank=True, null=True)), - ('credentials', models.JSONField()), - ('executors', models.JSONField(blank=True, null=True)), - ('controller_labels', models.ManyToManyField(blank=True, related_name='pattern_instances', to='core.controllerlabel')), - ( - 'created_by', + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ("organization_id", models.BigIntegerField()), + ("controller_project_id", models.BigIntegerField(blank=True)), + ("controller_ee_id", models.BigIntegerField(blank=True, null=True)), + ("credentials", models.JSONField()), + ("executors", models.JSONField(blank=True, null=True)), + ( + "controller_labels", + models.ManyToManyField( + blank=True, + related_name="pattern_instances", + to="core.controllerlabel", + ), + ), + ( + "created_by", models.ForeignKey( default=None, editable=False, - help_text='The user who created this resource.', + help_text="The user who created this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_created+', + related_name="%(app_label)s_%(class)s_created+", to=settings.AUTH_USER_MODEL, ), ), ( - 'modified_by', + "modified_by", models.ForeignKey( default=None, editable=False, - help_text='The user who last modified this resource.', + help_text="The user who last modified this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_modified+', + related_name="%(app_label)s_%(class)s_modified+", to=settings.AUTH_USER_MODEL, ), ), - ('pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pattern_instances', to='core.pattern')), + ( + "pattern", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="pattern_instances", + to="core.pattern", + ), + ), ], options={ - 'ordering': ['id'], + "ordering": ["id"], }, ), migrations.AddConstraint( - model_name='pattern', - constraint=models.UniqueConstraint(fields=('collection_name', 'collection_version', 'pattern_name'), name='unique_pattern_collection_version'), + model_name="pattern", + constraint=models.UniqueConstraint( + fields=("collection_name", "collection_version", "pattern_name"), + name="unique_pattern_collection_version", + ), ), migrations.AddField( - model_name='controllerlabel', - name='created_by', + model_name="controllerlabel", + name="created_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who created this resource.', + help_text="The user who created this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_created+', + related_name="%(app_label)s_%(class)s_created+", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( - model_name='controllerlabel', - name='modified_by', + model_name="controllerlabel", + name="modified_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who last modified this resource.', + help_text="The user who last modified this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_modified+', + related_name="%(app_label)s_%(class)s_modified+", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( - model_name='automation', - name='created_by', + model_name="automation", + name="created_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who created this resource.', + help_text="The user who created this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_created+', + related_name="%(app_label)s_%(class)s_created+", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( - model_name='automation', - name='modified_by', + model_name="automation", + name="modified_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who last modified this resource.', + help_text="The user who last modified this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_modified+', + related_name="%(app_label)s_%(class)s_modified+", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( - model_name='automation', - name='pattern_instance', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='automations', to='core.patterninstance'), + model_name="automation", + name="pattern_instance", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="automations", + to="core.patterninstance", + ), ), migrations.AddConstraint( - model_name='patterninstance', - constraint=models.UniqueConstraint(fields=('organization_id', 'pattern'), name='unique_pattern_instance_organization'), + model_name="patterninstance", + constraint=models.UniqueConstraint( + fields=("organization_id", "pattern"), + name="unique_pattern_instance_organization", + ), ), ] diff --git a/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py b/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py index 099ff977..7b9186bc 100644 --- a/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py +++ b/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py @@ -11,80 +11,86 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ migrations.AddField( - model_name='pattern', - name='created', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, help_text='The date/time this resource was created.'), + model_name="pattern", + name="created", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + help_text="The date/time this resource was created.", + ), preserve_default=False, ), migrations.AddField( - model_name='pattern', - name='created_by', + model_name="pattern", + name="created_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who created this resource.', + help_text="The user who created this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_created+', + related_name="%(app_label)s_%(class)s_created+", to=settings.AUTH_USER_MODEL, ), ), migrations.AddField( - model_name='pattern', - name='modified', - field=models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.'), + model_name="pattern", + name="modified", + field=models.DateTimeField( + auto_now=True, help_text="The date/time this resource was created." + ), ), migrations.AddField( - model_name='pattern', - name='modified_by', + model_name="pattern", + name="modified_by", field=models.ForeignKey( default=None, editable=False, - help_text='The user who last modified this resource.', + help_text="The user who last modified this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_modified+', + related_name="%(app_label)s_%(class)s_modified+", to=settings.AUTH_USER_MODEL, ), ), migrations.AlterField( - model_name='automation', - name='automation_id', + model_name="automation", + name="automation_id", field=models.PositiveBigIntegerField(), ), migrations.AlterField( - model_name='controllerlabel', - name='label_id', + model_name="controllerlabel", + name="label_id", field=models.PositiveBigIntegerField(unique=True), ), migrations.AlterField( - model_name='pattern', - name='collection_version_uri', + model_name="pattern", + name="collection_version_uri", field=models.CharField(blank=True, max_length=200, null=True), ), migrations.AlterField( - model_name='pattern', - name='pattern_definition', + model_name="pattern", + name="pattern_definition", field=models.JSONField(blank=True, null=True), ), migrations.AlterField( - model_name='patterninstance', - name='controller_ee_id', + model_name="patterninstance", + name="controller_ee_id", field=models.PositiveBigIntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='patterninstance', - name='controller_project_id', + model_name="patterninstance", + name="controller_project_id", field=models.PositiveBigIntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='patterninstance', - name='organization_id', + model_name="patterninstance", + name="organization_id", field=models.PositiveBigIntegerField(), ), ] diff --git a/core/migrations/0003_task.py b/core/migrations/0003_task.py index a6bdcf0a..837b5732 100644 --- a/core/migrations/0003_task.py +++ b/core/migrations/0003_task.py @@ -10,50 +10,76 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('core', '0002_pattern_created_pattern_created_by_pattern_modified_and_more'), + ("core", "0002_pattern_created_pattern_created_by_pattern_modified_and_more"), ] operations = [ migrations.CreateModel( - name='Task', + name="Task", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, help_text='The date/time this resource was created.')), - ('created', models.DateTimeField(auto_now_add=True, help_text='The date/time this resource was created.')), ( - 'status', + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "status", models.CharField( - choices=[('Initiated', 'Initiated'), ('Running', 'Running'), ('Completed', 'Completed'), ('Failed', 'Failed')], max_length=20 + choices=[ + ("Initiated", "Initiated"), + ("Running", "Running"), + ("Completed", "Completed"), + ("Failed", "Failed"), + ], + max_length=20, ), ), - ('details', models.JSONField(blank=True, null=True)), + ("details", models.JSONField(blank=True, null=True)), ( - 'created_by', + "created_by", models.ForeignKey( default=None, editable=False, - help_text='The user who created this resource.', + help_text="The user who created this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_created+', + related_name="%(app_label)s_%(class)s_created+", to=settings.AUTH_USER_MODEL, ), ), ( - 'modified_by', + "modified_by", models.ForeignKey( default=None, editable=False, - help_text='The user who last modified this resource.', + help_text="The user who last modified this resource.", null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='%(app_label)s_%(class)s_modified+', + related_name="%(app_label)s_%(class)s_modified+", to=settings.AUTH_USER_MODEL, ), ), ], options={ - 'ordering': ['id'], + "ordering": ["id"], }, ), ] diff --git a/core/models.py b/core/models.py index 2ea1b58b..ecd8ebe6 100644 --- a/core/models.py +++ b/core/models.py @@ -6,58 +6,84 @@ class Pattern(CommonModel): class Meta: - app_label = 'core' - ordering = ['id'] - constraints = [models.UniqueConstraint(fields=["collection_name", "collection_version", "pattern_name"], name="unique_pattern_collection_version")] + app_label = "core" + ordering = ["id"] + constraints = [ + models.UniqueConstraint( + fields=["collection_name", "collection_version", "pattern_name"], + name="unique_pattern_collection_version", + ) + ] collection_name: models.CharField = models.CharField(max_length=200) collection_version: models.CharField = models.CharField(max_length=50) - collection_version_uri: models.CharField = models.CharField(max_length=200, blank=True, null=True) + collection_version_uri: models.CharField = models.CharField( + max_length=200, blank=True, null=True + ) pattern_name: models.CharField = models.CharField(max_length=200) pattern_definition: models.JSONField = models.JSONField(blank=True, null=True) class ControllerLabel(CommonModel): class Meta: - app_label = 'core' - ordering = ['id'] + app_label = "core" + ordering = ["id"] - label_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField(unique=True) + label_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField( + unique=True + ) class PatternInstance(CommonModel): class Meta: - app_label = 'core' - ordering = ['id'] - constraints = [models.UniqueConstraint(fields=["organization_id", "pattern"], name="unique_pattern_instance_organization")] + app_label = "core" + ordering = ["id"] + constraints = [ + models.UniqueConstraint( + fields=["organization_id", "pattern"], + name="unique_pattern_instance_organization", + ) + ] organization_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField() - controller_project_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField(blank=True, null=True) - controller_ee_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField(null=True, blank=True) + controller_project_id: models.PositiveBigIntegerField = ( + models.PositiveBigIntegerField(blank=True, null=True) + ) + controller_ee_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField( + null=True, blank=True + ) credentials: models.JSONField = models.JSONField() executors: models.JSONField = models.JSONField(null=True, blank=True) - pattern: models.ForeignKey = models.ForeignKey(Pattern, on_delete=models.CASCADE, related_name="pattern_instances") - controller_labels: models.ManyToManyField = models.ManyToManyField(ControllerLabel, related_name="pattern_instances", blank=True) + pattern: models.ForeignKey = models.ForeignKey( + Pattern, on_delete=models.CASCADE, related_name="pattern_instances" + ) + controller_labels: models.ManyToManyField = models.ManyToManyField( + ControllerLabel, related_name="pattern_instances", blank=True + ) class Automation(CommonModel): class Meta: - app_label = 'core' - ordering = ['id'] + app_label = "core" + ordering = ["id"] automation_type_choices = (("job_template", "Job template"),) - automation_type: models.CharField = models.CharField(max_length=200, choices=automation_type_choices) + automation_type: models.CharField = models.CharField( + max_length=200, choices=automation_type_choices + ) automation_id: models.PositiveBigIntegerField = models.PositiveBigIntegerField() primary: models.BooleanField = models.BooleanField(default=False) - pattern_instance: models.ForeignKey = models.ForeignKey(PatternInstance, on_delete=models.CASCADE, related_name="automations") + pattern_instance: models.ForeignKey = models.ForeignKey( + PatternInstance, on_delete=models.CASCADE, related_name="automations" + ) class Task(CommonModel): class Meta: - app_label = 'core' - ordering = ['id'] + app_label = "core" + ordering = ["id"] status_choices = ( ("Initiated", "Initiated"), diff --git a/core/serializers.py b/core/serializers.py index 38b4a382..f909b5da 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -13,47 +13,51 @@ class PatternSerializer(CommonModelSerializer): class Meta(CommonModelSerializer.Meta): model = Pattern fields = CommonModelSerializer.Meta.fields + [ - 'id', - 'collection_name', - 'collection_version', - 'collection_version_uri', - 'pattern_name', - 'pattern_definition', + "id", + "collection_name", + "collection_version", + "collection_version_uri", + "pattern_name", + "pattern_definition", ] - read_only_fields = ['pattern_definition', 'collection_version_uri'] + read_only_fields = ["pattern_definition", "collection_version_uri"] class ControllerLabelSerializer(CommonModelSerializer): class Meta(CommonModelSerializer.Meta): model = ControllerLabel - fields = CommonModelSerializer.Meta.fields + ['id', 'label_id'] + fields = CommonModelSerializer.Meta.fields + ["id", "label_id"] class PatternInstanceSerializer(CommonModelSerializer): class Meta(CommonModelSerializer.Meta): model = PatternInstance fields = CommonModelSerializer.Meta.fields + [ - 'id', - 'organization_id', - 'controller_project_id', - 'controller_ee_id', - 'controller_labels', - 'credentials', - 'executors', - 'pattern', + "id", + "organization_id", + "controller_project_id", + "controller_ee_id", + "controller_labels", + "credentials", + "executors", + "pattern", + ] + read_only_fields = [ + "controller_project_id", + "controller_ee_id", + "controller_labels", ] - read_only_fields = ['controller_project_id', 'controller_ee_id', 'controller_labels'] class AutomationSerializer(CommonModelSerializer): class Meta(CommonModelSerializer.Meta): model = Automation fields = CommonModelSerializer.Meta.fields + [ - 'id', - 'automation_type', - 'automation_id', - 'primary', - 'pattern_instance', + "id", + "automation_type", + "automation_id", + "primary", + "pattern_instance", ] @@ -61,6 +65,6 @@ class TaskSerializer(CommonModelSerializer): class Meta(CommonModelSerializer.Meta): model = Task fields = CommonModelSerializer.Meta.fields + [ - 'status', - 'details', + "status", + "details", ] diff --git a/core/tests/test_models.py b/core/tests/test_models.py index 2beaeeaa..e44e7f18 100644 --- a/core/tests/test_models.py +++ b/core/tests/test_models.py @@ -23,20 +23,20 @@ def setUpTestData(cls): def create_pattern_instance(cls, org_id, **kwargs): defaults = { - 'controller_project_id': 10, - 'controller_ee_id': 20, - 'credentials': {"user": "admin"}, - 'executors': [], - 'pattern': cls.pattern, + "controller_project_id": 10, + "controller_ee_id": 20, + "credentials": {"user": "admin"}, + "executors": [], + "pattern": cls.pattern, } defaults.update(kwargs) return PatternInstance.objects.create(organization_id=org_id, **defaults) def create_automation(cls, pattern_instance, **kwargs): defaults = { - 'automation_type': "job_template", - 'automation_id': 12345, - 'primary': False, + "automation_type": "job_template", + "automation_id": 12345, + "primary": False, } defaults.update(kwargs) return Automation.objects.create(pattern_instance=pattern_instance, **defaults) @@ -44,7 +44,10 @@ def create_automation(cls, pattern_instance, **kwargs): class PatternModelTestCase(SharedDataMixin, TestCase): def test_pattern_unique_info_constraint(self): - """Test that patterns with same collection_name, collection_version, and pattern_name cannot be created""" + """ + Test that patterns with same collection_name, collection_version, + and pattern_name cannot be created + """ with self.assertRaises(IntegrityError): Pattern.objects.create( collection_name="mynamespace.mycollection", @@ -55,15 +58,30 @@ def test_pattern_unique_info_constraint(self): def test_pattern_character_length_limits(self): with self.assertRaises(ValidationError): - pattern = Pattern(collection_name="a" * 201, collection_version="1.0.0", pattern_name="test_pattern", pattern_definition={}) + pattern = Pattern( + collection_name="a" * 201, + collection_version="1.0.0", + pattern_name="test_pattern", + pattern_definition={}, + ) pattern.full_clean() with self.assertRaises(ValidationError): - pattern = Pattern(collection_name="test.collection", collection_version="a" * 51, pattern_name="test_pattern", pattern_definition={}) + pattern = Pattern( + collection_name="test.collection", + collection_version="a" * 51, + pattern_name="test_pattern", + pattern_definition={}, + ) pattern.full_clean() with self.assertRaises(ValidationError): - pattern = Pattern(collection_name="test.collection", collection_version="1.0.0", pattern_name="a" * 201, pattern_definition={}) + pattern = Pattern( + collection_name="test.collection", + collection_version="1.0.0", + pattern_name="a" * 201, + pattern_definition={}, + ) pattern.full_clean() with self.assertRaises(ValidationError): @@ -85,7 +103,9 @@ def test_controller_label_unique_constraint(self): class PatternInstanceModelTestCase(SharedDataMixin, TestCase): def test_create_pattern_instance(self): - instance = self.create_pattern_instance(org_id=1, executors=[{"type": "podman"}]) + instance = self.create_pattern_instance( + org_id=1, executors=[{"type": "podman"}] + ) instance.controller_labels.add(self.label) self.assertEqual(instance.pattern, self.pattern) self.assertEqual(instance.organization_id, 1) @@ -93,7 +113,12 @@ def test_create_pattern_instance(self): self.assertIn(self.label, instance.controller_labels.all()) def test_cascade_delete_pattern_to_instances(self): - instance = self.create_pattern_instance(org_id=3, controller_project_id=30, controller_ee_id=40, credentials={"token": "xyz"}) + instance = self.create_pattern_instance( + org_id=3, + controller_project_id=30, + controller_ee_id=40, + credentials={"token": "xyz"}, + ) # Verify instance exists self.assertTrue(PatternInstance.objects.filter(id=instance.id).exists()) @@ -105,14 +130,27 @@ def test_cascade_delete_pattern_to_instances(self): self.assertFalse(PatternInstance.objects.filter(id=instance.id).exists()) def test_pattern_unique_org_id_constraint(self): - self.create_pattern_instance(org_id=1, controller_project_id=100, controller_ee_id=200, credentials={"token": "abc"}) + self.create_pattern_instance( + org_id=1, + controller_project_id=100, + controller_ee_id=200, + credentials={"token": "abc"}, + ) with self.assertRaises(IntegrityError): - self.create_pattern_instance(org_id=1, controller_project_id=101, controller_ee_id=201, credentials={"token": "def"}) + self.create_pattern_instance( + org_id=1, + controller_project_id=101, + controller_ee_id=201, + credentials={"token": "def"}, + ) def test_pattern_instance_null_fields(self): """Test creating pattern instance with null/optional fields""" instance = self.create_pattern_instance( - org_id=5, controller_project_id=None, controller_ee_id=None, executors=None # This can be null # This can be null # This can be null + org_id=5, + controller_project_id=None, # This can be null + controller_ee_id=None, # This can be null + executors=None, # This can be null ) self.assertIsNone(instance.controller_project_id) @@ -132,9 +170,16 @@ def test_pattern_instance_required_fields(self): class PatternAutomationModelTestCase(SharedDataMixin, TestCase): def test_cascade_delete_instance_to_automations(self): - instance = self.create_pattern_instance(org_id=4, controller_project_id=50, controller_ee_id=60, credentials={"token": "xyz"}) + instance = self.create_pattern_instance( + org_id=4, + controller_project_id=50, + controller_ee_id=60, + credentials={"token": "xyz"}, + ) - automation = self.create_automation(pattern_instance=instance, automation_id=99999, primary=True) + automation = self.create_automation( + pattern_instance=instance, automation_id=99999, primary=True + ) # Verify automation exists self.assertTrue(Automation.objects.filter(id=automation.id).exists()) @@ -157,7 +202,13 @@ def test_automation_character_length_limits(self): automation.full_clean() def test_create_automation(self): - instance = self.create_pattern_instance(org_id=2, controller_project_id=99, controller_ee_id=98, credentials={}, executors=[]) + instance = self.create_pattern_instance( + org_id=2, + controller_project_id=99, + controller_ee_id=98, + credentials={}, + executors=[], + ) automation = self.create_automation(pattern_instance=instance, primary=True) self.assertEqual(automation.automation_type, "job_template") self.assertTrue(automation.primary) @@ -195,7 +246,9 @@ def test_task_all_valid_status_choices(self): """Test all valid status choices for Task; covers both with and without details""" valid_statuses = ["Initiated", "Running", "Completed", "Failed"] for status in valid_statuses: - task_with_details = Task.objects.create(status=status, details={"test": "data"}) + task_with_details = Task.objects.create( + status=status, details={"test": "data"} + ) self.assertEqual(task_with_details.status, status) task_with_details.delete() diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index 605d96b5..b9395130 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -38,37 +38,44 @@ def test_serializer_fields_present(self): serializer = PatternSerializer(instance=self.pattern) data = serializer.data - self.assertIn('id', data) - self.assertIn('collection_name', data) - self.assertIn('collection_version', data) - self.assertIn('collection_version_uri', data) - self.assertIn('pattern_name', data) - self.assertIn('pattern_definition', data) - - self.assertEqual(data['collection_name'], "mynamespace.mycollection") - self.assertEqual(data['collection_version'], "1.0.0") - self.assertEqual(data['collection_version_uri'], "https://example.com/mynamespace/mycollection/") - self.assertEqual(data['pattern_name'], "example_pattern") - self.assertEqual(data['pattern_definition'], {"Test": "Value"}) + self.assertIn("id", data) + self.assertIn("collection_name", data) + self.assertIn("collection_version", data) + self.assertIn("collection_version_uri", data) + self.assertIn("pattern_name", data) + self.assertIn("pattern_definition", data) + + self.assertEqual(data["collection_name"], "mynamespace.mycollection") + self.assertEqual(data["collection_version"], "1.0.0") + self.assertEqual( + data["collection_version_uri"], + "https://example.com/mynamespace/mycollection/", + ) + self.assertEqual(data["pattern_name"], "example_pattern") + self.assertEqual(data["pattern_definition"], {"Test": "Value"}) def test_pattern_definition_read_only(self): input_data = { "collection_name": "test-namespace.test-collection", "collection_version": "1.0.0", - "collection_version_uri": "https://example.com/test-namespace/test-collection/", + "collection_version_uri": ( + "https://example.com/test-namespace/test-collection/" + ), "pattern_name": "example_pattern", "pattern_definition": {"Test": "Value"}, } serializer = PatternSerializer(data=input_data) self.assertTrue(serializer.is_valid(), serializer.errors) - self.assertNotIn('pattern_definition', serializer.validated_data) + self.assertNotIn("pattern_definition", serializer.validated_data) def test_serializer_validation_success(self): input_data = { "collection_name": "test-namespace.test-collection", "collection_version": "1.0.0", - "collection_version_uri": "https://example.com/test-namespace/test-collection/", + "collection_version_uri": ( + "https://example.com/test-namespace/test-collection/" + ), "pattern_name": "example_pattern", "pattern_definition": {"Test": "Value"}, } @@ -85,16 +92,16 @@ def test_serializer_fields(self): serializer = ControllerLabelSerializer(instance=self.label) data = serializer.data - self.assertIn('id', data) - self.assertIn('label_id', data) - self.assertEqual(data['label_id'], 123) + self.assertIn("id", data) + self.assertIn("label_id", data) + self.assertEqual(data["label_id"], 123) def test_serializer_validation(self): - serializer = ControllerLabelSerializer(data={'label_id': 321}) + serializer = ControllerLabelSerializer(data={"label_id": 321}) self.assertTrue(serializer.is_valid(), serializer.errors) def test_valid_label_id(self): - serializer = ControllerLabelSerializer(data={'label_id': 5}) + serializer = ControllerLabelSerializer(data={"label_id": 5}) self.assertTrue(serializer.is_valid()) @@ -103,13 +110,13 @@ def test_serializer_fields(self): serializer = PatternInstanceSerializer(instance=self.pattern_instance) data = serializer.data - self.assertIn('id', data) - self.assertIn('organization_id', data) - self.assertIn('controller_project_id', data) - self.assertIn('controller_ee_id', data) - self.assertIn('pattern', data) - self.assertEqual(data['controller_project_id'], 123) - self.assertEqual(data['controller_ee_id'], 987) + self.assertIn("id", data) + self.assertIn("organization_id", data) + self.assertIn("controller_project_id", data) + self.assertIn("controller_ee_id", data) + self.assertIn("pattern", data) + self.assertEqual(data["controller_project_id"], 123) + self.assertEqual(data["controller_ee_id"], 987) def test_serializer_validation(self): input_data = { @@ -129,7 +136,7 @@ class AutomationSerializerTest(SharedTestFixture): def setUpTestData(cls): super().setUpTestData() cls.automation = Automation.objects.create( - automation_type='job_template', + automation_type="job_template", automation_id=321, primary=True, pattern_instance=cls.pattern_instance, @@ -139,32 +146,37 @@ def test_serializer_fields_present(self): serializer = AutomationSerializer(instance=self.automation) data = serializer.data - self.assertIn('id', data) - self.assertIn('automation_type', data) - self.assertIn('automation_id', data) - self.assertIn('primary', data) - self.assertIn('pattern_instance', data) + self.assertIn("id", data) + self.assertIn("automation_type", data) + self.assertIn("automation_id", data) + self.assertIn("primary", data) + self.assertIn("pattern_instance", data) - self.assertEqual(data['id'], self.automation.id) - self.assertEqual(data['automation_type'], self.automation.automation_type) - self.assertEqual(data['automation_id'], self.automation.automation_id) - self.assertEqual(data['primary'], self.automation.primary) + self.assertEqual(data["id"], self.automation.id) + self.assertEqual(data["automation_type"], self.automation.automation_type) + self.assertEqual(data["automation_id"], self.automation.automation_id) + self.assertEqual(data["primary"], self.automation.primary) def test_serializer_validation_success(self): - input_data = {'automation_type': 'job_template', 'automation_id': 123, 'primary': False, 'pattern_instance': self.pattern_instance.id} + input_data = { + "automation_type": "job_template", + "automation_id": 123, + "primary": False, + "pattern_instance": self.pattern_instance.id, + } serializer = AutomationSerializer(data=input_data) self.assertTrue(serializer.is_valid(), serializer.errors) def test_serializer_validation_failure(self): input_data = { - 'automation_type': '', - 'automation_id': '', - 'primary': False, + "automation_type": "", + "automation_id": "", + "primary": False, } serializer = AutomationSerializer(data=input_data) self.assertFalse(serializer.is_valid()) - self.assertIn('automation_type', serializer.errors) - self.assertIn('automation_id', serializer.errors) + self.assertIn("automation_type", serializer.errors) + self.assertIn("automation_id", serializer.errors) class TaskSerializerTest(SharedTestFixture): @@ -187,7 +199,9 @@ def test_serializer_validation_success(self): serializer = TaskSerializer(data=input_data) self.assertTrue(serializer.is_valid(), serializer.errors) self.assertEqual(serializer.validated_data["status"], "Running") - self.assertEqual(serializer.validated_data["details"], {"step": 1, "info": "in progress"}) + self.assertEqual( + serializer.validated_data["details"], {"step": 1, "info": "in progress"} + ) def test_serializer_invalid_status(self): input_data = { diff --git a/core/tests/test_views.py b/core/tests/test_views.py index 11a4dd1f..7040b766 100644 --- a/core/tests/test_views.py +++ b/core/tests/test_views.py @@ -40,7 +40,9 @@ def setUpTestData(cls): ) cls.task1 = Task.objects.create(status="Running", details={"progress": "50%"}) - cls.task2 = Task.objects.create(status="Completed", details={"result": "success"}) + cls.task2 = Task.objects.create( + status="Completed", details={"result": "success"} + ) cls.task3 = Task.objects.create(status="Failed", details={"error": "timeout"}) @@ -87,7 +89,10 @@ def test_pattern_create_view(self): def test_pattern_delete_view(self): # Create a separate pattern for deletion pattern_to_delete = Pattern.objects.create( - collection_name="delete.test", collection_version="1.0.0", pattern_name="deletable_pattern", pattern_definition={} + collection_name="delete.test", + collection_version="1.0.0", + pattern_name="deletable_pattern", + pattern_definition={}, ) url = reverse("pattern-detail", args=[pattern_to_delete.pk]) @@ -116,9 +121,9 @@ def test_label_detail_view(self): url = reverse("controllerlabel-detail", args=[self.label.id]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('id', response.data) - self.assertIn('label_id', response.data) - self.assertEqual(response.data['label_id'], 5) + self.assertIn("id", response.data) + self.assertIn("label_id", response.data) + self.assertEqual(response.data["label_id"], 5) def test_label_create_view(self): url = reverse("controllerlabel-list") @@ -189,7 +194,11 @@ def test_pattern_instance_create_view(self): def test_pattern_instance_delete_view(self): # Create a separate instance for deletion instance_to_delete = PatternInstance.objects.create( - organization_id=999, controller_project_id=111, controller_ee_id=222, credentials={"user": "deletable"}, pattern=self.pattern + organization_id=999, + controller_project_id=111, + controller_ee_id=222, + credentials={"user": "deletable"}, + pattern=self.pattern, ) url = reverse("patterninstance-detail", args=[instance_to_delete.pk]) @@ -197,11 +206,17 @@ def test_pattern_instance_delete_view(self): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) # Verify database change - instance should be deleted - self.assertFalse(PatternInstance.objects.filter(pk=instance_to_delete.pk).exists()) + self.assertFalse( + PatternInstance.objects.filter(pk=instance_to_delete.pk).exists() + ) def test_pattern_instance_create_view_with_invalid_pattern(self): url = reverse("patterninstance-list") - data = {"organization_id": 999, "credentials": {"user": "test"}, "pattern": 99999} # Non-existent pattern ID + data = { + "organization_id": 999, + "credentials": {"user": "test"}, + "pattern": 99999, + } # Non-existent pattern ID response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -222,7 +237,12 @@ def test_automation_detail_view(self): def test_automation_create_view(self): url = reverse("automation-list") - data = {"automation_type": "job_template", "automation_id": 1234, "primary": False, "pattern_instance": self.pattern_instance.id} + data = { + "automation_type": "job_template", + "automation_id": 1234, + "primary": False, + "pattern_instance": self.pattern_instance.id, + } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -236,7 +256,10 @@ def test_automation_create_view(self): def test_automation_delete_view(self): # Create a separate automation for deletion automation_to_delete = Automation.objects.create( - automation_type="job_template", automation_id=5555, primary=False, pattern_instance=self.pattern_instance + automation_type="job_template", + automation_id=5555, + primary=False, + pattern_instance=self.pattern_instance, ) url = reverse("automation-detail", args=[automation_to_delete.pk]) @@ -248,7 +271,11 @@ def test_automation_delete_view(self): def test_automation_create_view_with_invalid_pattern_instance(self): url = reverse("automation-list") - data = {"automation_type": "job_template", "automation_id": 1234, "pattern_instance": 99999} # Non-existent pattern instance ID + data = { + "automation_type": "job_template", + "automation_id": 1234, + "pattern_instance": 99999, + } # Non-existent pattern instance ID response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -265,21 +292,25 @@ def test_task_detail_view(self): url = reverse("task-detail", args=[self.task1.pk]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('id', response.data) - self.assertIn('status', response.data) - self.assertIn('details', response.data) + self.assertIn("id", response.data) + self.assertIn("status", response.data) + self.assertIn("details", response.data) def test_task_list_view_returns_all_tasks(self): url = reverse("task-list") response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) # Verify we get all created tasks - task_ids = [task['id'] for task in response.data] + task_ids = [task["id"] for task in response.data] expected_ids = [self.task1.id, self.task2.id, self.task3.id] self.assertEqual(sorted(task_ids), sorted(expected_ids)) def test_task_detail_view_for_different_statuses(self): - tasks_to_test = [(self.task1, "Running"), (self.task2, "Completed"), (self.task3, "Failed")] + tasks_to_test = [ + (self.task1, "Running"), + (self.task2, "Completed"), + (self.task3, "Failed"), + ] for task, expected_status in tasks_to_test: with self.subTest(status=expected_status): diff --git a/core/urls.py b/core/urls.py index 2d1b5c8c..6beb2bb9 100644 --- a/core/urls.py +++ b/core/urls.py @@ -7,10 +7,10 @@ from .views import TaskViewSet router = AssociationResourceRouter() -router.register(r'patterns', PatternViewSet, basename='pattern') -router.register(r'controllerlabels', ControllerLabelViewSet, basename='controllerlabel') -router.register(r'patterninstances', PatternInstanceViewSet, basename='patterninstance') -router.register(r'automations', AutomationViewSet, basename='automation') -router.register(r'tasks', TaskViewSet, basename='task') +router.register(r"patterns", PatternViewSet, basename="pattern") +router.register(r"controllerlabels", ControllerLabelViewSet, basename="controllerlabel") +router.register(r"patterninstances", PatternInstanceViewSet, basename="patterninstance") +router.register(r"automations", AutomationViewSet, basename="automation") +router.register(r"tasks", TaskViewSet, basename="task") urlpatterns = router.urls diff --git a/core/views.py b/core/views.py index 138b18d5..20ccf3a4 100644 --- a/core/views.py +++ b/core/views.py @@ -1,6 +1,7 @@ from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView from rest_framework import status from rest_framework.decorators import api_view +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet @@ -25,17 +26,21 @@ class PatternViewSet(CoreViewSet, ModelViewSet): queryset = Pattern.objects.all() serializer_class = PatternSerializer - def create(self, request, *args, **kwargs): + def create(self, request: Request, *args: tuple, **kwargs: dict) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) pattern = serializer.save() - task = Task.objects.create(status="Initiated", details={"model": "Pattern", "id": pattern.id}) + task = Task.objects.create( + status="Initiated", details={"model": "Pattern", "id": pattern.id} + ) return Response( { "task_id": task.id, - "message": "Pattern creation initiated. Check task status for progress.", + "message": ( + "Pattern creation initiated. Check task status for progress." + ), }, status=status.HTTP_202_ACCEPTED, ) @@ -50,19 +55,23 @@ class PatternInstanceViewSet(CoreViewSet, ModelViewSet): queryset = PatternInstance.objects.all() serializer_class = PatternInstanceSerializer - def create(self, request, *args, **kwargs): + def create(self, request: Request, *args: tuple, **kwargs: dict) -> Response: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # Save initial PatternInstance instance = serializer.save() # Create a Task entry to track this processing - task = Task.objects.create(status="Initiated", details={"model": "PatternInstance", "id": instance.id}) + task = Task.objects.create( + status="Initiated", details={"model": "PatternInstance", "id": instance.id} + ) return Response( { "task_id": task.id, - "message": "PatternInstance creation initiated. Check task status for progress.", + "message": ( + "PatternInstance creation initiated. Check task status for progress." + ), }, status=status.HTTP_202_ACCEPTED, ) @@ -79,10 +88,10 @@ class TaskViewSet(CoreViewSet, ReadOnlyModelViewSet): @api_view(["GET"]) -def ping(request): +def ping(request: Request) -> Response: return Response(data={"status": "ok"}, status=200) @api_view(["GET"]) -def test(request): +def test(request: Request) -> Response: return Response(data={"hello": "world"}, status=200) diff --git a/manage.py b/manage.py index bf4cf75d..bda1515b 100755 --- a/manage.py +++ b/manage.py @@ -4,7 +4,7 @@ import sys -def main(): +def main() -> None: """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pattern_service.settings") try: diff --git a/pattern_service/settings/__init__.py b/pattern_service/settings/__init__.py index 48e182c1..65454db8 100644 --- a/pattern_service/settings/__init__.py +++ b/pattern_service/settings/__init__.py @@ -1,7 +1,27 @@ +import os + from ansible_base.lib.dynamic_config import export from ansible_base.lib.dynamic_config import factory +from ansible_base.lib.dynamic_config import load_envvars +from ansible_base.lib.dynamic_config import load_standard_settings_files + +try: + from dotenv import load_dotenv + + load_dotenv() +except ImportError: + pass +os.environ["PATTERN_SERVICE_MODE"] = os.environ.get( + "PATTERN_SERVICE_MODE", "production" +) # Django Ansible Base Dynaconf settings -DYNACONF = factory(__name__, "PATTERN_SERVICE", settings_files=["defaults.py"]) -# manipulate DYNACONF as needed +DYNACONF = factory( + __name__, + "PATTERN_SERVICE", + environments=("development", "production", "testing"), + settings_files=["defaults.py"], +) +load_standard_settings_files(DYNACONF) +load_envvars(DYNACONF) export(__name__, DYNACONF) diff --git a/pattern_service/settings/defaults.py b/pattern_service/settings/defaults.py index 60dbe28a..fdf3c2eb 100644 --- a/pattern_service/settings/defaults.py +++ b/pattern_service/settings/defaults.py @@ -11,23 +11,10 @@ """ from pathlib import Path -from typing import List # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-_f^+pc=x%dd&p8ht4qv7rqr8&a%@j#lda6v!x9353m+)fm8&gk" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS: List[str] = ["localhost", "pattern-service", "127.0.0.1"] - +DEBUG = False # Application definition @@ -72,23 +59,14 @@ WSGI_APPLICATION = "pattern_service.wsgi.application" -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - - # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + "NAME": ( + "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" + ), }, { "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", @@ -123,3 +101,10 @@ # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} diff --git a/pattern_service/settings/development_defaults.py b/pattern_service/settings/development_defaults.py new file mode 100644 index 00000000..7b9760e7 --- /dev/null +++ b/pattern_service/settings/development_defaults.py @@ -0,0 +1,40 @@ +from pathlib import Path + +ALLOWED_HOSTS = ["localhost", "pattern-service", "127.0.0.1"] +BASE_DIR = Path(__file__).resolve().parent.parent +DEBUG = True +SECRET_KEY = "insecure" + +# Logging +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "{levelname} {name} {lineno} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": DEBUG, + "class": "logging.StreamHandler", + "formatter": "simple", + }, + }, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "loggers": { + "ansible_base": { + "handlers": ["console"], + "level": "DEBUG", + }, + "core": { + "handlers": ["console"], + "level": "DEBUG", + }, + "django": { + "handlers": ["console"], + "level": "INFO", + }, + }, +} diff --git a/pattern_service/settings/testing_defaults.py b/pattern_service/settings/testing_defaults.py new file mode 100644 index 00000000..480e4b87 --- /dev/null +++ b/pattern_service/settings/testing_defaults.py @@ -0,0 +1,5 @@ +from pathlib import Path + +ALLOWED_HOSTS = ["localhost", "pattern-service", "127.0.0.1"] +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = "insecure" diff --git a/pattern_service/urls.py b/pattern_service/urls.py index 9ac40eb0..4be21b51 100644 --- a/pattern_service/urls.py +++ b/pattern_service/urls.py @@ -24,7 +24,7 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/pattern-service/v1/", include('core.urls')), + path("api/pattern-service/v1/", include("core.urls")), path("ping/", ping), path("api/pattern-service/v1/test/", test), ] diff --git a/pyproject.toml b/pyproject.toml index ef592006..25c3a110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,158 @@ +# ------------------------------------- +# Project +# ------------------------------------- + +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + [project] name = "pattern_service" version = "0.1.0" -description = "Pattern Service Django project" +description = "Ansible Pattern Service" readme = "README.md" -requires-python = ">=3.10" +license = "Apache-2.0" +license-files = ["LICENSE.md"] +requires-python = ">=3.11,<3.14" dependencies = [ - "django-ansible-base==2025.5.8" + "django-ansible-base==2025.5.8", +] + +[project.urls] +Repository = "https://github.com/ansible/pattern-service" + + +# ------------------------------------- +# Project: optional dependencies +# ------------------------------------- + +[project.optional-dependencies] +dev = [ + "pip-tools>=7.4,<8.0", + "python-dotenv>=1.1.1,<2.0", +] +test = [ + "black>=24.0,<25.0", + "django-stubs>=5.2,<6.0", + "flake8>=6.0,<7.0", + "flynt>=1.0,<2.0", + "isort>=5.12,<6.0", + "mypy>=1.3,<2.0", + "pytest-django>=4.11,<5.0", + "tox>=4.27,<5.0", ] + +# ------------------------------------- +# Packaging +# ------------------------------------- + +[tool.setuptools] +packages = ["pattern_service"] + + +# ------------------------------------- +# Tools +# ------------------------------------- + [tool.black] -line-length = 160 -fast = true -skip-string-normalization = true +preview = true +target-version = ["py311", "py312", "py313"] [tool.isort] profile = "black" force_single_line = true -line_length = 120 -[build-system] -requires = ["setuptools>=61", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -packages = ["pattern_service"] +[tool.mypy] +exclude = ["tests/"] +exclude_gitignore = true +strict = true +disallow_any_generics = false +disallow_subclassing_any = false +disallow_untyped_decorators = false [[tool.mypy.overrides]] -module = ["ansible_base.*", "rest_framework.*"] +module = ["ansible_base.*", "dotenv.*"] ignore_missing_imports = true + + +# ------------------------------------- +# Tools: tox +# ------------------------------------- + +[tool.tox] +requires = ["tox>=4.26"] +env_list = ["check", "format", "lint", "test", "type"] + +[tool.tox.env_run_base] +package = "wheel" + +[tool.tox.env_run_base.set_env] +DJANGO_SETTINGS_MODULE = "pattern_service.settings" +PATTERN_SERVICE_MODE = "testing" +PIP_CONSTRAINT = "requirements/requirements-test.txt" + +[tool.tox.env.py311] +description = "run tests using Python 3.11" +deps = ["pytest-django"] +commands = [ + [ "pytest", "-v" ] +] + +[tool.tox.env.py312] +description = "run tests using Python 3.12" +deps = ["pytest-django"] +commands = [ + [ "pytest", "-v" ] +] + +[tool.tox.env.py313] +description = "run tests using Python 3.13" +deps = ["pytest-django"] +commands = [ + [ "pytest", "-v" ] +] + +[tool.tox.env.check] +description = "run pre-commit checks" +deps = ["pre-commit-uv"] +commands = [ + ["pre-commit", "run", "-a"] +] + +[tool.tox.env.format] +description = "format the codebase with black, flynt, and isort" +deps = ["black", "flynt", "isort"] +commands = [ + ["black", "."], + ["flynt", "."], + ["isort", "."], +] + +[tool.tox.env.lint] +description = "lint the codebase with black, flake8, flynt, and isort" +deps = ["black", "flake8", "flynt", "isort"] +commands = [ + ["black", "--check", "--diff", "--color", "."], + ["flake8", "--max-line-length", "90", "."], + ["flynt", "--dry-run", "--fail-on-change", "."], + ["isort", "--check-only", "--diff", "."], +] + +[tool.tox.env.test] +description = "run tests against all supported python versions" +deps = ["tox"] +commands = [ + [ "tox", "-e", "py311,py312,py313" ] +] + +[tool.tox.env.type] +description = "type check the codebase with mypy" +deps = [ + "django-stubs[compatible-mypy]", + "djangorestframework-stubs[compatible-mypy]", + "mypy", +] +commands = [ + ["mypy", "."] +] diff --git a/requirements/requirements-all.txt b/requirements/requirements-all.txt deleted file mode 100644 index da7d76cf..00000000 --- a/requirements/requirements-all.txt +++ /dev/null @@ -1,50 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.13 -# by the following command: -# -# pip-compile --output-file=requirements-all.txt requirements-dev.in requirements.in -# -asgiref==3.8.1 - # via django -astor==0.8.1 - # via flynt -black==24.10.0 - # via -r requirements-dev.in -click==8.2.1 - # via black -django==4.2.23 - # via - # -r requirements.in - # djangorestframework -djangorestframework==3.16.0 - # via -r requirements.in -flake8==6.1.0 - # via -r requirements-dev.in -flynt==1.0.2 - # via -r requirements-dev.in -isort==5.13.2 - # via -r requirements-dev.in -mccabe==0.7.0 - # via flake8 -mypy==1.16.0 - # via -r requirements-dev.in -mypy-extensions==1.1.0 - # via - # black - # mypy -packaging==25.0 - # via black -pathspec==0.12.1 - # via - # black - # mypy -platformdirs==4.3.8 - # via black -pycodestyle==2.11.1 - # via flake8 -pyflakes==3.1.0 - # via flake8 -sqlparse==0.5.3 - # via django -typing-extensions==4.13.2 - # via mypy diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in deleted file mode 100644 index 9f40fd49..00000000 --- a/requirements/requirements-dev.in +++ /dev/null @@ -1,5 +0,0 @@ -black>=24.0,<25.0 -isort>=5.12,<6.0 -flynt>=1,<2.0 -mypy>=1.3,<2.0 -flake8>=6.0,<7.0 diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index edd64442..88abde24 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -2,40 +2,133 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# pip-compile --extra=dev --extra=test --output-file=requirements/requirements-dev.txt pyproject.toml # +asgiref==3.8.1 + # via django astor==0.8.1 # via flynt black==24.10.0 - # via -r requirements-dev.in + # via pattern_service (pyproject.toml) +build==1.2.2.post1 + # via pip-tools +cachetools==6.1.0 + # via tox +cffi==1.17.1 + # via cryptography +chardet==5.2.0 + # via tox click==8.2.1 - # via black + # via + # black + # pip-tools +colorama==0.4.6 + # via tox +cryptography==45.0.4 + # via django-ansible-base +distlib==0.3.9 + # via virtualenv +django==4.2.23 + # via + # django-ansible-base + # django-crum + # django-stubs + # django-stubs-ext + # djangorestframework +django-ansible-base==2025.5.8 + # via pattern_service (pyproject.toml) +django-crum==0.7.9 + # via django-ansible-base +django-stubs==5.2.1 + # via pattern_service (pyproject.toml) +django-stubs-ext==5.2.1 + # via django-stubs +djangorestframework==3.16.0 + # via django-ansible-base +dynaconf==3.2.11 + # via django-ansible-base +filelock==3.18.0 + # via + # tox + # virtualenv flake8==6.1.0 - # via -r requirements-dev.in + # via pattern_service (pyproject.toml) flynt==1.0.2 - # via -r requirements-dev.in + # via pattern_service (pyproject.toml) +inflection==0.5.1 + # via django-ansible-base +iniconfig==2.1.0 + # via pytest isort==5.13.2 - # via -r requirements-dev.in + # via pattern_service (pyproject.toml) mccabe==0.7.0 # via flake8 mypy==1.16.0 - # via -r requirements-dev.in + # via pattern_service (pyproject.toml) mypy-extensions==1.1.0 # via # black # mypy packaging==25.0 - # via black + # via + # black + # build + # pyproject-api + # pytest + # tox pathspec==0.12.1 # via # black # mypy +pip-tools==7.4.1 + # via pattern_service (pyproject.toml) platformdirs==4.3.8 - # via black + # via + # black + # tox + # virtualenv +pluggy==1.6.0 + # via + # pytest + # tox pycodestyle==2.11.1 # via flake8 +pycparser==2.22 + # via cffi pyflakes==3.1.0 # via flake8 +pygments==2.19.2 + # via pytest +pyproject-api==1.9.1 + # via tox +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pytest==8.4.1 + # via pytest-django +pytest-django==4.11.1 + # via pattern_service (pyproject.toml) +python-dotenv==1.1.1 + # via pattern_service (pyproject.toml) +sqlparse==0.5.3 + # via + # django + # django-ansible-base +tox==4.27.0 + # via pattern_service (pyproject.toml) +types-pyyaml==6.0.12.20250516 + # via django-stubs typing-extensions==4.13.2 - # via mypy -django-ansible-base==2025.5.8 + # via + # django-stubs + # django-stubs-ext + # mypy +virtualenv==20.31.2 + # via tox +wheel==0.45.1 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt new file mode 100644 index 00000000..1ce5fad0 --- /dev/null +++ b/requirements/requirements-test.txt @@ -0,0 +1,115 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --extra=test --output-file=requirements/requirements-test.txt pyproject.toml +# +asgiref==3.9.1 + # via django +astor==0.8.1 + # via flynt +black==24.10.0 + # via pattern_service (pyproject.toml) +cachetools==6.1.0 + # via tox +cffi==1.17.1 + # via cryptography +chardet==5.2.0 + # via tox +click==8.2.1 + # via black +colorama==0.4.6 + # via tox +cryptography==45.0.5 + # via django-ansible-base +distlib==0.3.9 + # via virtualenv +django==4.2.23 + # via + # django-ansible-base + # django-crum + # django-stubs + # django-stubs-ext + # djangorestframework +django-ansible-base==2025.5.8 + # via pattern_service (pyproject.toml) +django-crum==0.7.9 + # via django-ansible-base +django-stubs==5.2.1 + # via pattern_service (pyproject.toml) +django-stubs-ext==5.2.1 + # via django-stubs +djangorestframework==3.16.0 + # via django-ansible-base +dynaconf==3.2.11 + # via django-ansible-base +filelock==3.18.0 + # via + # tox + # virtualenv +flake8==6.1.0 + # via pattern_service (pyproject.toml) +flynt==1.0.2 + # via pattern_service (pyproject.toml) +inflection==0.5.1 + # via django-ansible-base +iniconfig==2.1.0 + # via pytest +isort==5.13.2 + # via pattern_service (pyproject.toml) +mccabe==0.7.0 + # via flake8 +mypy==1.16.1 + # via pattern_service (pyproject.toml) +mypy-extensions==1.1.0 + # via + # black + # mypy +packaging==25.0 + # via + # black + # pyproject-api + # pytest + # tox +pathspec==0.12.1 + # via + # black + # mypy +platformdirs==4.3.8 + # via + # black + # tox + # virtualenv +pluggy==1.6.0 + # via + # pytest + # tox +pycodestyle==2.11.1 + # via flake8 +pycparser==2.22 + # via cffi +pyflakes==3.1.0 + # via flake8 +pygments==2.19.2 + # via pytest +pyproject-api==1.9.1 + # via tox +pytest==8.4.1 + # via pytest-django +pytest-django==4.11.1 + # via pattern_service (pyproject.toml) +sqlparse==0.5.3 + # via + # django + # django-ansible-base +tox==4.27.0 + # via pattern_service (pyproject.toml) +types-pyyaml==6.0.12.20250516 + # via django-stubs +typing-extensions==4.14.1 + # via + # django-stubs + # django-stubs-ext + # mypy +virtualenv==20.31.2 + # via tox diff --git a/requirements/requirements.in b/requirements/requirements.in deleted file mode 100644 index 9bffdcc0..00000000 --- a/requirements/requirements.in +++ /dev/null @@ -1 +0,0 @@ -django-ansible-base==2025.5.8 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 86f141e6..1414f87a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in +# pip-compile --output-file=requirements/requirements.txt pyproject.toml # asgiref==3.8.1 # via django @@ -16,7 +16,7 @@ django==4.2.23 # django-crum # djangorestframework django-ansible-base==2025.5.8 - # via -r requirements/requirements.in + # via pattern_service (pyproject.toml) django-crum==0.7.9 # via django-ansible-base djangorestframework==3.16.0 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b391f79e..00000000 --- a/tox.ini +++ /dev/null @@ -1,105 +0,0 @@ -[tox] -skipsdist = True -envlist = linters -requires = tox==4.26 - -[testenv] -setenv = - PIP_CONSTRAINT = {toxinidir}/requirements/requirements-all.txt - -[common] -code_dirs = {toxinidir}/pattern_service {toxinidir}/core - -[testenv:mypy] -deps = - -c {env:PIP_CONSTRAINT} - mypy - django-stubs[compatible-mypy] - djangorestframework-stubs[compatible-mypy] -skip_install = - true -commands = mypy -p core -p pattern_service - -[testenv:black] -depends = - flynt, isort -deps = - -c {env:PIP_CONSTRAINT} - black -commands = - black {[common]code_dirs} - -[testenv:black-lint] -deps = - {[testenv:black]deps} -commands = - black -v --check --diff {[common]code_dirs} - -[testenv:isort] -deps = - -c {env:PIP_CONSTRAINT} - isort -commands = - isort {[common]code_dirs} - -[testenv:isort-lint] -deps = - {[testenv:isort]deps} -commands = - isort --check-only --diff {[common]code_dirs} - -[testenv:flynt] -description = Apply flint (f-string) formatting -deps = - -c {env:PIP_CONSTRAINT} - flynt -commands = - flynt {posargs:{[common]code_dirs}} - -[testenv:flynt-lint] -deps = - flynt -commands = - flynt --dry-run --fail-on-change {[common]code_dirs} - -[testenv:linters] -deps = - -c {env:PIP_CONSTRAINT} - {[testenv:black]deps} - {[testenv:isort]deps} - {[testenv:mypy]deps} - {[testenv:flynt]deps} - {[testenv:flake8]deps} - pre-commit-uv -commands = - pre-commit run -a - {[testenv:mypy]commands} - {[testenv:black-lint]commands} - {[testenv:isort-lint]commands} - {[testenv:flynt-lint]commands} - {[testenv:flake8]commands} - -[testenv:flake8] -deps = - -c {env:PIP_CONSTRAINT} - flake8 -commands = - flake8 {[common]code_dirs} - -[testenv:pip-compile] -deps = - pip-tools -commands = - pip-compile --output-file=requirements/requirements.txt requirements/requirements.in - -[flake8] -# E123, E125 skipped as they are invalid PEP-8. -show-source = True -ignore = E123,E125,E203,E402,E501,E741,F401,F811,F841,W503 -max-line-length = 160 -builtins = _ - -[mypy] -disable_error_code = import-untyped -plugins = - mypy_drf_plugin.main