diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..9ae5924d --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,52 @@ +--- +name: Docker + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +on: # yamllint disable-line rule:truthy + pull_request: + branches: + - main + - stable-* + tags: + - "*" + +jobs: + build: + name: Build docker image + runs-on: ubuntu-latest + env: + image_name: "ansible-pattern-service-test" + image_tag: "${{ github.sha }}" + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Build the container image + run: make build + shell: bash + env: + CONTAINER_RUNTIME: docker + IMAGE_NAME: "${{ env.image_name }}" + IMAGE_TAG: "${{ env.image_tag }}" + + - name: Run the container image + run: docker run -d -p "8000:5000" --name ansible-pattern-service-api "${IMAGE_NAME}:${IMAGE_TAG}" + shell: bash + env: + IMAGE_NAME: "${{ env.image_name }}" + IMAGE_TAG: "${{ env.image_tag }}" + + - name: List running containers + run: docker ps -a + shell: bash + + - name: Wait for the services to be up and running + run: sleep 15s + shell: bash + + - name: Test the pattern service application + run: curl http://localhost:8000/ping/ + shell: bash diff --git a/.github/workflows/units.yml b/.github/workflows/units.yml index d73818d2..de8ed8d6 100644 --- a/.github/workflows/units.yml +++ b/.github/workflows/units.yml @@ -30,11 +30,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements/requirements-dev.txt - - - name: Run migrations - run: python manage.py migrate - - - name: Run core tests - run: python manage.py test core + python -m pip install -U tox + shell: bash + - name: Run unit tests + run: tox -e unit_tests -vv + shell: bash diff --git a/.gitignore b/.gitignore index 6afa2c4e..6e50fd8a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,8 @@ pre-commit-user *.py[c,o] /.eggs .python-version - +/.install +/.tests # Testing .cache diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 3453f514..00000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,21 +0,0 @@ -FROM registry.access.redhat.com/ubi9/ubi:latest - -WORKDIR /app - -COPY requirements/requirements.txt . - -RUN dnf install python3-pip -y - -RUN python3 -m pip install --no-cache-dir -r requirements.txt - -ADD core /app/core - -ADD pattern_service /app/pattern_service - -COPY manage.py . - -RUN python3 manage.py migrate - -EXPOSE 5000 - -CMD ["python3", "/app/manage.py", "runserver", "0.0.0.0:5000"] diff --git a/Makefile b/Makefile index 78f1c909..f3cd3352 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,19 @@ -.PHONY: build build-multi run test clean install-deps lint push-quay login-quay push-quay-multi +.PHONY: build clean push build_amd64 # Image name and tag CONTAINER_RUNTIME ?= podman IMAGE_NAME ?= ansible-pattern-service IMAGE_TAG ?= latest + # Build the Docker image -build: +build_amd64: @echo "Building container image..." - $(CONTAINER_RUNTIME) build -t $(IMAGE_NAME):$(IMAGE_TAG) -f Dockerfile.dev --arch amd64 . + $(CONTAINER_RUNTIME) build -t $(IMAGE_NAME):$(IMAGE_TAG) -f tools/docker/Dockerfile.dev --arch amd64 . -ensure-namespace: -ifndef QUAY_NAMESPACE -$(error QUAY_NAMESPACE is required to push quay.io) -endif +build: + @echo "Building container image..." + $(CONTAINER_RUNTIME) build -t $(IMAGE_NAME):$(IMAGE_TAG) -f tools/docker/Dockerfile.dev . # Clean up clean: @@ -25,3 +25,9 @@ push: ensure-namespace build @echo "Tagging and pushing to registry..." $(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) + +ensure-namespace: +ifndef QUAY_NAMESPACE + $(error QUAY_NAMESPACE is required to push quay.io) +endif + diff --git a/core/admin.py b/core/admin.py deleted file mode 100644 index 37252c7a..00000000 --- a/core/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib import admin - -from core import models - -admin.site.register(models.Pattern) -admin.site.register(models.ControllerLabel) -admin.site.register(models.PatternInstance) -admin.site.register(models.Automation) -admin.site.register(models.Task) diff --git a/pyproject.toml b/pyproject.toml index ef592006..d63a8cd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,142 @@ +[build-system] +requires = ["poetry-core>=2.0,<3.0"] +build-backend = "poetry.core.masonry.api" + [project] -name = "pattern_service" +name = "aap-pattern-service" version = "0.1.0" -description = "Pattern Service Django project" +description = "" readme = "README.md" -requires-python = ">=3.10" -dependencies = [ - "django-ansible-base==2025.5.8" +authors = [ + { name = "Ansible, Inc.", email = "Red Hat, Inc. " }, ] +requires-python = ">=3.11,<3.13" + +[project.scripts] +aap-pattern-service-manage = 'pattern_service.manage:main' + + +# ------------------------------------- +# Poetry: Metadata +# ------------------------------------- +[tool.poetry] +requires-poetry = ">=2.0,<3.0" +packages = [{ include = "pattern_service", from = "src" }] + + +# ------------------------------------- +# Poetry: Dependencies +# ------------------------------------- +[tool.poetry.extras] +all = ["psycopg"] +dev = ["psycopg-binary"] + +[tool.poetry.dependencies] +python = ">=3.11,<3.13" +django = ">=4.2,<4.3" +djangorestframework = "3.15.*" +drf-spectacular = ">=0.26.5,<0.27" +channels = { version = "4.0.*", extras = ["daphne"] } +psycopg-binary = { version = "*", optional = true } +django-filter = ">23.2,<24" +pydantic = ">=1.8.1,<1.11" +cryptography = ">=42,<43" +django-ansible-base = { git = "https://github.com/ansible/django-ansible-base.git", tag = "2025.5.8", extras = [ + "channel-auth", + "rbac", + "resource-registry", + "jwt-consumer", + "rest-filters", + "feature-flags", +] } +jinja2 = ">=3.1.3,<3.2" +django-split-settings = "^1.2.0" +pexpect = "^4.9.0" +python-gnupg = "^0.5.2" +autobahn = { git = "https://github.com/crossbario/autobahn-python.git", rev = "v24.4.2" } +psycopg = "^3.1.17" +xxhash = "3.4.*" +pyjwt = { version = "2.7.*", extras = ["crypto"] } +ecdsa = "0.18.*" +validators = "^0.34.0" +django-flags = "^5.0.13" +insights-analytics-collector = "^0.3.2" +distro = "^1.9.0" +dispatcherd = { version = "v2025.05.19", extras = ["pg_notify"] } + + +[tool.poetry.group.test.dependencies] +pytest = "*" +pytest-env = "*" +pytest-django = "*" +pytest-asyncio = "*" +requests = { version = "*", python = "<4.0" } +pytest-cov = "^4.1.0" +pytest-lazy-fixture = "^0.6.3" +requests-mock = "*" +httpie = "^3.2.3" + +[tool.poetry.group.lint.dependencies] +flake8 = "*" +isort = "*" +black = "*" +flake8-broken-line = { version = "*", python = "<4.0" } +flake8-string-format = "*" +# This is an experimental linter. +ruff = "*" +# The rull claims that the flake8 plugins listed below are re-implemented, +# These plugins will remain included until it's verified. +pep8-naming = "*" +flake8-bugbear = "*" +flake8-comprehensions = "*" +flake8-debugger = "*" +flake8-docstrings = "*" +flake8-eradicate = { version = "*", python = "<4.0" } +flake8-print = "*" + +[tool.poetry.group.dev.dependencies] +ipython = "*" + +# ------------------------------------- +# Tools +# ------------------------------------- [tool.black] -line-length = 160 -fast = true -skip-string-normalization = true +line-length = 79 +target-version = ["py39", "py310"] [tool.isort] profile = "black" -force_single_line = true -line_length = 120 +combine_as_imports = true +line_length = 79 -[build-system] -requires = ["setuptools>=61", "wheel"] -build-backend = "setuptools.build_meta" +[tool.ruff] +line-length = 79 + +[tool.ruff.lint] +select = [ + "E", + "F", + "D", # flake8-docstrings + "TID", # flake8-tidy-imports +] +extend-ignore = [ + "D1", # Missing docstrings errors +] + + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"src/pattern_service/core/migrations/*" = ["E501"] +"tests/**/*.py" = [ + "S101", # Asserts allowed in tests + "ARG", # Fixtures are not always used explicitly + "SLF001", # Call private methods in tests + "D", # Docstrings are not required in tests +] -[tool.setuptools] -packages = ["pattern_service"] +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "parents" -[[tool.mypy.overrides]] -module = ["ansible_base.*", "rest_framework.*"] -ignore_missing_imports = true +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/run_mypy.sh b/run_mypy.sh new file mode 100755 index 00000000..b1218d69 --- /dev/null +++ b/run_mypy.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -eux + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +rm -rf "${SCRIPT_DIR}/.install" +mkdir -p "${SCRIPT_DIR}/.install" +ln -s "${SCRIPT_DIR}/src/pattern_service" "${SCRIPT_DIR}/.install/pattern_service" +cd "${SCRIPT_DIR}/.install" +export MYPYPATH="${SCRIPT_DIR}/.install" +mypy -p pattern_service +rm -rf "${SCRIPT_DIR}/.install" \ No newline at end of file diff --git a/core/__init__.py b/src/pattern_service/__init__.py similarity index 100% rename from core/__init__.py rename to src/pattern_service/__init__.py diff --git a/pattern_service/asgi.py b/src/pattern_service/asgi.py similarity index 100% rename from pattern_service/asgi.py rename to src/pattern_service/asgi.py diff --git a/core/migrations/__init__.py b/src/pattern_service/core/__init__.py similarity index 100% rename from core/migrations/__init__.py rename to src/pattern_service/core/__init__.py diff --git a/src/pattern_service/core/admin.py b/src/pattern_service/core/admin.py new file mode 100644 index 00000000..987e24ee --- /dev/null +++ b/src/pattern_service/core/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from .models import Automation, ControllerLabel, Pattern, PatternInstance, Task + +admin.site.register(Pattern) +admin.site.register(ControllerLabel) +admin.site.register(PatternInstance) +admin.site.register(Automation) +admin.site.register(Task) diff --git a/core/apps.py b/src/pattern_service/core/apps.py similarity index 78% rename from core/apps.py rename to src/pattern_service/core/apps.py index c0ce093b..abe8e43d 100644 --- a/core/apps.py +++ b/src/pattern_service/core/apps.py @@ -3,4 +3,4 @@ class CoreConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "core" + name = "pattern_service.core" diff --git a/core/migrations/0001_initial.py b/src/pattern_service/core/migrations/0001_initial.py similarity index 100% rename from core/migrations/0001_initial.py rename to src/pattern_service/core/migrations/0001_initial.py diff --git a/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py b/src/pattern_service/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py similarity index 100% rename from core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py rename to src/pattern_service/core/migrations/0002_pattern_created_pattern_created_by_pattern_modified_and_more.py diff --git a/core/migrations/0003_task.py b/src/pattern_service/core/migrations/0003_task.py similarity index 100% rename from core/migrations/0003_task.py rename to src/pattern_service/core/migrations/0003_task.py diff --git a/core/tests/__init__.py b/src/pattern_service/core/migrations/__init__.py similarity index 100% rename from core/tests/__init__.py rename to src/pattern_service/core/migrations/__init__.py diff --git a/core/models.py b/src/pattern_service/core/models.py similarity index 97% rename from core/models.py rename to src/pattern_service/core/models.py index 2ea1b58b..76b65f2a 100644 --- a/core/models.py +++ b/src/pattern_service/core/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ansible_base.lib.abstract_models import CommonModel +from ansible_base.lib.abstract_models import CommonModel # type: ignore from django.db import models diff --git a/core/serializers.py b/src/pattern_service/core/serializers.py similarity index 98% rename from core/serializers.py rename to src/pattern_service/core/serializers.py index 38b4a382..2a6991cd 100644 --- a/core/serializers.py +++ b/src/pattern_service/core/serializers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from ansible_base.lib.serializers.common import CommonModelSerializer +from ansible_base.lib.serializers.common import CommonModelSerializer # type: ignore from .models import Automation from .models import ControllerLabel diff --git a/pattern_service/__init__.py b/src/pattern_service/core/tests/__init__.py similarity index 100% rename from pattern_service/__init__.py rename to src/pattern_service/core/tests/__init__.py diff --git a/core/tests/test_models.py b/src/pattern_service/core/tests/test_models.py similarity index 85% rename from core/tests/test_models.py rename to src/pattern_service/core/tests/test_models.py index 496ac3af..79d5124e 100644 --- a/core/tests/test_models.py +++ b/src/pattern_service/core/tests/test_models.py @@ -1,9 +1,7 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from core.models import ControllerLabel -from core.models import Pattern -from core.models import Task +from pattern_service.core.models import ControllerLabel, Pattern, Task class ModelTestCase(TestCase): @@ -23,7 +21,9 @@ def test_task_invalid_status_choice(self): task.full_clean() # triggers choice validation def test_task_status_choices_valid_running_with_info(self): - task = Task.objects.create(status="Running", details={"info": "in progress"}) + task = Task.objects.create( + status="Running", details={"info": "in progress"} + ) self.assertEqual(task.status, "Running") self.assertEqual(task.details["info"], "in progress") diff --git a/core/tests/test_serializers.py b/src/pattern_service/core/tests/test_serializers.py similarity index 64% rename from core/tests/test_serializers.py rename to src/pattern_service/core/tests/test_serializers.py index 605d96b5..5b8c7952 100644 --- a/core/tests/test_serializers.py +++ b/src/pattern_service/core/tests/test_serializers.py @@ -1,15 +1,19 @@ from django.test import TestCase -from core.models import Automation -from core.models import ControllerLabel -from core.models import Pattern -from core.models import PatternInstance -from core.models import Task -from core.serializers import AutomationSerializer -from core.serializers import ControllerLabelSerializer -from core.serializers import PatternInstanceSerializer -from core.serializers import PatternSerializer -from core.serializers import TaskSerializer +from pattern_service.core.models import ( + Automation, + ControllerLabel, + Pattern, + PatternInstance, + Task, +) +from pattern_service.core.serializers import ( + AutomationSerializer, + ControllerLabelSerializer, + PatternInstanceSerializer, + PatternSerializer, + TaskSerializer, +) class SharedTestFixture(TestCase): @@ -38,18 +42,21 @@ 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 = { @@ -62,7 +69,7 @@ def test_pattern_definition_read_only(self): 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 = { @@ -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,37 +146,46 @@ 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.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.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) 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): def test_serializer_fields_present(self): - task = Task.objects.create(status="Initiated", details={"info": "test"}) + task = Task.objects.create( + status="Initiated", details={"info": "test"} + ) serializer = TaskSerializer(instance=task) data = serializer.data @@ -187,7 +203,10 @@ 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/src/pattern_service/core/tests/test_views.py similarity index 84% rename from core/tests/test_views.py rename to src/pattern_service/core/tests/test_views.py index cff2a114..0a254436 100644 --- a/core/tests/test_views.py +++ b/src/pattern_service/core/tests/test_views.py @@ -2,11 +2,13 @@ from rest_framework import status from rest_framework.test import APITestCase -from core.models import Automation -from core.models import ControllerLabel -from core.models import Pattern -from core.models import PatternInstance -from core.models import Task +from pattern_service.core.models import ( + Automation, + ControllerLabel, + Pattern, + PatternInstance, + Task, +) class SharedDataMixin: @@ -39,9 +41,15 @@ def setUpTestData(cls): pattern_instance=cls.pattern_instance, ) - cls.task1 = Task.objects.create(status="Running", details={"progress": "50%"}) - cls.task2 = Task.objects.create(status="Completed", details={"result": "success"}) - cls.task3 = Task.objects.create(status="Failed", details={"error": "timeout"}) + cls.task1 = Task.objects.create( + status="Running", details={"progress": "50%"} + ) + cls.task2 = Task.objects.create( + status="Completed", details={"result": "success"} + ) + cls.task3 = Task.objects.create( + status="Failed", details={"error": "timeout"} + ) class TaskViewSetTest(SharedDataMixin, APITestCase): @@ -55,21 +63,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): @@ -95,7 +107,9 @@ def test_pattern_detail_view(self): url = reverse("pattern-detail", args=[self.pattern.pk]) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["collection_name"], "mynamespace.mycollection") + self.assertEqual( + response.data["collection_name"], "mynamespace.mycollection" + ) def test_pattern_create_view(self): url = reverse("pattern-list") @@ -132,7 +146,9 @@ def test_pattern_instance_list_view(self): self.assertEqual(len(response.data), 1) def test_pattern_instance_detail_view(self): - url = reverse("patterninstance-detail", args=[self.pattern_instance.pk]) + url = reverse( + "patterninstance-detail", args=[self.pattern_instance.pk] + ) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["organization_id"], 1) diff --git a/core/urls.py b/src/pattern_service/core/urls.py similarity index 88% rename from core/urls.py rename to src/pattern_service/core/urls.py index 2d1b5c8c..b68063aa 100644 --- a/core/urls.py +++ b/src/pattern_service/core/urls.py @@ -1,4 +1,4 @@ -from ansible_base.lib.routers import AssociationResourceRouter +from ansible_base.lib.routers import AssociationResourceRouter # type: ignore from .views import AutomationViewSet from .views import ControllerLabelViewSet diff --git a/core/views.py b/src/pattern_service/core/views.py similarity index 72% rename from core/views.py rename to src/pattern_service/core/views.py index 138b18d5..9aa81294 100644 --- a/core/views.py +++ b/src/pattern_service/core/views.py @@ -1,20 +1,19 @@ -from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView +from ansible_base.lib.utils.views.ansible_base import ( # type: ignore + AnsibleBaseView, +) from rest_framework import status from rest_framework.decorators import api_view from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet -from rest_framework.viewsets import ReadOnlyModelViewSet +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet -from .models import Automation -from .models import ControllerLabel -from .models import Pattern -from .models import PatternInstance -from .models import Task -from .serializers import AutomationSerializer -from .serializers import ControllerLabelSerializer -from .serializers import PatternInstanceSerializer -from .serializers import PatternSerializer -from .serializers import TaskSerializer +from .models import Automation, ControllerLabel, Pattern, PatternInstance, Task +from .serializers import ( + AutomationSerializer, + ControllerLabelSerializer, + PatternInstanceSerializer, + PatternSerializer, + TaskSerializer, +) class CoreViewSet(AnsibleBaseView): @@ -30,7 +29,9 @@ def create(self, request, *args, **kwargs): 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( { @@ -57,7 +58,10 @@ def create(self, request, *args, **kwargs): 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( { diff --git a/manage.py b/src/pattern_service/manage.py similarity index 100% rename from manage.py rename to src/pattern_service/manage.py diff --git a/pattern_service/settings/__init__.py b/src/pattern_service/settings/__init__.py similarity index 63% rename from pattern_service/settings/__init__.py rename to src/pattern_service/settings/__init__.py index 48e182c1..8ef61296 100644 --- a/pattern_service/settings/__init__.py +++ b/src/pattern_service/settings/__init__.py @@ -1,5 +1,4 @@ -from ansible_base.lib.dynamic_config import export -from ansible_base.lib.dynamic_config import factory +from ansible_base.lib.dynamic_config import export, factory # type: ignore # Django Ansible Base Dynaconf settings DYNACONF = factory(__name__, "PATTERN_SERVICE", settings_files=["defaults.py"]) diff --git a/pattern_service/settings/defaults.py b/src/pattern_service/settings/defaults.py similarity index 96% rename from pattern_service/settings/defaults.py rename to src/pattern_service/settings/defaults.py index 60dbe28a..841d133f 100644 --- a/pattern_service/settings/defaults.py +++ b/src/pattern_service/settings/defaults.py @@ -21,7 +21,9 @@ # 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" +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 @@ -39,7 +41,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "core", + "pattern_service.core", ] MIDDLEWARE = [ diff --git a/pattern_service/urls.py b/src/pattern_service/urls.py similarity index 80% rename from pattern_service/urls.py rename to src/pattern_service/urls.py index 9ac40eb0..149be144 100644 --- a/pattern_service/urls.py +++ b/src/pattern_service/urls.py @@ -16,15 +16,14 @@ """ from django.contrib import admin -from django.urls import include -from django.urls import path +from django.urls import include, path -from core.views import ping -from core.views import test +from pattern_service.core import urls as core_urls +from pattern_service.core.views import ping, test 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/pattern_service/wsgi.py b/src/pattern_service/wsgi.py similarity index 100% rename from pattern_service/wsgi.py rename to src/pattern_service/wsgi.py diff --git a/tools/docker/Dockerfile.dev b/tools/docker/Dockerfile.dev new file mode 100644 index 00000000..bb97b781 --- /dev/null +++ b/tools/docker/Dockerfile.dev @@ -0,0 +1,31 @@ +FROM registry.access.redhat.com/ubi9/ubi:latest + +WORKDIR /app + +COPY requirements/requirements.txt . + +RUN dnf install git python3.11 python3.11-devel python3.11-pip -y + +ENV VIRTUAL_ENV=/app/venv + +# create virtual environment +RUN python3.11 -m venv "$VIRTUAL_ENV" + +ENV PATH="${VIRTUAL_ENV}/bin:$PATH" + +RUN python -m pip install --no-cache-dir -r requirements.txt + +# install the pattern service application +ADD src /app/src + +COPY pyproject.toml /app/ + +COPY README.md /app/ + +RUN python -m pip install /app/ + +RUN aap-pattern-service-manage migrate + +EXPOSE 5000 + +CMD ["aap-pattern-service-manage", "runserver", "0.0.0.0:5000"] diff --git a/tox.ini b/tox.ini index b391f79e..a2ffab99 100644 --- a/tox.ini +++ b/tox.ini @@ -8,17 +8,17 @@ setenv = PIP_CONSTRAINT = {toxinidir}/requirements/requirements-all.txt [common] -code_dirs = {toxinidir}/pattern_service {toxinidir}/core +code_dirs = {toxinidir}/src/pattern_service [testenv:mypy] +allowlist_externals = bash deps = -c {env:PIP_CONSTRAINT} mypy django-stubs[compatible-mypy] djangorestframework-stubs[compatible-mypy] -skip_install = - true -commands = mypy -p core -p pattern_service +commands = bash {toxinidir}/run_mypy.sh + [testenv:black] depends = @@ -63,6 +63,7 @@ commands = flynt --dry-run --fail-on-change {[common]code_dirs} [testenv:linters] +allowlist_externals = {[testenv:mypy]allowlist_externals} deps = -c {env:PIP_CONSTRAINT} {[testenv:black]deps} @@ -87,10 +88,23 @@ commands = flake8 {[common]code_dirs} [testenv:pip-compile] -deps = +deps = pip-tools commands = pip-compile --output-file=requirements/requirements.txt requirements/requirements.in + pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in + +[testenv:unit_tests] +allowlist_externals = bash +setenv = + PS_FEATURE_DISPATCHERD = "false" +deps = + {toxinidir} + ; -r{toxinidir}/requirements/requirements-dev.txt +commands = + aap-pattern-service-manage makemigrations core --no-input + aap-pattern-service-manage migrate core --no-input + aap-pattern-service-manage test pattern_service.core --verbosity=2 [flake8] # E123, E125 skipped as they are invalid PEP-8.