diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ef4fac2..9c96d40c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,5 @@ { - "python.formatting.provider": "black", - "python.linting.pylintArgs": [ - "--enable=W0614", - "--django-settings-module=glitchtip.settings", - "--load-plugins", - "pylint_django" - ], - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "editor.formatOnSave": true, - "files.trimTrailingWhitespace": true + "files.trimTrailingWhitespace": true, + "ruff.lint.run": "onSave", + "editor.formatOnSave": true } diff --git a/apps/event_ingest/process_event.py b/apps/event_ingest/process_event.py index 707c2cbf..54b72b84 100644 --- a/apps/event_ingest/process_event.py +++ b/apps/event_ingest/process_event.py @@ -24,6 +24,7 @@ class ProcessingEvent: event: InterchangeIssueEvent issue_hash: str title: str + metadata: dict[str, Any] event_data: dict[str, Any] issue_id: Optional[int] = None issue_created = False @@ -83,6 +84,10 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]): issue_hash = generate_hash(title, culprit, event.type, event.fingerprint) event_data["culprit"] = culprit event_data["metadata"] = metadata + # if breadcrumbs := event.breadcrumbs: + # event_data["breadcrumbs"] = [ + # breadcrumb.dict() for breadcrumb in breadcrumbs + # ] if exception := event.exception: event_data["exception"] = exception.dict() processing_events.append( @@ -90,6 +95,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]): event=ingest_event, issue_hash=issue_hash, title=title, + metadata=metadata, event_data=event_data, ) ) @@ -106,6 +112,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]): issue_defaults = { "type": event_type, "title": processing_event.title, + "metadata": processing_event.metadata, } for hash_obj in hash_queryset: if ( @@ -136,6 +143,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]): issue_events.append( IssueEvent( id=processing_event.event.event_id, + date_created=processing_event.event.payload.timestamp, date_received=processing_event.event.received_at, issue_id=processing_event.issue_id, type=event_type, diff --git a/apps/event_ingest/schema.py b/apps/event_ingest/schema.py index c0e9e21d..d5815751 100644 --- a/apps/event_ingest/schema.py +++ b/apps/event_ingest/schema.py @@ -137,7 +137,7 @@ class EventBreadcrumb(Schema): category: Optional[str] = None message: Optional[str] = None data: Optional[dict[str, Any]] = None - level: Optional[Level] = None + level: Annotated[Optional[Level], WrapValidator(invalid_to_none)] = None timestamp: Optional[datetime] = None diff --git a/apps/event_ingest/tests/test_process_issue_event.py b/apps/event_ingest/tests/test_process_issue_event.py index 8b02c86b..f9779df5 100644 --- a/apps/event_ingest/tests/test_process_issue_event.py +++ b/apps/event_ingest/tests/test_process_issue_event.py @@ -6,7 +6,11 @@ from apps.issue_events.constants import EventStatus from apps.issue_events.models import Issue, IssueEvent, IssueHash from ..process_event import process_issue_events -from ..schema import ErrorIssueEventSchema, InterchangeIssueEvent, IssueEventSchema +from ..schema import ( + ErrorIssueEventSchema, + InterchangeIssueEvent, + IssueEventSchema, +) from .utils import EventIngestTestCase COMPAT_TEST_DATA_DIR = "events/test_data" @@ -74,6 +78,18 @@ class SentryCompatTestCase(IssueEventIngestTestCase): ) return event, sentry_json, api_sentry_event + def get_event_json(self, event: IssueEvent): + return self.client.get( + reverse( + "api:get_event_json", + kwargs={ + "organization_slug": self.organization.slug, + "issue_id": event.issue_id, + "event_id": event.id, + }, + ) + ).json() + # Upgrade functions handle intentional differences between GlitchTip and Sentry OSS def upgrade_title(self, value: str): """Sentry OSS uses ... while GlitchTip uses unicode …""" @@ -90,6 +106,10 @@ class SentryCompatTestCase(IssueEventIngestTestCase): for field in fields: field_value1 = data1.get(field) field_value2 = data2.get(field) + if field == "datetime": + # Check that it's close enough + field_value1 = field_value1[:23] + field_value2 = field_value2[:23] if field == "title" and isinstance(field_value1, str): field_value1 = self.upgrade_title(field_value1) if field_value2: @@ -110,7 +130,7 @@ class SentryCompatTestCase(IssueEventIngestTestCase): def get_project_events_detail(self, event_id: str): return reverse( - "api:project_issue_event_retrieve", + "api:get_project_issue_event", kwargs={ "organization_slug": self.project.organization.slug, "project_slug": self.project.slug, @@ -118,18 +138,20 @@ class SentryCompatTestCase(IssueEventIngestTestCase): }, ) + def submit_event(self, event_data: dict) -> IssueEvent: + event = InterchangeIssueEvent( + event_id=event_data["event_id"], + project_id=self.project.id, + payload=ErrorIssueEventSchema(**event_data), + ) + process_issue_events([event]) + return IssueEvent.objects.get(pk=event.event_id) + def test_template_error(self): sdk_error, sentry_json, sentry_data = self.get_json_test_data( "django_template_error" ) - event = InterchangeIssueEvent( - event_id=sdk_error["event_id"], - project_id=self.project.id, - payload=ErrorIssueEventSchema(**sdk_error), - ) - process_issue_events([event]) - - event = IssueEvent.objects.get(pk=event.event_id) + event = self.submit_event(sdk_error) url = self.get_project_events_detail(event.id.hex) res = self.client.get(url) @@ -165,9 +187,32 @@ class SentryCompatTestCase(IssueEventIngestTestCase): ["env", "headers", "url", "method", "inferredContentType"], ) - # url = reverse("issue-detail", kwargs={"pk": event.issue.pk}) - # res = self.client.get(url) - # self.assertEqual(res.status_code, 200) + url = reverse("api:get_issue", kwargs={"issue_id": event.issue.pk}) + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + res_data = res.json() - # data = self.get_json_data("events/test_data/django_template_error_issue.json") - # self.assertCompareData(res.data, data, ["title", "metadata"]) + data = self.get_json_data("events/test_data/django_template_error_issue.json") + self.assertCompareData(res_data, data, ["title", "metadata"]) + + def test_js_sdk_with_unix_timestamp(self): + sdk_error, sentry_json, sentry_data = self.get_json_test_data( + "js_event_with_unix_timestamp" + ) + event = self.submit_event(sdk_error) + self.assertNotEqual(event.date_created, sdk_error["timestamp"]) + self.assertEqual(event.date_created.year, 2020) + + event_json = self.get_event_json(event) + self.assertCompareData(event_json, sentry_json, ["datetime"]) + # self.assertCompareData(event_json, sentry_json, ["datetime", "breadcrumbs"]) + + # url = self.get_project_events_detail(event.pk) + # res = self.client.get(url) + # res_data = res.json() + # self.assertCompareData(res_data, sentry_data, ["datetime"]) + # self.assertEqual(res_data["entries"][1].get("type"), "breadcrumbs") + # self.assertEqual( + # res_data["entries"][1], + # self.upgrade_data(sentry_data["entries"][1]), + # ) diff --git a/apps/event_ingest/tests/test_store_api.py b/apps/event_ingest/tests/test_store_api.py index 453615bd..cb4f5369 100644 --- a/apps/event_ingest/tests/test_store_api.py +++ b/apps/event_ingest/tests/test_store_api.py @@ -1,4 +1,3 @@ - from django.test import override_settings from django.urls import reverse diff --git a/apps/issue_events/api/__init__.py b/apps/issue_events/api/__init__.py new file mode 100644 index 00000000..42ca91de --- /dev/null +++ b/apps/issue_events/api/__init__.py @@ -0,0 +1,2 @@ +from .api import router # noqa +from . import events, issues # noqa diff --git a/apps/issue_events/api/api.py b/apps/issue_events/api/api.py new file mode 100644 index 00000000..25bdec31 --- /dev/null +++ b/apps/issue_events/api/api.py @@ -0,0 +1,3 @@ +from ninja import Router + +router = Router() diff --git a/apps/issue_events/api.py b/apps/issue_events/api/events.py similarity index 77% rename from apps/issue_events/api.py rename to apps/issue_events/api/events.py index ec5adc03..4ad17a6e 100644 --- a/apps/issue_events/api.py +++ b/apps/issue_events/api/events.py @@ -4,14 +4,12 @@ from typing import Optional from django.db.models import OuterRef, Subquery, Window from django.db.models.functions import Lag from django.http import Http404 -from ninja import Router from glitchtip.api.authentication import AuthHttpRequest -from .models import IssueEvent -from .schema import IssueEventDetailSchema, IssueEventSchema - -router = Router() +from ..models import IssueEvent +from ..schema import IssueEventDetailSchema, IssueEventSchema, IssueEventJsonSchema +from . import router def get_queryset( @@ -34,7 +32,7 @@ def get_queryset( @router.get( "/issues/{int:issue_id}/events/", response=list[IssueEventSchema], by_alias=True ) -async def issue_event_list(request: AuthHttpRequest, issue_id: int): +async def list_issue_event(request: AuthHttpRequest, issue_id: int): return [obj async for obj in get_queryset(request, issue_id=issue_id)] @@ -43,7 +41,7 @@ async def issue_event_list(request: AuthHttpRequest, issue_id: int): response=IssueEventDetailSchema, by_alias=True, ) -async def issue_event_latest(request: AuthHttpRequest, issue_id: int): +async def get_latest_issue_event(request: AuthHttpRequest, issue_id: int): qs = get_queryset(request, issue_id) qs = qs.annotate( previous=Window(expression=Lag("id"), order_by="date_received"), @@ -60,9 +58,7 @@ async def issue_event_latest(request: AuthHttpRequest, issue_id: int): response=IssueEventDetailSchema, by_alias=True, ) -async def issue_event_retrieve( - request: AuthHttpRequest, issue_id: int, event_id: uuid.UUID -): +async def get_issue_event(request: AuthHttpRequest, issue_id: int, event_id: uuid.UUID): qs = get_queryset(request, issue_id) qs = qs.annotate( previous=Subquery( @@ -83,7 +79,7 @@ async def issue_event_retrieve( response=list[IssueEventSchema], by_alias=True, ) -async def project_issue_event_list( +async def list_project_issue_event( request: AuthHttpRequest, organization_slug: str, project_slug: str, @@ -101,7 +97,7 @@ async def project_issue_event_list( response=IssueEventDetailSchema, by_alias=True, ) -async def project_issue_event_retrieve( +async def get_project_issue_event( request: AuthHttpRequest, organization_slug: str, project_slug: str, @@ -122,3 +118,18 @@ async def project_issue_event_retrieve( return await qs.aget(id=event_id) except IssueEvent.DoesNotExist: raise Http404() + + +@router.get( + "/organizations/{slug:organization_slug}/issues/{int:issue_id}/events/{event_id}/json/", + response=IssueEventJsonSchema, + by_alias=True, +) +async def get_event_json( + request: AuthHttpRequest, organization_slug: str, issue_id: int, event_id: uuid.UUID +): + qs = get_queryset(request, organization_slug=organization_slug, issue_id=issue_id) + try: + return await qs.aget(id=event_id) + except IssueEvent.DoesNotExist: + raise Http404() diff --git a/apps/issue_events/api/issues.py b/apps/issue_events/api/issues.py new file mode 100644 index 00000000..28d92bc0 --- /dev/null +++ b/apps/issue_events/api/issues.py @@ -0,0 +1,25 @@ +from django.http import Http404 + +from glitchtip.api.authentication import AuthHttpRequest + +from ..models import Issue +from ..schema import IssueSchema +from . import router + + +def get_queryset(request: AuthHttpRequest): + user_id = request.auth + return Issue.objects.filter(project__organization__users=user_id) + + +@router.get( + "/issues/{int:issue_id}/", + response=IssueSchema, + by_alias=True, +) +async def get_issue(request: AuthHttpRequest, issue_id: int): + qs = get_queryset(request) + try: + return await qs.filter(id=issue_id).aget() + except Issue.DoesNotExist: + raise Http404() diff --git a/apps/issue_events/migrations/0001_initial.py b/apps/issue_events/migrations/0001_initial.py index a61898b7..a6383ea2 100644 --- a/apps/issue_events/migrations/0001_initial.py +++ b/apps/issue_events/migrations/0001_initial.py @@ -10,89 +10,219 @@ import uuid class Migration(migrations.Migration): - initial = True dependencies = [ - ('projects', '0013_merge_20231017_1350'), + ("projects", "0013_merge_20231017_1350"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Issue', + name="Issue", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('culprit', models.CharField(blank=True, max_length=1024, null=True)), - ('is_public', models.BooleanField(default=False)), - ('level', models.PositiveSmallIntegerField(choices=[(0, 'sample'), (1, 'debug'), (2, 'info'), (3, 'warning'), (4, 'error'), (5, 'fatal')], default=4)), - ('title', models.CharField(max_length=255)), - ('type', models.PositiveSmallIntegerField(choices=[(0, 'default'), (1, 'error'), (2, 'csp')], default=0)), - ('status', models.PositiveSmallIntegerField(choices=[(0, 'unresolved'), (1, 'resolved'), (2, 'ignored')], default=0)), - ('short_id', models.PositiveIntegerField(null=True)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='projects.project')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("culprit", models.CharField(blank=True, max_length=1024, null=True)), + ("is_public", models.BooleanField(default=False)), + ( + "level", + models.PositiveSmallIntegerField( + choices=[ + (0, "sample"), + (1, "debug"), + (2, "info"), + (3, "warning"), + (4, "error"), + (5, "fatal"), + ], + default=4, + ), + ), + ("metadata", models.JSONField()), + ("title", models.CharField(max_length=255)), + ( + "type", + models.PositiveSmallIntegerField( + choices=[(0, "default"), (1, "error"), (2, "csp")], default=0 + ), + ), + ( + "status", + models.PositiveSmallIntegerField( + choices=[(0, "unresolved"), (1, "resolved"), (2, "ignored")], + default=0, + ), + ), + ("short_id", models.PositiveIntegerField(null=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="issues", + to="projects.project", + ), + ), ], ), migrations.CreateModel( - name='IssueHash', + name="IssueHash", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.UUIDField(db_index=True)), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hashes', to='issue_events.issue')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='projects.project')), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.UUIDField(db_index=True)), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="hashes", + to="issue_events.issue", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="projects.project", + ), + ), ], ), migrations.CreateModel( - name='IssueEvent', + name="IssueEvent", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('type', models.PositiveSmallIntegerField(choices=[(0, 'default'), (1, 'error'), (2, 'csp')], default=0)), - ('date_created', models.DateTimeField(auto_now_add=True, help_text='Time at which event happened')), - ('date_received', models.DateTimeField(auto_now_add=True, help_text='Time at which GlitchTip accepted event')), - ('data', models.JSONField()), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue_events.issue')), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "type", + models.PositiveSmallIntegerField( + choices=[(0, "default"), (1, "error"), (2, "csp")], default=0 + ), + ), + ( + "date_created", + models.DateTimeField(help_text="Time at which event happened"), + ), + ( + "date_received", + models.DateTimeField( + help_text="Time at which GlitchTip accepted event", + ), + ), + ("data", models.JSONField()), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="issue_events.issue", + ), + ), ], options={ - 'abstract': False, - 'base_manager_name': 'objects', + "abstract": False, + "base_manager_name": "objects", }, managers=[ - ('objects', psqlextra.manager.manager.PostgresManager()), + ("objects", psqlextra.manager.manager.PostgresManager()), ], ), migrations.CreateModel( - name='Comment', + name="Comment", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('text', models.TextField(blank=True, null=True)), - ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='issue_events.issue')), - ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("text", models.TextField(blank=True, null=True)), + ( + "issue", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="comments", + to="issue_events.issue", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'ordering': ('-created',), + "ordering": ("-created",), }, ), migrations.CreateModel( - name='IssueStats', + name="IssueStats", fields=[ - ('issue', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='issue_events.issue')), - ('search_vector', django.contrib.postgres.search.SearchVectorField(editable=False, null=True)), - ('search_vector_created', models.DateTimeField(auto_now_add=True)), - ('count', models.PositiveIntegerField(default=1, editable=False)), - ('last_seen', models.DateTimeField(auto_now_add=True, db_index=True)), + ( + "issue", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="issue_events.issue", + ), + ), + ( + "search_vector", + django.contrib.postgres.search.SearchVectorField( + editable=False, null=True + ), + ), + ("search_vector_created", models.DateTimeField(auto_now_add=True)), + ("count", models.PositiveIntegerField(default=1, editable=False)), + ("last_seen", models.DateTimeField(auto_now_add=True, db_index=True)), ], options={ - 'indexes': [django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='issue_event_search__0754e8_gin')], + "indexes": [ + django.contrib.postgres.indexes.GinIndex( + fields=["search_vector"], name="issue_event_search__0754e8_gin" + ) + ], }, ), migrations.AddConstraint( - model_name='issuehash', - constraint=models.UniqueConstraint(fields=('project', 'value'), name='issue hash project'), + model_name="issuehash", + constraint=models.UniqueConstraint( + fields=("project", "value"), name="issue hash project" + ), ), migrations.AlterUniqueTogether( - name='issue', - unique_together={('project', 'short_id')}, + name="issue", + unique_together={("project", "short_id")}, ), ] diff --git a/apps/issue_events/models.py b/apps/issue_events/models.py index 86650702..e88907af 100644 --- a/apps/issue_events/models.py +++ b/apps/issue_events/models.py @@ -16,6 +16,7 @@ class Issue(models.Model): level = models.PositiveSmallIntegerField( choices=LogLevel.choices, default=LogLevel.ERROR ) + metadata = models.JSONField() project = models.ForeignKey( "projects.Project", on_delete=models.CASCADE, related_name="issues" ) @@ -75,11 +76,9 @@ class IssueEvent(PostgresPartitionedModel, models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) issue = models.ForeignKey(Issue, on_delete=models.CASCADE) type = models.PositiveSmallIntegerField(default=0, choices=IssueEventType.choices) - date_created = models.DateTimeField( - auto_now_add=True, help_text="Time at which event happened" - ) + date_created = models.DateTimeField(help_text="Time at which event happened") date_received = models.DateTimeField( - auto_now_add=True, help_text="Time at which GlitchTip accepted event" + help_text="Time at which GlitchTip accepted event" ) data = models.JSONField() diff --git a/apps/issue_events/schema.py b/apps/issue_events/schema.py index 0d094c22..e237ede7 100644 --- a/apps/issue_events/schema.py +++ b/apps/issue_events/schema.py @@ -1,12 +1,20 @@ -from typing import Optional +from datetime import datetime +from typing import Any, Optional -from ninja import Field, ModelSchema +from ninja import Field, ModelSchema, Schema from glitchtip.api.schema import CamelSchema from sentry.interfaces.stacktrace import get_context from .constants import IssueEventType -from .models import IssueEvent +from .models import Issue, IssueEvent + + +class IssueSchema(CamelSchema, ModelSchema): + class Config: + model = Issue + model_fields = ["id", "title", "metadata"] + populate_by_name = True class IssueEventSchema(CamelSchema, ModelSchema): @@ -108,3 +116,20 @@ class IssueEventDetailSchema(IssueEventSchema): def resolve_next_event_id(obj): if event_id := obj.next: return event_id.hex + + +class IssueEventJsonSchema(Schema): + """ + Represents a more raw view of the event, built with open source (legacy) Sentry compatibility + """ + + event_id: str = Field(validation_alias="id.hex") + timestamp: float = Field() + date_created: datetime = Field(serialization_alias="datetime") + breadcrumbs: Optional[Any] = Field( + validation_alias="data.breadcrumbs", default=None + ) + + @staticmethod + def resolve_timestamp(obj): + return obj.date_received.timestamp() diff --git a/apps/issue_events/tests/test_issue_event_api.py b/apps/issue_events/tests/test_issue_event_api.py index 1a29008a..f7fb1e18 100644 --- a/apps/issue_events/tests/test_issue_event_api.py +++ b/apps/issue_events/tests/test_issue_event_api.py @@ -13,7 +13,7 @@ class IssueEventAPITestCase(GlitchTipTestCaseMixin, TestCase): event = baker.make("issue_events.IssueEvent", issue__project=self.project) baker.make("issue_events.IssueEvent", issue__project=self.project, _quantity=3) not_my_event = baker.make("issue_events.IssueEvent") - url = reverse("api:issue_event_list", args=[event.issue_id]) + url = reverse("api:list_issue_event", args=[event.issue_id]) with self.assertNumQueries(1): res = self.client.get(url) @@ -23,25 +23,25 @@ class IssueEventAPITestCase(GlitchTipTestCaseMixin, TestCase): def test_retrieve(self): event = baker.make("issue_events.IssueEvent", issue__project=self.project) url = reverse( - "api:issue_event_retrieve", + "api:get_issue_event", kwargs={"issue_id": event.issue_id, "event_id": "a" * 32}, ) res = self.client.get(url) self.assertEqual(res.status_code, 404) url = reverse( - "api:issue_event_retrieve", + "api:get_issue_event", kwargs={"issue_id": event.issue_id, "event_id": event.id}, ) res = self.client.get(url) self.assertContains(res, event.pk.hex) - url = reverse("api:issue_event_latest", kwargs={"issue_id": event.issue_id}) + url = reverse("api:get_latest_issue_event", kwargs={"issue_id": event.issue_id}) res = self.client.get(url) self.assertContains(res, event.pk.hex) def test_authentication(self): - url = reverse("api:issue_event_list", args=[1]) + url = reverse("api:list_issue_event", args=[1]) self.client.logout() res = self.client.get(url) self.assertEqual(res.status_code, 401) diff --git a/poetry.lock b/poetry.lock index dab34542..fdc5b2f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,25 +374,25 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.31.83" +version = "1.31.84" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" files = [ - {file = "botocore-1.31.83-py3-none-any.whl", hash = "sha256:c742069e8bfd06d212d712228258ff09fb481b6ec02358e539381ce0fcad065a"}, - {file = "botocore-1.31.83.tar.gz", hash = "sha256:40914b0fb28f13d709e1f8a4481e278350b77a3987be81acd23715ec8d5fedca"}, + {file = "botocore-1.31.84-py3-none-any.whl", hash = "sha256:d65bc05793d1a8a8c191a739f742876b4b403c5c713dc76beef262d18f7984a2"}, + {file = "botocore-1.31.84.tar.gz", hash = "sha256:8913bedb96ad0427660dee083aeaa675466eb662bbf1a47781956b5882aadcc5"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" urllib3 = [ - {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""}, + {version = ">=1.25.4,<1.27", markers = "python_version < \"3.10\""}, ] [package.extras] -crt = ["awscrt (==0.16.26)"] +crt = ["awscrt (==0.19.10)"] [[package]] name = "brotli" @@ -1104,13 +1104,13 @@ Django = ">=3.2" [[package]] name = "django-import-export" -version = "3.3.2" +version = "3.3.3" description = "Django application and library for importing and exporting data with included admin integration." optional = false python-versions = ">=3.8" files = [ - {file = "django-import-export-3.3.2.tar.gz", hash = "sha256:9a5c7c191014e4defb01573ee94864b60724a203f1b8a7e5e67a03f06b27b62d"}, - {file = "django_import_export-3.3.2-py3-none-any.whl", hash = "sha256:d13e7508190f46442280bd7d04efcf49af6521350a70a6f9e06447ef28d6a41d"}, + {file = "django-import-export-3.3.3.tar.gz", hash = "sha256:2c1b16e1cf2ea5f62a165d8867e7c6dcff25673ab7201fd18aaf67c9ee90367e"}, + {file = "django_import_export-3.3.3-py3-none-any.whl", hash = "sha256:78973202e93897326ab0411d64eaf89b72779fcb21ee9e5f64f3fb96571a5978"}, ] [package.dependencies] @@ -1306,6 +1306,7 @@ files = [ [package.dependencies] django = "*" django-stubs-ext = ">=4.2.5" +mypy = {version = ">=1.6.0,<1.7.0", optional = true, markers = "extra == \"compatible-mypy\""} tomli = {version = "*", markers = "python_version < \"3.11\""} types-pytz = "*" types-PyYAML = "*" @@ -1587,8 +1588,8 @@ files = [ [package.dependencies] cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} greenlet = [ - {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}, {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""}, + {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}, ] "zope.event" = "*" "zope.interface" = "*" @@ -1738,12 +1739,12 @@ files = [ google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -1790,8 +1791,8 @@ files = [ [package.dependencies] google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} proto-plus = [ - {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, + {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" @@ -1846,8 +1847,8 @@ google-cloud-audit-log = ">=0.1.0,<1.0.0dev" google-cloud-core = ">=2.0.0,<3.0.0dev" grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" proto-plus = [ - {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, + {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" @@ -2479,13 +2480,13 @@ zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "locust" -version = "2.18.1" +version = "2.18.2" description = "Developer friendly load testing framework" optional = false python-versions = ">=3.8" files = [ - {file = "locust-2.18.1-py3-none-any.whl", hash = "sha256:95cb37490487db693b9a85914ca13a962d74de7b9588519909a5a678512dfaef"}, - {file = "locust-2.18.1.tar.gz", hash = "sha256:503985a240f8f8098636c9493689caca2cc923b83f241393bb2c92bde5801278"}, + {file = "locust-2.18.2-py3-none-any.whl", hash = "sha256:a1916347bedcfa6598fcc6ba45022fcd31ed1352d84797650673296129223891"}, + {file = "locust-2.18.2.tar.gz", hash = "sha256:802300a30b4d88555854e3687ff30a0cf5e73b35f264e45ec6f0aa2c8792569f"}, ] [package.dependencies] @@ -2868,23 +2869,38 @@ files = [ [[package]] name = "mypy" -version = "1.7.0" +version = "1.6.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"}, - {file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"}, - {file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"}, - {file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe"}, - {file = "mypy-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d"}, - {file = "mypy-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc"}, - {file = "mypy-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41"}, - {file = "mypy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391"}, - {file = "mypy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5"}, - {file = "mypy-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8"}, - {file = "mypy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010"}, + {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, + {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, + {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, + {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, + {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, + {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, + {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, + {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, + {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, + {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, + {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, + {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, + {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, + {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, + {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, + {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, + {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, + {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, + {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, ] [package.dependencies] @@ -2895,7 +2911,6 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -3064,13 +3079,13 @@ files = [ [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, ] [package.extras] @@ -4477,4 +4492,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "b6bfd51963494aee8459dd8c45d89badc99318dd72ee4be800426fc33358c51a" +content-hash = "3a538fb29d5544cedb83ba975669a1ec2b6112df82dac0c1a262a5eddb952468" diff --git a/pyproject.toml b/pyproject.toml index e9364277..795d8c64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,9 +53,8 @@ locust = "^2.10.1" freezegun = "^1.1.0" rope = "^1.3.0" model_bakery = "^1.1.1" -mypy = "^1.0.0" django-debug-toolbar = "^4.0.0" -django-stubs = "^4.0.0" +django-stubs = {extras = ["compatible-mypy"], version = "^4.0.0"} django-sslserver = "^0.22" aioresponses = "^0.7.2" requests-mock = "^1.9.3"