From 7bdd9a22378a1b27cd5102c643175c92bb8db985 Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Mon, 7 Jul 2025 12:19:23 +0200 Subject: [PATCH 1/5] Use poetry and pytest --- .gitignore | 22 +- LICENSE.md | 2 +- Makefile | 19 + README.md | 74 +- drf_hooks/admin.py | 16 +- drf_hooks/client.py | 21 +- drf_hooks/migrations/0001_initial.py | 36 +- drf_hooks/migrations/0002_alter_hook_user.py | 2 +- drf_hooks/models.py | 92 +- drf_hooks/serializers.py | 4 +- drf_hooks/signals.py | 1 - drf_hooks/tests.py | 260 ------ drf_hooks/urls.py | 5 +- drf_hooks/utils.py | 19 +- drf_hooks/views.py | 3 + poetry.lock | 901 +++++++++++++++++++ pyproject.toml | 62 ++ requirements.txt | 6 - runtests.py | 63 -- setup.py | 44 +- tests/__init__.py | 0 tests/settings.py | 88 ++ tests/test_hooks.py | 227 +++++ 23 files changed, 1471 insertions(+), 496 deletions(-) create mode 100644 Makefile delete mode 100644 drf_hooks/tests.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100755 runtests.py create mode 100644 tests/__init__.py create mode 100644 tests/settings.py create mode 100644 tests/test_hooks.py diff --git a/.gitignore b/.gitignore index 05847e4..4119d28 100644 --- a/.gitignore +++ b/.gitignore @@ -7,27 +7,6 @@ ## Directory-based project format: .idea/ -# if you remove the above rule, at least ignore the following: - -# User-specific stuff: -# .idea/workspace.xml -# .idea/tasks.xml -# .idea/dictionaries -# .idea/shelf - -# Sensitive or high-churn files: -# .idea/dataSources.ids -# .idea/dataSources.xml -# .idea/sqlDataSources.xml -# .idea/dynamic.xml -# .idea/uiDesigner.xml - -# Gradle: -# .idea/gradle.xml -# .idea/libraries - -# Mongo Explorer plugin: -# .idea/mongoSettings.xml ## File-based project format: *.ipr @@ -118,3 +97,4 @@ target/ #Test Coverage tests/reports/ +/pytest_report.xml diff --git a/LICENSE.md b/LICENSE.md index 88bfc22..c2b2cdc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ ## ISC License -Copyright (c) 2021 AM-Flow b.v. +Copyright (c) 2021-2025 AM-Flow b.v. Copyright (c) 2016 Zapier Inc. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0775b82 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +PATH := $(PATH):$(HOME)/.local/bin +SHELL := env PATH=$(PATH) /bin/bash + +build: + poetry install + +format: + poetry run ruff check . --select I --fix + poetry run ruff format . + +lint: + poetry run ruff check . --diff + poetry run ruff format . --check --diff + +test: + poetry run pytest tests/ + +mypy: + poetry run mypy . diff --git a/README.md b/README.md index 4c3705d..eec72bf 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,16 @@ drf-hooks is a fork of [Zapier's django-rest-hooks](https://github.com/zapier/django-rest-hooks), which is unfortunately not maintained anymore. -drf-hooks adds closer DRF integration by allowing you to specify DRF serializers to use for each model rather than requiring a `serialize_hook()` method on your models. It also allows hooks to specify custom headers to be added in the hook request (for instance for authentication). - ## What are REST Hooks? REST Hooks are fancier versions of webhooks. Traditional webhooks are usually managed manually by the user, but REST Hooks are not! They encourage RESTful access to the hooks (or subscriptions) themselves. Add one, two or 15 hooks for -any combination of event and URLs, then get notificatied in real-time by our +any combination of event and URLs, then get notified in real-time by our bundled threaded callback mechanism. -The best part is: by reusing Django's great signals framework, this library is +The best part is: by reusing Django's signals framework, this library is dead simple. Here's how to get started: 1. Add `'drf_hooks'` to installed apps in settings.py. @@ -34,50 +32,48 @@ provides a handy framework or reference implementation for which to build upon. If you want to make a Django form or API resource, you'll need to do that yourself (though we've provided some example bits of code below). -### Changelog - -#### Version 0.1.0: - -- Forked from zapier rest hooks -- Support for DRF serializers -- Custom hook header support - ### Development -Running the tests for Django REST Hooks is very easy, just: +#### Running tests + +Clone the repo: ``` git clone https://github.com/am-flow/drf-hooks && cd drf-hooks ``` -Next, you'll want to make a virtual environment (we recommend using virtualenvwrapper -but you could skip this we suppose) and then install dependencies: +Install dependencies: ``` -mkvirtualenv drf-hooks -pip install -r requirements.txt +make build ``` -Now you can run the tests! +Run tests: ``` -python runtests.py +make tests ``` ### Requirements -* Python 3.7+ (tested on 3.7) -* Django 3.1+ (tested on 3.1) +* Python 3.9+ +* Django 3.1+ +* Django REST framework 3.11+ -### Installing & Configuring -We recommend pip to install drf-hooks: +### Installing & Configuring ``` pip install drf-hooks ``` +or + +``` +poetry add drf-hooks +``` + Next, you'll need to add `drf_hooks` to `INSTALLED_APPS` and configure your `HOOK_EVENTS` and `HOOK_SERIALIZER` setting: @@ -106,6 +102,9 @@ HOOK_SERIALIZERS = { ### bookstore/models.py ### +from django.db import models +from rest_framework import serializers + class Book(models.Model): # NOTE: it is important to have a user property @@ -134,7 +133,7 @@ class Book(models.Model): ### bookstore/serializers.py ### -class BookSerializer(serializers.ModelSerializer) +class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = '__all__' @@ -144,19 +143,19 @@ For the simplest experience, you'll just piggyback off the standard ORM which wi handle the basic `created`, `updated` and `deleted` signals & events: ```python ->>> from django.contrib.auth.models import User ->>> from drf_hooks.models import Hook ->>> jrrtolkien = User.objects.create(username='jrrtolkien') ->>> hook = Hook(user=jrrtolkien, +from django.contrib.auth.models import User +from drf_hooks.models import Hook +jrrtolkien = User.objects.create(username='jrrtolkien') +hook = Hook(user=jrrtolkien, event='book.added', target='http://example.com/target.php') ->>> hook.save() # creates the hook and stores it for later... ->>> from bookstore.models import Book ->>> book = Book(user=jrrtolkien, +hook.save() # creates the hook and stores it for later... +from bookstore.models import Book +book = Book(user=jrrtolkien, title='The Two Towers', pages=327, fiction=True) ->>> book.save() # fires off 'bookstore.Book.created' hook automatically +book.save() # fires off 'bookstore.Book.created' hook automatically ... ``` @@ -184,15 +183,18 @@ triggered anyways. ```python ... ->>> book.title += ': Deluxe Edition' ->>> book.pages = 352 ->>> book.save() # would fire off 'bookstore.Book.updated' hook automatically ->>> book.delete() # would fire off 'bookstore.Book.deleted' hook automatically +book.title += ': Deluxe Edition' +book.pages = 352 +book.save() # would fire off 'bookstore.Book.updated' hook automatically +book.delete() # would fire off 'bookstore.Book.deleted' hook automatically ``` You can also fire custom events with an arbitrary payload: ```python +import datetime +from django.contrib.auth.models import User + from drf_hooks.signals import raw_hook_event user = User.objects.get(id=123) diff --git a/drf_hooks/admin.py b/drf_hooks/admin.py index ae18315..aeea015 100644 --- a/drf_hooks/admin.py +++ b/drf_hooks/admin.py @@ -1,31 +1,35 @@ -from django.contrib import admin -from django.conf import settings from django import forms +from django.conf import settings +from django.contrib import admin + from .models import get_hook_model class HookForm(forms.ModelForm): """ - Model form to handle registered events, asuring + Model form to handle registered events, assuring only events declared on HOOK_EVENTS settings can be registered. """ class Meta: model = get_hook_model() - fields = ['user', 'target', 'event', 'headers'] + fields = ["user", "target", "event", "headers"] def __init__(self, *args, **kwargs): super(HookForm, self).__init__(*args, **kwargs) - self.fields['event'] = forms.ChoiceField(choices=self.get_admin_events()) + self.fields["event"] = forms.ChoiceField(choices=self.get_admin_events()) @classmethod def get_admin_events(cls): return [(x, x) for x in settings.HOOK_EVENTS] + class HookAdmin(admin.ModelAdmin): list_display = [f.name for f in get_hook_model()._meta.fields] - raw_id_fields = ['user', ] + raw_id_fields = [ + "user", + ] form = HookForm diff --git a/drf_hooks/client.py b/drf_hooks/client.py index 5184c16..6d3f676 100644 --- a/drf_hooks/client.py +++ b/drf_hooks/client.py @@ -1,5 +1,6 @@ -import threading import collections +import threading + import requests from django.conf import settings @@ -9,9 +10,10 @@ def get_client(): global __CLIENT if __CLIENT is None: - __CLIENT = Client() if getattr(settings, 'HOOK_THREADING', True) else requests.Session() + __CLIENT = Client() if getattr(settings, "HOOK_THREADING", True) else requests.Session() return __CLIENT + class FlushThread(threading.Thread): def __init__(self, client): threading.Thread.__init__(self) @@ -25,6 +27,7 @@ class Client(object): """ Manages a simple pool of threads to flush the queue of requests. """ + def __init__(self, num_threads=3): self.queue = collections.deque() @@ -38,22 +41,26 @@ def enqueue(self, method, *args, **kwargs): self.refresh_threads() def get(self, *args, **kwargs): - self.enqueue('get', *args, **kwargs) + self.enqueue("get", *args, **kwargs) def post(self, *args, **kwargs): - self.enqueue('post', *args, **kwargs) + self.enqueue("post", *args, **kwargs) def put(self, *args, **kwargs): - self.enqueue('put', *args, **kwargs) + self.enqueue("put", *args, **kwargs) def delete(self, *args, **kwargs): - self.enqueue('delete', *args, **kwargs) + self.enqueue("delete", *args, **kwargs) def refresh_threads(self): with self.flush_lock: # refresh if there are jobs to do and no threads are alive if len(self.queue) > 0: - to_refresh = [index for index, thread in enumerate(self.flush_threads) if not thread.is_alive()] + to_refresh = [ + index + for index, thread in enumerate(self.flush_threads) + if not thread.is_alive() + ] for index in to_refresh: self.flush_threads[index] = FlushThread(self) self.flush_threads[index].start() diff --git a/drf_hooks/migrations/0001_initial.py b/drf_hooks/migrations/0001_initial.py index 76bc7cf..3cb3817 100644 --- a/drf_hooks/migrations/0001_initial.py +++ b/drf_hooks/migrations/0001_initial.py @@ -1,13 +1,13 @@ # Generated by Django 3.1 on 2021-07-27 12:28 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion + import drf_hooks.models class Migration(migrations.Migration): - initial = True dependencies = [ @@ -16,19 +16,31 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Hook', + name="Hook", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('updated', models.DateTimeField(auto_now=True)), - ('event', models.CharField(db_index=True, max_length=64, verbose_name='Event')), - ('target', models.URLField(max_length=255, verbose_name='Target URL')), - ('headers', models.JSONField(default=drf_hooks.models.get_default_headers)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hooks', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("event", models.CharField(db_index=True, max_length=64, verbose_name="Event")), + ("target", models.URLField(max_length=255, verbose_name="Target URL")), + ("headers", models.JSONField(default=drf_hooks.models.get_default_headers)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="hooks", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'abstract': False, - 'swappable': 'HOOK_CUSTOM_MODEL', + "abstract": False, + "swappable": "HOOK_CUSTOM_MODEL", }, ), ] diff --git a/drf_hooks/migrations/0002_alter_hook_user.py b/drf_hooks/migrations/0002_alter_hook_user.py index 43fbc5c..36c9195 100644 --- a/drf_hooks/migrations/0002_alter_hook_user.py +++ b/drf_hooks/migrations/0002_alter_hook_user.py @@ -1,8 +1,8 @@ # Generated by Django 4.1.7 on 2023-03-17 15:00 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/drf_hooks/models.py b/drf_hooks/models.py index 661cefe..2a98c45 100644 --- a/drf_hooks/models.py +++ b/drf_hooks/models.py @@ -3,23 +3,24 @@ from django.apps import apps from django.conf import settings +from django.contrib.auth import get_user_model from django.core import serializers -from django.core.exceptions import ValidationError, ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.serializers.json import DjangoJSONEncoder -from django.contrib.auth import get_user_model from django.db import models -from django.db.models.signals import post_save, post_delete -from django.utils.module_loading import import_string +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from django.utils.module_loading import import_string -from .signals import hook_event, raw_hook_event from .client import get_client +from .signals import hook_event, raw_hook_event __EVENT_LOOKUP = None __HOOK_MODEL = None -if not hasattr(settings, 'HOOK_EVENTS'): - raise Exception('You need to define settings.HOOK_EVENTS!') +if not hasattr(settings, "HOOK_EVENTS"): + raise Exception("You need to define settings.HOOK_EVENTS!") + def get_event_lookup(): global __EVENT_LOOKUP @@ -28,20 +29,23 @@ def get_event_lookup(): for event_name, auto in settings.HOOK_EVENTS.items(): if not auto: continue - model, action = auto.rstrip('+').rsplit('.', 1) - all_users = auto.endswith('+') + model, action = auto.rstrip("+").rsplit(".", 1) + all_users = auto.endswith("+") if action in __EVENT_LOOKUP[model]: raise ImproperlyConfigured( - "settings.HOOK_EVENTS has a duplicate {action} for model " - "{model_path}".format(action=action, model=model) + "settings.HOOK_EVENTS has a duplicate {action} for model " "{model}".format( + action=action, model=model + ) ) __EVENT_LOOKUP[model][action] = (event_name, all_users) return __EVENT_LOOKUP + def clear_event_lookup(): global __EVENT_LOOKUP __EVENT_LOOKUP = None + def get_hook_model(): """ Returns the Custom Hook model if defined in settings, @@ -49,73 +53,72 @@ def get_hook_model(): """ global __HOOK_MODEL if __HOOK_MODEL is None: - model_label = getattr(settings, 'HOOK_CUSTOM_MODEL', 'drf_hooks.Hook') + model_label = getattr(settings, "HOOK_CUSTOM_MODEL", "drf_hooks.Hook") try: __HOOK_MODEL = apps.get_model(model_label, require_ready=False) except ValueError: - raise ImproperlyConfigured("HOOK_CUSTOM_MODEL must be of the form 'app_label.model_name'") + raise ImproperlyConfigured( + "HOOK_CUSTOM_MODEL must be of the form 'app_label.model_name'" + ) except LookupError: - raise ImproperlyConfigured("HOOK_CUSTOM_MODEL refers to unknown model '%s'" % model_label) + raise ImproperlyConfigured( + "HOOK_CUSTOM_MODEL refers to unknown model '%s'" % model_label + ) return __HOOK_MODEL def get_default_headers(): - return {'Content-Type': 'application/json'} + return {"Content-Type": "application/json"} class AbstractHook(models.Model): """ Stores a representation of a Hook. """ + created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) - user = models.ForeignKey(get_user_model(), related_name='%(class)ss', on_delete=models.CASCADE) - event = models.CharField('Event', max_length=64, db_index=True) - target = models.URLField('Target URL', max_length=255) + user = models.ForeignKey(get_user_model(), related_name="%(class)ss", on_delete=models.CASCADE) + event = models.CharField("Event", max_length=64, db_index=True) + target = models.URLField("Target URL", max_length=255) headers = models.JSONField(default=get_default_headers) class Meta: abstract = True def clean(self): - """ Validation for events. """ + """Validation for events.""" if self.event not in settings.HOOK_EVENTS.keys(): - raise ValidationError( - "Invalid hook event {evt}.".format(evt=self.event) - ) + raise ValidationError("Invalid hook event {evt}.".format(evt=self.event)) @staticmethod def serialize_model(instance): - hook_srls = getattr(settings, 'HOOK_SERIALIZERS', {}) + hook_srls = getattr(settings, "HOOK_SERIALIZERS", {}) if instance._meta.label in hook_srls: serializer = import_string(hook_srls[instance._meta.label]) - context = {'request': None} + context = {"request": None} data = serializer(instance, context=context).data else: # if no user defined serializers, fallback to the django builtin! - data = serializers.serialize('python', [instance])[0] + data = serializers.serialize("python", [instance])[0] for k, v in data.items(): if isinstance(v, OrderedDict): data[k] = dict(v) if isinstance(data, OrderedDict): data = dict(data) return data - + def serialize_hook(self, payload): serialized_hook = { - 'hook': {'id': self.id, 'event': self.event, 'target': self.target}, - 'data': payload, + "hook": {"id": self.id, "event": self.event, "target": self.target}, + "data": payload, } return json.dumps(serialized_hook, cls=DjangoJSONEncoder) def deliver_hook(self, serialized_hook): """Deliver the payload to the target URL.""" - get_client().post( - url=self.target, - data=serialized_hook, - headers=self.headers - ) + get_client().post(url=self.target, data=serialized_hook, headers=self.headers) @classmethod def find_hooks(cls, event_name, user=None): @@ -134,12 +137,12 @@ def find_and_fire_hooks(cls, event_name, payload, user=None): def get_user(instance, all_users=False): if all_users: return - if hasattr(instance, 'user'): + if hasattr(instance, "user"): return instance.user elif isinstance(instance, get_user_model()): return instance else: - raise ValueError('{} has no `user` property.'.format(repr(instance))) + raise ValueError("{} has no `user` property.".format(repr(instance))) @classmethod def handle_model_event(cls, instance, action): @@ -153,38 +156,39 @@ def handle_model_event(cls, instance, action): cls.find_and_fire_hooks(event_name, payload, user) def __unicode__(self): - return u'{} => {}'.format(self.event, self.target) + return "{} => {}".format(self.event, self.target) class Hook(AbstractHook): class Meta(AbstractHook.Meta): - swappable = 'HOOK_CUSTOM_MODEL' + swappable = "HOOK_CUSTOM_MODEL" ############## ### EVENTS ### ############## -@receiver(hook_event, dispatch_uid='instance-custom-hook') + +@receiver(hook_event, dispatch_uid="instance-custom-hook") def custom_event(sender, instance, action, *args, **kwargs): """Manually trigger a custom action (or even a standard action).""" get_hook_model().handle_model_event(instance, action) -@receiver(post_save, dispatch_uid='instance-saved-hook') +@receiver(post_save, dispatch_uid="instance-saved-hook") def model_saved(sender, instance, created, *args, **kwargs): """Automatically triggers "created" and "updated" actions.""" - action = 'created' if created else 'updated' + action = "created" if created else "updated" get_hook_model().handle_model_event(instance, action) -@receiver(post_delete, dispatch_uid='instance-deleted-hook') +@receiver(post_delete, dispatch_uid="instance-deleted-hook") def model_deleted(sender, instance, *args, **kwargs): """Automatically triggers "deleted" actions.""" - get_hook_model().handle_model_event(instance, 'deleted') + get_hook_model().handle_model_event(instance, "deleted") -@receiver(raw_hook_event, dispatch_uid='raw-custom-hook') +@receiver(raw_hook_event, dispatch_uid="raw-custom-hook") def raw_custom_event(sender, event_name, payload, user, **kwargs): """Give a full payload""" - get_hook_model().find_and_fire_hooks(event_name, payload, user) \ No newline at end of file + get_hook_model().find_and_fire_hooks(event_name, payload, user) diff --git a/drf_hooks/serializers.py b/drf_hooks/serializers.py index b193f90..979d520 100644 --- a/drf_hooks/serializers.py +++ b/drf_hooks/serializers.py @@ -16,5 +16,5 @@ def create(self, validated_data): class Meta: model = get_hook_model() - fields = '__all__' - read_only_fields = ('user',) + fields = "__all__" + read_only_fields = ("user",) diff --git a/drf_hooks/signals.py b/drf_hooks/signals.py index a7cc629..a0def06 100644 --- a/drf_hooks/signals.py +++ b/drf_hooks/signals.py @@ -1,5 +1,4 @@ from django.dispatch import Signal - hook_event = Signal() raw_hook_event = Signal() diff --git a/drf_hooks/tests.py b/drf_hooks/tests.py deleted file mode 100644 index 7194a92..0000000 --- a/drf_hooks/tests.py +++ /dev/null @@ -1,260 +0,0 @@ -import requests -import json -import time -import copy -from datetime import datetime - -from mock import patch, MagicMock, ANY - -from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.test import TestCase -from django.test.utils import override_settings -from django_comments.models import Comment -from django.test.signals import setting_changed -from django.dispatch import receiver - -from rest_framework import serializers - -from drf_hooks import models -from drf_hooks.client import get_client - -from drf_hooks.admin import HookForm - -Hook = models.Hook - -urlpatterns = [] - -HOOK_EVENTS_OVERRIDE = { - 'comment.added': 'django_comments.Comment.created', - 'comment.changed': 'django_comments.Comment.updated', - 'comment.removed': 'django_comments.Comment.deleted', - 'comment.moderated': 'django_comments.Comment.moderated', - 'special.thing': None, -} - -HOOK_SERIALIZERS_OVERRIDE = { - 'django_comments.Comment': 'drf_hooks.tests.CommentSerializer', -} - -ALT_HOOK_EVENTS = dict(HOOK_EVENTS_OVERRIDE) -ALT_HOOK_EVENTS['comment.moderated'] += '+' -ALT_HOOK_SERIALIZERS = {} -CLIENT = get_client() - -class CommentSerializer(serializers.ModelSerializer): - class Meta: - model = Comment - fields = '__all__' - - - -@receiver(setting_changed) -def handle_hook_events_change(sender, setting, *args, **kwargs): - if setting == 'HOOK_EVENTS': - models.clear_event_lookup() - - -@override_settings(HOOK_EVENTS=HOOK_EVENTS_OVERRIDE, HOOK_SERIALIZERS=HOOK_SERIALIZERS_OVERRIDE, HOOK_DELIVERER=None) -class DRFHooksTest(TestCase): - """ - This test Class uses real HTTP calls to a requestbin service, making it easy - to check responses and endpoint history. - """ - - ############# - ### TOOLS ### - ############# - - def setUp(self): - self.client = requests # force non-async for test cases - - self.user = User.objects.create_user('bob', 'bob@example.com', 'password') - self.site, created = Site.objects.get_or_create(domain='example.com', name='example.com') - - def make_hook(self, event, target): - return Hook.objects.create( - user=self.user, - event=event, - target=target - ) - - ############# - ### TESTS ### - ############# - - @override_settings(HOOK_EVENTS=ALT_HOOK_EVENTS) - def test_get_event_actions_config(self): - self.assertEquals( - models.get_event_lookup(), - { - 'django_comments.Comment': { - 'created': ('comment.added', False), - 'updated': ('comment.changed', False), - 'deleted': ('comment.removed', False), - 'moderated': ('comment.moderated', True), - }, - } - ) - - def test_no_hook(self): - comment = Comment.objects.create( - site=self.site, - content_object=self.user, - user=self.user, - comment='Hello world!' - ) - - @patch('drf_hooks.tests.CLIENT.post', autospec=True) - def perform_create_request_cycle(self, method_mock): - method_mock.return_value = None - target = 'http://example.com/perform_create_request_cycle' - hook = self.make_hook('comment.added', target) - comment = Comment.objects.create( - site=self.site, - content_object=self.user, - user=self.user, - comment='Hello world!' - ) - return hook, comment, json.loads(method_mock.call_args_list[0][1]['data']) - - @override_settings(HOOK_SERIALIZERS=ALT_HOOK_SERIALIZERS) - def test_simple_comment_hook(self): - """ - Uses the default serializer. - """ - hook, comment, payload = self.perform_create_request_cycle() - self.assertEquals(hook.id, payload['hook']['id']) - self.assertEquals('comment.added', payload['hook']['event']) - self.assertEquals(hook.target, payload['hook']['target']) - - self.assertEquals(comment.id, payload['data']['fields']['object_pk']) - self.assertEquals('Hello world!', payload['data']['fields']['comment']) - self.assertEquals(1, payload['data']['fields']['user']) - - def test_drf_comment_hook(self): - """ - Uses the drf serializer. - """ - hook, comment, payload = self.perform_create_request_cycle() - self.assertEquals(hook.id, payload['hook']['id']) - self.assertEquals('comment.added', payload['hook']['event']) - self.assertEquals(hook.target, payload['hook']['target']) - - self.assertEquals(str(comment.id), payload['data']['object_pk']) - self.assertEquals('Hello world!', payload['data']['comment']) - self.assertEquals(1, payload['data']['user']) - - @patch('drf_hooks.tests.CLIENT.post') - def test_full_cycle_comment_hook(self, method_mock): - method_mock.return_value = None - target = 'http://example.com/test_full_cycle_comment_hook' - - [self.make_hook(event, target) for event in ['comment.added', 'comment.changed', 'comment.removed']] - - # created - comment = Comment.objects.create( - site=self.site, - content_object=self.user, - user=self.user, - comment='Hello world!' - ) - # updated - comment.comment = 'Goodbye world...' - comment.save() - # deleted - comment.delete() - - payloads = [json.loads(call[2]['data']) for call in method_mock.mock_calls] - - self.assertEquals('comment.added', payloads[0]['hook']['event']) - self.assertEquals('comment.changed', payloads[1]['hook']['event']) - self.assertEquals('comment.removed', payloads[2]['hook']['event']) - - self.assertEquals('Hello world!', payloads[0]['data']['comment']) - self.assertEquals('Goodbye world...', payloads[1]['data']['comment']) - self.assertEquals('Goodbye world...', payloads[2]['data']['comment']) - - @patch('drf_hooks.tests.CLIENT.post') - def test_custom_instance_hook(self, method_mock): - from drf_hooks.signals import hook_event - - method_mock.return_value = None - target = 'http://example.com/test_custom_instance_hook' - - self.make_hook('comment.moderated', target) - - comment = Comment.objects.create( - site=self.site, - content_object=self.user, - user=self.user, - comment='Hello world!' - ) - - hook_event.send( - sender=comment.__class__, - action='moderated', - instance=comment - ) - # time.sleep(1) # should change a setting to turn off async - payloads = [json.loads(call[2]['data']) for call in method_mock.mock_calls] - self.assertEquals('comment.moderated', payloads[0]['hook']['event']) - self.assertEquals('Hello world!', payloads[0]['data']['comment']) - - @patch('drf_hooks.tests.CLIENT.post') - def test_raw_custom_event(self, method_mock): - from drf_hooks.signals import raw_hook_event - - method_mock.return_value = None - target = 'http://example.com/test_raw_custom_event' - - self.make_hook('special.thing', target) - - raw_hook_event.send( - sender=None, - event_name='special.thing', - payload={ - 'hello': 'world!' - }, - user=self.user - ) - # time.sleep(1) # should change a setting to turn off async - - payload = json.loads(method_mock.mock_calls[0][2]['data']) - - self.assertEquals('special.thing', payload['hook']['event']) - self.assertEquals('world!', payload['data']['hello']) - - def test_valid_form(self): - form_data = { - 'user': self.user.id, - 'target': "http://example.com", - 'event': HookForm.get_admin_events()[0][0] - } - form = HookForm(data=form_data) - self.assertTrue(form.is_valid()) - - def test_form_save(self): - form_data = { - 'user': self.user.id, - 'target': "http://example.com", - 'event': HookForm.get_admin_events()[0][0] - } - form = HookForm(data=form_data) - - self.assertTrue(form.is_valid()) - instance = form.save() - self.assertIsInstance(instance, Hook) - - def test_invalid_form(self): - form = HookForm(data={}) - self.assertFalse(form.is_valid()) - - @override_settings(HOOK_CUSTOM_MODEL='drf_hooks.Hook') - def test_get_custom_hook_model(self): - # Using the default Hook model just to exercise get_hook_model's - # lookup machinery. - from drf_hooks.models import AbstractHook, get_hook_model - HookModel = get_hook_model() - self.assertIs(HookModel, Hook) - self.assertTrue(issubclass(HookModel, AbstractHook)) diff --git a/drf_hooks/urls.py b/drf_hooks/urls.py index 7d204e3..12f2a6c 100644 --- a/drf_hooks/urls.py +++ b/drf_hooks/urls.py @@ -1,9 +1,8 @@ - from rest_framework import routers from .views import HookViewSet router = routers.SimpleRouter() -router.register(r'webhooks', HookViewSet, 'webhook') +router.register(r"webhooks", HookViewSet, "webhook") -urlpatterns = router.urls \ No newline at end of file +urlpatterns = router.urls diff --git a/drf_hooks/utils.py b/drf_hooks/utils.py index 7677ae9..1d8daad 100644 --- a/drf_hooks/utils.py +++ b/drf_hooks/utils.py @@ -1,8 +1,8 @@ -from django.apps import apps as django_apps -from django.core.exceptions import ImproperlyConfigured -from django.conf import settings from importlib import import_module +from django.apps import apps as django_apps +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured def get_module(path): @@ -13,17 +13,16 @@ def get_module(path): slugify = get_module('django.template.defaultfilters.slugify') """ try: - mod_name, func_name = path.rsplit('.', 1) + mod_name, func_name = path.rsplit(".", 1) mod = import_module(mod_name) except ImportError as e: - raise ImportError( - 'Error importing alert function {0}: "{1}"'.format(mod_name, e)) + raise ImportError('Error importing alert function {0}: "{1}"'.format(mod_name, e)) try: func = getattr(mod, func_name) except AttributeError: raise ImportError( - ('Module "{0}" does not define a "{1}" function' - ).format(mod_name, func_name)) + ('Module "{0}" does not define a "{1}" function').format(mod_name, func_name) + ) return func @@ -32,10 +31,10 @@ def get_hook_model(): Returns the Custom Hook model if defined in settings, otherwise the default Hook model. """ - model_label = getattr(settings, 'HOOK_CUSTOM_MODEL', 'drf_hooks.Hook') + model_label = getattr(settings, "HOOK_CUSTOM_MODEL", "drf_hooks.Hook") try: return django_apps.get_model(model_label, require_ready=False) except ValueError: raise ImproperlyConfigured("HOOK_CUSTOM_MODEL must be of the form 'app_label.model_name'") except LookupError: - raise ImproperlyConfigured("HOOK_CUSTOM_MODEL refers to unknown model '%s'" % model_label) \ No newline at end of file + raise ImproperlyConfigured("HOOK_CUSTOM_MODEL refers to unknown model '%s'" % model_label) diff --git a/drf_hooks/views.py b/drf_hooks/views.py index cbb7bf9..a801001 100644 --- a/drf_hooks/views.py +++ b/drf_hooks/views.py @@ -1,9 +1,12 @@ from rest_framework import viewsets + from .models import get_hook_model from .serializers import HookSerializer + class HookViewSet(viewsets.ModelViewSet): """Retrieve, create, update or destroy webhooks.""" + queryset = get_hook_model().objects.all() model = get_hook_model() serializer_class = HookSerializer diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..43988b4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,901 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "asttokens" +version = "3.0.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, +] + +[package.extras] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "certifi" +version = "2025.6.15" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.9.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca"}, + {file = "coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509"}, + {file = "coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b"}, + {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3"}, + {file = "coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3"}, + {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5"}, + {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187"}, + {file = "coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce"}, + {file = "coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70"}, + {file = "coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe"}, + {file = "coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582"}, + {file = "coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86"}, + {file = "coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed"}, + {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d"}, + {file = "coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338"}, + {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875"}, + {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250"}, + {file = "coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c"}, + {file = "coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32"}, + {file = "coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125"}, + {file = "coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e"}, + {file = "coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626"}, + {file = "coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb"}, + {file = "coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300"}, + {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8"}, + {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5"}, + {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd"}, + {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898"}, + {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d"}, + {file = "coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74"}, + {file = "coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e"}, + {file = "coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342"}, + {file = "coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631"}, + {file = "coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f"}, + {file = "coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd"}, + {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86"}, + {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43"}, + {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1"}, + {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751"}, + {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67"}, + {file = "coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643"}, + {file = "coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a"}, + {file = "coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d"}, + {file = "coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0"}, + {file = "coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d"}, + {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f"}, + {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029"}, + {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece"}, + {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683"}, + {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f"}, + {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10"}, + {file = "coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363"}, + {file = "coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7"}, + {file = "coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c"}, + {file = "coverage-7.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951"}, + {file = "coverage-7.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58"}, + {file = "coverage-7.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71"}, + {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55"}, + {file = "coverage-7.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b"}, + {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7"}, + {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385"}, + {file = "coverage-7.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed"}, + {file = "coverage-7.9.1-cp39-cp39-win32.whl", hash = "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d"}, + {file = "coverage-7.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244"}, + {file = "coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514"}, + {file = "coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c"}, + {file = "coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "decorator" +version = "5.2.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.8" +files = [ + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, +] + +[[package]] +name = "django" +version = "4.2.23" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-4.2.23-py3-none-any.whl", hash = "sha256:dafbfaf52c2f289bd65f4ab935791cb4fb9a198f2a5ba9faf35d7338a77e9803"}, + {file = "django-4.2.23.tar.gz", hash = "sha256:42fdeaba6e6449d88d4f66de47871015097dc6f1b87910db00a91946295cfae4"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-contrib-comments" +version = "2.1.0" +description = "The code formerly known as django.contrib.comments." +optional = false +python-versions = "*" +files = [ + {file = "django-contrib-comments-2.1.0.tar.gz", hash = "sha256:d82f1d04690550df026553053903deec0c52dc54212e1b79241b08f0355cff2c"}, + {file = "django_contrib_comments-2.1.0-py3-none-any.whl", hash = "sha256:e02c7341ea1f4bcdfa347851dbf5e632d3e591d84b4f77de2f90b93398897f3c"}, +] + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "djangorestframework" +version = "3.16.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.9" +files = [ + {file = "djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361"}, + {file = "djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"}, +] + +[package.dependencies] +django = ">=4.2" + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +files = [ + {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, + {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "ipdb" +version = "0.13.13" +description = "IPython-enabled pdb" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, + {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, +] + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""} +tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""} + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mypy" +version = "1.16.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, + {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, + {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, + {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, + {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, + {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, + {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, + {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, + {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, + {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, + {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, + {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, + {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, + {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, + {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, + {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, + {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, + {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, + {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, + {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, + {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, + {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, + {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.11.1" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10"}, + {file = "pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx", "sphinx_rtd_theme"] +testing = ["Django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, + {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "requests" +version = "2.32.4" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.3" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, + {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "4694376763fe3464eac0b9544d301965c7ad3e3fbde57597dd79606434c0cd4c" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ed3d797 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[tool.poetry] +name = "drf-hooks" +version = "0.1.0" +description = "" +authors = ["Angira Tripathi ", "Sander Koelstra ", "Domenico Nappo "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +Django = ">=3.1,<5" +django-contrib-comments = "2.1.0" +djangorestframework = ">=3.11.1" +pytz = "2025.2" +requests = "^2.32" + +[tool.poetry.group.dev.dependencies] +# general +ipdb = "^0.13.11" +ipython = "^8.11.0" +# tests +pytest = "^7.2.2" +pytest-cov = "^4.0.0" +pytest-mock = "^3.14.0" +pytest-django = "^4.11.1" + +ruff = "^0.1.14" +mypy = "^1.9.0" + +[tool.pytest.ini_options] +addopts = "--junitxml=pytest_report.xml --cov=./ --cov-report=term --cov-report=xml" +DJANGO_SETTINGS_MODULE = "tests.settings" +django_find_project = false +cache_dir = ".cache/pytest" +testpaths = ["./tests/"] + +[tool.coverage.run] +omit = ["*/tests/*"] +disable_warnings = ["couldnt-parse"] + +[tool.ruff] +lint.select = ["F", "E", "W", "I"] +line-length = 100 +lint.ignore = ["E501"] +cache-dir = ".cache/ruff" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.djlint] +profile = "django" +quiet = true +use_gitignore = true +max_attribute_length = 1 +max_blank_lines = 1 +close_void_tags = true + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b6fa128..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -Django>=3.1 -django-contrib-comments==2.1.0 -djangorestframework>=3.11.1 -mock==4.0.3 -pytz==2021.1 -requests==1.2.3 diff --git a/runtests.py b/runtests.py deleted file mode 100755 index 8e9b87d..0000000 --- a/runtests.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python - -import sys -import django -from django.conf import settings - - -APP_NAME = 'drf_hooks' -comments = 'django_comments' - -settings.configure( - DEBUG=True, - DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - } - }, - SECRET_KEY='D', - USE_TZ=True, - ROOT_URLCONF='{0}.tests'.format(APP_NAME), - MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ], - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, - ], - SITE_ID=1, - HOOK_EVENTS={}, - HOOK_THREADING=False, - INSTALLED_APPS=( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.admin', - 'django.contrib.sites', - comments, - APP_NAME, - ), -) - -from django.test.utils import get_runner - -if hasattr(django, 'setup'): - django.setup() -TestRunner = get_runner(settings) -test_runner = TestRunner() -failures = test_runner.run_tests([APP_NAME]) -if failures: - sys.exit(failures) diff --git a/setup.py b/setup.py index f7ebfea..6b313aa 100644 --- a/setup.py +++ b/setup.py @@ -4,33 +4,31 @@ from distutils.core import setup # if setuptools breaks # Dynamically calculate the version -version_tuple = __import__('drf_hooks').VERSION -version = '.'.join([str(v) for v in version_tuple]) +version_tuple = __import__("drf_hooks").VERSION +version = ".".join([str(v) for v in version_tuple]) setup( - name = 'drf-hooks', - description = 'A powerful mechanism for sending real time API notifications via a new subscription model.', - version = version, - author = 'Angira Tripathi', - author_email = 'angira.tripathi@am-flow.com', - url = 'http://github.com/am-flow/drf-hooks', - install_requires=['Django>=3.1', 'requests'], - packages=['drf_hooks'], + name="drf-hooks", + description="A powerful mechanism for sending real time API notifications via a new subscription model.", + version=version, + author="Angira Tripathi", + author_email="angira.tripathi@am-flow.com", + url="http://github.com/am-flow/drf-hooks", + install_requires=["Django>=3.1", "requests"], + packages=["drf_hooks"], package_data={ - 'drf_hooks': [ - 'migrations/*.py', + "drf_hooks": [ + "migrations/*.py", ] }, - classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Utilities', + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Utilities", ], ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..2475939 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,88 @@ +import os + +HOOK_EVENTS_OVERRIDE = { + "comment.added": "django_comments.Comment.created", + "comment.changed": "django_comments.Comment.updated", + "comment.removed": "django_comments.Comment.deleted", + "comment.moderated": "django_comments.Comment.moderated", + "special.thing": None, +} + +HOOK_SERIALIZERS_OVERRIDE = { + "django_comments.Comment": "tests.test_hooks.CommentSerializer", +} + +ALT_HOOK_EVENTS = dict(HOOK_EVENTS_OVERRIDE) +ALT_HOOK_EVENTS["comment.moderated"] += "+" +ALT_HOOK_SERIALIZERS = {} + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +SECRET_KEY = "test-secret-key-for-testing" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.admin", + "django.contrib.sites", + "django_comments", + "drf_hooks", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", +] + +ROOT_URLCONF = "test_urls" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} + +TEMPLATES = ( + [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, + ], +) +SITE_ID = 1 +HOOK_EVENTS = HOOK_EVENTS_OVERRIDE +HOOK_THREADING = False +HOOK_SERIALIZERS = HOOK_SERIALIZERS_OVERRIDE +# Internationalization +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +STATIC_URL = "/static/" + +# Default primary key field type +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 0000000..31a87c5 --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,227 @@ +import json +import typing as tp +from unittest.mock import MagicMock + +import pytest +from pytest_mock import MockFixture + +from drf_hooks.client import get_client +from tests.settings import ALT_HOOK_EVENTS + +CLIENT = get_client() + + +from django.contrib.auth.models import User +from django.contrib.sites.models import Site +from django.dispatch import receiver +from django.test.signals import setting_changed +from django_comments.models import Comment +from rest_framework import serializers + +from drf_hooks import models +from drf_hooks.admin import HookForm + +Hook = models.Hook + +urlpatterns = [] + + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = "__all__" + + +@receiver(setting_changed) +def handle_hook_events_change(sender, setting, *args, **kwargs): + if setting == "HOOK_EVENTS": + models.clear_event_lookup() + + +@pytest.fixture +def setup(db) -> tp.Generator[tuple[User, Site], None, None]: + user = User.objects.create_user("bob", "bob@example.com", "password") + site, created = Site.objects.get_or_create(domain="example.com", name="example.com") + yield user, site + + +@pytest.fixture +def mocked_post(mocker: MockFixture) -> MagicMock: + return mocker.patch.object(CLIENT, attribute="post", autospec=True) + + +# @pytest.mark.usefixtures("setup", "mocked_post") +class TestDRFHooks: + """This test Class uses real HTTP calls to a requestbin service, + making it easy to check responses and endpoint history.""" + + def make_hook(self, user, event, target): + return Hook.objects.create(user=user, event=event, target=target) + + ############# + ### TESTS ### + ############# + + def test_get_event_actions_config(self, settings) -> None: + settings.HOOK_EVENTS = ALT_HOOK_EVENTS + assert dict(models.get_event_lookup()) == { + "django_comments.Comment": { + "created": ("comment.added", False), + "updated": ("comment.changed", False), + "deleted": ("comment.removed", False), + "moderated": ("comment.moderated", True), + }, + } + # self.assertEquals( + # models.get_event_lookup(), + # { + # "django_comments.Comment": { + # "created": ("comment.added", False), + # "updated": ("comment.changed", False), + # "deleted": ("comment.removed", False), + # "moderated": ("comment.moderated", True), + # }, + # }, + # ) + + def test_no_hook(self, setup: tuple[User, Site]): + user, site = setup + comment = Comment.objects.create( + site=site, content_object=user, user=user, comment="Hello world!" + ) + + def perform_create_request_cycle(self, site, user, mocked_post): + mocked_post.return_value = None + target = "http://example.com/perform_create_request_cycle" + hook = self.make_hook(user, "comment.added", target) + comment = Comment.objects.create( + site=site, content_object=user, user=user, comment="Hello world!" + ) + return hook, comment, json.loads(mocked_post.call_args_list[0][1]["data"]) + + # @override_settings(HOOK_SERIALIZERS=ALT_HOOK_SERIALIZERS) + def test_simple_comment_hook(self, setup: tuple[User, Site], mocked_post): + """Uses the default serializer.""" + user, site = setup + hook, comment, payload = self.perform_create_request_cycle(site, user, mocked_post) + assert hook.id == payload["hook"]["id"] + assert "comment.added" == payload["hook"]["event"] + assert hook.target == payload["hook"]["target"] + assert str(comment.id) == payload["data"]["object_pk"] + assert "Hello world!" == payload["data"]["comment"] + assert 1 == payload["data"]["user"] + + def test_drf_comment_hook(self, setup: tuple[User, Site], mocked_post): + """Uses the drf serializer.""" + user, site = setup + hook, comment, payload = self.perform_create_request_cycle(site, user, mocked_post) + assert hook.id == payload["hook"]["id"] + assert "comment.added" == payload["hook"]["event"] + assert hook.target == payload["hook"]["target"] + + assert str(comment.id) == payload["data"]["object_pk"] + assert "Hello world!" == payload["data"]["comment"] + assert 1 == payload["data"]["user"] + + def test_full_cycle_comment_hook(self, mocked_post, setup: tuple[User, Site]): + mocked_post.return_value = None + user, site = setup + target = "http://example.com/test_full_cycle_comment_hook" + for event in ("comment.added", "comment.changed", "comment.removed"): + self.make_hook(user, event, target) + + # created + comment = Comment.objects.create( + site=site, content_object=user, user=user, comment="Hello world!" + ) + # updated + comment.comment = "Goodbye world..." + comment.save() + # deleted + comment.delete() + + payloads = [json.loads(call[2]["data"]) for call in mocked_post.mock_calls] + + assert "comment.added" == payloads[0]["hook"]["event"] + assert "comment.changed" == payloads[1]["hook"]["event"] + assert "comment.removed" == payloads[2]["hook"]["event"] + + assert "Hello world!" == payloads[0]["data"]["comment"] + assert "Goodbye world..." == payloads[1]["data"]["comment"] + assert "Goodbye world..." == payloads[2]["data"]["comment"] + + def test_custom_instance_hook(self, mocked_post, setup: tuple[User, Site]): + from drf_hooks.signals import hook_event + + user, site = setup + mocked_post.return_value = None + target = "http://example.com/test_custom_instance_hook" + + self.make_hook(user, "comment.moderated", target) + + comment = Comment.objects.create( + site=site, content_object=user, user=user, comment="Hello world!" + ) + + hook_event.send(sender=comment.__class__, action="moderated", instance=comment) + # time.sleep(1) # should change a setting to turn off async + payloads = [json.loads(call[2]["data"]) for call in mocked_post.mock_calls] + assert "comment.moderated" == payloads[0]["hook"]["event"] + assert "Hello world!" == payloads[0]["data"]["comment"] + + def test_raw_custom_event(self, mocked_post, setup: tuple[User, Site]): + from drf_hooks.signals import raw_hook_event + + user, site = setup + mocked_post.return_value = None + target = "http://example.com/test_raw_custom_event" + + self.make_hook(user, "special.thing", target) + + raw_hook_event.send( + sender=None, event_name="special.thing", payload={"hello": "world!"}, user=user + ) + + payload = json.loads(mocked_post.mock_calls[0][2]["data"]) + + assert "special.thing" == payload["hook"]["event"] + assert "world!" == payload["data"]["hello"] + + def test_valid_form(self, setup: tuple[User, Site]) -> None: + user, site = setup + form_data = { + "user": user.id, + "target": "http://example.com", + "event": HookForm.get_admin_events()[0][0], + "headers": json.dumps({"Content-Type": "application/json"}), + } + form = HookForm(data=form_data) + assert form.is_valid() + + def test_form_save(self, setup: tuple[User, Site]): + user, site = setup + form_data = { + "user": user.id, + "target": "http://example.com", + "event": HookForm.get_admin_events()[0][0], + "headers": json.dumps({"Content-Type": "application/json"}), + } + form = HookForm(data=form_data) + + assert form.is_valid() + instance = form.save() + assert isinstance(instance, Hook) + + def test_invalid_form(self): + form = HookForm(data={}) + assert not form.is_valid() + + # @override_settings(HOOK_CUSTOM_MODEL="drf_hooks.Hook") + def test_get_custom_hook_model(self): + # Using the default Hook model just to exercise get_hook_model's + # lookup machinery. + from drf_hooks.models import AbstractHook, get_hook_model + + HookModel = get_hook_model() + assert HookModel is Hook + assert issubclass(HookModel, AbstractHook) From 46ad90e7a43516f8d5e1a7fce419c210a55050f2 Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Mon, 7 Jul 2025 14:53:30 +0200 Subject: [PATCH 2/5] Remove setup.py --- poetry.lock | 8 ++++---- pyproject.toml | 45 ++++++++++++++++++++++++++++++++------------- setup.cfg | 2 -- setup.py | 34 ---------------------------------- 4 files changed, 36 insertions(+), 53 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/poetry.lock b/poetry.lock index 43988b4..72bdbcc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -270,13 +270,13 @@ bcrypt = ["bcrypt"] [[package]] name = "django-contrib-comments" -version = "2.1.0" +version = "2.2.0" description = "The code formerly known as django.contrib.comments." optional = false python-versions = "*" files = [ - {file = "django-contrib-comments-2.1.0.tar.gz", hash = "sha256:d82f1d04690550df026553053903deec0c52dc54212e1b79241b08f0355cff2c"}, - {file = "django_contrib_comments-2.1.0-py3-none-any.whl", hash = "sha256:e02c7341ea1f4bcdfa347851dbf5e632d3e591d84b4f77de2f90b93398897f3c"}, + {file = "django-contrib-comments-2.2.0.tar.gz", hash = "sha256:48de00f15677e016a216aeff205d6e00e4391c9a5702136c64119c472b7356da"}, + {file = "django_contrib_comments-2.2.0-py3-none-any.whl", hash = "sha256:2ca79060bbc8fc5b636981ef6e50f35ab83649af75fc1be47bf770636be3271c"}, ] [package.dependencies] @@ -898,4 +898,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "4694376763fe3464eac0b9544d301965c7ad3e3fbde57597dd79606434c0cd4c" +content-hash = "c4214d796edde58e807bb7b5b14707c45e472038dcc9e71d3f0a09eaffa88dff" diff --git a/pyproject.toml b/pyproject.toml index ed3d797..5ca8adc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,41 @@ [tool.poetry] name = "drf-hooks" -version = "0.1.0" -description = "" -authors = ["Angira Tripathi ", "Sander Koelstra ", "Domenico Nappo "] +version = "0.1.4" +authors = ["Angira Tripathi ", "Sander Koelstra "] readme = "README.md" +description = "A Django app for webhooks functionality" +license = "ISC" +homepage = "https://github.com/am-flow/drf-hooks" +repository = "https://github.com/am-flow/drf-hooks" +keywords = ["django", "hooks", "webhooks"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +] +packages = [{include = "drf_hooks"}] +exclude = [ + "drf_hooks/tests/*", +] [tool.poetry.dependencies] python = "^3.9" Django = ">=3.1,<5" -django-contrib-comments = "2.1.0" +django-contrib-comments = "2.2.0" djangorestframework = ">=3.11.1" pytz = "2025.2" requests = "^2.32" @@ -22,7 +49,7 @@ pytest = "^7.2.2" pytest-cov = "^4.0.0" pytest-mock = "^3.14.0" pytest-django = "^4.11.1" - +# linting ruff = "^0.1.14" mypy = "^1.9.0" @@ -49,14 +76,6 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" -[tool.djlint] -profile = "django" -quiet = true -use_gitignore = true -max_attribute_length = 1 -max_blank_lines = 1 -close_void_tags = true - [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6b313aa..0000000 --- a/setup.py +++ /dev/null @@ -1,34 +0,0 @@ -try: - from setuptools import setup -except ImportError: - from distutils.core import setup # if setuptools breaks - -# Dynamically calculate the version -version_tuple = __import__("drf_hooks").VERSION -version = ".".join([str(v) for v in version_tuple]) - -setup( - name="drf-hooks", - description="A powerful mechanism for sending real time API notifications via a new subscription model.", - version=version, - author="Angira Tripathi", - author_email="angira.tripathi@am-flow.com", - url="http://github.com/am-flow/drf-hooks", - install_requires=["Django>=3.1", "requests"], - packages=["drf_hooks"], - package_data={ - "drf_hooks": [ - "migrations/*.py", - ] - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Django", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Topic :: Utilities", - ], -) From 5908ffa9de366f7605b80295164fdb7f667cf76d Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Mon, 7 Jul 2025 15:00:40 +0200 Subject: [PATCH 3/5] add CI --- .github/workflows/ci.yml | 94 ++++++++++++++++++++++++++++++++++++++++ Makefile | 5 +-- pyproject.toml | 1 - 3 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bcf56a8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11'] + django-version: ['4.0', '4.1', '4.2'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Install specific Django version + run: poetry add django~=${{ matrix.django-version }} + + - name: Run tests + run: make test + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + if: matrix.python-version == '3.10' && matrix.django-version == '4.2' + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v3 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Run linting + run: make lint diff --git a/Makefile b/Makefile index 0775b82..663c915 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ PATH := $(PATH):$(HOME)/.local/bin SHELL := env PATH=$(PATH) /bin/bash +.PHONY: build format test lint + build: poetry install @@ -14,6 +16,3 @@ lint: test: poetry run pytest tests/ - -mypy: - poetry run mypy . diff --git a/pyproject.toml b/pyproject.toml index 5ca8adc..9b8902c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ pytest-mock = "^3.14.0" pytest-django = "^4.11.1" # linting ruff = "^0.1.14" -mypy = "^1.9.0" [tool.pytest.ini_options] addopts = "--junitxml=pytest_report.xml --cov=./ --cov-report=term --cov-report=xml" From 6a94824b7db73c62348641d087858a15420d5b7c Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Mon, 7 Jul 2025 15:03:10 +0200 Subject: [PATCH 4/5] add CI --- poetry.lock | 78 +---------------------------------------------------- 1 file changed, 1 insertion(+), 77 deletions(-) diff --git a/poetry.lock b/poetry.lock index 72bdbcc..aad34a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -438,71 +438,6 @@ files = [ [package.dependencies] traitlets = "*" -[[package]] -name = "mypy" -version = "1.16.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, - {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea"}, - {file = "mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574"}, - {file = "mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d"}, - {file = "mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc"}, - {file = "mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507"}, - {file = "mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca"}, - {file = "mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4"}, - {file = "mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, - {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, - {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, - {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, - {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069"}, - {file = "mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c"}, - {file = "mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383"}, - {file = "mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40"}, - {file = "mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b"}, - {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, - {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - [[package]] name = "packaging" version = "25.0" @@ -529,17 +464,6 @@ files = [ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["docopt", "pytest"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -898,4 +822,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c4214d796edde58e807bb7b5b14707c45e472038dcc9e71d3f0a09eaffa88dff" +content-hash = "feacf42991e4129b70a5da85f19369140c74f8462044a299d5a96a03ac285a28" From c1cb2ed31a944033e139321744dea742247721a5 Mon Sep 17 00:00:00 2001 From: Domenico Nappo Date: Tue, 8 Jul 2025 14:07:33 +0200 Subject: [PATCH 5/5] minor edits to the README --- README.md | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index eec72bf..560a043 100644 --- a/README.md +++ b/README.md @@ -33,28 +33,6 @@ If you want to make a Django form or API resource, you'll need to do that yourse (though we've provided some example bits of code below). -### Development - -#### Running tests - -Clone the repo: - -``` -git clone https://github.com/am-flow/drf-hooks && cd drf-hooks -``` - -Install dependencies: - -``` -make build -``` - -Run tests: - -``` -make tests -``` - ### Requirements * Python 3.9+ @@ -344,3 +322,26 @@ class CeleryHook(AbstractHook): We also don't handle retries or cleanup. Generally, if you get a `410` or a bunch of `4xx` or `5xx`, you should delete the Hook and let the user know. + + +### Development + +#### Running tests + +Clone the repo: + +``` +git clone https://github.com/am-flow/drf-hooks && cd drf-hooks +``` + +Install dependencies: + +``` +make build +``` + +Run tests: + +``` +make tests +```