diff --git a/hub/apps.py b/hub/apps.py index 18b4b240c..f911ba887 100644 --- a/hub/apps.py +++ b/hub/apps.py @@ -1,6 +1,16 @@ from django.apps import AppConfig +from django.db.utils import ProgrammingError class HubConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "hub" + + def ready(self): + try: + from hub.models import refresh_tokens_cache + + refresh_tokens_cache() + except ProgrammingError: + # This is expected when running migrations + pass diff --git a/hub/graphql/dataloaders.py b/hub/graphql/dataloaders.py index 68983aa2d..2955cce2d 100644 --- a/hub/graphql/dataloaders.py +++ b/hub/graphql/dataloaders.py @@ -145,12 +145,16 @@ class ReverseFKWithFiltersDataLoader(dataloaders.BasicReverseFKDataLoader): @classmethod @sync_to_async def load_fn(cls, keys: list[str]) -> list[list[DjangoModel]]: - filter_dict = strawberry.asdict(cls.filters) - results: list["DjangoModel"] = list( - cls.model.objects.filter(**{f"{cls.reverse_path}__in": keys}) - .prefetch_related(*cls.prefetch) - .filter(**filter_dict) + unsanitised_filter_dict = strawberry.asdict(cls.filters) + filter_dict = {} + for key in unsanitised_filter_dict: + if unsanitised_filter_dict[key] is not strawberry.UNSET: + filter_dict[key] = unsanitised_filter_dict[key] + results = cls.model.objects.prefetch_related(*cls.prefetch).filter( + **{f"{cls.reverse_path}__in": keys} ) + if len(filter_dict.keys()) > 0: + results = results.filter(**filter_dict) return [ [result for result in results if getattr(result, cls.reverse_path) == key] for key in keys diff --git a/hub/graphql/extensions.py b/hub/graphql/extensions/__init__.py similarity index 100% rename from hub/graphql/extensions.py rename to hub/graphql/extensions/__init__.py diff --git a/hub/graphql/extensions/analytics.py b/hub/graphql/extensions/analytics.py new file mode 100644 index 000000000..4b3fd31bd --- /dev/null +++ b/hub/graphql/extensions/analytics.py @@ -0,0 +1,43 @@ +import posthog +from gqlauth.core.utils import app_settings +from graphql import ExecutionResult as GraphQLExecutionResult +from graphql.error import GraphQLError +from strawberry.extensions import SchemaExtension + +from hub.models import APIToken, get_api_token + + +class APIAnalyticsExtension(SchemaExtension): + def on_operation(self): + yield + request = self.execution_context.context.request + if token_str := app_settings.JWT_TOKEN_FINDER(request): + try: + signature = token_str.split(".")[2] + except IndexError: + self.execution_context.result = GraphQLExecutionResult( + data=None, + errors=[GraphQLError("Invalid auth token")], + ) + return + try: + db_token = get_api_token(signature) + if db_token is not None: + if db_token.revoked: + self.execution_context.result = GraphQLExecutionResult( + data=None, + errors=[GraphQLError("Token has been revoked")], + ) + if not posthog.disabled: + posthog.capture( + db_token.user_id, + "API request", + { + "operation_name": self.execution_context.operation_name, + "operation_type": self.execution_context.operation_type, + }, + ) + else: + print("API request run by User ID", db_token.user_id) + except APIToken.DoesNotExist: + pass diff --git a/hub/graphql/schema.py b/hub/graphql/schema.py index 08e601f4f..2b152af2a 100644 --- a/hub/graphql/schema.py +++ b/hub/graphql/schema.py @@ -9,6 +9,7 @@ from gqlauth.user import arg_mutations as auth_mutations from gqlauth.user.queries import UserQueries from graphql import GraphQLError +from strawberry.extensions import QueryDepthLimiter from strawberry.types import ExecutionContext from strawberry_django import mutations as django_mutations from strawberry_django.optimizer import DjangoOptimizerExtension @@ -16,7 +17,8 @@ from hub import models from hub.graphql import mutations as mutation_types -from hub.graphql.types import model_types +from hub.graphql.extensions.analytics import APIAnalyticsExtension +from hub.graphql.types import model_types, public_queries logger = logging.getLogger(__name__) @@ -43,7 +45,6 @@ class Query(UserQueries): my_organisations: List[model_types.Organisation] = strawberry_django.field( extensions=[IsAuthenticated()] ) - external_data_source: model_types.ExternalDataSource = strawberry_django.field( extensions=[IsAuthenticated()] ) @@ -87,6 +88,15 @@ class Query(UserQueries): area: Optional[model_types.Area] = model_types.area_by_gss dataSet: Optional[model_types.DataSet] = model_types.dataset_by_name + enrich_postcode: public_queries.PostcodeQueryResponse = strawberry.field( + resolver=public_queries.enrich_postcode, + extensions=[IsAuthenticated()], + ) + enrich_postcodes: List[public_queries.PostcodeQueryResponse] = strawberry.field( + resolver=public_queries.enrich_postcodes, + extensions=[IsAuthenticated()], + ) + @strawberry.field def test_data_source( self, info: strawberry.types.Info, input: TestDataSourceInput @@ -100,6 +110,8 @@ def test_data_source( else: raise ValueError("Unsupported data source type") + list_api_tokens = public_queries.list_api_tokens + @strawberry.type class Mutation: @@ -108,6 +120,9 @@ class Mutation: verify_account = auth_mutations.VerifyAccount.field resend_activation_email = auth_mutations.ResendActivationEmail.field + create_api_token = public_queries.create_api_token + revoke_api_token = public_queries.revoke_api_token + create_external_data_source: mutation_types.CreateExternalDataSourceOutput = ( mutation_types.create_external_data_source ) @@ -203,5 +218,7 @@ def process_errors( mutation=Mutation, extensions=[ DjangoOptimizerExtension, # not required, but highly recommended + APIAnalyticsExtension, + QueryDepthLimiter(max_depth=10), ], ) diff --git a/hub/graphql/types/model_types.py b/hub/graphql/types/model_types.py index f7775bd91..d4a666e04 100644 --- a/hub/graphql/types/model_types.py +++ b/hub/graphql/types/model_types.py @@ -175,6 +175,7 @@ class FieldDefinition: value: str = dict_key_field() label: Optional[str] = dict_key_field() description: Optional[str] = dict_key_field() + external_id: Optional[str] = dict_key_field() @strawberry_django.filter(models.ExternalDataSource) diff --git a/hub/graphql/types/postcodes.py b/hub/graphql/types/postcodes.py index 98c6f2c03..273dba161 100644 --- a/hub/graphql/types/postcodes.py +++ b/hub/graphql/types/postcodes.py @@ -14,7 +14,7 @@ class PostcodesIOCodes: admin_district: str = dict_key_field() admin_county: str = dict_key_field() admin_ward: str = dict_key_field() - parish: str = dict_key_field() + parish: Optional[str] = dict_key_field() parliamentary_constituency: str = dict_key_field() parliamentary_constituency_2025: str = dict_key_field() ccg: str = dict_key_field() @@ -59,4 +59,6 @@ class PostcodesIOResult: @strawberry.field def feature(self, info: strawberry.types.info.Info) -> PointFeature: - return PointFeature(point=Point(self.longitude, self.latitude), properties=self) + return PointFeature.from_geodjango( + point=Point(self.longitude, self.latitude), properties=self + ) diff --git a/hub/graphql/types/public_queries.py b/hub/graphql/types/public_queries.py new file mode 100644 index 000000000..91fcc420d --- /dev/null +++ b/hub/graphql/types/public_queries.py @@ -0,0 +1,187 @@ +import datetime +import json +from typing import List, Optional, cast + +from django.conf import settings +from django.db.models import Q + +import jwt +import pytz +import strawberry +import strawberry_django +from gqlauth.core.utils import app_settings +from gqlauth.jwt.types_ import TokenPayloadType, TokenType +from strawberry.dataloader import DataLoader +from strawberry.types.info import Info +from strawberry_django.auth.utils import get_current_user +from strawberry_django.permissions import IsAuthenticated + +from hub import models +from hub.graphql.types import model_types +from hub.graphql.types.postcodes import PostcodesIOResult +from utils.postcodesIO import get_bulk_postcode_geo + + +@strawberry.type +class PostcodeQueryResponse: + postcode: str + loaders: strawberry.Private[models.Loaders] + + @strawberry.field + async def postcodesIO(self) -> Optional[PostcodesIOResult]: + return await self.loaders["postcodesIO"].load(self.postcode) + + @strawberry.field + async def constituency(self) -> Optional[model_types.Area]: + postcode_data = await self.loaders["postcodesIO"].load(self.postcode) + if postcode_data is None: + return None + id = postcode_data.codes.parliamentary_constituency + return await models.Area.objects.aget(Q(gss=id) | Q(name=id)) + + @strawberry.field + async def custom_source_data( + self, source: str, source_path: str, info: Info + ) -> Optional[str]: + source_loader = self.loaders["source_loaders"].get(source, None) + postcode_data = await self.loaders["postcodesIO"].load(self.postcode) + if source_loader is not None and postcode_data is not None: + return await source_loader.load( + models.EnrichmentLookup( + member_id=self.postcode, + postcode_data=postcode_data, + source_id=source, + source_path=source_path, + ) + ) + + +async def enrich_postcode(postcode: str, info: Info) -> PostcodeQueryResponse: + user = get_current_user(info) + loaders = models.Loaders( + postcodesIO=DataLoader(load_fn=get_bulk_postcode_geo), + source_loaders={ + str(source.id): source.data_loader_factory() + async for source in models.ExternalDataSource.objects.filter( + organisation__members__user=user, + geography_column__isnull=False, + geography_column_type__isnull=False, + ).all() + }, + ) + return PostcodeQueryResponse(postcode=postcode, loaders=loaders) + + +async def enrich_postcodes(postcodes: List[str], info: Info) -> PostcodeQueryResponse: + if len(postcodes) > settings.POSTCODES_IO_BATCH_MAXIMUM: + raise ValueError( + f"Batch query takes a maximum of 100 postcodes. You provided {len(postcodes)}" + ) + user = get_current_user(info) + loaders = models.Loaders( + postcodesIO=DataLoader(load_fn=get_bulk_postcode_geo), + source_loaders={ + str(source.id): source.data_loader_factory() + async for source in models.ExternalDataSource.objects.filter( + organisation__members__user=user, + geography_column__isnull=False, + geography_column_type__isnull=False, + ).all() + }, + ) + return [ + PostcodeQueryResponse(postcode=postcode, loaders=loaders) + for postcode in postcodes + ] + + +######################## +# API token management +######################## + + +@strawberry_django.type(models.APIToken) +class APIToken: + token: str + expires_at: datetime.datetime + signature: strawberry.ID + created_at: datetime.datetime + revoked: bool + + +@strawberry_django.mutation(extensions=[IsAuthenticated()]) +def create_api_token(info: Info, expiry_days: int = 3650) -> APIToken: + user = get_current_user(info) + user_pk = app_settings.JWT_PAYLOAD_PK.python_name + pk_field = {user_pk: getattr(user, user_pk)} + expires_at = datetime.datetime.now(tz=pytz.utc) + datetime.timedelta( + days=expiry_days + ) + payload = TokenPayloadType(**pk_field, exp=expires_at) + serialized = json.dumps(payload.as_dict(), sort_keys=True, indent=1) + token = TokenType( + token=str( + jwt.encode( + payload={"payload": serialized}, + key=cast(str, app_settings.JWT_SECRET_KEY.value), + algorithm=app_settings.JWT_ALGORITHM, + ) + ), + payload=payload, + ) + + token_db = models.APIToken.objects.create( + signature=token.token.split(".")[2], + token=token.token, + user=user, + expires_at=expires_at, + ) + models.refresh_tokens_cache() + return token_db + + +@strawberry_django.mutation(extensions=[IsAuthenticated()]) +def revoke_api_token(signature: strawberry.ID, info: Info) -> APIToken: + token = models.APIToken.objects.get(signature=signature) + token.revoked = True + token.save() + models.refresh_tokens_cache() + return token + + +@strawberry_django.field(extensions=[IsAuthenticated()]) +def list_api_tokens(info: Info) -> List[APIToken]: + tokens = models.APIToken.objects.filter(user=get_current_user(info), revoked=False) + return tokens + + +def decode_jwt(token: str) -> "TokenType": + from gqlauth.core.utils import app_settings + from gqlauth.jwt.types_ import TokenPayloadType, TokenType + + decoded = json.loads( + jwt.decode( + token, + key=cast(str, app_settings.JWT_SECRET_KEY.value), + algorithms=[ + app_settings.JWT_ALGORITHM, + ], + )["payload"] + ) + + signature = token.split(".")[2] + + if models.is_api_token_revoked(signature): + return ExpiredTokenType( + token=token, payload=TokenPayloadType.from_dict(decoded) + ) + + return TokenType(token=token, payload=TokenPayloadType.from_dict(decoded)) + + +class ExpiredTokenType(TokenType): + def is_expired(self) -> bool: + return True + + def expires_at(self) -> datetime.datetime: + return datetime.datetime.now() diff --git a/hub/migrations/0101_apitoken.py b/hub/migrations/0101_apitoken.py new file mode 100644 index 000000000..62da476d9 --- /dev/null +++ b/hub/migrations/0101_apitoken.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.10 on 2024-04-18 08:22 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_cryptography.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("hub", "0100_merge_20240408_1328"), + ] + + operations = [ + migrations.CreateModel( + name="APIToken", + fields=[ + ( + "token", + django_cryptography.fields.encrypt( + models.CharField(max_length=1500) + ), + ), + ("expires_at", models.DateTimeField()), + ( + "signature", + models.CharField( + editable=False, + max_length=1500, + primary_key=True, + serialize=False, + ), + ), + ("revoked", models.BooleanField(default=False)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("last_update", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="tokens", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/hub/models.py b/hub/models.py index 3c1d861a1..5f3417fba 100644 --- a/hub/models.py +++ b/hub/models.py @@ -28,6 +28,7 @@ import pytz from asgiref.sync import async_to_sync, sync_to_async from django_choices_field import TextChoicesField +from django_cryptography.fields import encrypt from django_jsonform.models.fields import JSONField from mailchimp3 import MailChimp from polymorphic.models import PolymorphicModel @@ -852,6 +853,20 @@ def cast_data(sender, instance, *args, **kwargs): instance.data = "" +class Loaders(TypedDict): + postcodesIO: DataLoader + fetch_record: DataLoader + source_loaders: dict[str, DataLoader] + + +class EnrichmentLookup(TypedDict): + member_id: str + postcode_data: PostcodesIOResult + source_id: "ExternalDataSource" + source_path: str + source_data: Optional[any] + + class ExternalDataSource(PolymorphicModel, Analytics): """ A third-party data source that can be read and optionally written back to. @@ -935,6 +950,7 @@ class FieldDefinition(TypedDict): value: str label: Optional[str] description: Optional[str] + external_id: Optional[str] fields = JSONField(blank=True, null=True, default=list) # Auto-updates @@ -1301,18 +1317,6 @@ async def fetch_many_loader(self, keys): for key in keys ] - class Loaders(TypedDict): - postcodesIO: DataLoader - fetch_record: DataLoader - source_loaders: dict[str, DataLoader] - - class EnrichmentLookup(TypedDict): - member_id: str - postcode_data: PostcodesIOResult - source_id: "ExternalDataSource" - source_path: str - source_data: Optional[any] - @classmethod def _get_import_data(self, id: str): """ @@ -1343,7 +1347,7 @@ def get_imported_dataframe(self): return enrichment_df def data_loader_factory(self): - async def fetch_enrichment_data(keys: List[self.EnrichmentLookup]) -> list[str]: + async def fetch_enrichment_data(keys: List[EnrichmentLookup]) -> list[str]: return_data = [] enrichment_df = await sync_to_async(self.get_imported_dataframe)() for key in keys: @@ -1390,7 +1394,7 @@ async def fetch_enrichment_data(keys: List[self.EnrichmentLookup]) -> list[str]: # and return the requested value for this enrichment source row key["source_path"], ].values - if enrichment_value: + if enrichment_value is not None: enrichment_value = enrichment_value[0] if enrichment_value is np.nan or enrichment_value == np.nan: print( @@ -1413,13 +1417,13 @@ async def fetch_enrichment_data(keys: List[self.EnrichmentLookup]) -> list[str]: return return_data - def cache_key_fn(key: self.EnrichmentLookup) -> str: + def cache_key_fn(key: EnrichmentLookup) -> str: return f"{key['member_id']}_{key['source_id']}_{key['source_path']}" return DataLoader(load_fn=fetch_enrichment_data, cache_key_fn=cache_key_fn) async def get_loaders(self) -> Loaders: - loaders = self.Loaders( + loaders = Loaders( postcodesIO=DataLoader(load_fn=get_bulk_postcode_geo), fetch_record=DataLoader(load_fn=self.fetch_many_loader, cache=False), source_loaders={ @@ -1470,7 +1474,7 @@ async def map_one(self, member: Union[str, dict], loaders: Loaders) -> MappedMem source_loader = loaders["source_loaders"].get(source, None) if source_loader is not None and postcode_data is not None: loaded = await source_loader.load( - self.EnrichmentLookup( + EnrichmentLookup( member_id=self.get_record_id(member), postcode_data=postcode_data, source_id=source, @@ -1866,6 +1870,7 @@ def field_definitions(self): # TODO: implement a field ID lookup in the UI, then revisit this value=field.name, description=field.description, + external_id=field.id, ) for field in self.table.schema().fields ] @@ -2400,3 +2405,55 @@ def get_import_data(self): def get_analytics_queryset(self): return self.get_import_data() + + +class APIToken(models.Model): + """ + A model to store generated and revoked JWT tokens. + """ + + # So we can list tokens for a user + user = models.ForeignKey(User, on_delete=models.DO_NOTHING, related_name="tokens") + # In case you need to copy/paste the token again + token = encrypt(models.CharField(max_length=1500)) + expires_at = models.DateTimeField() + + # Unencrypted so we can check if the token is revoked or not + signature = models.CharField(primary_key=True, editable=False, max_length=1500) + revoked = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True) + last_update = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Revoked JWT Token {self.jti}" + + +def api_token_key(signature: str) -> str: + return f"api_token:{signature}" + + +def refresh_tokens_cache(): + tokens = APIToken.objects.all() + for token in tokens: + cache.set(api_token_key(token.signature), token) + + +def get_api_token(signature: str) -> APIToken: + return cache.get(api_token_key(signature)) + + +def is_api_token_revoked(signature: str) -> APIToken: + token = cache.get(api_token_key(signature)) + return token.revoked if token else False + + +# a signal that, when APIToken is created, updated, updates the apitoken cache +@receiver(models.signals.post_save, sender=APIToken) +def update_apitoken_cache_on_save(sender, instance, *args, **kwargs): + refresh_tokens_cache() + + +@receiver(models.signals.post_delete, sender=APIToken) +def update_apitoken_cache_on_delete(sender, instance, *args, **kwargs): + refresh_tokens_cache() diff --git a/hub/tests/test_api.py b/hub/tests/test_api.py new file mode 100644 index 000000000..269e49616 --- /dev/null +++ b/hub/tests/test_api.py @@ -0,0 +1,370 @@ +import json + +from django.conf import settings +from django.test import Client, TestCase +from django.urls import reverse + +from hub import models + + +class TestPublicAPI(TestCase): + @classmethod + def setUpTestData(cls): + cls.client = Client() + # Create user + cls.user = models.User.objects.create_user( + username="testuser", password="12345" + ) + # Create org for user + cls.org = models.Organisation.objects.create(name="testorg", slug="testorg") + cls.membership = models.Membership.objects.create( + user=cls.user, organisation=cls.org, role="owner" + ) + # Create source + cls.custom_data_layer: models.AirtableSource = models.AirtableSource.objects.create( + name="Mayoral regions custom data layer", + data_type=models.AirtableSource.DataSourceType.OTHER, + organisation=cls.org, + base_id=settings.TEST_AIRTABLE_CUSTOMDATALAYER_BASE_ID, + table_id=settings.TEST_AIRTABLE_CUSTOMDATALAYER_TABLE_NAME, + api_key=settings.TEST_AIRTABLE_CUSTOMDATALAYER_API_KEY, + geography_column="council district", + geography_column_type=models.AirtableSource.PostcodesIOGeographyTypes.COUNCIL, + ) + # Some dummy data + ds, x = models.DataSet.objects.update_or_create( + name="xyz", external_data_source=cls.custom_data_layer + ) + dt, x = models.DataType.objects.update_or_create(name="xyz", data_set=ds) + models.GenericData.objects.update_or_create( + json={ + "mayoral region": "North East Mayoral Combined Authority", + "council district": "Newcastle upon Tyne", + }, + data_type=dt, + ) + models.GenericData.objects.update_or_create( + json={ + "mayoral region": "North East Mayoral Combined Authority", + "council district": "County Durham", + }, + data_type=dt, + ) + # Make a dummy region + area_type, x = models.AreaType.objects.update_or_create( + name="2010 Parliamentary Constituency", + code="WMC", + area_type="Westminster Constituency", + description="Westminster Parliamentary Constituency boundaries, as created in 2010", + ) + models.Area.objects.update_or_create( + mapit_id="66055", + gss="E14000831", + name="Newcastle upon Tyne Central", + area_type=area_type, + ) + models.Area.objects.update_or_create( + name="City of Durham", + gss="E14000641", + mapit_id="66021", + area_type=area_type, + ) + cls.client.login(username="testuser", password="12345") + res = cls.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "variables": {"username": "testuser", "password": "12345"}, + "query": """ + mutation Login($username: String!, $password: String!) { + tokenAuth(username: $username, password: $password) { + errors + success + token { + token + } + } + } + """, + }, + ) + cls.token = res.json()["data"]["tokenAuth"]["token"]["token"] + + def test_single_enrich_postcode(self): + postcode = "NE13AF" + query = """ + query EnrichPostcode($postcode: String!, $customSourceId: String!) { + enrichPostcode(postcode: $postcode) { + postcode + mayoralRegion: customSourceData( + source: $customSourceId, + sourcePath: "mayoral region" + ) + councilDistrict: customSourceData( + source: $customSourceId, + sourcePath: "council district" + ) + postcodesIO { + parliamentaryConstituency + } + constituency { + name + # TODO: import/mock political data and test this + # lastElection { + # date + # } + # people { + # name + # personType + # } + } + } + } + """ + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": query, + "variables": { + "postcode": postcode, + "customSourceId": str(self.custom_data_layer.id), + }, + }, + headers={ + "Authorization": f"JWT {self.token}", + }, + ) + result = res.json() + + self.assertIsNone(result.get("errors", None)) + self.assertJSONEqual( + json.dumps(result["data"]["enrichPostcode"]), + { + "postcode": "NE13AF", + "mayoralRegion": "North East Mayoral Combined Authority", + "councilDistrict": "Newcastle upon Tyne", + "postcodesIO": { + "parliamentaryConstituency": "Newcastle upon Tyne Central" + }, + "constituency": { + "name": "Newcastle upon Tyne Central", + # TODO: import/mock political data and test this + # "lastElection": { + # "date": "2019-12-12" + # }, + # "people": [ + # { + # "name": "Chi Onwurah", + # "personType": "MP" + # } + # ] + }, + }, + ) + + def test_bulk_enrich_postcode(self): + postcodes = ["NE13AF", "DH13SG"] + query = """ + query BulkEnrichPostcodes($postcodes: [String!]!, $customSourceId: String!) { + enrichPostcodes(postcodes: $postcodes) { + postcode + mayoralRegion: customSourceData( + source: $customSourceId, + sourcePath: "mayoral region" + ) + councilDistrict: customSourceData( + source: $customSourceId, + sourcePath: "council district" + ) + postcodesIO { + parliamentaryConstituency + } + constituency { + name + # TODO: import/mock political data and test this + # lastElection { + # date + # } + # people { + # name + # personType + # } + } + } + } + """ + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": query, + "variables": { + "postcodes": postcodes, + "customSourceId": str(self.custom_data_layer.id), + }, + }, + headers={ + "Authorization": f"JWT {self.token}", + }, + ) + result = res.json() + + self.assertIsNone(result.get("errors", None)) + self.assertJSONEqual( + json.dumps(result["data"]["enrichPostcodes"]), + [ + { + "postcode": "NE13AF", + "mayoralRegion": "North East Mayoral Combined Authority", + "councilDistrict": "Newcastle upon Tyne", + "postcodesIO": { + "parliamentaryConstituency": "Newcastle upon Tyne Central" + }, + "constituency": { + "name": "Newcastle upon Tyne Central", + # TODO: import/mock political data and test this + # "lastElection": { + # "date": "2019-12-12" + # }, + # "people": [ + # { + # "name": "Chi Onwurah", + # "personType": "MP" + # } + # ] + }, + }, + { + "postcode": "DH13SG", + "mayoralRegion": "North East Mayoral Combined Authority", + "councilDistrict": "County Durham", + "postcodesIO": {"parliamentaryConstituency": "City of Durham"}, + "constituency": { + "name": "City of Durham", + # "gss": "E14000641", + # TODO: import/mock political data and test this + # "mapitId": "66021", + # "lastElection": { + # "date": "2019-12-12" + # }, + # "people": [ + # { + # "name": "Mary Foy", + # "personType": "MP" + # } + # ] + }, + }, + ], + ) + + def test_create_use_revoke_api_token(self): + # Generate an API token + query = """ + mutation CreateRevokeApiToken { + createApiToken { + token + signature + } + } + """ + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": query, + }, + headers={ + "Authorization": f"JWT {self.token}", + }, + ) + result = res.json() + + generated_token = result["data"]["createApiToken"]["token"] + generated_signature = result["data"]["createApiToken"]["signature"] + + self.assertIsNotNone(generated_token) + + # Test the new token + + postcode = "NE13AF" + postcode_query = """ + query EnrichPostcode($postcode: String!) { + enrichPostcode(postcode: $postcode) { + postcode + } + } + """ + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": postcode_query, + "variables": { + "postcode": postcode, + }, + }, + headers={ + "Authorization": f"JWT {generated_token}", + }, + ) + result = res.json() + + self.assertIsNone(result.get("errors", None)) + self.assertIsNotNone(result["data"]["enrichPostcode"]) + + # Revoke the token + + revoke_query = """ + mutation RevokeToken($signature: ID!) { + revokeApiToken(signature: $signature) { + signature + revoked + } + } + """ + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": revoke_query, + "variables": { + "signature": generated_signature, + }, + }, + headers={ + "Authorization": f"JWT {self.token}", + }, + ) + + result = res.json() + self.assertIsNone(result.get("errors", None)) + self.assertTrue(result["data"]["revokeApiToken"]["revoked"]) + + # Now make a new query with the revoked token and check it doesn't work anymore + + res = self.client.post( + reverse("graphql"), + content_type="application/json", + data={ + "query": postcode_query, + "variables": { + "postcode": postcode, + }, + }, + headers={ + "Authorization": f"JWT {generated_token}", + }, + ) + + result = res.json() + self.assertJSONEqual( + json.dumps(result), + {"data": None, "errors": [{"message": "Token has been revoked"}]}, + ) diff --git a/local_intelligence_hub/settings.py b/local_intelligence_hub/settings.py index 51be2e50b..39a6673bb 100644 --- a/local_intelligence_hub/settings.py +++ b/local_intelligence_hub/settings.py @@ -10,14 +10,12 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ -import os from datetime import timedelta from pathlib import Path import environ +import posthog from gqlauth.settings_type import GqlAuthSettings -from sentry_sdk import init -from sentry_sdk.integrations.django import DjangoIntegration # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -51,9 +49,25 @@ TEST_MAILCHIMP_MEMBERLIST_API_KEY=(str, ""), TEST_MAILCHIMP_MEMBERLIST_AUDIENCE_ID=(str, ""), DJANGO_LOG_LEVEL=(str, "INFO"), + POSTHOG_API_KEY=(str, False), + POSTHOG_HOST=(str, False), + ENVIRONMENT=(str, "development"), + SENTRY_DSN=(str, False), + CRYPTOGRAPHY_KEY=(str, "somemadeupcryptographickeywhichshouldbereplaced"), + CRYPTOGRAPHY_SALT=(str, "somesaltthatshouldbereplaced"), ) + environ.Env.read_env(BASE_DIR / ".env") +# Should be alphanumeric +CRYPTOGRAPHY_KEY = env("CRYPTOGRAPHY_KEY") +CRYPTOGRAPHY_SALT = env("CRYPTOGRAPHY_SALT") + +if CRYPTOGRAPHY_KEY is None: + raise ValueError("CRYPTOGRAPHY_KEY must be set") +if CRYPTOGRAPHY_SALT is None: + raise ValueError("CRYPTOGRAPHY_SALT must be set") + BASE_URL = env("BASE_URL") FRONTEND_BASE_URL = env("FRONTEND_BASE_URL") FRONTEND_SITE_TITLE = env("FRONTEND_SITE_TITLE") @@ -123,6 +137,7 @@ "corsheaders", "procrastinate.contrib.django", "strawberry_django", + "django_cryptography", ] MIDDLEWARE = [ @@ -313,6 +328,13 @@ IMPORT_UPDATE_ALL_BATCH_SIZE = 100 IMPORT_UPDATE_MANY_RETRY_COUNT = 3 + +def jwt_handler(token): + from hub.graphql.types.public_queries import decode_jwt + + return decode_jwt(token) + + # TODO: Decrease this when we go public one_week = timedelta(days=7) GQL_AUTH = GqlAuthSettings( @@ -321,6 +343,7 @@ "frontend_site_title": FRONTEND_SITE_TITLE, }, JWT_EXPIRATION_DELTA=one_week, + JWT_DECODE_HANDLER=jwt_handler, LOGIN_REQUIRE_CAPTCHA=False, REGISTER_REQUIRE_CAPTCHA=False, ALLOW_LOGIN_NOT_VERIFIED=True, @@ -333,16 +356,33 @@ SCHEDULED_UPDATE_SECONDS_DELAY = env("SCHEDULED_UPDATE_SECONDS_DELAY") -environment = os.getenv("ENVIRONMENT") +environment = env("ENVIRONMENT") + +posthog.disabled = True # Configure Sentry only if in production if environment == "production": - init( - dsn=os.getenv("SENTRY_DSN"), - environment=environment, - integrations=[DjangoIntegration()], - traces_sample_rate=1.0, - ) + if env("SENTRY_DSN") is not False: + import sentry_sdk + from sentry_sdk.integrations.django import DjangoIntegration + from sentry_sdk.integrations.strawberry import StrawberryIntegration + + sentry_sdk.init( + dsn=env("SENTRY_DSN"), + environment=environment, + integrations=[ + DjangoIntegration(), + StrawberryIntegration(async_execution=True), + ], + # Optionally, you can adjust the logging level + traces_sample_rate=1.0, # Adjust sample rate as needed + ) + + if env("POSTHOG_API_KEY") is not False and env("POSTHOG_HOST") is not False: + posthog.disabled = False + posthog.project_api_key = env("POSTHOG_API_KEY") + posthog.host = env("POSTHOG_HOST") + MINIO_STORAGE_ENDPOINT = env("MINIO_STORAGE_ENDPOINT") if MINIO_STORAGE_ENDPOINT is not False: diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json index 7b9a18e3c..b9c2dc7d9 100644 --- a/nextjs/package-lock.json +++ b/nextjs/package-lock.json @@ -3449,6 +3449,25 @@ "@parcel/watcher-win32-x64": "2.4.1" } }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", @@ -3468,6 +3487,196 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@peculiar/asn1-schema": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz", diff --git a/nextjs/src/__generated__/gql.ts b/nextjs/src/__generated__/gql.ts index d8c809707..f55907ffb 100644 --- a/nextjs/src/__generated__/gql.ts +++ b/nextjs/src/__generated__/gql.ts @@ -31,6 +31,9 @@ const documents = { "\n query ShareWithOrgPage($orgSlug: String!) {\n allOrganisations(filters: {slug: $orgSlug}) {\n id\n name\n }\n }\n": types.ShareWithOrgPageDocument, "\n query ListReports {\n reports {\n id\n name\n lastUpdate\n }\n }\n": types.ListReportsDocument, "\nmutation CreateMapReport($data: MapReportInput!) {\n createMapReport(data: $data) {\n ... on MapReport {\n id\n }\n ... on OperationInfo {\n messages {\n message\n }\n }\n }\n}\n": types.CreateMapReportDocument, + "\n query DeveloperAPIContext {\n listApiTokens {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n": types.DeveloperApiContextDocument, + "\n mutation CreateToken {\n createApiToken {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n ": types.CreateTokenDocument, + "\n mutation RevokeToken($signature: ID!) {\n revokeApiToken(signature: $signature) {\n signature\n revoked\n }\n }\n ": types.RevokeTokenDocument, "\n mutation Verify($token: String!) {\n verifyAccount(token: $token) {\n errors\n success\n }\n }\n": types.VerifyDocument, "\n query Example {\n myOrganisations {\n id\n name\n }\n }\n": types.ExampleDocument, "\n mutation Login($username: String!, $password: String!) {\n tokenAuth(username: $username, password: $password) {\n errors\n success\n token {\n token\n payload {\n exp\n }\n }\n }\n }\n": types.LoginDocument, @@ -148,6 +151,18 @@ export function gql(source: "\n query ListReports {\n reports {\n id\n * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\nmutation CreateMapReport($data: MapReportInput!) {\n createMapReport(data: $data) {\n ... on MapReport {\n id\n }\n ... on OperationInfo {\n messages {\n message\n }\n }\n }\n}\n"): (typeof documents)["\nmutation CreateMapReport($data: MapReportInput!) {\n createMapReport(data: $data) {\n ... on MapReport {\n id\n }\n ... on OperationInfo {\n messages {\n message\n }\n }\n }\n}\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n query DeveloperAPIContext {\n listApiTokens {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n"): (typeof documents)["\n query DeveloperAPIContext {\n listApiTokens {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation CreateToken {\n createApiToken {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n "): (typeof documents)["\n mutation CreateToken {\n createApiToken {\n token\n signature\n revoked\n createdAt\n expiresAt\n }\n }\n "]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n mutation RevokeToken($signature: ID!) {\n revokeApiToken(signature: $signature) {\n signature\n revoked\n }\n }\n "): (typeof documents)["\n mutation RevokeToken($signature: ID!) {\n revokeApiToken(signature: $signature) {\n signature\n revoked\n }\n }\n "]; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/nextjs/src/__generated__/graphql.ts b/nextjs/src/__generated__/graphql.ts index c4e7015f7..4a80ba861 100644 --- a/nextjs/src/__generated__/graphql.ts +++ b/nextjs/src/__generated__/graphql.ts @@ -51,6 +51,16 @@ export type Scalars = { UUID: { input: any; output: any; } }; +/** A model to store generated and revoked JWT tokens. */ +export type ApiToken = { + __typename?: 'APIToken'; + createdAt: Scalars['DateTime']['output']; + expiresAt: Scalars['DateTime']['output']; + revoked: Scalars['Boolean']['output']; + signature: Scalars['ID']['output']; + token: Scalars['String']['output']; +}; + /** An Airtable table. */ export type AirtableSource = Analytics & { __typename?: 'AirtableSource'; @@ -599,6 +609,7 @@ export type Feature = { export type FieldDefinition = { __typename?: 'FieldDefinition'; description?: Maybe; + externalId?: Maybe; label?: Maybe; value: Scalars['String']['output']; }; @@ -909,6 +920,7 @@ export type MultiPolygonGeometry = { export type Mutation = { __typename?: 'Mutation'; + createApiToken: ApiToken; createExternalDataSource: CreateExternalDataSourceOutput; createMapReport: CreateMapReportPayload; createOrganisation: Membership; @@ -949,6 +961,7 @@ export type Mutation = { * */ resendActivationEmail: MutationNormalOutput; + revokeApiToken: ApiToken; /** * Obtain JSON web token for given user. * @@ -981,6 +994,11 @@ export type Mutation = { }; +export type MutationCreateApiTokenArgs = { + expiryDays?: Scalars['Int']['input']; +}; + + export type MutationCreateExternalDataSourceArgs = { input: CreateExternalDataSourceInput; }; @@ -1049,6 +1067,11 @@ export type MutationResendActivationEmailArgs = { }; +export type MutationRevokeApiTokenArgs = { + signature: Scalars['ID']['input']; +}; + + export type MutationTokenAuthArgs = { password: Scalars['String']['input']; username: Scalars['String']['input']; @@ -1259,6 +1282,20 @@ export type PointGeometry = { type: GeoJsonTypes; }; +export type PostcodeQueryResponse = { + __typename?: 'PostcodeQueryResponse'; + constituency?: Maybe; + customSourceData?: Maybe; + postcode: Scalars['String']['output']; + postcodesIO?: Maybe; +}; + + +export type PostcodeQueryResponseCustomSourceDataArgs = { + source: Scalars['String']['input']; + sourcePath: Scalars['String']['input']; +}; + export type PostcodesIoCodes = { __typename?: 'PostcodesIOCodes'; adminCounty: Scalars['String']['output']; @@ -1339,11 +1376,14 @@ export type Query = { allOrganisations: Array; area?: Maybe; dataSet?: Maybe; + enrichPostcode: PostcodeQueryResponse; + enrichPostcodes: Array; externalDataSource: ExternalDataSource; externalDataSources: Array; importedDataGeojsonPoint?: Maybe; job: QueueJob; jobs: Array; + listApiTokens: Array; mailchimpSource: MailchimpSource; mailchimpSources: Array; mapReport: MapReport; @@ -1380,6 +1420,16 @@ export type QueryDataSetArgs = { }; +export type QueryEnrichPostcodeArgs = { + postcode: Scalars['String']['input']; +}; + + +export type QueryEnrichPostcodesArgs = { + postcodes: Array; +}; + + export type QueryExternalDataSourceArgs = { pk: Scalars['ID']['input']; }; @@ -1846,6 +1896,23 @@ export type CreateMapReportMutationVariables = Exact<{ export type CreateMapReportMutation = { __typename?: 'Mutation', createMapReport: { __typename?: 'MapReport', id: any } | { __typename?: 'OperationInfo', messages: Array<{ __typename?: 'OperationMessage', message: string }> } }; +export type DeveloperApiContextQueryVariables = Exact<{ [key: string]: never; }>; + + +export type DeveloperApiContextQuery = { __typename?: 'Query', listApiTokens: Array<{ __typename?: 'APIToken', token: string, signature: string, revoked: boolean, createdAt: any, expiresAt: any }> }; + +export type CreateTokenMutationVariables = Exact<{ [key: string]: never; }>; + + +export type CreateTokenMutation = { __typename?: 'Mutation', createApiToken: { __typename?: 'APIToken', token: string, signature: string, revoked: boolean, createdAt: any, expiresAt: any } }; + +export type RevokeTokenMutationVariables = Exact<{ + signature: Scalars['ID']['input']; +}>; + + +export type RevokeTokenMutation = { __typename?: 'Mutation', revokeApiToken: { __typename?: 'APIToken', signature: string, revoked: boolean } }; + export type VerifyMutationVariables = Exact<{ token: Scalars['String']['input']; }>; @@ -2062,6 +2129,9 @@ export const YourSourcesForSharingDocument = {"kind":"Document","definitions":[{ export const ShareWithOrgPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ShareWithOrgPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allOrganisations"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgSlug"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const ListReportsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListReports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdate"}}]}}]}}]} as unknown as DocumentNode; export const CreateMapReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateMapReport"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MapReportInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createMapReport"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MapReport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OperationInfo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeveloperApiContextDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DeveloperAPIContext"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"listApiTokens"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"signature"}},{"kind":"Field","name":{"kind":"Name","value":"revoked"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]} as unknown as DocumentNode; +export const CreateTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createApiToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"signature"}},{"kind":"Field","name":{"kind":"Name","value":"revoked"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]} as unknown as DocumentNode; +export const RevokeTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"signature"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeApiToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"signature"},"value":{"kind":"Variable","name":{"kind":"Name","value":"signature"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"signature"}},{"kind":"Field","name":{"kind":"Name","value":"revoked"}}]}}]}}]} as unknown as DocumentNode; export const VerifyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Verify"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"verifyAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode; export const ExampleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Example"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myOrganisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tokenAuth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}},{"kind":"Argument","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"token"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"payload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exp"}}]}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/nextjs/src/app/(auth)/account/developer/page.tsx b/nextjs/src/app/(auth)/account/developer/page.tsx new file mode 100644 index 000000000..4c875159d --- /dev/null +++ b/nextjs/src/app/(auth)/account/developer/page.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { CreateTokenMutation, CreateTokenMutationVariables, DeveloperApiContextQuery, RevokeTokenMutation, RevokeTokenMutationVariables } from "@/__generated__/graphql"; +import { GraphQLPlayground } from "@/components/graphiql"; +import { Button } from "@/components/ui/button"; +import { toastPromise } from "@/lib/toast"; +import { gql, useApolloClient } from "@apollo/client"; +import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { toast } from "sonner"; +import { Input } from "@/components/ui/input"; +import { ClipboardCopy, Trash } from "lucide-react"; +import Link from "next/link"; +import { LoadingIcon } from "@/components/ui/loadingIcon"; + +const YOUR_API_TOKENS = gql` + query DeveloperAPIContext { + listApiTokens { + token + signature + revoked + createdAt + expiresAt + } + } +` + +export default function DeveloperConfig() { + const { data, error, loading, refetch, client } = useQuery(YOUR_API_TOKENS); + + if (error && !loading) { + return

