diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5d93a6f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +# https://docs.docker.com/engine/reference/builder/#dockerignore-file +* +!django_outbox_pattern +!scripts +!pyproject.toml +!poetry.lock diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..8813207 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +/.github @juntossomosmais/core +* @juntossomosmais/loyalty-backends +* @juntossomosmais/loja-virtual-backends diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b3b393b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + time: "08:00" + day: "sunday" + target-branch: "main" + reviewers: + - "juntossomosmais/loyalty" + - "juntossomosmais/loja-virtual" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" + time: "08:00" + day: "sunday" + target-branch: "main" + reviewers: + - "juntossomosmais/loyalty" + - "juntossomosmais/loja-virtual" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..096825c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,24 @@ +# Infos + +## [Task name on sprint board](https://juntossomosmais.monday.com/boards/) + +### What is being delivered? + +- Describe here all the changes that will be made and what the expected result is. + +### What impacts? + +- Describe what impacts this delivery has and whether it can cause side effects in other parts of the application. + +### Reversal plan + +- Describe which plan we should follow if this delivery has to be reversed. +Ex.: Revert applied migrations, revert commits, redeploy the application via Azure DevOps + +### Where to monitor + +- Describe how this delivery can be monitored and how to analyze the data to ensure it works. + +- Ex.: [Specific Kibana query name]() + +
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2fd586..7de7d66 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,16 +8,26 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - - repo: https://github.com/pycqa/pylint - rev: v3.2.6 - hooks: - - id: pylint - additional_dependencies: [ django, 'stomp.py' ] - exclude: migrations - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: - id: flake8 + additional_dependencies: [ + 'flake8-bugbear', + 'flake8-comprehensions', + 'flake8-debugger', + ] + - repo: https://github.com/pycqa/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + args: + [ + "--in-place", + "--remove-all-unused-imports", + "--recursive", + "--ignore-init-module-imports", + ] - repo: https://github.com/psf/black rev: 24.8.0 hooks: @@ -27,3 +37,11 @@ repos: rev: 5.13.2 hooks: - id: isort + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.11.2 + hooks: + - id: mypy + additional_dependencies: + ["types-all", "types-pkg-resources==0.1.3"] + pass_filenames: false + args: ['.'] diff --git a/django_outbox_pattern/bases.py b/django_outbox_pattern/bases.py index c045a94..eb334cb 100644 --- a/django_outbox_pattern/bases.py +++ b/django_outbox_pattern/bases.py @@ -11,7 +11,7 @@ class Base: def __init__(self, connection, username, passcode): - self._credentials = dict(username=username, passcode=passcode) # pylint: disable=R1735 + self._credentials = {"username": username, "passcode": passcode} self.attempts = 0 self.connection = connection diff --git a/django_outbox_pattern/consumers.py b/django_outbox_pattern/consumers.py index cda31bf..47b45d4 100644 --- a/django_outbox_pattern/consumers.py +++ b/django_outbox_pattern/consumers.py @@ -1,6 +1,6 @@ -# pylint: disable=R0902 import json import logging + from datetime import timedelta from django import db @@ -68,7 +68,7 @@ def message_handler(self, body, headers): message_id, ) - except BaseException: # pylint: disable=broad-exception-caught + except Exception: logger.exception("An exception has been caught during callback processing flow") payload.nack() @@ -107,7 +107,6 @@ def _create_queue(self, destination, headers, queue_name=None): self._subscribe(destination, self.subscribe_id, headers, queue_name) def _subscribe(self, destination, subscribe_id, headers, queue_name=None, dlq=False): - # pylint: disable=too-many-arguments routing_key = destination.split("/")[-1] queue_name = queue_name if queue_name else routing_key if dlq: diff --git a/django_outbox_pattern/decorators.py b/django_outbox_pattern/decorators.py index 81d5984..0a31e6a 100644 --- a/django_outbox_pattern/decorators.py +++ b/django_outbox_pattern/decorators.py @@ -1,4 +1,5 @@ import json + from typing import List from typing import NamedTuple from typing import Optional diff --git a/django_outbox_pattern/exceptions.py b/django_outbox_pattern/exceptions.py index 9b39e2b..9341f4d 100644 --- a/django_outbox_pattern/exceptions.py +++ b/django_outbox_pattern/exceptions.py @@ -1,5 +1,5 @@ class ExceededSendAttemptsException(Exception): """Raised when the limit of attempts to send messages to the broker is exceeded""" - def __init__(self, attempts): # pylint: disable=super-init-not-called + def __init__(self, attempts): self.attempts = attempts diff --git a/django_outbox_pattern/management/commands/publish.py b/django_outbox_pattern/management/commands/publish.py index e8df6a0..6f86b4b 100644 --- a/django_outbox_pattern/management/commands/publish.py +++ b/django_outbox_pattern/management/commands/publish.py @@ -1,5 +1,6 @@ import logging import sys + from datetime import timedelta from time import sleep diff --git a/django_outbox_pattern/management/commands/subscribe.py b/django_outbox_pattern/management/commands/subscribe.py index 7c0c738..6f4bc74 100644 --- a/django_outbox_pattern/management/commands/subscribe.py +++ b/django_outbox_pattern/management/commands/subscribe.py @@ -1,4 +1,5 @@ import sys + from time import sleep from django.core.management.base import BaseCommand diff --git a/django_outbox_pattern/migrations/0006_published_published_status_27c9ec_btree.py b/django_outbox_pattern/migrations/0006_published_published_status_27c9ec_btree.py index c6429b3..22fec41 100644 --- a/django_outbox_pattern/migrations/0006_published_published_status_27c9ec_btree.py +++ b/django_outbox_pattern/migrations/0006_published_published_status_27c9ec_btree.py @@ -1,6 +1,7 @@ # Generated by Django 4.2.14 on 2024-07-15 16:14 import django.contrib.postgres.indexes + from django.contrib.postgres.operations import AddIndexConcurrently from django.db import migrations diff --git a/django_outbox_pattern/models.py b/django_outbox_pattern/models.py index a2cc6c8..4419410 100644 --- a/django_outbox_pattern/models.py +++ b/django_outbox_pattern/models.py @@ -1,4 +1,5 @@ import uuid + from datetime import timedelta from django.contrib.postgres.indexes import BTreeIndex diff --git a/django_outbox_pattern/producers.py b/django_outbox_pattern/producers.py index 94977f9..b9cb78c 100644 --- a/django_outbox_pattern/producers.py +++ b/django_outbox_pattern/producers.py @@ -1,5 +1,6 @@ import json import logging + from datetime import timedelta from time import sleep diff --git a/docker-compose.yml b/docker-compose.yml index a34c38d..5b627be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,7 +57,7 @@ services: build: . volumes: - .:/app - command: [ "./scripts/start-formatter-lint.sh" ] + command: ["./scripts/start-formatter-lint.sh"] networks: - djangooutboxpattern diff --git a/poetry.lock b/poetry.lock index 9c3f5ed..5bdd076 100644 --- a/poetry.lock +++ b/poetry.lock @@ -125,13 +125,13 @@ files = [ [[package]] name = "django" -version = "5.1" +version = "5.1.1" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.1-py3-none-any.whl", hash = "sha256:d3b811bf5371a26def053d7ee42a9df1267ef7622323fe70a601936725aa4557"}, - {file = "Django-5.1.tar.gz", hash = "sha256:848a5980e8efb76eea70872fb0e4bc5e371619c70fffbe48e3e1b50b2c09455d"}, + {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"}, + {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"}, ] [package.dependencies] @@ -261,6 +261,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -269,6 +270,8 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -459,4 +462,4 @@ test = ["websockets"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "677528d9fb8dcd499fbb72160669d5f42523939b59c7f3230e2f08a0f6a15b0d" +content-hash = "c76cd77b1fecefd82f19f64bb75933c57c1c003ad4aac49b99d49ad56ed7d1ad" diff --git a/pyproject.toml b/pyproject.toml index 558e7ce..7ba36c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-outbox-pattern" -version = "2.0.0" +version = "2.1.0" description = "A django application to make it easier to use the transactional outbox pattern" license = "MIT" authors = ["Hugo Brilhante "] @@ -36,7 +36,7 @@ django = ">=5.0.8" [tool.poetry.group.dev.dependencies] psycopg2-binary = "^2.9.6" coverage = "*" -pre-commit = "*" +pre-commit = "^3.8.0" [tool.black] line-length = 120 @@ -59,33 +59,53 @@ exclude = ''' ) ''' [tool.isort] -multi_line_output = 3 -include_trailing_comma = 'True' -force_grid_wrap = 0 force_single_line = true -use_parentheses = 'True' -ensure_newline_before_comments = 'True' line_length = 120 +py_version = 312 +use_parentheses = true +multi_line_output = 3 +include_trailing_comma = true +lines_between_types = 1 +sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -[tool.pylint.master] -ignore = [] +[tool.mypy] +python_version = "3.10" -[tool.pylint.messages_control] -disable = [ - "no-member", - "missing-class-docstring", - "missing-function-docstring", - "missing-module-docstring", - "too-few-public-methods", - "logging-not-lazy", - "consider-using-f-string", - "logging-fstring-interpolation", - "too-many-ancestors" -] +# flake8-mypy expects the two following for sensible formatting +show_column_numbers = true +show_error_context = false + +# do not follow imports (except for ones found in typeshed) +follow_imports = "skip" + +# suppress errors about unsatisfied imports +ignore_missing_imports = true + +# allow untyped calls as a consequence of the options above +disallow_untyped_calls = false + +# allow returning Any as a consequence of the options above +warn_return_any = false + +# treat Optional per PEP 484 +strict_optional = true + +# ensure all execution paths are returning +warn_no_return = true + +# lint-style cleanliness for typing needs to be disabled; returns more errors +# than the full run. +warn_redundant_casts = false +warn_unused_ignores = false -[tool.pylint.format] -max-line-length = "120" +# The following are off by default since they're too noisy. +# Flip them on if you feel adventurous. +disallow_untyped_defs = false +check_untyped_defs = false +[[tool.mypy.overrides]] +module = "*.migrations.*" +ignore_errors = true [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/scripts/start-formatter-lint.sh b/scripts/start-formatter-lint.sh index f8dd2c3..36a4cb5 100755 --- a/scripts/start-formatter-lint.sh +++ b/scripts/start-formatter-lint.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash - +set -e git config --global --add safe.directory /app pre-commit run --all-files diff --git a/tests/integration/test_consumer.py b/tests/integration/test_consumer.py index 4756172..161a1aa 100644 --- a/tests/integration/test_consumer.py +++ b/tests/integration/test_consumer.py @@ -13,7 +13,7 @@ def get_callback(raise_except=False): - def callback(payload): # pylint: disable=unused-argument + def callback(payload): if raise_except: raise KeyError("Test exception") payload.save() @@ -113,10 +113,10 @@ def test_consumer_message_should_remove_old_messages(self): def test_consumer_should_not_remove_old_message_when_cache_exists(self): self._create_message_in_the_past(35, 1) - self.consumer._remove_old_messages() # pylint: disable=W0212 + self.consumer._remove_old_messages() self.assertFalse(self.consumer.received_class.objects.filter(msg_id=1).exists()) self._create_message_in_the_past(35, 2) - self.consumer._remove_old_messages() # pylint: disable=W0212 + self.consumer._remove_old_messages() self.assertTrue(self.consumer.received_class.objects.filter(msg_id=2).exists()) def _create_message_in_the_past(self, day_ago, msg_id): @@ -133,7 +133,7 @@ def test_consumer_should_consume_message_event_if_a_exception_happen_in_cache(se destination = "/topic/consumer.v5" callback = get_callback() self.consumer.start(callback, destination) - with self.assertRaises(Exception): + with self.assertRaises(Exception): # noqa self.consumer.connection.send(destination=destination, body='{"message": "Message test no raise"}') sleep(1) self.listener.get_latest_message() diff --git a/tests/integration/test_producer.py b/tests/integration/test_producer.py index e2722d8..0312969 100644 --- a/tests/integration/test_producer.py +++ b/tests/integration/test_producer.py @@ -1,4 +1,5 @@ import json + from datetime import datetime from datetime import timedelta from unittest import mock @@ -112,10 +113,10 @@ def test_producer_should_remove_old_messages(self): def test_consumer_should_not_remove_old_message_when_cache_exists(self): message = self._create_message_in_the_past(31) producer = factory_producer() - producer._remove_old_messages() # pylint: disable=W0212 + producer._remove_old_messages() self.assertFalse(Published.objects.filter(id=message.id).exists()) message1 = self._create_message_in_the_past(31) - producer._remove_old_messages() # pylint: disable=W0212 + producer._remove_old_messages() self.assertTrue(Published.objects.filter(id=message1.id).exists()) @mock.patch("django_outbox_pattern.producers.cache") @@ -125,7 +126,7 @@ def test_producer_should_send_message_event_if_a_exception_happen_in_cache(self, producer.set_listener("test_listener", TestListener(print_to_log=True)) producer.connection.subscribe(destination=self.fake_destination, id=1) listener = producer.get_listener("test_listener") - with self.assertRaises(Exception): + with self.assertRaisesRegex(Exception, "Cache get failed"): producer.send_event(destination=self.fake_destination, body={"message": "fake message body"}) listener.wait_for_message() diff --git a/tests/integration/test_publish_command.py b/tests/integration/test_publish_command.py index 61ce139..7389242 100644 --- a/tests/integration/test_publish_command.py +++ b/tests/integration/test_publish_command.py @@ -1,4 +1,3 @@ -# pylint: disable=R0801 from io import StringIO from unittest.mock import MagicMock from unittest.mock import PropertyMock diff --git a/tests/unit/test_consumer.py b/tests/unit/test_consumer.py index bb94802..c33cf29 100644 --- a/tests/unit/test_consumer.py +++ b/tests/unit/test_consumer.py @@ -44,7 +44,7 @@ def test_consumer_message_handler_should_not_save_message_when_have_a_exception_ def callback(payload: Payload): with transaction.atomic(): payload.save() - raise StompException("mocked") # pylint: disable=broad-exception-raised + raise StompException("mocked") with self.assertLogs(level="ERROR") as log: self.consumer.callback = callback diff --git a/tests/unit/test_publish_command.py b/tests/unit/test_publish_command.py index 95af3a1..699e26e 100644 --- a/tests/unit/test_publish_command.py +++ b/tests/unit/test_publish_command.py @@ -1,4 +1,3 @@ -# pylint: disable=R0801 from io import StringIO from unittest.mock import MagicMock from unittest.mock import PropertyMock