diff --git a/config/api.py b/config/api.py new file mode 100644 index 0000000..1c73ecd --- /dev/null +++ b/config/api.py @@ -0,0 +1,8 @@ +from ninja import NinjaAPI + +from core.api import router as core_router + + +api = NinjaAPI() + +api.add_router("", core_router) diff --git a/config/urls.py b/config/urls.py index f6f0497..55351b2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,8 +18,11 @@ from django.contrib import admin from django.urls import include, path +from .api import api + urlpatterns = [ + path("api/", api.urls), path("", include("core.urls")), path("pingdom/", include("pingdom.urls")), path("admin/", admin.site.urls), diff --git a/core/api.py b/core/api.py new file mode 100644 index 0000000..c744e62 --- /dev/null +++ b/core/api.py @@ -0,0 +1,59 @@ +from typing import List + +from ninja import Router + +from identity.core.models import PeopleFinderProfile, Proifle, StaffSSOProfile + +from .schemas.scim_schema import SCIMUser, user +from .services.user import UserService + + +router = Router() + + +@router.get("scim/v2/Users", response=List[SCIMUser]) +def get_users(request): + users = user.objects.all() + return users + + +responsecodes = frozenset({200, 201}) + + +@router.get("/user/emails?profile=peoplefinder") +def get_emails(request): + emails = CoreService.get_user_emails(request) + return emails + + +@router.post("scim/v2/Users", response={responsecodes: SCIMUser}) +def create_user(request, scim_request: SCIMUser): + user, created = CoreService.createUser(scim_request) + # user, created = UserService.createUser(scim_request) + + if created: + return 201, user + else: + return 200, user + + +class CoreService: + def create_user(scim_request): + user = UserService.createUser(scim_request) + + sso_profile_request = {"user": user, "sso_id": scim_request.external_id} + + profile = ProfileService.createProfile(sso_profile_request, StaffSSOProfile) + + pf_profile_request = {"user": user, "first_name": scim_request.name.givenName} + + profile = ProfileService.create_profile(pf_profile_request, PeopleFinderProfile) + + def create_profile(request): + request = {} + ProfileService.createProfile(request) + + +class ProfileService: + def create_profile(self, request, type): + profile, created = type.objects.get_or_create(request) diff --git a/core/models.py b/core/models.py index c389f5d..0cf17b5 100644 --- a/core/models.py +++ b/core/models.py @@ -1,4 +1,30 @@ +from django.contrib.postgres.fields import ArrayField from django.db import models -# Create your models here. +class User(models.Model): + external_id: str + + +class Email(models.Model): + address = models.EmailField() + + +class Proifle(models.Model): + user: User + + +class StaffSSOProfile(Proifle): + user: User + sso_id: int + given_name: str + family_name: str + prefered_email = models.EmailField() + emails = ArrayField(Email) + + +class PeopleFinderProfile(Proifle): + first_name: str + last_name: str + prefered_email = models.EmailField() + emails = ArrayField(Email) diff --git a/core/schemas/__init__.py b/core/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/schemas/scim_schema.py b/core/schemas/scim_schema.py new file mode 100644 index 0000000..e4eff94 --- /dev/null +++ b/core/schemas/scim_schema.py @@ -0,0 +1,64 @@ +from dataclasses import dataclass +from typing import Dict, List + +from django.contrib.auth import get_user_model +from ninja import Field, Schema + + +user = get_user_model() + + +@dataclass +class Name: + givenName: str + familyName: str + formatted: str | None = None + middleName: str | None = None + honorificPrefix: str | None = None + honorificSuffix: str | None = None + + +@dataclass +class Email: + value: str | None = None + type: str | None = None + primary: bool = False + + +class SCIMUser(Schema): + class Config: + populate_by_name = True + + schemas: List = ["urn:ietf:params:scim:schemas:core:2.0:User"] + userName: str = Field(alias="username") + name: Name + displayName: str | None = None + nickName: str | None = None + profileUrl: str | None = None + title: str | None = None + userType: str | None = None + preferredLanguage: str = None + locale: str | None = None + timezone: str | None = None + active: bool = Field(alias="is_active") + emails: list[Email] | None = None + ims: str | None = None + photos: str | None = None + + @staticmethod + def resolve_emails(obj: Dict[any, any] | get_user_model): + if type(obj) is get_user_model(): + if obj.email: + return [Email(obj.email, "work", True)] + else: + if "emails" in obj: + return obj["emails"] + else: + return None + + @staticmethod + def resolve_name(obj: Dict[any, any] | get_user_model): + if type(obj) is get_user_model(): + return Name(obj.first_name, obj.last_name) + else: + return obj["name"] diff --git a/core/services/__init__.py b/core/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/services/scim.py b/core/services/scim.py new file mode 100644 index 0000000..9c43aec --- /dev/null +++ b/core/services/scim.py @@ -0,0 +1,45 @@ +from django.contrib.auth import get_user_model + +from core.schemas.scim_schema import SCIMUser + + +class SCIMService: + def __init__(): + super() + + def transform_user(scim_request: SCIMUser) -> get_user_model: + user_model = get_user_model() + user = { + "username": scim_request.userName, + "email": scim_request.emails, + "first_name": scim_request.name.givenName, + "last_name": scim_request.name.familyName, + "is_active": scim_request.active, + "is_superuser": False, + "is_staff": False, + } + + user, _ = user_model.objects.get_or_create( + username=scim_request.userName, + email=scim_request.emails, + first_name=scim_request.name.givenName, + last_name=scim_request.name.familyName, + is_active=scim_request.active, + is_superuser=False, + is_staff=False, + ) + + return user + + def to_scim(user: get_user_model) -> SCIMUser: + # print(user) + return { + "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], + "userName": user.username, + "name": { + "givenName": user.first_name, + "familyName": user.last_name, + }, + "emails": user.email, + "active": user.is_active, + } diff --git a/core/services/user.py b/core/services/user.py new file mode 100644 index 0000000..1d0353f --- /dev/null +++ b/core/services/user.py @@ -0,0 +1,23 @@ +from django.contrib.auth import get_user_model + +from core.schemas.scim_schema import SCIMUser + + +class UserService: + @staticmethod + def get_primary_email(scim_user: SCIMUser) -> str: + emails = scim_user.emails + primary_email = [email for email in emails if email.primary][0] + return primary_email.value + + def createUser(scim_user: SCIMUser) -> tuple[get_user_model, bool]: + user_model = get_user_model() + + user, created = user_model.objects.get_or_create( + username=scim_user.externalId, + is_active=scim_user.active, + is_staff=False, + is_superuser=False, + ) + + return user, created diff --git a/okta_tests b/okta_tests new file mode 100644 index 0000000..340a6d7 --- /dev/null +++ b/okta_tests @@ -0,0 +1,778 @@ +{ + "version": "1.0", + "exported_at": 1715608731, + "name": "Okta SCIM 2.0 SPEC Test", + "description": "Basic tests to see if your SCIM server will work with Okta", + "trigger_url": "https://api.runscope.com/radar/37d9f10e-e250-4071-9cec-1fa30e56b42b/trigger", + "steps": [ + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Test Users endpoint", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Accept": [ + "application/scim+json" + ], + "Authorization": [ + "{{auth}}" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users?count=1&startIndex=1", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:ListResponse", + "property": "schemas" + }, + { + "comparison": "is_a_number", + "source": "response_json", + "value": null, + "property": "itemsPerPage" + }, + { + "comparison": "is_a_number", + "source": "response_json", + "value": null, + "property": "startIndex" + }, + { + "comparison": "is_a_number", + "source": "response_json", + "value": null, + "property": "totalResults" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].id" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].name.familyName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].name.givenName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].userName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].active" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "Resources[0].emails[0].value" + } + ], + "variables": [ + { + "source": "response_json", + "name": "ISVUserid", + "property": "Resources[0].id" + } + ], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Get Users/{{id}} ", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Accept": [ + "application/scim+json" + ], + "Authorization": [ + "{{auth}}" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users/{{ISVUserid}}", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "id" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "name.familyName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "name.givenName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "userName" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "active" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "emails[0].value" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{ISVUserid}}", + "property": "id" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Test invalid User by username", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Accept": [ + "application/scim+json" + ], + "Authorization": [ + "{{auth}}" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users?filter=userName eq \"{{InvalidUserEmail}}\"", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:ListResponse", + "property": "schemas" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "0", + "property": "totalResults" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Test invalid User by ID", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users/{{UserIdThatDoesNotExist}}", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "404" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "detail" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:Error", + "property": "schemas" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Make sure random user doesn't exist", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users?filter=userName eq \"{{randomEmail}}\"", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "equal_number", + "source": "response_json", + "value": "0", + "property": "totalResults" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:ListResponse", + "property": "schemas" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Create Okta user with realistic values", + "auth": { + + }, + "body": "{\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:User\"],\"userName\":\"{{randomUsername}}\",\"name\":{\"givenName\":\"{{randomGivenName}}\",\"familyName\":\"{{randomFamilyName}}\"},\"emails\":[{\"primary\":true,\"value\":\"{{randomEmail}}\",\"type\":\"work\"}],\"displayName\":\"{{randomGivenName}} {{randomFamilyName}}\",\"active\":true}", + "form": { + + }, + "multipart_form": [], + "binary_body": null, + "headers": { + "Content-Type": [ + "application/json" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json; charset=utf-8" + ] + }, + "method": "POST", + "url": "{{SCIMBaseURL}}/Users", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "201" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "true", + "property": "active" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "id" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomFamilyName}}", + "property": "name.familyName" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomGivenName}}", + "property": "name.givenName" + }, + { + "comparison": "contains", + "source": "response_json", + "value": "urn:ietf:params:scim:schemas:core:2.0:User", + "property": "schemas" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomUsername}}", + "property": "userName" + } + ], + "variables": [ + { + "source": "response_json", + "name": "idUserOne", + "property": "id" + }, + { + "source": "response_json", + "name": "randomUserEmail", + "property": "emails[0].value" + } + ], + "scripts": [ + "" + ], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Verify that user was created", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users/{{idUserOne}}", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomUsername}}", + "property": "userName" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomFamilyName}}", + "property": "name.familyName" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "{{randomGivenName}}", + "property": "name.givenName" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 10 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Expect failure when recreating user with same values", + "auth": { + + }, + "body": "{\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:User\"],\"userName\":\"{{randomUsername}}\",\"name\":{\"givenName\":\"{{randomGivenName}}\",\"familyName\":\"{{randomFamilyName}}\"},\"emails\":[{\"primary\":true,\"value\":\"{{randomUsername}}\",\"type\":\"work\"}],\"displayName\":\"{{randomGivenName}} {{randomFamilyName}}\",\"active\":true}", + "form": { + + }, + "multipart_form": [], + "binary_body": null, + "headers": { + "Content-Type": [ + "application/json" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json; charset=utf-8" + ] + }, + "method": "POST", + "url": "{{SCIMBaseURL}}/Users", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "409" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Username Case Sensitivity Check", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Authorization": [ + "{{auth}}" + ], + "Accept": [ + "application/scim+json" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users?filter=userName eq \"{{randomUsernameCaps}}\"", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Optional Test: Verify Groups endpoint", + "auth": { + + }, + "multipart_form": [], + "headers": { + "Accept-Charset": [ + "utf-8" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "Accept": [ + "application/scim+json" + ], + "Authorization": [ + "{{auth}}" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Groups", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "200" + }, + { + "comparison": "is_less_than", + "source": "response_time", + "value": "600" + } + ], + "variables": [], + "scripts": [ + "var data = JSON.parse(response.body);\nvar max = data.totalResults;\nvar res = data.Resources;\nvar exists = false;\n\nif (max === 0)\n\tassert(\"nogroups\", \"No Groups found in the endpoint\");\nelse if (max \u003E= 1 && Array.isArray(res)) {\n exists = true;\n assert.ok(exists, \"Resources is of type Array\");\n\tlog(exists);\n}" + ], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Check status 401", + "multipart_form": [], + "headers": { + "Accept": [ + "application/scim+json" + ], + "Accept-Charset": [ + "utf-8" + ], + "Authorization": [ + "non-token" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "auth": { + + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users?filter=userName eq \"{{randomUsernameCaps}}\"", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "401" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "detail" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "401", + "property": "status" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:Error", + "property": "schemas" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + }, + { + "step_type": "pause", + "skipped": false, + "duration": 5 + }, + { + "step_type": "request", + "skipped": false, + "note": "Required Test: Check status 404", + "multipart_form": [], + "headers": { + "Accept": [ + "application/scim+json" + ], + "Accept-Charset": [ + "utf-8" + ], + "Authorization": [ + "{{auth}}" + ], + "Content-Type": [ + "application/scim+json; charset=utf-8" + ], + "User-Agent": [ + "OKTA SCIM Integration" + ] + }, + "auth": { + + }, + "method": "GET", + "url": "{{SCIMBaseURL}}/Users/00919288221112222", + "assertions": [ + { + "comparison": "equal_number", + "source": "response_status", + "value": "404" + }, + { + "comparison": "not_empty", + "source": "response_json", + "value": null, + "property": "detail" + }, + { + "comparison": "equal", + "source": "response_json", + "value": "404", + "property": "status" + }, + { + "comparison": "has_value", + "source": "response_json", + "value": "urn:ietf:params:scim:api:messages:2.0:Error", + "property": "schemas" + } + ], + "variables": [], + "scripts": [], + "before_scripts": [] + } + ] +} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 38f399f..53663dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "amqp" @@ -14,6 +14,17 @@ files = [ [package.dependencies] vine = ">=5.0.0,<6.0.0" +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "asgiref" version = "3.8.1" @@ -560,6 +571,26 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-ninja" +version = "1.3.0" +description = "Django Ninja - Fast Django REST framework" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django_ninja-1.3.0-py3-none-any.whl", hash = "sha256:f58096b6c767d1403dfd6c49743f82d780d7b9688d9302ecab316ac1fa6131bb"}, + {file = "django_ninja-1.3.0.tar.gz", hash = "sha256:5b320e2dc0f41a6032bfa7e1ebc33559ae1e911a426f0c6be6674a50b20819be"}, +] + +[package.dependencies] +Django = ">=3.1" +pydantic = ">=2.0,<3.0.0" + +[package.extras] +dev = ["pre-commit"] +doc = ["markdown-include", "mkdocs", "mkdocs-material", "mkdocstrings"] +test = ["django-stubs", "mypy (==1.7.1)", "psycopg2-binary", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django", "ruff (==0.5.7)"] + [[package]] name = "django-staff-sso-client" version = "4.3.0" @@ -1642,6 +1673,138 @@ docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)" pool = ["psycopg-pool"] test = ["anyio (>=4.0)", "mypy (>=1.11)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] +[[package]] +name = "pydantic" +version = "2.10.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, + {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.1" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.18.0" @@ -2326,4 +2489,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8135f503546f75367166ef9e21635d63c9b01017c7908fcdc159041c2ec0d716" +content-hash = "8223518bd0ccf49ed308af7f7b578f05cfd15fb1c6dc801f8c2a2ef5a2188987" diff --git a/pyproject.toml b/pyproject.toml index 03be9d2..2c3177d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dbt-copilot-python = "^0.2.2" dj-database-url = "^2.2.0" granian = "^1.6.3" django-staff-sso-client = "^4.3.0" +django-ninja = "^1.3.0" [tool.poetry.group.dev.dependencies] black = "^24.10.0"