David Burke 1 year ago
parent
commit
e6cb54ce12

+ 0 - 8
apps/event_ingest/api.py

@@ -1,8 +1,6 @@
-
 from asgiref.sync import sync_to_async
 from django.conf import settings
 from django.http import HttpRequest, HttpResponse
-from django.utils.timezone import now
 from ninja import Router, Schema
 
 from .authentication import event_auth
@@ -54,12 +52,10 @@ async def event_store(
     Event store is the original event ingest API from OSS Sentry but is used less often
     Unlike Envelope, it accepts only one Issue event.
     """
-    received_at = now()
     issue_event_class = get_issue_event_class(payload)
     issue_event = InterchangeIssueEvent(
         event_id=payload.event_id,
         project_id=project_id,
-        received_at=received_at,
         payload=issue_event_class(**payload.dict()),
     )
     await async_call_celery_task(ingest_event, issue_event.dict())
@@ -80,7 +76,6 @@ async def event_envelope(
     Make as few io calls as possible. Some language SDKs (PHP) cannot run async code
     and will block while waiting for GlitchTip to respond.
     """
-    received_at = now()
     header = payload._header
     for item_header, item in payload._items:
         if item_header.type == "event":
@@ -88,7 +83,6 @@ async def event_envelope(
             issue_event = InterchangeIssueEvent(
                 event_id=header.event_id,
                 project_id=project_id,
-                received_at=received_at,
                 payload=issue_event_class(**item.dict()),
             )
             await async_call_celery_task(ingest_event, issue_event.dict())
@@ -110,11 +104,9 @@ async def event_security(
     Reformats event to make CSP browser format match more standard
     event format.
     """
-    received_at = now()
     event = CSPIssueEventSchema(csp=payload.csp_report.dict(by_alias=True))
     issue_event = InterchangeIssueEvent(
         project_id=project_id,
-        received_at=received_at,
         payload=event.dict(by_alias=True),
     )
     await async_call_celery_task(ingest_event, issue_event.dict(by_alias=True))

+ 27 - 14
apps/event_ingest/process_event.py

@@ -9,12 +9,15 @@ from django.db.utils import IntegrityError
 from alerts.models import Notification
 from apps.issue_events.constants import EventStatus
 from apps.issue_events.models import Issue, IssueEvent, IssueEventType, IssueHash
+
+# from apps.issue_events.schema import CSPIssueEventDataSchema, IssueEventDataSchema
 from sentry.culprit import generate_culprit
 from sentry.eventtypes.error import ErrorEvent
 
 from .schema import (
     EventMessage,
     InterchangeIssueEvent,
+    ValueEventException,
 )
 from .utils import generate_hash
 
@@ -24,6 +27,7 @@ class ProcessingEvent:
     event: InterchangeIssueEvent
     issue_hash: str
     title: str
+    transaction: str
     metadata: dict[str, Any]
     event_data: dict[str, Any]
     issue_id: Optional[int] = None
@@ -63,10 +67,20 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
         title = ""
         culprit = ""
         metadata: dict[str, Any] = {}
-        if event.type == IssueEventType.ERROR:
+        if event.type in [IssueEventType.ERROR, IssueEventType.DEFAULT]:
             sentry_event = ErrorEvent()
             metadata = sentry_event.get_metadata(event.dict())
-            title = sentry_event.get_title(metadata)
+            if event.type == IssueEventType.ERROR:
+                title = sentry_event.get_title(metadata)
+            else:
+                title = (
+                    transform_message(event.message) if event.message else "<untitled>"
+                )
+                culprit = (
+                    event.transaction
+                    if event.transaction
+                    else generate_culprit(event.dict())
+                )
             culprit = sentry_event.get_location(event.dict())
         elif event.type == IssueEventType.CSP:
             humanized_directive = event.csp.effective_directive.replace("-src", "")
@@ -74,27 +88,24 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
             title = f"Blocked '{humanized_directive}' from '{uri}'"
             culprit = "fake culprit"
             event_data["csp"] = event.csp.dict()
-        else:  # Default Event Type
-            title = transform_message(event.message) if event.message else "<untitled>"
-            culprit = (
-                event.transaction
-                if event.transaction
-                else generate_culprit(event.dict())
-            )
+
         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()
+            if isinstance(exception, ValueEventException):
+                event_data["exception"] = exception.dict()["values"]
+            else:
+                event_data["exception"] = exception
         processing_events.append(
             ProcessingEvent(
                 event=ingest_event,
                 issue_hash=issue_hash,
                 title=title,
+                transaction=culprit,
                 metadata=metadata,
                 event_data=event_data,
             )
@@ -139,14 +150,16 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
                 processing_event.issue_id = IssueHash.objects.get(
                     project_id=project_id, value=issue_hash
                 ).issue_id
-        processing_event.event_data["title"] = processing_event.title
         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,
+                timestamp=processing_event.event.payload.timestamp,
+                received=processing_event.event.received,
+                title=processing_event.title,
+                transaction=processing_event.transaction,
+                message="",
                 data=processing_event.event_data,
             )
         )

+ 1 - 1
apps/event_ingest/schema.py

@@ -268,7 +268,7 @@ class InterchangeIssueEvent(Schema):
 
     event_id: uuid.UUID = Field(default_factory=uuid.uuid4)
     project_id: int
-    received_at: datetime = Field(default_factory=now)
+    received: datetime = Field(default_factory=now)
     payload: Union[
         IssueEventSchema, ErrorIssueEventSchema, CSPIssueEventSchema
     ] = Field(discriminator="type")

+ 3 - 4
apps/event_ingest/tests/test_process_issue_event.py

@@ -200,11 +200,10 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
             "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)
+        self.assertNotEqual(event.timestamp, sdk_error["timestamp"])
+        self.assertEqual(event.timestamp.year, 2020)
 
-        event_json = self.get_event_json(event)
-        self.assertCompareData(event_json, sentry_json, ["datetime"])
+        # event_json = self.get_event_json(event)
         # self.assertCompareData(event_json, sentry_json, ["datetime", "breadcrumbs"])
 
         # url = self.get_project_events_detail(event.pk)

+ 2 - 2
apps/issue_events/admin.py

@@ -5,8 +5,8 @@ from .models import Issue, IssueEvent
 
 @admin.register(IssueEvent)
 class IssueEventAdmin(admin.ModelAdmin):
-    list_display = ("id", "date_created")
-    list_filter = ("date_created",)
+    list_display = ("id", "timestamp")
+    list_filter = ("timestamp",)
     raw_id_fields = ("issue",)
     search_fields = ("id",)
     show_full_result_count = False

+ 7 - 11
apps/issue_events/api/events.py

@@ -8,7 +8,7 @@ from django.http import Http404
 from glitchtip.api.authentication import AuthHttpRequest
 
 from ..models import IssueEvent
-from ..schema import IssueEventDetailSchema, IssueEventSchema, IssueEventJsonSchema
+from ..schema import IssueEventDetailSchema, IssueEventJsonSchema, IssueEventSchema
 from . import router
 
 
@@ -26,7 +26,7 @@ def get_queryset(
         qs = qs.filter(issue__project__organization__slug=organization_slug)
     if project_slug:
         qs = qs.filter(issue__project__slug=project_slug)
-    return qs.select_related("issue").order_by("-date_received")
+    return qs.select_related("issue").order_by("-received")
 
 
 @router.get(
@@ -44,7 +44,7 @@ async def list_issue_event(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"),
+        previous=Window(expression=Lag("id"), order_by="received"),
     )
     obj = await qs.afirst()
     if not obj:
@@ -62,11 +62,9 @@ async def get_issue_event(request: AuthHttpRequest, issue_id: int, event_id: uui
     qs = get_queryset(request, issue_id)
     qs = qs.annotate(
         previous=Subquery(
-            qs.filter(date_received__lt=OuterRef("date_received")).values("id")[:1]
-        ),
-        next=Subquery(
-            qs.filter(date_received__gt=OuterRef("date_received")).values("id")[:1]
+            qs.filter(received__lt=OuterRef("received")).values("id")[:1]
         ),
+        next=Subquery(qs.filter(received__gt=OuterRef("received")).values("id")[:1]),
     )
     try:
         return await qs.filter(id=event_id).aget()
@@ -108,11 +106,9 @@ async def get_project_issue_event(
     )
     qs = qs.annotate(
         previous=Subquery(
-            qs.filter(date_received__lt=OuterRef("date_received")).values("id")[:1]
-        ),
-        next=Subquery(
-            qs.filter(date_received__gt=OuterRef("date_received")).values("id")[:1]
+            qs.filter(received__lt=OuterRef("received")).values("id")[:1]
         ),
+        next=Subquery(qs.filter(received__gt=OuterRef("received")).values("id")[:1]),
     )
     try:
         return await qs.aget(id=event_id)

+ 52 - 177
apps/issue_events/migrations/0001_initial.py

@@ -1,4 +1,4 @@
-# Generated by Django 4.2.7 on 2023-11-08 14:45
+# Generated by Django 4.2.7 on 2023-11-18 16:47
 
 from django.conf import settings
 import django.contrib.postgres.indexes
@@ -10,219 +10,94 @@ 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,
-                    ),
-                ),
-                ("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",
-                    ),
-                ),
+                ('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(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",
-                    ),
-                ),
+                ('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)),
+                ('timestamp', models.DateTimeField(help_text='Time at which event happened')),
+                ('received', models.DateTimeField(help_text='Time at which GlitchTip accepted event')),
+                ('title', models.CharField(max_length=255)),
+                ('transaction', models.CharField(max_length=200)),
+                ('level', models.PositiveSmallIntegerField(choices=[(0, 'sample'), (1, 'debug'), (2, 'info'), (3, 'warning'), (4, 'error'), (5, 'fatal')], default=4)),
+                ('message', models.CharField(max_length=1000)),
+                ('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')},
         ),
     ]

+ 10 - 4
apps/issue_events/models.py

@@ -6,6 +6,8 @@ from django.db import models
 from psqlextra.models import PostgresPartitionedModel
 from psqlextra.types import PostgresPartitioningMethod
 
+from sentry.constants import MAX_CULPRIT_LENGTH
+
 from .constants import EventStatus, IssueEventType, LogLevel
 
 
@@ -76,15 +78,19 @@ 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(help_text="Time at which event happened")
-    date_received = models.DateTimeField(
-        help_text="Time at which GlitchTip accepted event"
+    timestamp = models.DateTimeField(help_text="Time at which event happened")
+    received = models.DateTimeField(help_text="Time at which GlitchTip accepted event")
+    title = models.CharField(max_length=255)
+    transaction = models.CharField(max_length=MAX_CULPRIT_LENGTH)
+    level = models.PositiveSmallIntegerField(
+        choices=LogLevel.choices, default=LogLevel.ERROR
     )
+    message = models.CharField(max_length=1000)
     data = models.JSONField()
 
     class PartitioningMeta:
         method = PostgresPartitioningMethod.RANGE
-        key = ["date_received"]
+        key = ["received"]
 
     def __str__(self):
         return self.eventID

+ 21 - 10
apps/issue_events/schema.py

@@ -3,6 +3,7 @@ from typing import Any, Optional
 
 from ninja import Field, ModelSchema, Schema
 
+from apps.event_ingest.schema import CSPReportSchema, EventBreadcrumb, EventException
 from glitchtip.api.schema import CamelSchema
 from sentry.interfaces.stacktrace import get_context
 
@@ -22,10 +23,10 @@ class IssueEventSchema(CamelSchema, ModelSchema):
     event_id: str
     project_id: int = Field(validation_alias="issue.project_id")
     group_id: int = Field(validation_alias="issue_id")
+    date_created: datetime = Field(validation_alias="timestamp")
+    date_received: datetime = Field(validation_alias="received")
     dist: Optional[str] = None
-    message: Optional[str] = Field(validation_alias="data.message", default=None)
-    culprit: Optional[str] = Field(validation_alias="data.culprit", default=None)
-    title: Optional[str] = Field(validation_alias="data.title", default=None)
+    culprit: Optional[str] = Field(validation_alias="transaction", default=None)
     platform: Optional[str] = Field(validation_alias="data.platform", default=None)
     type: str = Field(validation_alias="get_type_display")
     metadata: dict[str, str] = Field(
@@ -36,19 +37,17 @@ class IssueEventSchema(CamelSchema, ModelSchema):
 
     class Config:
         model = IssueEvent
-        model_fields = ["id", "type", "date_created", "date_received"]
+        model_fields = ["id", "type", "title", "message"]
         populate_by_name = True
 
     @staticmethod
     def resolve_entries(obj: IssueEvent):
         entries = []
         data = obj.data
-        exception = data.get("exception")
-        # Some, but not all, keys are made more JS camel case like
-        if exception and exception.get("values"):
+        if exception := data.get("exception"):
+            exception = {"values": exception, "hasSystemFrames": False}
             # https://gitlab.com/glitchtip/sentry-open-source/sentry/-/blob/master/src/sentry/interfaces/stacktrace.py#L487
             # if any frame is "in_app" set this to True
-            exception["hasSystemFrames"] = False
             for value in exception["values"]:
                 if (
                     value.get("stacktrace", None) is not None
@@ -125,11 +124,23 @@ class IssueEventJsonSchema(Schema):
 
     event_id: str = Field(validation_alias="id.hex")
     timestamp: float = Field()
-    date_created: datetime = Field(serialization_alias="datetime")
+    date_created: datetime = Field(validation_alias="timestamp")
     breadcrumbs: Optional[Any] = Field(
         validation_alias="data.breadcrumbs", default=None
     )
 
     @staticmethod
     def resolve_timestamp(obj):
-        return obj.date_received.timestamp()
+        return obj.timestamp.timestamp()
+
+
+class IssueEventDataSchema(Schema):
+    """IssueEvent model data json schema"""
+
+    metadata: Optional[dict[str, Any]] = None
+    breadcrumbs: Optional[list[EventBreadcrumb]] = None
+    exception: Optional[list[EventException]] = None
+
+
+class CSPIssueEventDataSchema(IssueEventDataSchema):
+    csp: CSPReportSchema

+ 1 - 1
poetry.lock

@@ -4481,4 +4481,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "b9246f44796997e3d2cdac7ea812d82435f36318e0845534fc2569a37be37ce6"
+content-hash = "7b4b2652c5d5c253b9f213201e7c7444f7e0110de8d1e9ab1e5f2be020d5ec63"

Some files were not shown because too many files changed in this diff