.
This commit is contained in:
1 parent
96801ef853
commit
480a404021
15 files changed
+387
-134
No files matched your search
Vendored
+3
-11
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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]),
|
||||
# )
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
from .api import router # noqa
|
||||
from . import events, issues # noqa
|
||||
@@ -0,0 +1,3 @@
|
||||
from ninja import Router
|
||||
|
||||
router = Router()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")},
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Generated
+49
-34
@@ -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"
|
||||
+1
-2
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user