feat(backend): add SCIM for user provisioning (#12713)

* Add SCIM
Closes https://github.com/inventree/InvenTree/issues/6339

* also test scim

* extend testing with conformance suite

* fix test suite results

* coverage completion

* fil test gaps

* add missing schema values

* fix more type issues

* fix error view

* remove route from OpenAPI

* add changelog entry

* add codeowners
This commit is contained in:
Matthias Mair
2026-08-31 09:24:14 +10:00
committed by GitHub
parent c821fa0f43
commit 4066fa6e0a
34 changed files with 2288 additions and 9 deletions
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 535
INVENTREE_API_VERSION = 536
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v536 -> 2026-08-30 : https://github.com/inventree/InvenTree/pull/xxxx
- Adds SCIM 2 provisioning support
v535 -> 2026-08-29 : https://github.com/inventree/InvenTree/pull/12739
- Adds SSO registration API endpoint for creating new users via SSO
@@ -66,6 +66,7 @@ urls = [
paths_ignore_handling = [
'/api/',
'/plugin/',
'/scim/',
reverse('auth-check'),
settings.MEDIA_URL,
settings.STATIC_URL,
@@ -77,6 +78,7 @@ paths_own_security = [
'/o/', # oAuth2 library - has its own auth model
'/anymail/', # Mails - webhooks etc
'/accounts/', # allauth account management - has its own auth model
'/scim/', # SCIM provisioning endpoint - authenticated via its own bearer secret
'/assets/', # Web assets - only used for testing, no security model needed
'/.well-known/',
ensure_slashes(
@@ -316,6 +316,7 @@ INSTALLED_APPS = [
'order.apps.OrderConfig',
'part.apps.PartConfig',
'report.apps.ReportConfig',
'scim.apps.ScimConfig',
'stock.apps.StockConfig',
'users.apps.UsersConfig',
'machine.apps.MachineConfig',
+3
View File
@@ -26,6 +26,7 @@ import order.api
import part.api
import plugin.api
import report.api
import scim.api
import stock.api
import users.api
from plugin.urls import get_plugin_urls, get_wellknown_urls
@@ -137,6 +138,8 @@ backendpatterns = [
path('accounts/', include('allauth.urls')),
# OAuth2
flagged_path('OIDC', 'o/', include(oauth2_urls)),
# SCIM 2 provisioning endpoint
path('scim/v2/', include(scim.api)),
path(
'accounts/login/',
RedirectView.as_view(url=f'/{settings.FRONTEND_URL_BASE}', permanent=False),
+4
View File
@@ -84,6 +84,7 @@ from InvenTree.permissions import (
UserSettingsPermissionsOrScope,
)
from InvenTree.serializers import EmptySerializer
from scim.admin_api import ScimConfigViewSet
admin_router = InvenTreeApiRouter()
common_router = InvenTreeApiRouter()
@@ -1698,4 +1699,7 @@ common_api_urls = [
path('', include(common_router.urls)),
]
# SCIM admin
admin_router.register('scim', ScimConfigViewSet, basename='api-scim')
admin_api_urls = admin_router.urls
+1
View File
@@ -0,0 +1 @@
"""SCIM 2 provisioning app."""
+62
View File
@@ -0,0 +1,62 @@
"""Admin-facing API for managing the SCIM provisioning configuration."""
import structlog
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
import InvenTree.permissions
from scim.models import ScimConfiguration
from scim.serializers import ScimConfigurationSerializer, ScimSecretSerializer
logger = structlog.get_logger('inventree')
class ScimConfigViewSet(viewsets.GenericViewSet):
"""Admin viewset for managing the (singleton) SCIM provisioning configuration."""
permission_classes = [InvenTree.permissions.IsSuperuserOrSuperScope]
serializer_class = ScimConfigurationSerializer
def get_object(self) -> ScimConfiguration:
"""Return the (singleton) SCIM configuration object."""
return ScimConfiguration.load()
def list(self, request, *args, **kwargs):
"""Return the current SCIM configuration status."""
serializer = self.get_serializer(self.get_object())
return Response(serializer.data)
@extend_schema(request=None, responses={200: ScimSecretSerializer()})
@action(detail=False, methods=['post'], serializer_class=ScimSecretSerializer)
def generate(self, request, *args, **kwargs):
"""Generate a new SCIM bearer secret, and enable the endpoint.
The raw secret is returned exactly once in this response - only its
HMAC digest is persisted. Generating a new secret invalidates any
previously issued one.
"""
config = self.get_object()
secret = config.generate_secret()
config.enabled = True
config.save()
logger.info('SCIM bearer secret (re)generated', user=str(request.user))
serializer = self.get_serializer({'secret': secret})
return Response(serializer.data)
@extend_schema(request=None, responses={200: ScimConfigurationSerializer()})
@action(detail=False, methods=['post'])
def disable(self, request, *args, **kwargs):
"""Disable the SCIM provisioning endpoint and revoke its secret."""
config = self.get_object()
config.revoke()
logger.info('SCIM provisioning disabled', user=str(request.user))
serializer = ScimConfigurationSerializer(
config, context=self.get_serializer_context()
)
return Response(serializer.data)
+38
View File
@@ -0,0 +1,38 @@
"""URL patterns for the SCIM 2.0 provisioning endpoint (`/scim/v2/...`)."""
from django.urls import path, re_path
from scim.views import (
GroupDetailView,
GroupsView,
ResourceTypesView,
SchemasView,
ServiceProviderConfigView,
UserDetailView,
UsersView,
scim_error_view,
)
scim_urls = [
path(
'ServiceProviderConfig',
ServiceProviderConfigView.as_view(),
name='scim-service-provider-config',
),
path('ResourceTypes', ResourceTypesView.as_view(), name='scim-resource-types'),
path(
'ResourceTypes/<str:name>',
ResourceTypesView.as_view(),
name='scim-resource-type-detail',
),
path('Schemas', SchemasView.as_view(), name='scim-schemas'),
path('Schemas/<str:schema_id>', SchemasView.as_view(), name='scim-schema-detail'),
path('Users', UsersView.as_view(), name='scim-users'),
path('Users/<int:pk>', UserDetailView.as_view(), name='scim-user-detail'),
path('Groups', GroupsView.as_view(), name='scim-groups'),
path('Groups/<int:pk>', GroupDetailView.as_view(), name='scim-group-detail'),
# catchall
re_path('.*', scim_error_view, name='scim-not-found'),
]
urlpatterns = scim_urls
+11
View File
@@ -0,0 +1,11 @@
"""AppConfig for the 'scim' app."""
from django.apps import AppConfig
class ScimConfig(AppConfig):
"""AppConfig class for the 'scim' app."""
default_auto_field = 'django.db.models.BigAutoField'
name = 'scim'
verbose_name = 'SCIM'
@@ -0,0 +1,70 @@
"""Authentication for the SCIM provisioning endpoint.
The SCIM endpoint is *not* authenticated against InvenTree user accounts.
Instead, a single bearer secret is generated on-demand from the Admin Center
(see `scim.api`) and is used by the external Identity Provider to
authenticate every SCIM request. Only an HMAC digest of that secret is ever
stored - see `scim.models.ScimConfiguration`.
"""
from django.utils.translation import gettext_lazy as _
from rest_framework import authentication, exceptions
from scim.models import ScimConfiguration
class ScimServiceUser:
"""Lightweight stand-in for a Django user, representing the SCIM integration itself.
The SCIM protocol is authenticated via a shared bearer secret rather than
as any particular InvenTree user account, so a full User instance would be
misleading here. This object satisfies the minimal interface that DRF and
InvenTree's request handling expect from `request.user`.
"""
is_authenticated = True
is_anonymous = False
is_active = True
is_staff = False
is_superuser = False
pk = None
id = None
username = 'scim-provisioning'
def __str__(self):
"""String representation of the SCIM service user."""
return self.username # pragma: no cover
class ScimBearerAuthentication(authentication.BaseAuthentication):
"""DRF authentication class which validates the SCIM bearer secret.
Expects an `Authorization: Bearer <secret>` header. The provided secret is
hashed (HMAC-SHA256, seeded with the Django `SECRET_KEY`) and compared
against the stored digest using a constant-time comparison.
"""
www_authenticate_realm = 'scim'
def authenticate(self, request):
"""Validate the bearer token against the configured SCIM secret."""
header = authentication.get_authorization_header(request).split()
if not header or header[0].lower() != b'bearer':
return None
if len(header) != 2:
raise exceptions.AuthenticationFailed(
_('Invalid SCIM authorization header')
)
secret = header[1].decode('utf-8')
config = ScimConfiguration.load()
if not config.verify_secret(secret):
raise exceptions.AuthenticationFailed(_('Invalid SCIM bearer token'))
config.mark_used()
return (ScimServiceUser(), None)
@@ -0,0 +1,58 @@
"""Initial migration for the 'scim' app."""
from django.db import migrations, models
class Migration(migrations.Migration):
"""Create the singleton ScimConfiguration model."""
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='ScimConfiguration',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'enabled',
models.BooleanField(
default=False,
help_text='Enable the SCIM provisioning endpoint',
verbose_name='Enabled',
),
),
(
'secret_digest',
models.CharField(
blank=True,
help_text='HMAC digest of the current SCIM bearer secret',
max_length=64,
verbose_name='Secret Digest',
),
),
(
'secret_generated',
models.DateTimeField(
blank=True, null=True, verbose_name='Secret Generated'
),
),
(
'last_used',
models.DateTimeField(
blank=True, null=True, verbose_name='Last Used'
),
),
],
options={'verbose_name': 'SCIM Configuration'},
)
]
+101
View File
@@ -0,0 +1,101 @@
"""Database models for the 'scim' app."""
import hashlib
import hmac
import secrets
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
import InvenTree.helpers
def _digest(secret: str) -> str:
"""Compute a hmac digest of a secret, seeded with the secret_key from settings."""
return hmac.new(
settings.SECRET_KEY.encode('utf-8'), secret.encode('utf-8'), hashlib.sha256
).hexdigest()
class ScimConfiguration(models.Model):
"""Simple model for storing config of SCIM."""
class Meta:
"""Metaclass."""
verbose_name = _('SCIM Configuration')
enabled = models.BooleanField(
default=False,
verbose_name=_('Enabled'),
help_text=_('Enable the SCIM provisioning endpoint'),
)
secret_digest = models.CharField(
max_length=64,
blank=True,
verbose_name=_('Secret Digest'),
help_text=_('HMAC digest of the current SCIM bearer secret'),
)
secret_generated = models.DateTimeField(
null=True, blank=True, verbose_name=_('Secret Generated')
)
last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Last Used'))
def __str__(self):
"""String representation of the SCIM configuration."""
return 'SCIM Configuration' # pragma: no cover
def save(self, *args, **kwargs):
"""Ensure that only a single instance of this model can ever exist."""
self.pk = 1
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""Prevent deletion of the singleton configuration object."""
@classmethod
def load(cls) -> 'ScimConfiguration':
"""Return the (singleton) SCIM configuration object, creating it if required."""
obj, _created = cls.objects.get_or_create(pk=1)
return obj
@property
def has_secret(self) -> bool:
"""Return True if a bearer secret has been generated."""
return bool(self.secret_digest)
def generate_secret(self) -> str:
"""Generate a new bearer secret, persist its digest, and return the raw secret.
The raw secret is only ever available at generation time - it cannot be
recovered afterwards, only rotated.
"""
secret = secrets.token_urlsafe(48)
self.secret_digest = _digest(secret)
self.secret_generated = InvenTree.helpers.current_time()
self.last_used = None
self.save()
return secret
def revoke(self):
"""Revoke the current secret and disable the SCIM provisioning."""
self.enabled = False
self.secret_digest = ''
self.secret_generated = None
self.save()
def verify_secret(self, secret: str) -> bool:
"""Return True if the provided raw secret matches the stored digest."""
if not self.enabled or not self.secret_digest or not secret:
return False
return hmac.compare_digest(self.secret_digest, _digest(secret))
def mark_used(self):
"""Record that the SCIM endpoint was just used for a successful request."""
self.last_used = InvenTree.helpers.current_time()
self.save(update_fields=['last_used'])
+13
View File
@@ -0,0 +1,13 @@
"""Permission classes for the 'scim' app."""
from rest_framework import permissions
from scim.authentication import ScimServiceUser
class IsScimAuthenticated(permissions.BasePermission):
"""Require that the request was authenticated via the SCIM bearer secret."""
def has_permission(self, request, view):
"""Only allow access if the request was authenticated as the SCIM service user."""
return isinstance(request.user, ScimServiceUser)
+136
View File
@@ -0,0 +1,136 @@
"""Mapping helpers between Django User/Group objects and SCIM resource models."""
import re
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser
from django.urls import reverse
from scim2_models import Email, Group, GroupMember, GroupMembership, Meta, Name, User
#: Simple filter grammar supported by this SCIM implementation: `<attr> eq "<value>"`
FILTER_RE = re.compile(r'^\s*(\w+)\s+eq\s+"([^"]*)"\s*$', re.IGNORECASE)
def parse_filter(filter_string: str | None) -> tuple[str, str] | None:
"""Parse a (very limited) SCIM filter expression of the form `attribute eq "value"`."""
if not filter_string:
return None
match = FILTER_RE.match(filter_string)
if not match:
return None
return match.group(1).lower(), match.group(2)
def user_location(pk) -> str:
"""Return the canonical SCIM location URL for a given user id."""
return reverse('scim-user-detail', kwargs={'pk': pk})
def group_location(pk) -> str:
"""Return the canonical SCIM location URL for a given group id."""
return reverse('scim-group-detail', kwargs={'pk': pk})
def user_to_scim(user: DjangoUser) -> User:
"""Convert a Django User instance into a SCIM User resource."""
name = None
if user.first_name or user.last_name:
name = Name(
given_name=user.first_name or None, family_name=user.last_name or None
)
emails = [Email(value=user.email, primary=True)] if user.email else None
groups = [
GroupMembership(value=str(group.pk), display=group.name, type='direct')
for group in user.groups.all()
] or None
return User(
schemas=['urn:ietf:params:scim:schemas:core:2.0:User'],
id=str(user.pk),
user_name=user.username,
name=name,
emails=emails,
active=user.is_active,
groups=groups,
meta=Meta(
resource_type='User',
created=user.date_joined,
last_modified=user.last_login or user.date_joined,
location=user_location(user.pk),
),
)
def apply_scim_to_user(scim_user: User, user: DjangoUser) -> DjangoUser:
"""Apply the fields of a SCIM User resource onto a Django User instance."""
if scim_user.user_name:
user.username = scim_user.user_name
if scim_user.name:
if scim_user.name.given_name is not None:
user.first_name = scim_user.name.given_name
if scim_user.name.family_name is not None:
user.last_name = scim_user.name.family_name
if scim_user.emails:
primary = next((e for e in scim_user.emails if e.primary), scim_user.emails[0])
if primary.value:
user.email = primary.value
if scim_user.active is not None:
user.is_active = scim_user.active
if scim_user.password:
user.set_password(scim_user.password)
return user
def group_to_scim(group: DjangoGroup) -> Group:
"""Convert a Django Group instance into a SCIM Group resource."""
members = [
GroupMember(value=str(user.pk), display=user.username, type='User')
for user in group.user_set.all()
] or None
return Group(
schemas=['urn:ietf:params:scim:schemas:core:2.0:Group'],
id=str(group.pk),
display_name=group.name,
members=members,
meta=Meta(resource_type='Group', location=group_location(group.pk)),
)
def apply_scim_to_group(scim_group: Group, group: DjangoGroup) -> DjangoGroup:
"""Apply the fields of a SCIM Group resource onto a Django Group instance."""
if scim_group.display_name:
group.name = scim_group.display_name
return group
def set_group_members(group: DjangoGroup, members: list[GroupMember] | None) -> None:
"""Replace the membership of a Django Group based on a list of SCIM GroupMember entries.
Membership changes are applied via `user.groups` (rather than
`group.user_set`) so that InvenTree's own `m2m_changed` signal handlers
(which assume the changed instance is a User) fire correctly.
"""
if members is None:
return
target_ids = {member.value for member in members if member.value}
current_ids = {str(pk) for pk in group.user_set.values_list('pk', flat=True)}
for user in DjangoUser.objects.filter(pk__in=target_ids - current_ids):
user.groups.add(group)
for user in DjangoUser.objects.filter(pk__in=current_ids - target_ids):
user.groups.remove(group)
+55
View File
@@ -0,0 +1,55 @@
"""DRF serializers for the SCIM admin configuration API."""
from django.urls import reverse
from rest_framework import serializers
from scim.models import ScimConfiguration
class ScimConfigurationSerializer(serializers.ModelSerializer):
"""Serializer for the (read-only, status-only) SCIM configuration."""
class Meta:
"""Metaclass options."""
model = ScimConfiguration
fields = ['enabled', 'has_secret', 'secret_generated', 'last_used', 'base_url']
read_only_fields = fields
has_secret = serializers.BooleanField(read_only=True)
base_url = serializers.SerializerMethodField()
def get_base_url(self, obj) -> str:
"""Return the absolute base URL that should be configured in the Identity Provider."""
request = self.context.get('request')
path = reverse('scim-service-provider-config').rsplit(
'ServiceProviderConfig', 1
)[0]
if request is not None:
return request.build_absolute_uri(path)
return path # pragma: no cover
class ScimSecretSerializer(serializers.Serializer):
"""Serializer for a freshly (re)generated SCIM bearer secret.
This is the *only* place the raw secret is ever exposed - it cannot be
retrieved again afterwards.
"""
secret = serializers.CharField(read_only=True)
base_url = serializers.SerializerMethodField()
def get_base_url(self, obj) -> str:
"""Return the absolute base URL that should be configured in the Identity Provider."""
request = self.context.get('request')
path = reverse('scim-service-provider-config').rsplit(
'ServiceProviderConfig', 1
)[0]
if request is not None:
return request.build_absolute_uri(path)
return path # pragma: no cover
+255
View File
@@ -0,0 +1,255 @@
"""Tests for the 'scim' app."""
import json
from django.contrib.auth.models import Group, User
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APIClient
from scim2_client.engines.httpx import SyncSCIMClient
from scim2_tester import check_server
from InvenTree.unit_test import InvenTreeAPITestCase
from scim.models import ScimConfiguration
class ScimConfigurationModelTests(InvenTreeAPITestCase):
"""Tests for the ScimConfiguration model."""
def test_singleton(self):
"""Only a single configuration object can ever exist."""
a = ScimConfiguration.load()
b = ScimConfiguration.load()
self.assertEqual(a.pk, b.pk)
self.assertEqual(ScimConfiguration.objects.count(), 1)
def test_generate_and_verify_secret(self):
"""A generated secret can be verified, but only while enabled."""
config = ScimConfiguration.load()
self.assertFalse(config.has_secret)
secret = config.generate_secret()
config.enabled = True
config.save()
self.assertTrue(config.has_secret)
self.assertTrue(config.verify_secret(secret))
self.assertFalse(config.verify_secret('not-the-secret'))
self.assertFalse(config.verify_secret(''))
# Disabling the endpoint rejects the (still valid) secret
config.enabled = False
config.save()
self.assertFalse(config.verify_secret(secret))
def test_rotate_invalidates_previous_secret(self):
"""Generating a new secret invalidates the previous one."""
config = ScimConfiguration.load()
first = config.generate_secret()
config.enabled = True
config.save()
second = config.generate_secret()
self.assertFalse(config.verify_secret(first))
self.assertTrue(config.verify_secret(second))
def test_revoke(self):
"""Revoking clears the secret and disables the endpoint."""
config = ScimConfiguration.load()
secret = config.generate_secret()
config.enabled = True
config.save()
config.revoke()
self.assertFalse(config.enabled)
self.assertFalse(config.has_secret)
self.assertFalse(config.verify_secret(secret))
class ScimAdminAPITests(InvenTreeAPITestCase):
"""Tests for the Admin Center facing SCIM configuration API."""
def test_non_superuser_denied(self):
"""A non-superuser cannot view or manage the SCIM configuration."""
self.get(reverse('api-scim-list'), expected_code=403)
self.post(reverse('api-scim-generate'), expected_code=403)
self.post(reverse('api-scim-disable'), expected_code=403)
def test_generate_rotate_disable(self):
"""A superuser can generate, rotate and disable the SCIM secret."""
self.user.is_superuser = True
self.user.save()
response = self.get(reverse('api-scim-list'), expected_code=200)
self.assertFalse(response.data['enabled'])
self.assertFalse(response.data['has_secret'])
response = self.post(reverse('api-scim-generate'), expected_code=200)
secret = response.data['secret']
self.assertTrue(secret)
config = ScimConfiguration.load()
self.assertTrue(config.enabled)
self.assertTrue(config.verify_secret(secret))
response = self.post(reverse('api-scim-generate'), expected_code=200)
new_secret = response.data['secret']
self.assertNotEqual(secret, new_secret)
self.post(reverse('api-scim-disable'), expected_code=200)
config.refresh_from_db()
self.assertFalse(config.enabled)
self.assertFalse(config.has_secret)
class ScimProtocolTests(InvenTreeAPITestCase):
"""Tests for the SCIM 2.0 protocol endpoint."""
def setUp(self):
"""Enable SCIM and generate a bearer secret for use in tests."""
super().setUp()
self.config = ScimConfiguration.load()
self.secret = self.config.generate_secret()
self.config.enabled = True
self.config.save()
def auth_header(self, secret=None):
"""Return the kwargs required to attach a SCIM bearer token to a request."""
return {'HTTP_AUTHORIZATION': f'Bearer {secret or self.secret}'}
def test_service_provider_config_is_public(self):
"""The discovery endpoints do not require authentication."""
self.get(reverse('scim-service-provider-config'), expected_code=200)
self.get(reverse('scim-resource-types'), expected_code=200)
self.get(reverse('scim-schemas'), expected_code=200)
def test_users_endpoint_requires_bearer_token(self):
"""The Users endpoint rejects requests without a valid bearer token."""
self.get(reverse('scim-users'), expected_code=401)
self.get(reverse('scim-users'), expected_code=401, **self.auth_header('wrong'))
self.get(reverse('scim-users'), expected_code=401, HTTP_AUTHORIZATION='Bearer')
def test_filter_users_by_username(self):
"""Users can be filtered by an exact userName match."""
response = self.get(
reverse('scim-users'),
data={'filter': f'userName eq "{self.user.username}"'},
expected_code=200,
**self.auth_header(),
)
self.assertEqual(response.data['totalResults'], 1)
self.assertEqual(response.data['Resources'][0]['userName'], self.user.username)
# wrong field name
self.get(
reverse('scim-users'),
data={'filter': f'qbc eq "{self.user.username}"'},
expected_code=400,
**self.auth_header(),
)
# no known filter
self.get(
reverse('scim-users'),
data={'filter': f'userName noq "{self.user.username}"'},
expected_code=200,
**self.auth_header(),
)
def test_create_group_with_members(self):
"""A group can be provisioned with initial membership via SCIM."""
payload = {
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Group'],
'displayName': 'scim-engineering',
'members': [{'value': str(self.user.pk), 'type': 'User'}],
}
response = self.post(
reverse('scim-groups'),
data=payload,
expected_code=201,
**self.auth_header(),
)
group = Group.objects.get(pk=response.data['id'])
self.assertEqual(group.name, 'scim-engineering')
self.assertIn(self.user, group.user_set.all())
# assign a user
new_user = User.objects.create_user(username='scim-user', password='test')
new_user.groups.add(group)
self.assertIn(new_user, group.user_set.all())
# remove membership via update
payload['members'] = [{'value': str(self.user.pk), 'type': 'User'}]
self.put(
reverse('scim-group-detail', kwargs={'pk': group.pk}),
data=payload,
expected_code=200,
**self.auth_header(),
)
group.refresh_from_db()
self.assertNotIn(new_user, group.user_set.all())
# and remove group via delete
self.delete(
reverse('scim-group-detail', kwargs={'pk': group.pk}),
expected_code=204,
**self.auth_header(),
)
self.assertFalse(Group.objects.filter(pk=group.pk).exists())
@override_settings(
SITE_URL='http://testserver', CSRF_TRUSTED_ORIGINS=['http://testserver']
)
def test_suite(self):
"""Run the SCIM 2.0 conformance test suite against the endpoint."""
cls = PatchedApiClient(base_url='http://testserver/scim/v2')
cls.logout()
cls.credentials(HTTP_AUTHORIZATION=f'Bearer {self.secret}')
scim_client = SyncSCIMClient(cls)
ignore_tags = {
'crud:read:attributes', # 1: we do not have this attribute
'patch:add', # 2: we do not map these attributes to the User model right now
'patch:remove', # 2:see above
'patch:replace', # 2: see above
'check_replace', # BD
'crud:delete', # 3: there is no deleting users right now
}
results = check_server(scim_client)
failures = [result for result in results if result.status.value not in (1, 7)]
if failures:
details = '\n'.join(
f'{result.status.name}: {result.title} - {result.reason}'
for result in failures
if not ignore_tags.intersection(result.tags)
)
if details:
self.fail(
f'SCIM conformance suite reported failures:\n{details}'
) # pragma: no cover
class PatchedApiClient(APIClient):
"""A DRF APIClient subclass that supports the SCIM media type."""
def __init__(self, base_url: str, *args, **kwargs):
"""Initialize the client."""
self.base_url = base_url
super().__init__(*args, **kwargs)
def generic(self, method, path, data=None, format=None, content_type=None, **extra):
"""Override the generic method to set the SCIM media type."""
if content_type is None:
content_type = 'application/scim+json'
path = self.base_url + path if not '/scim/v2/' in path else path
if json_data := extra.pop('json', None):
data = json.dumps(json_data)
format = 'json' # noqa: A001
return super().generic(
method, path, data=data, format=format, content_type=content_type, **extra
)
+470
View File
@@ -0,0 +1,470 @@
"""SCIM 2.0 protocol views."""
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser
from django.http import Http404
from django.shortcuts import get_object_or_404
import structlog
from drf_spectacular.utils import extend_schema
from pydantic import ValidationError
from rest_framework.decorators import (
api_view,
authentication_classes,
permission_classes,
renderer_classes,
)
from rest_framework.exceptions import (
APIException,
AuthenticationFailed,
NotAuthenticated,
)
from rest_framework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from scim2_models import (
AuthenticationScheme,
Bulk,
ChangePassword,
Error,
ETag,
Filter,
Group,
ListResponse,
Patch,
PatchOp,
ResourceType,
Schema,
ServiceProviderConfig,
Sort,
User,
)
from scim2_models.exceptions import SCIMException
from scim.authentication import ScimBearerAuthentication
from scim.permissions import IsScimAuthenticated
from scim.resources import (
apply_scim_to_group,
apply_scim_to_user,
group_location,
group_to_scim,
parse_filter,
set_group_members,
user_location,
user_to_scim,
)
logger = structlog.get_logger('inventree')
class ScimRenderer(JSONRenderer):
"""JSON renderer which advertises the `application/scim+json` media type."""
media_type = 'application/scim+json'
class ScimParser(JSONParser):
"""JSON parser which also accepts the `application/scim+json` media type."""
media_type = 'application/scim+json'
def scim_dump(obj) -> dict:
"""Serialize a scim2_models object to a plain (JSON-safe) dict."""
return obj.model_dump(mode='json', exclude_none=True, by_alias=True)
def scim_error(status: int, detail: str, scim_type: str | None = None) -> Response:
"""Build a SCIM-formatted error response."""
error = Error(
schemas=['urn:ietf:params:scim:api:messages:2.0:Error'],
status=status,
scim_type=scim_type,
detail=detail,
)
return Response(
scim_dump(error), status=status, content_type='application/scim+json'
)
class ScimAPIView(APIView):
"""Base class for all SCIM protocol views.
These endpoints follow the SCIM 2.0 protocol (not InvenTree's own REST
API conventions) and authenticate via a bearer secret rather than any of
InvenTree's normal authentication schemes - they are excluded from the
OpenAPI schema entirely (`schema = None`) rather than being documented
alongside the versioned `/api/` surface.
"""
schema = None
authentication_classes = [ScimBearerAuthentication]
permission_classes = [IsScimAuthenticated]
renderer_classes = [ScimRenderer, JSONRenderer]
parser_classes = [ScimParser, JSONParser]
def handle_exception(self, exc):
"""Convert SCIM/pydantic exceptions into RFC7644-formatted error responses."""
if isinstance(exc, SCIMException):
return scim_error(
exc.status, exc.detail, exc.scim_type or None
) # pragma: no cover
if isinstance(exc, ValidationError):
return scim_error(400, str(exc), 'invalidValue') # pragma: no cover
if isinstance(exc, Http404):
return scim_error(404, 'Resource not found')
if isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
return scim_error(
exc.status_code,
str(exc.detail) if hasattr(exc, 'detail') else str(exc),
None,
)
if isinstance(exc, APIException): # pragma: no cover
detail = exc.detail
if isinstance(detail, (list, dict)):
detail = str(detail)
return scim_error(exc.status_code, str(detail), None)
return super().handle_exception(exc) # pragma: no cover
@extend_schema(exclude=True)
@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
@renderer_classes([ScimRenderer])
@permission_classes([])
@authentication_classes([])
def scim_error_view(request, format=None):
"""Always return a SCIM-formatted 404 error, rather than a Django 404 page."""
return scim_error(404, f"Resource '{request.path}' not found")
class ServiceProviderConfigView(ScimAPIView):
"""Advertise the SCIM features supported by this service provider."""
authentication_classes = []
permission_classes = []
def get(self, request, *args, **kwargs):
"""Return the service provider configuration."""
config = ServiceProviderConfig(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
documentation_uri='https://docs.inventree.org/en/latest/settings/scim/',
patch=Patch(supported=True),
bulk=Bulk(supported=False, max_operations=0, max_payload_size=0),
filter=Filter(supported=True, max_results=200),
change_password=ChangePassword(supported=False),
sort=Sort(supported=False),
etag=ETag(supported=False),
authentication_schemes=[
AuthenticationScheme(
type=AuthenticationScheme.Type.oauthbearertoken,
name='Bearer Token',
description='Authentication using a single admin-generated bearer secret',
)
],
)
return Response(scim_dump(config), content_type='application/scim+json')
class ResourceTypesView(ScimAPIView):
"""List the resource types supported by this service provider."""
authentication_classes = []
permission_classes = []
resource_types = {
'User': ResourceType(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
id='User',
name='User',
endpoint='/scim/v2/Users',
description='User Account',
schema_='urn:ietf:params:scim:schemas:core:2.0:User',
),
'Group': ResourceType(
schemas=['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
id='Group',
name='Group',
endpoint='/scim/v2/Groups',
description='Group',
schema_='urn:ietf:params:scim:schemas:core:2.0:Group',
),
}
def get(self, request, name=None, *args, **kwargs):
"""Return either a single resource type, or the full list."""
if name:
resource_type = self.resource_types.get(name)
if not resource_type:
return scim_error(404, f"ResourceType '{name}' not found")
return Response(
scim_dump(resource_type), content_type='application/scim+json'
)
resources = list(self.resource_types.values())
response = ListResponse[ResourceType](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=len(resources),
items_per_page=len(resources),
start_index=1,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class SchemasView(ScimAPIView):
"""Expose the SCIM schema definitions for the supported resource types."""
authentication_classes = []
permission_classes = []
def get(self, request, schema_id=None, *args, **kwargs):
"""Return either a single schema, or the full list."""
schemas = {
schema.id: schema for schema in (User.to_schema(), Group.to_schema())
}
if schema_id:
schema = schemas.get(schema_id)
if not schema:
return scim_error(404, f"Schema '{schema_id}' not found")
return Response(scim_dump(schema), content_type='application/scim+json')
resources = list(schemas.values())
response = ListResponse[Schema](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=len(resources),
items_per_page=len(resources),
start_index=1,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class BaseResourceView(ScimAPIView):
"""Shared list/create logic for the Users and Groups endpoints."""
django_model = None
scim_model = None
filterable_fields = {}
def get_queryset(self):
"""Return the base queryset for this resource type."""
return self.django_model.objects.all().order_by('pk')
def to_scim(self, instance):
"""Convert a Django model instance into its SCIM representation. Implemented by subclasses."""
raise NotImplementedError # pragma: no cover
def list(self, request):
"""Handle GET (list) requests, with minimal filter and pagination support."""
if not self.scim_model:
raise NotImplementedError('Missing scim_model') # pragma: no cover
queryset = self.get_queryset()
parsed_filter = parse_filter(request.query_params.get('filter'))
if parsed_filter:
attr, value = parsed_filter
field = self.filterable_fields.get(attr)
if field is None:
return scim_error(
400, f"Filtering on '{attr}' is not supported", 'invalidFilter'
)
queryset = queryset.filter(**{field: value})
start_index = max(int(request.query_params.get('startIndex', 1)), 1)
count = int(request.query_params.get('count', 100))
total_results = queryset.count()
page = queryset[start_index - 1 : start_index - 1 + count]
resources = [self.to_scim(obj) for obj in page]
response = ListResponse[self.scim_model](
schemas=['urn:ietf:params:scim:api:messages:2.0:ListResponse'],
total_results=total_results,
items_per_page=len(resources),
start_index=start_index,
resources=resources,
)
return Response(scim_dump(response), content_type='application/scim+json')
class UsersView(BaseResourceView):
"""`/scim/v2/Users` - list and create Users."""
django_model = DjangoUser
scim_model = User
filterable_fields = {'username': 'username__iexact', 'useremail': 'email__iexact'}
def get(self, request, *args, **kwargs):
"""List users."""
return self.list(request)
def to_scim(self, instance):
"""Convert a Django User into its SCIM representation."""
return user_to_scim(instance)
def post(self, request, *args, **kwargs):
"""Provision a new user."""
scim_user = User.model_validate(request.data)
if DjangoUser.objects.filter(username__iexact=scim_user.user_name).exists():
return scim_error(
409, 'A user with this userName already exists', 'uniqueness'
) # pragma: no cover
user = DjangoUser(username=scim_user.user_name)
user.set_unusable_password()
apply_scim_to_user(scim_user, user)
user.save()
logger.info('SCIM: provisioned new user', username=user.username)
return Response(
scim_dump(user_to_scim(user)),
status=201,
content_type='application/scim+json',
headers={'Location': user_location(user.pk)},
)
class UserDetailView(ScimAPIView):
"""`/scim/v2/Users/<id>` - retrieve, replace, patch and remove a single User."""
def get_object(self, pk):
"""Look up a user by primary key, raising a SCIM-formatted 404 if not found."""
return get_object_or_404(DjangoUser, pk=pk)
def get(self, request, pk, *args, **kwargs):
"""Retrieve a single user."""
user = self.get_object(pk)
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def put(self, request, pk, *args, **kwargs):
"""Replace a user's attributes."""
user = self.get_object(pk)
scim_user = User.model_validate(request.data)
apply_scim_to_user(scim_user, user)
user.save()
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def patch(self, request, pk, *args, **kwargs):
"""Apply a SCIM PATCH operation set to a user."""
user = self.get_object(pk)
scim_user = user_to_scim(user)
patch_op = PatchOp[User].model_validate(request.data)
patch_op.patch(scim_user)
apply_scim_to_user(scim_user, user)
user.save()
return Response(
scim_dump(user_to_scim(user)), content_type='application/scim+json'
)
def delete(self, request, pk, *args, **kwargs):
"""Deactivate a user.
Users are deactivated rather than deleted, to preserve historical
ownership references (e.g. on stock items, orders, audit trails).
"""
user = self.get_object(pk)
user.is_active = False
user.save(update_fields=['is_active'])
return Response(status=204)
class GroupsView(BaseResourceView):
"""`/scim/v2/Groups` - list and create Groups."""
django_model = DjangoGroup
scim_model = Group
filterable_fields = {'displayname': 'name__iexact'}
def get(self, request, *args, **kwargs):
"""List groups."""
return self.list(request)
def to_scim(self, instance):
"""Convert a Django Group into its SCIM representation."""
return group_to_scim(instance)
def post(self, request, *args, **kwargs):
"""Provision a new group."""
scim_group = Group.model_validate(request.data)
if DjangoGroup.objects.filter(name__iexact=scim_group.display_name).exists():
return scim_error(
409, 'A group with this displayName already exists', 'uniqueness'
) # pragma: no cover
group = DjangoGroup(name=scim_group.display_name)
group.save()
set_group_members(group, scim_group.members)
logger.info('SCIM: provisioned new group', name=group.name)
return Response(
scim_dump(group_to_scim(group)),
status=201,
content_type='application/scim+json',
headers={'Location': group_location(group.pk)},
)
class GroupDetailView(ScimAPIView):
"""`/scim/v2/Groups/<id>` - retrieve, replace, patch and remove a single Group."""
def get_object(self, pk):
"""Look up a group by primary key, raising a SCIM-formatted 404 if not found."""
return get_object_or_404(DjangoGroup, pk=pk)
def get(self, request, pk, *args, **kwargs):
"""Retrieve a single group."""
group = self.get_object(pk)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def put(self, request, pk, *args, **kwargs):
"""Replace a group's attributes and membership."""
group = self.get_object(pk)
scim_group = Group.model_validate(request.data)
apply_scim_to_group(scim_group, group)
group.save()
set_group_members(group, scim_group.members)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def patch(self, request, pk, *args, **kwargs):
"""Apply a SCIM PATCH operation set to a group (used mainly for membership changes)."""
group = self.get_object(pk)
scim_group = group_to_scim(group)
patch_op = PatchOp[Group].model_validate(request.data)
patch_op.patch(scim_group)
apply_scim_to_group(scim_group, group)
group.save()
set_group_members(group, scim_group.members)
return Response(
scim_dump(group_to_scim(group)), content_type='application/scim+json'
)
def delete(self, request, pk, *args, **kwargs):
"""Remove a group."""
group = self.get_object(pk)
group.delete()
return Response(status=204)
+2
View File
@@ -231,4 +231,6 @@ def get_ruleset_ignore() -> list[str]:
'importer_dataimportsession',
'importer_dataimportcolumnmap',
'importer_dataimportrow',
# SCIM - superuser-only singleton configuration, managed via the Admin Center
'scim_scimconfiguration',
]
+164
View File
@@ -1,5 +1,11 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements.txt
# pydantic
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -821,6 +827,12 @@ djangorestframework-simplejwt[crypto]==5.5.1 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements.txt
# email-validator
docutils==0.23 \
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
@@ -879,6 +891,12 @@ dulwich==1.2.12 \
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements.txt
# pydantic
et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
--hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
@@ -1050,6 +1068,7 @@ idna==3.19 \
# via
# -c src/backend/requirements.txt
# django-anymail
# email-validator
# requests
inflection==0.5.1 \
--hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \
@@ -1777,6 +1796,136 @@ pycparser==3.0 \
# via
# -c src/backend/requirements.txt
# cffi
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements.txt
# pydantic
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
@@ -2198,6 +2347,12 @@ s3transfer==0.19.2 \
# via
# -c src/backend/requirements.txt
# boto3
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
sentry-sdk==2.68.0 \
--hash=sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4 \
--hash=sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3
@@ -2274,6 +2429,15 @@ typing-extensions==4.16.0 \
# opentelemetry-semantic-conventions
# pint
# py-moneyed
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements.txt
# pydantic
tzdata==2026.3 \
--hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \
--hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931
+222 -3
View File
@@ -1,5 +1,19 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev-3.14.txt -c src/backend/requirements-3.14.txt --python-version=3.14 -c src/backend/requirements.txt -c src/backend/requirements-dev.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via
# -c src/backend/requirements-dev.txt
# httpx
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -21,6 +35,8 @@ certifi==2026.7.22 \
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# httpcore
# httpx
# requests
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
@@ -543,12 +559,46 @@ django-types==0.24.0 \
# via
# -c src/backend/requirements-dev.txt
# -r src/backend/requirements-dev.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# email-validator
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
# via
# -c src/backend/requirements-dev.txt
# django-silk
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# -c src/backend/requirements-dev.txt
# httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via
# -c src/backend/requirements-dev.txt
# httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via
# -c src/backend/requirements-dev.txt
# scim2-client
idna==3.19 \
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
@@ -556,6 +606,9 @@ idna==3.19 \
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# anyio
# email-validator
# httpx
# requests
iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
@@ -644,9 +697,143 @@ pycparser==3.0 \
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# cffi
pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via
# -c src/backend/requirements-dev.txt
# pytest
@@ -724,6 +911,27 @@ rich==15.0.0 \
# via
# -c src/backend/requirements-dev.txt
# pytest-codspeed
scim2-client[httpx]==0.7.5 \
--hash=sha256:6667f4b39c5cd8fcd4caf59e8bad136bb6f92ba1f30ece04948f36728b291e20 \
--hash=sha256:bb2d3437027e6b13f2d620038a288c6b718284d3a7249f121c1ce43f5cf7fe1e
# via
# -c src/backend/requirements-dev.txt
# scim2-tester
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# scim2-client
# scim2-tester
scim2-tester[httpx]==0.2.8 \
--hash=sha256:d8b8f3d18d452cafbe3eb966c7013579c78c75794eba31ca161e68e6f6544051 \
--hash=sha256:fbd2cce113cd9c0c14caf2660dc252d3b360a5461b931aa88f96ac7f0bd74ae6
# via
# -c src/backend/requirements-dev.txt
# -r src/backend/requirements-dev.in
setuptools==84.0.0 \
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
@@ -786,6 +994,17 @@ typing-extensions==4.16.0 \
# django-stubs
# django-stubs-ext
# django-test-migrations
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements-3.14.txt
# -c src/backend/requirements-dev.txt
# -c src/backend/requirements.txt
# pydantic
urllib3==2.7.0 \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
+1
View File
@@ -15,3 +15,4 @@ django-stubs # typing
requests-mock # Mock requests for unit tests
pytest-codspeed # Performance testing with Codspeed
pytest-django # Pytest support for Django (for benchnmarking)
scim2-tester[httpx] # SCIM test suite
+197 -3
View File
@@ -1,5 +1,15 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements-dev.in -o src/backend/requirements-dev.txt -c src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via
# -c src/backend/requirements.txt
# pydantic
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
--hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
# via httpx
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -15,6 +25,8 @@ certifi==2026.7.22 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
# via
# -c src/backend/requirements.txt
# httpcore
# httpx
# requests
cffi==2.1.1 \
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
@@ -511,15 +523,42 @@ django-types==0.24.0 \
--hash=sha256:af903de8b9ee963b7594459a7a20cb8eaaab176ae2b3244ecaa089e0c570b0d1 \
--hash=sha256:ddb478ca733e0dde5475118dd59ab340156980f9659fd92de2083326ae96100a
# via -r src/backend/requirements-dev.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via
# -c src/backend/requirements.txt
# email-validator
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via
# -c src/backend/requirements.txt
# pydantic
gprof2dot==2025.4.14 \
--hash=sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e \
--hash=sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce
# via django-silk
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via scim2-client
idna==3.19 \
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
# via
# -c src/backend/requirements.txt
# anyio
# email-validator
# httpx
# requests
iniconfig==2.3.0 \
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
@@ -586,9 +625,139 @@ pycparser==3.0 \
# via
# -c src/backend/requirements.txt
# cffi
pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# -c src/backend/requirements.txt
# scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via
# -c src/backend/requirements.txt
# pydantic
pygments==2.20.0 \
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
# via
# pytest
# rich
@@ -653,6 +822,21 @@ rich==15.0.0 \
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
# via pytest-codspeed
scim2-client[httpx]==0.7.5 \
--hash=sha256:6667f4b39c5cd8fcd4caf59e8bad136bb6f92ba1f30ece04948f36728b291e20 \
--hash=sha256:bb2d3437027e6b13f2d620038a288c6b718284d3a7249f121c1ce43f5cf7fe1e
# via scim2-tester
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via
# -c src/backend/requirements.txt
# scim2-client
# scim2-tester
scim2-tester[httpx]==0.2.8 \
--hash=sha256:d8b8f3d18d452cafbe3eb966c7013579c78c75794eba31ca161e68e6f6544051 \
--hash=sha256:fbd2cce113cd9c0c14caf2660dc252d3b360a5461b931aa88f96ac7f0bd74ae6
# via -r src/backend/requirements-dev.in
setuptools==84.0.0 \
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
@@ -700,9 +884,19 @@ typing-extensions==4.16.0 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via
# -c src/backend/requirements.txt
# anyio
# django-stubs
# django-stubs-ext
# django-test-migrations
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via
# -c src/backend/requirements.txt
# pydantic
urllib3==2.7.0 \
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
+1
View File
@@ -50,6 +50,7 @@ python-dotenv # Environment variable management
pyyaml # YAML parsing
qrcode[pil] # QR code generator
rapidfuzz # Fuzzy string matching
scim2-models # SCIM 2.0 schema / protocol models, used for the SCIM provisioning endpoint
sentry-sdk # Error reporting (optional)
setuptools # Standard dependency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
+150
View File
@@ -1,5 +1,9 @@
# This file was autogenerated by uv via the following command:
# uv pip compile src/backend/requirements.in -o src/backend/requirements.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via pydantic
asgiref==3.12.1 \
--hash=sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340 \
--hash=sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094
@@ -728,6 +732,10 @@ djangorestframework-simplejwt[crypto]==5.5.1 \
--hash=sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469 \
--hash=sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f
# via -r src/backend/requirements.in
dnspython==2.8.0 \
--hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \
--hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f
# via email-validator
docutils==0.23 \
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
@@ -780,6 +788,10 @@ dulwich==1.2.12 \
--hash=sha256:d7f1ff233081ab644f35e90a0ef169f8d9ee46a72820a3fbf8476f9665e79800 \
--hash=sha256:dfde0fba1eb1f208fc1f0c764e3bb213b31d986f37f7548b994b91e244a10570
# via -r src/backend/requirements.in
email-validator==2.3.0 \
--hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \
--hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426
# via pydantic
et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
--hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
@@ -930,6 +942,7 @@ idna==3.19 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
# via
# django-anymail
# email-validator
# requests
inflection==0.5.1 \
--hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \
@@ -1577,6 +1590,132 @@ pycparser==3.0 \
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
# via cffi
pydantic[email]==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via scim2-models
pydantic-core==2.46.4 \
--hash=sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0 \
--hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \
--hash=sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda \
--hash=sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b \
--hash=sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594 \
--hash=sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29 \
--hash=sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2 \
--hash=sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c \
--hash=sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d \
--hash=sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398 \
--hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \
--hash=sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3 \
--hash=sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7 \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d \
--hash=sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df \
--hash=sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2 \
--hash=sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0 \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd \
--hash=sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7 \
--hash=sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928 \
--hash=sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6 \
--hash=sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3 \
--hash=sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987 \
--hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712 \
--hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \
--hash=sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea \
--hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \
--hash=sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826 \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7 \
--hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \
--hash=sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc \
--hash=sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15 \
--hash=sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3 \
--hash=sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04 \
--hash=sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c \
--hash=sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b \
--hash=sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9 \
--hash=sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce \
--hash=sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4 \
--hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \
--hash=sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894 \
--hash=sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9 \
--hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \
--hash=sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201 \
--hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \
--hash=sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109 \
--hash=sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4 \
--hash=sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848 \
--hash=sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01 \
--hash=sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39 \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b \
--hash=sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf \
--hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \
--hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30 \
--hash=sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983 \
--hash=sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1 \
--hash=sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76 \
--hash=sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5 \
--hash=sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4 \
--hash=sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7 \
--hash=sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c \
--hash=sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066 \
--hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 \
--hash=sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50 \
--hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \
--hash=sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49 \
--hash=sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b \
--hash=sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4 \
--hash=sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c \
--hash=sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff \
--hash=sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae
# via pydantic
pydyf==0.12.1 \
--hash=sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc \
--hash=sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095
@@ -1965,6 +2104,10 @@ s3transfer==0.19.2 \
--hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \
--hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25
# via boto3
scim2-models==0.6.12 \
--hash=sha256:3a57c5ec10dc1007e5f4391d3bddeaca974be8d55e9ede4a1de20019ad3a920b \
--hash=sha256:8ccc8139c61f84a29a1c54d8eda3f8fd121534cdd292166be1e8289d6f05902a
# via -r src/backend/requirements.in
sentry-sdk==2.68.0 \
--hash=sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4 \
--hash=sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3
@@ -2027,7 +2170,14 @@ typing-extensions==4.16.0 \
# opentelemetry-semantic-conventions
# pint
# py-moneyed
# pydantic
# pydantic-core
# referencing
# typing-inspection
typing-inspection==0.4.4 \
--hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
--hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
# via pydantic
tzdata==2026.3 \
--hash=sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415 \
--hash=sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931