Error: {String(error)}

; + } + + if (loading || !data) { + return ; + } + + return ( +
+

API Tokens

+ + + + Token + Expiry date + Actions + + + + {data?.listApiTokens?.filter(t => !t.revoked).map(token => ( + + + { + // highlight on click + const input: HTMLInputElement = document.getElementById(`input-${token.signature}`) as any + input?.focus() + input?.select() + navigator.clipboard.writeText(token.token) + toast.success("Copied token to clipboard") + }} + value={token.token} + /> + + + {new Date(token.expiresAt).toLocaleDateString()} + + + + + + + + + + + + Are you sure? + + + + + + + + + + + ))} + +
+ + +

How to authenticate the API

+

+ To use the API, simply make a request to {process.env.NEXT_PUBLIC_BACKEND_BASE_URL}/graphql and place the token in the Authorization header following {"'"}JWT{"'"}, like so: Authorization: {"'"}JWT eyJh...{"'"}. +

+
+ Visit the API playground → +
+
+ ) + + function createToken() { + const createToken = client.mutate({ + mutation: gql` + mutation CreateToken { + createApiToken { + token + signature + revoked + createdAt + expiresAt + } + } + ` + }) + toastPromise(createToken, { + loading: "Creating token...", + success: () => { + refetch() + return "Token has been created" + }, + error: "Token creation failed", + }); + } + + function revoke(signature: string) { + const revokeToken = client.mutate({ + mutation: gql` + mutation RevokeToken($signature: ID!) { + revokeApiToken(signature: $signature) { + signature + revoked + } + } + `, + variables: { signature } + }) + toastPromise(revokeToken, { + loading: "Revoking token...", + success: () => { + refetch() + return "Token has been revoked" + }, + error: "Token revocation failed", + }); + } +} \ No newline at end of file diff --git a/nextjs/src/app/(auth)/account/layout.tsx b/nextjs/src/app/(auth)/account/layout.tsx new file mode 100644 index 000000000..63a6d32c1 --- /dev/null +++ b/nextjs/src/app/(auth)/account/layout.tsx @@ -0,0 +1,52 @@ +import Link from "next/link" +import { CircleUser, Menu, Package2, Search } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" + +export default function Dashboard({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
+

Account

+
+
+ +
+ {children} +
+
+
+ ) +} \ No newline at end of file diff --git a/nextjs/src/app/(auth)/account/page.tsx b/nextjs/src/app/(auth)/account/page.tsx index c78e12c4d..104bb7d59 100644 --- a/nextjs/src/app/(auth)/account/page.tsx +++ b/nextjs/src/app/(auth)/account/page.tsx @@ -1,38 +1,34 @@ import { Metadata } from "next"; import { useRequireAuth } from "../../../hooks/auth"; -import YourOrganisations from "./your-organisations"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import DeveloperConfig from "./developer"; +import YourOrganisations from "./your-organisations"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; export default async function Account() { const user = await useRequireAuth(); return ( - <> - -

Welcome to your Mapped Account, {user.username}

- - - Account Information - Your organisations - - -
-
- Username -
{user.username}
-
-
- Email -
{user.email}
-
-
-
- +
+

Welcome to your Mapped Account, {user.username}

+ + + + Your email + + + {user.email} + + + + + Your organisations + + - - - - + + +
); } diff --git a/nextjs/src/app/graphiql/components.tsx b/nextjs/src/app/graphiql/components.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/nextjs/src/app/graphiql/layout.tsx b/nextjs/src/app/graphiql/layout.tsx index 812dd3720..0def7b7c7 100644 --- a/nextjs/src/app/graphiql/layout.tsx +++ b/nextjs/src/app/graphiql/layout.tsx @@ -1,5 +1,6 @@ import { useRequireAuth } from "@/hooks/auth"; import { Metadata } from "next"; +import 'graphiql/graphiql.css'; export default function GraphiQL ({ children }: { children: React.ReactNode }) { useRequireAuth() diff --git a/nextjs/src/app/graphiql/page.tsx b/nextjs/src/app/graphiql/page.tsx index 3dbe13902..e756832f1 100644 --- a/nextjs/src/app/graphiql/page.tsx +++ b/nextjs/src/app/graphiql/page.tsx @@ -1,24 +1,7 @@ "use client" -import { createGraphiQLFetcher } from '@graphiql/toolkit'; -import { GraphiQL } from 'graphiql'; -import React, { useMemo } from 'react'; -import 'graphiql/graphiql.css'; -import { authenticationHeaders } from '@/lib/auth'; - -function getAuthenticatedFetcher () { - try { - return createGraphiQLFetcher({ - url: `${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}/graphql`, - headers: authenticationHeaders() - }); - } catch (e) { - console.log(e) - } -} +import { GraphQLPlayground } from "@/components/graphiql" export default function Page () { - const fetcher = useMemo(getAuthenticatedFetcher, []) - if (!fetcher) return null - return + return } \ No newline at end of file diff --git a/nextjs/src/app/layout.tsx b/nextjs/src/app/layout.tsx index c5c025e4e..d2ac76892 100644 --- a/nextjs/src/app/layout.tsx +++ b/nextjs/src/app/layout.tsx @@ -39,5 +39,5 @@ export const metadata: Metadata = { template: '%s | Mapped by CK', default: 'Mapped by Common Knowledge', // a default is required when creating a template }, - metadataBase: new URL(process.env.NEXT_PUBLIC_FRONTEND_BASE_URL || '') + metadataBase: new URL(process.env.NEXT_PUBLIC_FRONTEND_BASE_URL || "http://localhost:3000") } diff --git a/nextjs/src/components/graphiql.tsx b/nextjs/src/components/graphiql.tsx new file mode 100644 index 000000000..da37c24d2 --- /dev/null +++ b/nextjs/src/components/graphiql.tsx @@ -0,0 +1,22 @@ +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL, GraphiQLProps } from 'graphiql'; +import React, { useMemo } from 'react'; +import 'graphiql/graphiql.css'; +import { authenticationHeaders } from '@/lib/auth'; + +function getAuthenticatedFetcher () { + try { + return createGraphiQLFetcher({ + url: `${process.env.NEXT_PUBLIC_BACKEND_BASE_URL}/graphql`, + headers: authenticationHeaders() + }); + } catch (e) { + console.log(e) + } +} + +export function GraphQLPlayground(props: Partial) { + const fetcher = useMemo(getAuthenticatedFetcher, []) + if (!fetcher) return null + return +} \ No newline at end of file diff --git a/nextjs/src/components/ui/card.tsx b/nextjs/src/components/ui/card.tsx index 2791e96bb..8e4341745 100644 --- a/nextjs/src/components/ui/card.tsx +++ b/nextjs/src/components/ui/card.tsx @@ -23,7 +23,7 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)) @@ -36,7 +36,7 @@ const CardTitle = React.forwardRef<

(({ className, ...props }, ref) => (

)) @@ -60,7 +60,7 @@ const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

+
)) CardContent.displayName = "CardContent" diff --git a/poetry.lock b/poetry.lock index 044d45129..0ed98930f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -161,6 +161,17 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -539,6 +550,60 @@ files = [ python-dateutil = "*" pytz = ">2021.1" +[[package]] +name = "cryptography" +version = "42.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, + {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, + {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, + {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, + {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, + {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, + {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "django" version = "4.2.10" @@ -633,6 +698,21 @@ files = [ asgiref = ">=3.6" Django = ">=3.2" +[[package]] +name = "django-cryptography" +version = "1.1" +description = "Easily encrypt data in Django" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django_cryptography-1.1-py2.py3-none-any.whl", hash = "sha256:93702fcf0d75865d55362f20ecd95274c4eef60ccdce46cbdade0420acee07cb"}, +] + +[package.dependencies] +cryptography = "*" +Django = "*" +django-appconf = "*" + [[package]] name = "django-debug-toolbar" version = "4.3.0" @@ -773,9 +853,9 @@ django = "*" mercantile = "*" [package.extras] -dev = ["black", "coverage", "django-debug-toolbar", "djangorestframework", "factory-boy", "flake8", "isort", "mapbox_vector_tile", "protobuf (<4.21.0)", "psycopg2-binary", "sphinx-rtd-theme"] -mapbox = ["mapbox_vector_tile", "protobuf (<4.21.0)"] -python = ["mapbox_vector_tile", "protobuf (<4.21.0)"] +dev = ["black", "coverage", "django-debug-toolbar", "djangorestframework", "factory-boy", "flake8", "isort", "mapbox-vector-tile", "protobuf (<4.21.0)", "psycopg2-binary", "sphinx-rtd-theme"] +mapbox = ["mapbox-vector-tile", "protobuf (<4.21.0)"] +python = ["mapbox-vector-tile", "protobuf (<4.21.0)"] test = ["black", "coverage", "djangorestframework", "factory-boy", "flake8", "isort", "psycopg2-binary"] [package.source] @@ -900,13 +980,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1204,6 +1284,17 @@ pycryptodome = "*" typing-extensions = "*" urllib3 = "*" +[[package]] +name = "monotonic" +version = "1.6" +description = "An implementation of time.monotonic() for Python 2 & < 3.3" +optional = false +python-versions = "*" +files = [ + {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, + {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, +] + [[package]] name = "mypy" version = "1.7.1" @@ -1349,46 +1440,46 @@ files = [ [[package]] name = "pandas" -version = "2.2.1" +version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, - {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, - {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, - {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, - {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, - {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, - {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, - {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, - {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, - {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, - {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, - {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, - {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, - {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, - {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, - {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, - {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, - {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, ] [package.dependencies] numpy = [ - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -1531,6 +1622,29 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "posthog" +version = "3.5.0" +description = "Integrate PostHog into any python application." +optional = false +python-versions = "*" +files = [ + {file = "posthog-3.5.0-py2.py3-none-any.whl", hash = "sha256:3c672be7ba6f95d555ea207d4486c171d06657eb34b3ce25eb043bfe7b6b5b76"}, + {file = "posthog-3.5.0.tar.gz", hash = "sha256:8f7e3b2c6e8714d0c0c542a2109b83a7549f63b7113a133ab2763a89245ef2ef"}, +] + +[package.dependencies] +backoff = ">=1.10.0" +monotonic = ">=1.5" +python-dateutil = ">2.1" +requests = ">=2.7,<3.0" +six = ">=1.5" + +[package.extras] +dev = ["black", "flake8", "flake8-print", "isort", "pre-commit"] +sentry = ["django", "sentry-sdk"] +test = ["coverage", "flake8", "freezegun (==0.3.15)", "mock (>=2.0.0)", "pylint", "pytest", "pytest-timeout"] + [[package]] name = "procrastinate" version = "2.0.0b4" @@ -1809,18 +1923,18 @@ files = [ [[package]] name = "pydantic" -version = "2.6.4" +version = "2.7.0" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" +pydantic-core = "2.18.1" typing-extensions = ">=4.6.1" [package.extras] @@ -1828,90 +1942,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.16.3" -description = "" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, ] [package.dependencies] @@ -2230,27 +2344,31 @@ files = [ [[package]] name = "scalene" -version = "1.5.38" +version = "1.5.39" description = "Scalene: A high-resolution, low-overhead CPU, GPU, and memory profiler for Python with AI-powered optimization suggestions" optional = false python-versions = "!=3.11.0,>=3.8" files = [ - {file = "scalene-1.5.38-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8e8f7468be3f8c30cdc3ca97a46717a81337dafdad003d1b800e954fd00530eb"}, - {file = "scalene-1.5.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1bc516b5749330151987b187bb89fc362011e2e52b73d304c5e0fbfea12e190"}, - {file = "scalene-1.5.38-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0024e1c2205ed6bf8696bd5a1483c61d0b434cb479cbdc45d7549bcedda26557"}, - {file = "scalene-1.5.38-cp310-cp310-win_amd64.whl", hash = "sha256:228293a23393d2d64fe4b316b0b2419f39748de1a18b25c3baa34c96efc31b25"}, - {file = "scalene-1.5.38-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:aa678fdc7c4b1a777a88e82252202199d68fd694556626fde3163f3d33cbad53"}, - {file = "scalene-1.5.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b36f839ebcc61c634232b2d09d06b8b0931ebea75843b7b8b9377b3c6502a3e"}, - {file = "scalene-1.5.38-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c665df7c3449b9ec720d600b4b6a83fc7b84234deb161f50315a199620790844"}, - {file = "scalene-1.5.38-cp38-cp38-macosx_12_0_universal2.whl", hash = "sha256:e7ceedb4e961ad926052c16fb6ebedaba1b6d03adcfa37eec929234c8c20f760"}, - {file = "scalene-1.5.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2406a618941deb640170aa5502b9eed23476accd584ababafbcb5a52c89b48b"}, - {file = "scalene-1.5.38-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c1168e7c0e30ae002b2e3e91ea4eceb4f9c606554b15530b438d1e98bcf4750"}, - {file = "scalene-1.5.38-cp38-cp38-win_amd64.whl", hash = "sha256:2f7a7050b9cdb4e255a97bdec7ce322fd68bc0ee2534abc0ca03692c7452581d"}, - {file = "scalene-1.5.38-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:4abd42862c8b065b0988a7471b41f69a0832a5757bb10ca62fc777555e8ac3e3"}, - {file = "scalene-1.5.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be4bceaf963a96e64cd1c5e3b2a545a899ef8d20ebcff144e5d833ab85d918d"}, - {file = "scalene-1.5.38-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aeae550535cc81b7d9e1e61b2a924827decbe777b661bd85d65d0be6760ac067"}, - {file = "scalene-1.5.38-cp39-cp39-win_amd64.whl", hash = "sha256:a8d4875b7ec2115a6798d89eb2f5a4ce4d5693e70a9dac94e696977e0f7cdbbe"}, - {file = "scalene-1.5.38.tar.gz", hash = "sha256:2d1d5ebe49f69ba14d06626751426e6deb3120f7871ba44380b1411c3bb17b7f"}, + {file = "scalene-1.5.39-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ae9912f938dfaf4da40df3235c173b94474c8d7a663a3c0bea5adcebef8af3dd"}, + {file = "scalene-1.5.39-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82a7f49bf19946081b369caff70eaad3f7fd01f3d3f294962131f9714e490bdc"}, + {file = "scalene-1.5.39-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc68f7ee18d76f53cc86426624c7d9c957c8780d8d053d111ed961da60e4decb"}, + {file = "scalene-1.5.39-cp310-cp310-win_amd64.whl", hash = "sha256:1e275d3f1a5f0b44133ad2b22cf7451ea50a73ca1f0b32369376bf96cabb51d3"}, + {file = "scalene-1.5.39-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:024db771d38a4d3405986a3159cc9055294b20e90532ab1f23f60b4fbc6dd331"}, + {file = "scalene-1.5.39-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:317e32c4e3aad680673eb6bcac5c71f988689bed6466795a140f882587ec1f2a"}, + {file = "scalene-1.5.39-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cae3016a013257fdbfd7bd6b6e27ba84bf8374fb0a7872bd22bf2423ac98005"}, + {file = "scalene-1.5.39-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:b0f76fb25261bed5e2f38c2be061efb256eb5bc426a3d3970838213ef2c4d232"}, + {file = "scalene-1.5.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e8f2b32b59c4a0722b6162eb1bb386de95769574fc5e892c56328d0bfded49"}, + {file = "scalene-1.5.39-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67cb12d2c42c8592af7f8c874c2d6da7c939607e92cdc724216d1b4f058995f3"}, + {file = "scalene-1.5.39-cp312-cp312-win_amd64.whl", hash = "sha256:c6524e85b78e18a9a3431356ba503a55d0c84d496c03bd6e1f4fc4b3dfe19855"}, + {file = "scalene-1.5.39-cp38-cp38-macosx_12_0_universal2.whl", hash = "sha256:4e903faf50b831b198ae774b2f462a19f8a79a98637800d0912992bcc4d953e8"}, + {file = "scalene-1.5.39-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bdb0411157940f2d9c1c68eca980bd1ce162355d24e7da98f205bd888fcb51"}, + {file = "scalene-1.5.39-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecbd6bcefc0e2ac07216bba2305b4c75809421906726cdff0f96a49b62cfb85a"}, + {file = "scalene-1.5.39-cp38-cp38-win_amd64.whl", hash = "sha256:bdecf0a24c8f66e75470cada744441691f028c7665b737e0f68bfde2c7ff4806"}, + {file = "scalene-1.5.39-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:aeaf460507b6b4940d914c7d4a24067c44b7f0ed7f5ebc02d1980a7fc7e27503"}, + {file = "scalene-1.5.39-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:135770e9e88ce57ace0bf533de563e3e25963ff4ae7a5b4c4e4519e1f3b04c64"}, + {file = "scalene-1.5.39-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e14bbec870293426d8c2d103dd9f2e3e43c4d131d8625a7584bfe22edbc6c9e7"}, + {file = "scalene-1.5.39-cp39-cp39-win_amd64.whl", hash = "sha256:c7e977f6aea19f029ee66d3d2fa8b8d5dc5c9f0fe1a3531a1ca40db9ac4fab3a"}, + {file = "scalene-1.5.39.tar.gz", hash = "sha256:078a432cfdfee7ab68419caf87afba3629822afd1ca5c3b4c9d72a575b496648"}, ] [package.dependencies] @@ -2263,13 +2381,13 @@ wheel = ">=0.36.1" [[package]] name = "sentry-sdk" -version = "1.44.1" +version = "1.45.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = "*" files = [ - {file = "sentry-sdk-1.44.1.tar.gz", hash = "sha256:24e6a53eeabffd2f95d952aa35ca52f0f4201d17f820ac9d3ff7244c665aaf68"}, - {file = "sentry_sdk-1.44.1-py2.py3-none-any.whl", hash = "sha256:5f75eb91d8ab6037c754a87b8501cc581b2827e923682f593bed3539ce5b3999"}, + {file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"}, + {file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"}, ] [package.dependencies] @@ -2310,18 +2428,18 @@ tornado = ["tornado (>=5)"] [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2359,19 +2477,18 @@ files = [ [[package]] name = "sqlparse" -version = "0.4.4" +version = "0.5.0" description = "A non-validating SQL parser." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, - {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, ] [package.extras] -dev = ["build", "flake8"] +dev = ["build", "hatch"] doc = ["sphinx"] -test = ["pytest", "pytest-cov"] [[package]] name = "starlette" @@ -2392,13 +2509,13 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "strawberry-django-auth" -version = "0.376.10" +version = "0.376.11" description = "Graphql authentication system with Strawberry for Django." optional = false python-versions = "<3.13,>=3.8" files = [ - {file = "strawberry_django_auth-0.376.10-py3-none-any.whl", hash = "sha256:d86a15f361f2d0968e756917551e74dcf56c36341c5386feee3a33a16e6b973c"}, - {file = "strawberry_django_auth-0.376.10.tar.gz", hash = "sha256:52b7fb04187c3a71d1d6d77891533ce2250116dd24e9f5bb3e86428c3d2528c9"}, + {file = "strawberry_django_auth-0.376.11-py3-none-any.whl", hash = "sha256:44eb62c6ac8ee6ae945240ac0a3a4ec1e0beeb35109de94040ce91e94c473350"}, + {file = "strawberry_django_auth-0.376.11.tar.gz", hash = "sha256:0e8d277f8ad5a8c9e9b19746f6eb0e378c9dc1b57141e350e9533be6663b5ba0"}, ] [package.dependencies] @@ -2514,13 +2631,13 @@ telegram = ["requests"] [[package]] name = "types-pytz" -version = "2024.1.0.20240203" +version = "2024.1.0.20240417" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" files = [ - {file = "types-pytz-2024.1.0.20240203.tar.gz", hash = "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e"}, - {file = "types_pytz-2024.1.0.20240203-py3-none-any.whl", hash = "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3"}, + {file = "types-pytz-2024.1.0.20240417.tar.gz", hash = "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981"}, + {file = "types_pytz-2024.1.0.20240417-py3-none-any.whl", hash = "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659"}, ] [[package]] @@ -2636,4 +2753,4 @@ brotli = ["Brotli"] [metadata] lock-version = "2.0" python-versions = ">3.11,<3.13" -content-hash = "7a1112d8cb8b7ea80658744ca2b2a953ce0a62aaa9ff0dd5178c16c24c98684a" +content-hash = "49ca7e2f4042bdbd25b1a54132e1c1c9086bcfcdb2cd640c2b7966310f5c1219" diff --git a/pyproject.toml b/pyproject.toml index d399c9834..92f580b1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,8 @@ django-minio-storage = "^0.5.7" mailchimp3 = "^3.0.21" scalene = "^1.5.38" django-vectortiles = { git = "https://github.com/submarcos/django-vectortiles.git" } +django-cryptography = "^1.1" +posthog = "^3.5.0" [tool.poetry.dev-dependencies] black = "^22.8.0" diff --git a/utils/postcodesIO.py b/utils/postcodesIO.py index 1519e0920..00b10672f 100644 --- a/utils/postcodesIO.py +++ b/utils/postcodesIO.py @@ -77,11 +77,14 @@ class PostcodesIOBulkResult: result: List[ResultElement] -def get_postcode_geo(postcode: str) -> PostcodesIOResult: - response = requests.get(f"{settings.POSTCODES_IO_URL}/postcodes/{postcode}") +async def get_postcode_geo(postcode: str) -> PostcodesIOResult: + async with httpx.AsyncClient() as client: + response = await client.get(f"{settings.POSTCODES_IO_URL}/postcodes/{postcode}") + if response.status_code != httpx.codes.OK: + raise Exception(f"Failed to geocode postcode: {postcode}.") data = response.json() status = get(data, "status") - result = get(data, "result") + result: PostcodesIOResult = get(data, "result") if status != 200 or result is None: raise Exception(f"Failed to geocode postcode: {postcode}.")