remove celery everywhere (almost)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -100,9 +100,6 @@ ipython_config.py | |||||||
| # pyenv | # pyenv | ||||||
| .python-version | .python-version | ||||||
|  |  | ||||||
| # celery beat schedule file |  | ||||||
| celerybeat-schedule |  | ||||||
|  |  | ||||||
| # SageMath parsed files | # SageMath parsed files | ||||||
| *.sage.py | *.sage.py | ||||||
|  |  | ||||||
| @ -166,8 +163,6 @@ dmypy.json | |||||||
|  |  | ||||||
| # pyenv | # pyenv | ||||||
|  |  | ||||||
| # celery beat schedule file |  | ||||||
|  |  | ||||||
| # SageMath parsed files | # SageMath parsed files | ||||||
|  |  | ||||||
| # Environments | # Environments | ||||||
|  | |||||||
| @ -1,57 +0,0 @@ | |||||||
| """authentik administration overview""" |  | ||||||
|  |  | ||||||
| from socket import gethostname |  | ||||||
|  |  | ||||||
| from django.conf import settings |  | ||||||
| from drf_spectacular.utils import extend_schema, inline_serializer |  | ||||||
| from packaging.version import parse |  | ||||||
| from rest_framework.fields import BooleanField, CharField |  | ||||||
| from rest_framework.request import Request |  | ||||||
| from rest_framework.response import Response |  | ||||||
| from rest_framework.views import APIView |  | ||||||
|  |  | ||||||
| from authentik import get_full_version |  | ||||||
| from authentik.rbac.permissions import HasPermission |  | ||||||
| from authentik.root.celery import CELERY_APP |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class WorkerView(APIView): |  | ||||||
|     """Get currently connected worker count.""" |  | ||||||
|  |  | ||||||
|     permission_classes = [HasPermission("authentik_rbac.view_system_info")] |  | ||||||
|  |  | ||||||
|     @extend_schema( |  | ||||||
|         responses=inline_serializer( |  | ||||||
|             "Worker", |  | ||||||
|             fields={ |  | ||||||
|                 "worker_id": CharField(), |  | ||||||
|                 "version": CharField(), |  | ||||||
|                 "version_matching": BooleanField(), |  | ||||||
|             }, |  | ||||||
|             many=True, |  | ||||||
|         ) |  | ||||||
|     ) |  | ||||||
|     def get(self, request: Request) -> Response: |  | ||||||
|         """Get currently connected worker count.""" |  | ||||||
|         raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) |  | ||||||
|         our_version = parse(get_full_version()) |  | ||||||
|         response = [] |  | ||||||
|         for worker in raw: |  | ||||||
|             key = list(worker.keys())[0] |  | ||||||
|             version = worker[key].get("version") |  | ||||||
|             version_matching = False |  | ||||||
|             if version: |  | ||||||
|                 version_matching = parse(version) == our_version |  | ||||||
|             response.append( |  | ||||||
|                 {"worker_id": key, "version": version, "version_matching": version_matching} |  | ||||||
|             ) |  | ||||||
|         # In debug we run with `task_always_eager`, so tasks are ran on the main process |  | ||||||
|         if settings.DEBUG:  # pragma: no cover |  | ||||||
|             response.append( |  | ||||||
|                 { |  | ||||||
|                     "worker_id": f"authentik-debug@{gethostname()}", |  | ||||||
|                     "version": get_full_version(), |  | ||||||
|                     "version_matching": True, |  | ||||||
|                 } |  | ||||||
|             ) |  | ||||||
|         return Response(response) |  | ||||||
| @ -5,7 +5,6 @@ from packaging.version import parse | |||||||
| from prometheus_client import Gauge | from prometheus_client import Gauge | ||||||
|  |  | ||||||
| from authentik import get_full_version | from authentik import get_full_version | ||||||
| from authentik.root.celery import CELERY_APP |  | ||||||
| from authentik.root.monitoring import monitoring_set | from authentik.root.monitoring import monitoring_set | ||||||
|  |  | ||||||
| GAUGE_WORKERS = Gauge( | GAUGE_WORKERS = Gauge( | ||||||
| @ -21,15 +20,16 @@ _version = parse(get_full_version()) | |||||||
| @receiver(monitoring_set) | @receiver(monitoring_set) | ||||||
| def monitoring_set_workers(sender, **kwargs): | def monitoring_set_workers(sender, **kwargs): | ||||||
|     """Set worker gauge""" |     """Set worker gauge""" | ||||||
|     raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) |     # TODO | ||||||
|     worker_version_count = {} |     # raw: list[dict[str, dict]] = app.control.ping(timeout=0.5) | ||||||
|     for worker in raw: |     # worker_version_count = {} | ||||||
|         key = list(worker.keys())[0] |     # for worker in raw: | ||||||
|         version = worker[key].get("version") |     #     key = list(worker.keys())[0] | ||||||
|         version_matching = False |     #     version = worker[key].get("version") | ||||||
|         if version: |     #     version_matching = False | ||||||
|             version_matching = parse(version) == _version |     #     if version: | ||||||
|         worker_version_count.setdefault(version, {"count": 0, "matching": version_matching}) |     #         version_matching = parse(version) == _version | ||||||
|         worker_version_count[version]["count"] += 1 |     #     worker_version_count.setdefault(version, {"count": 0, "matching": version_matching}) | ||||||
|     for version, stats in worker_version_count.items(): |     #     worker_version_count[version]["count"] += 1 | ||||||
|         GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"]) |     # for version, stats in worker_version_count.items(): | ||||||
|  |     #     GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"]) | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ from authentik.admin.api.metrics import AdministrationMetricsViewSet | |||||||
| from authentik.admin.api.system import SystemView | from authentik.admin.api.system import SystemView | ||||||
| from authentik.admin.api.version import VersionView | from authentik.admin.api.version import VersionView | ||||||
| from authentik.admin.api.version_history import VersionHistoryViewSet | from authentik.admin.api.version_history import VersionHistoryViewSet | ||||||
| from authentik.admin.api.workers import WorkerView |  | ||||||
|  |  | ||||||
| api_urlpatterns = [ | api_urlpatterns = [ | ||||||
|     ("admin/apps", AppsViewSet, "apps"), |     ("admin/apps", AppsViewSet, "apps"), | ||||||
| @ -19,6 +18,5 @@ api_urlpatterns = [ | |||||||
|     ), |     ), | ||||||
|     path("admin/version/", VersionView.as_view(), name="admin_version"), |     path("admin/version/", VersionView.as_view(), name="admin_version"), | ||||||
|     ("admin/version/history", VersionHistoryViewSet, "version_history"), |     ("admin/version/history", VersionHistoryViewSet, "version_history"), | ||||||
|     path("admin/workers/", WorkerView.as_view(), name="admin_workers"), |  | ||||||
|     path("admin/system/", SystemView.as_view(), name="admin_system"), |     path("admin/system/", SystemView.as_view(), name="admin_system"), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -57,7 +57,6 @@ from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( | |||||||
|     EndpointDeviceConnection, |     EndpointDeviceConnection, | ||||||
| ) | ) | ||||||
| from authentik.events.logs import LogEvent, capture_logs | from authentik.events.logs import LogEvent, capture_logs | ||||||
| from authentik.events.models import SystemTask |  | ||||||
| from authentik.events.utils import cleanse_dict | from authentik.events.utils import cleanse_dict | ||||||
| from authentik.flows.models import FlowToken, Stage | from authentik.flows.models import FlowToken, Stage | ||||||
| from authentik.lib.models import SerializerModel | from authentik.lib.models import SerializerModel | ||||||
| @ -119,7 +118,6 @@ def excluded_models() -> list[type[Model]]: | |||||||
|         SCIMProviderGroup, |         SCIMProviderGroup, | ||||||
|         SCIMProviderUser, |         SCIMProviderUser, | ||||||
|         Tenant, |         Tenant, | ||||||
|         SystemTask, |  | ||||||
|         Task, |         Task, | ||||||
|         ConnectionToken, |         ConnectionToken, | ||||||
|         AuthorizationCode, |         AuthorizationCode, | ||||||
|  | |||||||
| @ -1,104 +0,0 @@ | |||||||
| """Tasks API""" |  | ||||||
|  |  | ||||||
| from importlib import import_module |  | ||||||
|  |  | ||||||
| from django.contrib import messages |  | ||||||
| from django.utils.translation import gettext_lazy as _ |  | ||||||
| from drf_spectacular.types import OpenApiTypes |  | ||||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema |  | ||||||
| from rest_framework.decorators import action |  | ||||||
| from rest_framework.fields import ( |  | ||||||
|     CharField, |  | ||||||
|     ChoiceField, |  | ||||||
|     DateTimeField, |  | ||||||
|     FloatField, |  | ||||||
|     SerializerMethodField, |  | ||||||
| ) |  | ||||||
| from rest_framework.request import Request |  | ||||||
| from rest_framework.response import Response |  | ||||||
| from rest_framework.viewsets import ReadOnlyModelViewSet |  | ||||||
| from structlog.stdlib import get_logger |  | ||||||
|  |  | ||||||
| from authentik.core.api.utils import ModelSerializer |  | ||||||
| from authentik.events.logs import LogEventSerializer |  | ||||||
| from authentik.events.models import SystemTask, TaskStatus |  | ||||||
| from authentik.rbac.decorators import permission_required |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SystemTaskSerializer(ModelSerializer): |  | ||||||
|     """Serialize TaskInfo and TaskResult""" |  | ||||||
|  |  | ||||||
|     name = CharField() |  | ||||||
|     full_name = SerializerMethodField() |  | ||||||
|     uid = CharField(required=False) |  | ||||||
|     description = CharField() |  | ||||||
|     start_timestamp = DateTimeField(read_only=True) |  | ||||||
|     finish_timestamp = DateTimeField(read_only=True) |  | ||||||
|     duration = FloatField(read_only=True) |  | ||||||
|  |  | ||||||
|     status = ChoiceField(choices=[(x.value, x.name) for x in TaskStatus]) |  | ||||||
|     messages = LogEventSerializer(many=True) |  | ||||||
|  |  | ||||||
|     def get_full_name(self, instance: SystemTask) -> str: |  | ||||||
|         """Get full name with UID""" |  | ||||||
|         if instance.uid: |  | ||||||
|             return f"{instance.name}:{instance.uid}" |  | ||||||
|         return instance.name |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         model = SystemTask |  | ||||||
|         fields = [ |  | ||||||
|             "uuid", |  | ||||||
|             "name", |  | ||||||
|             "full_name", |  | ||||||
|             "uid", |  | ||||||
|             "description", |  | ||||||
|             "start_timestamp", |  | ||||||
|             "finish_timestamp", |  | ||||||
|             "duration", |  | ||||||
|             "status", |  | ||||||
|             "messages", |  | ||||||
|             "expires", |  | ||||||
|             "expiring", |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SystemTaskViewSet(ReadOnlyModelViewSet): |  | ||||||
|     """Read-only view set that returns all background tasks""" |  | ||||||
|  |  | ||||||
|     queryset = SystemTask.objects.all() |  | ||||||
|     serializer_class = SystemTaskSerializer |  | ||||||
|     filterset_fields = ["name", "uid", "status"] |  | ||||||
|     ordering = ["name", "uid", "status"] |  | ||||||
|     search_fields = ["name", "description", "uid", "status"] |  | ||||||
|  |  | ||||||
|     @permission_required(None, ["authentik_events.run_task"]) |  | ||||||
|     @extend_schema( |  | ||||||
|         request=OpenApiTypes.NONE, |  | ||||||
|         responses={ |  | ||||||
|             204: OpenApiResponse(description="Task retried successfully"), |  | ||||||
|             404: OpenApiResponse(description="Task not found"), |  | ||||||
|             500: OpenApiResponse(description="Failed to retry task"), |  | ||||||
|         }, |  | ||||||
|     ) |  | ||||||
|     @action(detail=True, methods=["POST"], permission_classes=[]) |  | ||||||
|     def run(self, request: Request, pk=None) -> Response: |  | ||||||
|         """Run task""" |  | ||||||
|         task: SystemTask = self.get_object() |  | ||||||
|         try: |  | ||||||
|             task_module = import_module(task.task_call_module) |  | ||||||
|             task_func = getattr(task_module, task.task_call_func) |  | ||||||
|             LOGGER.info("Running task", task=task_func) |  | ||||||
|             task_func.delay(*task.task_call_args, **task.task_call_kwargs) |  | ||||||
|             messages.success( |  | ||||||
|                 self.request, |  | ||||||
|                 _("Successfully started task {name}.".format_map({"name": task.name})), |  | ||||||
|             ) |  | ||||||
|             return Response(status=204) |  | ||||||
|         except (ImportError, AttributeError) as exc:  # pragma: no cover |  | ||||||
|             LOGGER.warning("Failed to run task, remove state", task=task.name, exc=exc) |  | ||||||
|             # if we get an import error, the module path has probably changed |  | ||||||
|             task.delete() |  | ||||||
|             return Response(status=500) |  | ||||||
| @ -1,12 +1,11 @@ | |||||||
| """authentik events app""" | """authentik events app""" | ||||||
|  |  | ||||||
| from celery.schedules import crontab |  | ||||||
| from prometheus_client import Gauge, Histogram | from prometheus_client import Gauge, Histogram | ||||||
|  |  | ||||||
| from authentik.blueprints.apps import ManagedAppConfig | from authentik.blueprints.apps import ManagedAppConfig | ||||||
| from authentik.lib.config import CONFIG, ENV_PREFIX | from authentik.lib.config import CONFIG, ENV_PREFIX | ||||||
| from authentik.lib.utils.reflection import path_to_class | from authentik.lib.utils.time import fqdn_rand | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.tasks.schedules.lib import ScheduleSpec | ||||||
|  |  | ||||||
| # TODO: Deprecated metric - remove in 2024.2 or later | # TODO: Deprecated metric - remove in 2024.2 or later | ||||||
| GAUGE_TASKS = Gauge( | GAUGE_TASKS = Gauge( | ||||||
| @ -35,6 +34,17 @@ class AuthentikEventsConfig(ManagedAppConfig): | |||||||
|     verbose_name = "authentik Events" |     verbose_name = "authentik Events" | ||||||
|     default = True |     default = True | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def tenant_schedule_specs(self) -> list[ScheduleSpec]: | ||||||
|  |         from authentik.events.tasks import notification_cleanup | ||||||
|  |  | ||||||
|  |         return [ | ||||||
|  |             ScheduleSpec( | ||||||
|  |                 actor=notification_cleanup, | ||||||
|  |                 crontab=f"{fqdn_rand('notification_cleanup')} */8 * * *", | ||||||
|  |             ), | ||||||
|  |         ] | ||||||
|  |  | ||||||
|     @ManagedAppConfig.reconcile_global |     @ManagedAppConfig.reconcile_global | ||||||
|     def check_deprecations(self): |     def check_deprecations(self): | ||||||
|         """Check for config deprecations""" |         """Check for config deprecations""" | ||||||
| @ -56,29 +66,3 @@ class AuthentikEventsConfig(ManagedAppConfig): | |||||||
|                 replacement_env=replace_env, |                 replacement_env=replace_env, | ||||||
|                 message=msg, |                 message=msg, | ||||||
|             ).save() |             ).save() | ||||||
|  |  | ||||||
|     @ManagedAppConfig.reconcile_tenant |  | ||||||
|     def run_scheduled_tasks(self): |  | ||||||
|         """Run schedule tasks which are behind schedule (only applies |  | ||||||
|         to tasks of which we keep metrics)""" |  | ||||||
|         from authentik.events.models import TaskStatus |  | ||||||
|         from authentik.events.system_tasks import SystemTask as CelerySystemTask |  | ||||||
|  |  | ||||||
|         for task in CELERY_APP.conf["beat_schedule"].values(): |  | ||||||
|             schedule = task["schedule"] |  | ||||||
|             if not isinstance(schedule, crontab): |  | ||||||
|                 continue |  | ||||||
|             task_class: CelerySystemTask = path_to_class(task["task"]) |  | ||||||
|             if not isinstance(task_class, CelerySystemTask): |  | ||||||
|                 continue |  | ||||||
|             db_task = task_class.db() |  | ||||||
|             if not db_task: |  | ||||||
|                 continue |  | ||||||
|             due, _ = schedule.is_due(db_task.finish_timestamp) |  | ||||||
|             if due or db_task.status == TaskStatus.UNKNOWN: |  | ||||||
|                 self.logger.debug("Running past-due scheduled task", task=task["task"]) |  | ||||||
|                 task_class.apply_async( |  | ||||||
|                     args=task.get("args", None), |  | ||||||
|                     kwargs=task.get("kwargs", None), |  | ||||||
|                     **task.get("options", {}), |  | ||||||
|                 ) |  | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								authentik/events/migrations/0010_delete_systemtask.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								authentik/events/migrations/0010_delete_systemtask.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | # Generated by Django 5.1.9 on 2025-06-06 13:25 | ||||||
|  |  | ||||||
|  | from django.db import migrations | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_events", "0009_remove_notificationtransport_webhook_mapping_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.DeleteModel( | ||||||
|  |             name="SystemTask", | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -10,7 +10,7 @@ from smtplib import SMTPException | |||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| from django.apps import apps | from django.apps import apps | ||||||
| from django.db import connection, models | from django.db import models | ||||||
| from django.db.models import Count, ExpressionWrapper, F | from django.db.models import Count, ExpressionWrapper, F | ||||||
| from django.db.models.fields import DurationField | from django.db.models.fields import DurationField | ||||||
| from django.db.models.functions import Extract | from django.db.models.functions import Extract | ||||||
| @ -32,7 +32,6 @@ from authentik.core.middleware import ( | |||||||
|     SESSION_KEY_IMPERSONATE_USER, |     SESSION_KEY_IMPERSONATE_USER, | ||||||
| ) | ) | ||||||
| from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | from authentik.core.models import ExpiringModel, Group, PropertyMapping, User | ||||||
| from authentik.events.apps import GAUGE_TASKS, SYSTEM_TASK_STATUS, SYSTEM_TASK_TIME |  | ||||||
| from authentik.events.context_processors.base import get_context_processors | from authentik.events.context_processors.base import get_context_processors | ||||||
| from authentik.events.utils import ( | from authentik.events.utils import ( | ||||||
|     cleanse_dict, |     cleanse_dict, | ||||||
| @ -654,73 +653,3 @@ class NotificationWebhookMapping(PropertyMapping): | |||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = _("Webhook Mapping") |         verbose_name = _("Webhook Mapping") | ||||||
|         verbose_name_plural = _("Webhook Mappings") |         verbose_name_plural = _("Webhook Mappings") | ||||||
|  |  | ||||||
|  |  | ||||||
| class TaskStatus(models.TextChoices): |  | ||||||
|     """Possible states of tasks""" |  | ||||||
|  |  | ||||||
|     UNKNOWN = "unknown" |  | ||||||
|     SUCCESSFUL = "successful" |  | ||||||
|     WARNING = "warning" |  | ||||||
|     ERROR = "error" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SystemTask(SerializerModel, ExpiringModel): |  | ||||||
|     """Info about a system task running in the background along with details to restart the task""" |  | ||||||
|  |  | ||||||
|     uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) |  | ||||||
|     name = models.TextField() |  | ||||||
|     uid = models.TextField(null=True) |  | ||||||
|  |  | ||||||
|     start_timestamp = models.DateTimeField(default=now) |  | ||||||
|     finish_timestamp = models.DateTimeField(default=now) |  | ||||||
|     duration = models.FloatField(default=0) |  | ||||||
|  |  | ||||||
|     status = models.TextField(choices=TaskStatus.choices) |  | ||||||
|  |  | ||||||
|     description = models.TextField(null=True) |  | ||||||
|     messages = models.JSONField() |  | ||||||
|  |  | ||||||
|     task_call_module = models.TextField() |  | ||||||
|     task_call_func = models.TextField() |  | ||||||
|     task_call_args = models.JSONField(default=list) |  | ||||||
|     task_call_kwargs = models.JSONField(default=dict) |  | ||||||
|  |  | ||||||
|     @property |  | ||||||
|     def serializer(self) -> type[Serializer]: |  | ||||||
|         from authentik.events.api.tasks import SystemTaskSerializer |  | ||||||
|  |  | ||||||
|         return SystemTaskSerializer |  | ||||||
|  |  | ||||||
|     def update_metrics(self): |  | ||||||
|         """Update prometheus metrics""" |  | ||||||
|         # TODO: Deprecated metric - remove in 2024.2 or later |  | ||||||
|         GAUGE_TASKS.labels( |  | ||||||
|             tenant=connection.schema_name, |  | ||||||
|             task_name=self.name, |  | ||||||
|             task_uid=self.uid or "", |  | ||||||
|             status=self.status.lower(), |  | ||||||
|         ).set(self.duration) |  | ||||||
|         SYSTEM_TASK_TIME.labels( |  | ||||||
|             tenant=connection.schema_name, |  | ||||||
|             task_name=self.name, |  | ||||||
|             task_uid=self.uid or "", |  | ||||||
|         ).observe(self.duration) |  | ||||||
|         SYSTEM_TASK_STATUS.labels( |  | ||||||
|             tenant=connection.schema_name, |  | ||||||
|             task_name=self.name, |  | ||||||
|             task_uid=self.uid or "", |  | ||||||
|             status=self.status.lower(), |  | ||||||
|         ).inc() |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |  | ||||||
|         return f"System Task {self.name}" |  | ||||||
|  |  | ||||||
|     class Meta: |  | ||||||
|         unique_together = (("name", "uid"),) |  | ||||||
|         # Remove "add", "change" and "delete" permissions as those are not used |  | ||||||
|         default_permissions = ["view"] |  | ||||||
|         permissions = [("run_task", _("Run task"))] |  | ||||||
|         verbose_name = _("System Task") |  | ||||||
|         verbose_name_plural = _("System Tasks") |  | ||||||
|         indexes = ExpiringModel.Meta.indexes |  | ||||||
|  | |||||||
| @ -1,13 +0,0 @@ | |||||||
| """Event Settings""" |  | ||||||
|  |  | ||||||
| from celery.schedules import crontab |  | ||||||
|  |  | ||||||
| from authentik.lib.utils.time import fqdn_rand |  | ||||||
|  |  | ||||||
| CELERY_BEAT_SCHEDULE = { |  | ||||||
|     "events_notification_cleanup": { |  | ||||||
|         "task": "authentik.events.tasks.notification_cleanup", |  | ||||||
|         "schedule": crontab(minute=fqdn_rand("notification_cleanup"), hour="*/8"), |  | ||||||
|         "options": {"queue": "authentik_scheduled"}, |  | ||||||
|     }, |  | ||||||
| } |  | ||||||
| @ -12,13 +12,11 @@ from rest_framework.request import Request | |||||||
|  |  | ||||||
| from authentik.core.models import AuthenticatedSession, User | from authentik.core.models import AuthenticatedSession, User | ||||||
| from authentik.core.signals import login_failed, password_changed | from authentik.core.signals import login_failed, password_changed | ||||||
| from authentik.events.apps import SYSTEM_TASK_STATUS | from authentik.events.models import Event, EventAction, NotificationRule | ||||||
| from authentik.events.models import Event, EventAction, NotificationRule, SystemTask |  | ||||||
| from authentik.events.tasks import event_trigger_handler, gdpr_cleanup | from authentik.events.tasks import event_trigger_handler, gdpr_cleanup | ||||||
| from authentik.flows.models import Stage | from authentik.flows.models import Stage | ||||||
| from authentik.flows.planner import PLAN_CONTEXT_OUTPOST, PLAN_CONTEXT_SOURCE, FlowPlan | from authentik.flows.planner import PLAN_CONTEXT_OUTPOST, PLAN_CONTEXT_SOURCE, FlowPlan | ||||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||||
| from authentik.root.monitoring import monitoring_set |  | ||||||
| from authentik.stages.invitation.models import Invitation | from authentik.stages.invitation.models import Invitation | ||||||
| from authentik.stages.invitation.signals import invitation_used | from authentik.stages.invitation.signals import invitation_used | ||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||||
| @ -123,11 +121,3 @@ def event_user_pre_delete_cleanup(sender, instance: User, **_): | |||||||
|     """If gdpr_compliance is enabled, remove all the user's events""" |     """If gdpr_compliance is enabled, remove all the user's events""" | ||||||
|     if get_current_tenant().gdpr_compliance: |     if get_current_tenant().gdpr_compliance: | ||||||
|         gdpr_cleanup.send(instance.pk) |         gdpr_cleanup.send(instance.pk) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(monitoring_set) |  | ||||||
| def monitoring_system_task(sender, **_): |  | ||||||
|     """Update metrics when task is saved""" |  | ||||||
|     SYSTEM_TASK_STATUS.clear() |  | ||||||
|     for task in SystemTask.objects.all(): |  | ||||||
|         task.update_metrics() |  | ||||||
|  | |||||||
| @ -1,133 +0,0 @@ | |||||||
| """Monitored tasks""" |  | ||||||
|  |  | ||||||
| from datetime import datetime, timedelta |  | ||||||
| from time import perf_counter |  | ||||||
| from typing import Any |  | ||||||
|  |  | ||||||
| from django.utils.timezone import now |  | ||||||
| from structlog.stdlib import BoundLogger, get_logger |  | ||||||
| from tenant_schemas_celery.task import TenantTask |  | ||||||
|  |  | ||||||
| from authentik.events.logs import LogEvent |  | ||||||
| from authentik.events.models import Event, EventAction, TaskStatus |  | ||||||
| from authentik.events.models import SystemTask as DBSystemTask |  | ||||||
| from authentik.events.utils import sanitize_item |  | ||||||
| from authentik.lib.utils.errors import exception_to_string |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SystemTask(TenantTask): |  | ||||||
|     """Task which can save its state to the cache""" |  | ||||||
|  |  | ||||||
|     logger: BoundLogger |  | ||||||
|  |  | ||||||
|     # For tasks that should only be listed if they failed, set this to False |  | ||||||
|     save_on_success: bool |  | ||||||
|  |  | ||||||
|     _status: TaskStatus |  | ||||||
|     _messages: list[LogEvent] |  | ||||||
|  |  | ||||||
|     _uid: str | None |  | ||||||
|     # Precise start time from perf_counter |  | ||||||
|     _start_precise: float | None = None |  | ||||||
|     _start: datetime | None = None |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs) -> None: |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|         self._status = TaskStatus.SUCCESSFUL |  | ||||||
|         self.save_on_success = True |  | ||||||
|         self._uid = None |  | ||||||
|         self._status = None |  | ||||||
|         self._messages = [] |  | ||||||
|         self.result_timeout_hours = 6 |  | ||||||
|  |  | ||||||
|     def set_uid(self, uid: str): |  | ||||||
|         """Set UID, so in the case of an unexpected error its saved correctly""" |  | ||||||
|         self._uid = uid |  | ||||||
|  |  | ||||||
|     def set_status(self, status: TaskStatus, *messages: LogEvent): |  | ||||||
|         """Set result for current run, will overwrite previous result.""" |  | ||||||
|         self._status = status |  | ||||||
|         self._messages = list(messages) |  | ||||||
|         for idx, msg in enumerate(self._messages): |  | ||||||
|             if not isinstance(msg, LogEvent): |  | ||||||
|                 self._messages[idx] = LogEvent(msg, logger=self.__name__, log_level="info") |  | ||||||
|  |  | ||||||
|     def set_error(self, exception: Exception, *messages: LogEvent): |  | ||||||
|         """Set result to error and save exception""" |  | ||||||
|         self._status = TaskStatus.ERROR |  | ||||||
|         self._messages = list(messages) |  | ||||||
|         self._messages.extend( |  | ||||||
|             [LogEvent(exception_to_string(exception), logger=self.__name__, log_level="error")] |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def before_start(self, task_id, args, kwargs): |  | ||||||
|         self._start_precise = perf_counter() |  | ||||||
|         self._start = now() |  | ||||||
|         self.logger = get_logger().bind(task_id=task_id) |  | ||||||
|         return super().before_start(task_id, args, kwargs) |  | ||||||
|  |  | ||||||
|     def db(self) -> DBSystemTask | None: |  | ||||||
|         """Get DB object for latest task""" |  | ||||||
|         return DBSystemTask.objects.filter( |  | ||||||
|             name=self.__name__, |  | ||||||
|             uid=self._uid, |  | ||||||
|         ).first() |  | ||||||
|  |  | ||||||
|     def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo): |  | ||||||
|         super().after_return(status, retval, task_id, args, kwargs, einfo=einfo) |  | ||||||
|         if not self._status: |  | ||||||
|             return |  | ||||||
|         if self._status == TaskStatus.SUCCESSFUL and not self.save_on_success: |  | ||||||
|             DBSystemTask.objects.filter( |  | ||||||
|                 name=self.__name__, |  | ||||||
|                 uid=self._uid, |  | ||||||
|             ).delete() |  | ||||||
|             return |  | ||||||
|         DBSystemTask.objects.update_or_create( |  | ||||||
|             name=self.__name__, |  | ||||||
|             uid=self._uid, |  | ||||||
|             defaults={ |  | ||||||
|                 "description": self.__doc__, |  | ||||||
|                 "start_timestamp": self._start or now(), |  | ||||||
|                 "finish_timestamp": now(), |  | ||||||
|                 "duration": max(perf_counter() - self._start_precise, 0), |  | ||||||
|                 "task_call_module": self.__module__, |  | ||||||
|                 "task_call_func": self.__name__, |  | ||||||
|                 "task_call_args": sanitize_item(args), |  | ||||||
|                 "task_call_kwargs": sanitize_item(kwargs), |  | ||||||
|                 "status": self._status, |  | ||||||
|                 "messages": sanitize_item(self._messages), |  | ||||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours), |  | ||||||
|                 "expiring": True, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def on_failure(self, exc, task_id, args, kwargs, einfo): |  | ||||||
|         super().on_failure(exc, task_id, args, kwargs, einfo=einfo) |  | ||||||
|         if not self._status: |  | ||||||
|             self.set_error(exc) |  | ||||||
|         DBSystemTask.objects.update_or_create( |  | ||||||
|             name=self.__name__, |  | ||||||
|             uid=self._uid, |  | ||||||
|             defaults={ |  | ||||||
|                 "description": self.__doc__, |  | ||||||
|                 "start_timestamp": self._start or now(), |  | ||||||
|                 "finish_timestamp": now(), |  | ||||||
|                 "duration": max(perf_counter() - self._start_precise, 0), |  | ||||||
|                 "task_call_module": self.__module__, |  | ||||||
|                 "task_call_func": self.__name__, |  | ||||||
|                 "task_call_args": sanitize_item(args), |  | ||||||
|                 "task_call_kwargs": sanitize_item(kwargs), |  | ||||||
|                 "status": self._status, |  | ||||||
|                 "messages": sanitize_item(self._messages), |  | ||||||
|                 "expires": now() + timedelta(hours=self.result_timeout_hours + 3), |  | ||||||
|                 "expiring": True, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         Event.new( |  | ||||||
|             EventAction.SYSTEM_TASK_EXCEPTION, |  | ||||||
|             message=f"Task {self.__name__} encountered an error: {exception_to_string(exc)}", |  | ||||||
|         ).save() |  | ||||||
|  |  | ||||||
|     def run(self, *args, **kwargs): |  | ||||||
|         raise NotImplementedError |  | ||||||
| @ -1,104 +0,0 @@ | |||||||
| """Test Monitored tasks""" |  | ||||||
|  |  | ||||||
| # from json import loads |  | ||||||
|  |  | ||||||
| from django.urls import reverse |  | ||||||
| from rest_framework.test import APITestCase |  | ||||||
|  |  | ||||||
| # from authentik.core.tasks import clean_expired_models |  | ||||||
| from authentik.core.tests.utils import create_test_admin_user |  | ||||||
| from authentik.events.models import SystemTask as DBSystemTask |  | ||||||
| from authentik.events.models import TaskStatus |  | ||||||
| from authentik.events.system_tasks import SystemTask |  | ||||||
| from authentik.lib.generators import generate_id |  | ||||||
| from authentik.root.celery import CELERY_APP |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestSystemTasks(APITestCase): |  | ||||||
|     """Test Monitored tasks""" |  | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         super().setUp() |  | ||||||
|         self.user = create_test_admin_user() |  | ||||||
|         self.client.force_login(self.user) |  | ||||||
|  |  | ||||||
|     def test_failed_successful_remove_state(self): |  | ||||||
|         """Test that a task with `save_on_success` set to `False` that failed saves |  | ||||||
|         a state, and upon successful completion will delete the state""" |  | ||||||
|         should_fail = True |  | ||||||
|         uid = generate_id() |  | ||||||
|  |  | ||||||
|         @CELERY_APP.task( |  | ||||||
|             bind=True, |  | ||||||
|             base=SystemTask, |  | ||||||
|         ) |  | ||||||
|         def test_task(self: SystemTask): |  | ||||||
|             self.save_on_success = False |  | ||||||
|             self.set_uid(uid) |  | ||||||
|             self.set_status(TaskStatus.ERROR if should_fail else TaskStatus.SUCCESSFUL) |  | ||||||
|  |  | ||||||
|         # First test successful run |  | ||||||
|         should_fail = False |  | ||||||
|         test_task.delay().get() |  | ||||||
|         self.assertIsNone(DBSystemTask.objects.filter(name="test_task", uid=uid).first()) |  | ||||||
|  |  | ||||||
|         # Then test failed |  | ||||||
|         should_fail = True |  | ||||||
|         test_task.delay().get() |  | ||||||
|         task = DBSystemTask.objects.filter(name="test_task", uid=uid).first() |  | ||||||
|         self.assertEqual(task.status, TaskStatus.ERROR) |  | ||||||
|  |  | ||||||
|         # Then after that, the state should be removed |  | ||||||
|         should_fail = False |  | ||||||
|         test_task.delay().get() |  | ||||||
|         self.assertIsNone(DBSystemTask.objects.filter(name="test_task", uid=uid).first()) |  | ||||||
|  |  | ||||||
|     # |  | ||||||
|     # def test_tasks(self): |  | ||||||
|     #     """Test Task API""" |  | ||||||
|     #     clean_expired_models.send() |  | ||||||
|     #     response = self.client.get(reverse("authentik_api:systemtask-list")) |  | ||||||
|     #     self.assertEqual(response.status_code, 200) |  | ||||||
|     #     body = loads(response.content) |  | ||||||
|     #     self.assertTrue(any(task["name"] == "clean_expired_models" for task in body["results"])) |  | ||||||
|     # |  | ||||||
|     # def test_tasks_single(self): |  | ||||||
|     #     """Test Task API (read single)""" |  | ||||||
|     #     clean_expired_models.delay().get() |  | ||||||
|     #     task = DBSystemTask.objects.filter(name="clean_expired_models").first() |  | ||||||
|     #     response = self.client.get( |  | ||||||
|     #         reverse( |  | ||||||
|     #             "authentik_api:systemtask-detail", |  | ||||||
|     #             kwargs={"pk": str(task.pk)}, |  | ||||||
|     #         ) |  | ||||||
|     #     ) |  | ||||||
|     #     self.assertEqual(response.status_code, 200) |  | ||||||
|     #     body = loads(response.content) |  | ||||||
|     #     self.assertEqual(body["status"], TaskStatus.SUCCESSFUL.value) |  | ||||||
|     #     self.assertEqual(body["name"], "clean_expired_models") |  | ||||||
|     #     response = self.client.get( |  | ||||||
|     #         reverse("authentik_api:systemtask-detail", kwargs={"pk": "qwerqwer"}) |  | ||||||
|     #     ) |  | ||||||
|     #     self.assertEqual(response.status_code, 404) |  | ||||||
|     # |  | ||||||
|     # def test_tasks_run(self): |  | ||||||
|     #     """Test Task API (run)""" |  | ||||||
|     #     clean_expired_models.delay().get() |  | ||||||
|     #     task = DBSystemTask.objects.filter(name="clean_expired_models").first() |  | ||||||
|     #     response = self.client.post( |  | ||||||
|     #         reverse( |  | ||||||
|     #             "authentik_api:systemtask-run", |  | ||||||
|     #             kwargs={"pk": str(task.pk)}, |  | ||||||
|     #         ) |  | ||||||
|     #     ) |  | ||||||
|     #     self.assertEqual(response.status_code, 204) |  | ||||||
|  |  | ||||||
|     def test_tasks_run_404(self): |  | ||||||
|         """Test Task API (run, 404)""" |  | ||||||
|         response = self.client.post( |  | ||||||
|             reverse( |  | ||||||
|                 "authentik_api:systemtask-run", |  | ||||||
|                 kwargs={"pk": "qwerqewrqrqewrqewr"}, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(response.status_code, 404) |  | ||||||
| @ -5,13 +5,11 @@ from authentik.events.api.notification_mappings import NotificationWebhookMappin | |||||||
| from authentik.events.api.notification_rules import NotificationRuleViewSet | from authentik.events.api.notification_rules import NotificationRuleViewSet | ||||||
| from authentik.events.api.notification_transports import NotificationTransportViewSet | from authentik.events.api.notification_transports import NotificationTransportViewSet | ||||||
| from authentik.events.api.notifications import NotificationViewSet | from authentik.events.api.notifications import NotificationViewSet | ||||||
| from authentik.events.api.tasks import SystemTaskViewSet |  | ||||||
|  |  | ||||||
| api_urlpatterns = [ | api_urlpatterns = [ | ||||||
|     ("events/events", EventViewSet), |     ("events/events", EventViewSet), | ||||||
|     ("events/notifications", NotificationViewSet), |     ("events/notifications", NotificationViewSet), | ||||||
|     ("events/transports", NotificationTransportViewSet), |     ("events/transports", NotificationTransportViewSet), | ||||||
|     ("events/rules", NotificationRuleViewSet), |     ("events/rules", NotificationRuleViewSet), | ||||||
|     ("events/system_tasks", SystemTaskViewSet), |  | ||||||
|     ("propertymappings/notification", NotificationWebhookMappingViewSet), |     ("propertymappings/notification", NotificationWebhookMappingViewSet), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -88,7 +88,6 @@ def get_logger_config(): | |||||||
|         "authentik": global_level, |         "authentik": global_level, | ||||||
|         "django": "WARNING", |         "django": "WARNING", | ||||||
|         "django.request": "ERROR", |         "django.request": "ERROR", | ||||||
|         "celery": "WARNING", |  | ||||||
|         "selenium": "WARNING", |         "selenium": "WARNING", | ||||||
|         "docker": "WARNING", |         "docker": "WARNING", | ||||||
|         "urllib3": "WARNING", |         "urllib3": "WARNING", | ||||||
|  | |||||||
| @ -3,8 +3,6 @@ | |||||||
| from asyncio.exceptions import CancelledError | from asyncio.exceptions import CancelledError | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError |  | ||||||
| from celery.exceptions import CeleryError |  | ||||||
| from channels_redis.core import ChannelFull | from channels_redis.core import ChannelFull | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError | from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError | ||||||
| @ -21,7 +19,6 @@ from sentry_sdk import HttpTransport, get_current_scope | |||||||
| from sentry_sdk import init as sentry_sdk_init | from sentry_sdk import init as sentry_sdk_init | ||||||
| from sentry_sdk.api import set_tag | from sentry_sdk.api import set_tag | ||||||
| from sentry_sdk.integrations.argv import ArgvIntegration | from sentry_sdk.integrations.argv import ArgvIntegration | ||||||
| from sentry_sdk.integrations.celery import CeleryIntegration |  | ||||||
| from sentry_sdk.integrations.django import DjangoIntegration | from sentry_sdk.integrations.django import DjangoIntegration | ||||||
| from sentry_sdk.integrations.redis import RedisIntegration | from sentry_sdk.integrations.redis import RedisIntegration | ||||||
| from sentry_sdk.integrations.socket import SocketIntegration | from sentry_sdk.integrations.socket import SocketIntegration | ||||||
| @ -71,7 +68,6 @@ def sentry_init(**sentry_init_kwargs): | |||||||
|             ArgvIntegration(), |             ArgvIntegration(), | ||||||
|             StdlibIntegration(), |             StdlibIntegration(), | ||||||
|             DjangoIntegration(transaction_style="function_name", cache_spans=True), |             DjangoIntegration(transaction_style="function_name", cache_spans=True), | ||||||
|             CeleryIntegration(), |  | ||||||
|             RedisIntegration(), |             RedisIntegration(), | ||||||
|             ThreadingIntegration(propagate_hub=True), |             ThreadingIntegration(propagate_hub=True), | ||||||
|             SocketIntegration(), |             SocketIntegration(), | ||||||
| @ -132,10 +128,6 @@ def before_send(event: dict, hint: dict) -> dict | None: | |||||||
|         LocalProtocolError, |         LocalProtocolError, | ||||||
|         # rest_framework error |         # rest_framework error | ||||||
|         APIException, |         APIException, | ||||||
|         # celery errors |  | ||||||
|         WorkerLostError, |  | ||||||
|         CeleryError, |  | ||||||
|         SoftTimeLimitExceeded, |  | ||||||
|         # custom baseclass |         # custom baseclass | ||||||
|         SentryIgnoredException, |         SentryIgnoredException, | ||||||
|         # ldap errors |         # ldap errors | ||||||
| @ -161,8 +153,6 @@ def before_send(event: dict, hint: dict) -> dict | None: | |||||||
|             "django_redis", |             "django_redis", | ||||||
|             "django.security.DisallowedHost", |             "django.security.DisallowedHost", | ||||||
|             "django_redis.cache", |             "django_redis.cache", | ||||||
|             "celery.backends.redis", |  | ||||||
|             "celery.worker", |  | ||||||
|             "paramiko.transport", |             "paramiko.transport", | ||||||
|         ]: |         ]: | ||||||
|             return None |             return None | ||||||
|  | |||||||
| @ -9,7 +9,6 @@ from rest_framework.response import Response | |||||||
|  |  | ||||||
| from authentik.core.api.utils import ModelSerializer, PassiveSerializer | from authentik.core.api.utils import ModelSerializer, PassiveSerializer | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.events.api.tasks import SystemTaskSerializer |  | ||||||
| from authentik.events.logs import LogEvent, LogEventSerializer | from authentik.events.logs import LogEvent, LogEventSerializer | ||||||
| from authentik.lib.sync.outgoing.models import OutgoingSyncProvider | from authentik.lib.sync.outgoing.models import OutgoingSyncProvider | ||||||
| from authentik.lib.utils.reflection import class_to_path | from authentik.lib.utils.reflection import class_to_path | ||||||
| @ -20,7 +19,6 @@ class SyncStatusSerializer(PassiveSerializer): | |||||||
|     """Provider sync status""" |     """Provider sync status""" | ||||||
|  |  | ||||||
|     is_running = BooleanField(read_only=True) |     is_running = BooleanField(read_only=True) | ||||||
|     tasks = SystemTaskSerializer(many=True, read_only=True) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SyncObjectSerializer(PassiveSerializer): | class SyncObjectSerializer(PassiveSerializer): | ||||||
| @ -48,12 +46,7 @@ class OutgoingSyncProviderStatusMixin: | |||||||
|     sync_task: type[Actor] = None |     sync_task: type[Actor] = None | ||||||
|     sync_objects_task: type[Actor] = None |     sync_objects_task: type[Actor] = None | ||||||
|  |  | ||||||
|     @extend_schema( |     @extend_schema(responses={200: SyncStatusSerializer()}) | ||||||
|         responses={ |  | ||||||
|             200: SyncStatusSerializer(), |  | ||||||
|             404: OpenApiResponse(description="Task not found"), |  | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     @action( |     @action( | ||||||
|         methods=["GET"], |         methods=["GET"], | ||||||
|         detail=True, |         detail=True, | ||||||
| @ -64,16 +57,8 @@ class OutgoingSyncProviderStatusMixin: | |||||||
|     def sync_status(self, request: Request, pk: int) -> Response: |     def sync_status(self, request: Request, pk: int) -> Response: | ||||||
|         """Get provider's sync status""" |         """Get provider's sync status""" | ||||||
|         provider: OutgoingSyncProvider = self.get_object() |         provider: OutgoingSyncProvider = self.get_object() | ||||||
|         # TODO: fixme |  | ||||||
|         tasks = list( |  | ||||||
|             get_objects_for_user(request.user, "authentik_events.view_systemtask").filter( |  | ||||||
|                 name=self.sync_task.__name__, |  | ||||||
|                 uid=slugify(provider.name), |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         with provider.sync_lock as lock_acquired: |         with provider.sync_lock as lock_acquired: | ||||||
|             status = { |             status = { | ||||||
|                 "tasks": tasks, |  | ||||||
|                 # If we could not acquire the lock, it means a task is using it, and thus is running |                 # If we could not acquire the lock, it means a task is using it, and thus is running | ||||||
|                 "is_running": not lock_acquired, |                 "is_running": not lock_acquired, | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -3,13 +3,11 @@ | |||||||
| from json import loads | from json import loads | ||||||
|  |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils.text import slugify |  | ||||||
| from jsonschema import validate | from jsonschema import validate | ||||||
| from requests_mock import Mocker | from requests_mock import Mocker | ||||||
|  |  | ||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Application, Group, User | from authentik.core.models import Application, Group, User | ||||||
| from authentik.events.models import SystemTask |  | ||||||
| from authentik.lib.generators import generate_id | from authentik.lib.generators import generate_id | ||||||
| from authentik.lib.sync.outgoing.base import SAFE_METHODS | from authentik.lib.sync.outgoing.base import SAFE_METHODS | ||||||
| from authentik.providers.scim.models import SCIMMapping, SCIMProvider | from authentik.providers.scim.models import SCIMMapping, SCIMProvider | ||||||
| @ -433,9 +431,10 @@ class SCIMUserTests(TestCase): | |||||||
|             self.assertEqual(mock.call_count, 3) |             self.assertEqual(mock.call_count, 3) | ||||||
|             for request in mock.request_history: |             for request in mock.request_history: | ||||||
|                 self.assertIn(request.method, SAFE_METHODS) |                 self.assertIn(request.method, SAFE_METHODS) | ||||||
|         task = SystemTask.objects.filter(uid=slugify(self.provider.name)).first() |         drop_msg = {} | ||||||
|         self.assertIsNotNone(task) |         # task = Task.objects.filter(uid=slugify(self.provider.name)).first() | ||||||
|         drop_msg = task.messages[3] |         # self.assertIsNotNone(task) | ||||||
|  |         # drop_msg = task.messages[3] | ||||||
|         self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run") |         self.assertEqual(drop_msg["event"], "Dropping mutating request due to dry run") | ||||||
|         self.assertIsNotNone(drop_msg["attributes"]["url"]) |         self.assertIsNotNone(drop_msg["attributes"]["url"]) | ||||||
|         self.assertIsNotNone(drop_msg["attributes"]["body"]) |         self.assertIsNotNone(drop_msg["attributes"]["body"]) | ||||||
|  | |||||||
| @ -343,25 +343,6 @@ USE_TZ = True | |||||||
|  |  | ||||||
| LOCALE_PATHS = ["./locale"] | LOCALE_PATHS = ["./locale"] | ||||||
|  |  | ||||||
| CELERY = { |  | ||||||
|     "task_soft_time_limit": 600, |  | ||||||
|     "worker_max_tasks_per_child": 50, |  | ||||||
|     "worker_concurrency": CONFIG.get_int("worker.concurrency"), |  | ||||||
|     "beat_schedule": {}, |  | ||||||
|     "beat_scheduler": "authentik.tenants.scheduler:TenantAwarePersistentScheduler", |  | ||||||
|     "task_create_missing_queues": True, |  | ||||||
|     "task_default_queue": "authentik", |  | ||||||
|     "broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")), |  | ||||||
|     "result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")), |  | ||||||
|     "broker_transport_options": CONFIG.get_dict_from_b64_json( |  | ||||||
|         "broker.transport_options", {"retry_policy": {"timeout": 5.0}} |  | ||||||
|     ), |  | ||||||
|     "result_backend_transport_options": CONFIG.get_dict_from_b64_json( |  | ||||||
|         "result_backend.transport_options", {"retry_policy": {"timeout": 5.0}} |  | ||||||
|     ), |  | ||||||
|     "redis_retry_on_timeout": True, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| # Sentry integration | # Sentry integration | ||||||
| env = get_env() | env = get_env() | ||||||
| _ERROR_REPORTING = CONFIG.get_bool("error_reporting.enabled", False) | _ERROR_REPORTING = CONFIG.get_bool("error_reporting.enabled", False) | ||||||
| @ -436,7 +417,6 @@ _DISALLOWED_ITEMS = [ | |||||||
|     "INSTALLED_APPS", |     "INSTALLED_APPS", | ||||||
|     "MIDDLEWARE", |     "MIDDLEWARE", | ||||||
|     "AUTHENTICATION_BACKENDS", |     "AUTHENTICATION_BACKENDS", | ||||||
|     "CELERY", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| SILENCED_SYSTEM_CHECKS = [ | SILENCED_SYSTEM_CHECKS = [ | ||||||
| @ -459,7 +439,6 @@ def _update_settings(app_path: str): | |||||||
|         TENANT_APPS.extend(getattr(settings_module, "TENANT_APPS", [])) |         TENANT_APPS.extend(getattr(settings_module, "TENANT_APPS", [])) | ||||||
|         MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", [])) |         MIDDLEWARE.extend(getattr(settings_module, "MIDDLEWARE", [])) | ||||||
|         AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", [])) |         AUTHENTICATION_BACKENDS.extend(getattr(settings_module, "AUTHENTICATION_BACKENDS", [])) | ||||||
|         CELERY["beat_schedule"].update(getattr(settings_module, "CELERY_BEAT_SCHEDULE", {})) |  | ||||||
|         for _attr in dir(settings_module): |         for _attr in dir(settings_module): | ||||||
|             if not _attr.startswith("__") and _attr not in _DISALLOWED_ITEMS: |             if not _attr.startswith("__") and _attr not in _DISALLOWED_ITEMS: | ||||||
|                 globals()[_attr] = getattr(settings_module, _attr) |                 globals()[_attr] = getattr(settings_module, _attr) | ||||||
| @ -468,7 +447,6 @@ def _update_settings(app_path: str): | |||||||
|  |  | ||||||
|  |  | ||||||
| if DEBUG: | if DEBUG: | ||||||
|     CELERY["task_always_eager"] = True |  | ||||||
|     REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append( |     REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append( | ||||||
|         "rest_framework.renderers.BrowsableAPIRenderer" |         "rest_framework.renderers.BrowsableAPIRenderer" | ||||||
|     ) |     ) | ||||||
|  | |||||||
| @ -36,7 +36,6 @@ class PytestTestRunner(DiscoverRunner):  # pragma: no cover | |||||||
|             self.args.append("--capture=no") |             self.args.append("--capture=no") | ||||||
|  |  | ||||||
|         settings.TEST = True |         settings.TEST = True | ||||||
|         settings.CELERY["task_always_eager"] = True |  | ||||||
|         CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb") |         CONFIG.set("events.context_processors.geoip", "tests/GeoLite2-City-Test.mmdb") | ||||||
|         CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb") |         CONFIG.set("events.context_processors.asn", "tests/GeoLite2-ASN-Test.mmdb") | ||||||
|         CONFIG.set("blueprints_dir", "./blueprints") |         CONFIG.set("blueprints_dir", "./blueprints") | ||||||
|  | |||||||
| @ -12,7 +12,6 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from authentik.core.api.sources import SourceSerializer | from authentik.core.api.sources import SourceSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.events.api.tasks import SystemTaskSerializer |  | ||||||
| from authentik.sources.kerberos.models import KerberosSource | from authentik.sources.kerberos.models import KerberosSource | ||||||
| from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS | from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS | ||||||
|  |  | ||||||
| @ -56,7 +55,6 @@ class KerberosSyncStatusSerializer(PassiveSerializer): | |||||||
|     """Kerberos Source sync status""" |     """Kerberos Source sync status""" | ||||||
|  |  | ||||||
|     is_running = BooleanField(read_only=True) |     is_running = BooleanField(read_only=True) | ||||||
|     tasks = SystemTaskSerializer(many=True, read_only=True) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class KerberosSourceViewSet(UsedByMixin, ModelViewSet): | class KerberosSourceViewSet(UsedByMixin, ModelViewSet): | ||||||
| @ -88,11 +86,7 @@ class KerberosSourceViewSet(UsedByMixin, ModelViewSet): | |||||||
|     ] |     ] | ||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|  |  | ||||||
|     @extend_schema( |     @extend_schema(responses={200: KerberosSyncStatusSerializer()}) | ||||||
|         responses={ |  | ||||||
|             200: KerberosSyncStatusSerializer(), |  | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     @action( |     @action( | ||||||
|         methods=["GET"], |         methods=["GET"], | ||||||
|         detail=True, |         detail=True, | ||||||
| @ -103,15 +97,8 @@ class KerberosSourceViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def sync_status(self, request: Request, slug: str) -> Response: |     def sync_status(self, request: Request, slug: str) -> Response: | ||||||
|         """Get source's sync status""" |         """Get source's sync status""" | ||||||
|         source: KerberosSource = self.get_object() |         source: KerberosSource = self.get_object() | ||||||
|         tasks = list( |  | ||||||
|             get_objects_for_user(request.user, "authentik_events.view_systemtask").filter( |  | ||||||
|                 name="kerberos_sync", |  | ||||||
|                 uid__startswith=source.slug, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         with source.sync_lock as lock_acquired: |         with source.sync_lock as lock_acquired: | ||||||
|             status = { |             status = { | ||||||
|                 "tasks": tasks, |  | ||||||
|                 "is_running": not lock_acquired, |                 "is_running": not lock_acquired, | ||||||
|             } |             } | ||||||
|         return Response(KerberosSyncStatusSerializer(status).data) |         return Response(KerberosSyncStatusSerializer(status).data) | ||||||
|  | |||||||
| @ -8,8 +8,7 @@ from django.test import TestCase | |||||||
| from authentik.blueprints.tests import apply_blueprint | from authentik.blueprints.tests import apply_blueprint | ||||||
| from authentik.core.models import Group, User | from authentik.core.models import Group, User | ||||||
| from authentik.core.tests.utils import create_test_admin_user | from authentik.core.tests.utils import create_test_admin_user | ||||||
| from authentik.events.models import Event, EventAction, SystemTask | from authentik.events.models import Event, EventAction | ||||||
| from authentik.events.system_tasks import TaskStatus |  | ||||||
| from authentik.lib.generators import generate_id, generate_key | from authentik.lib.generators import generate_id, generate_key | ||||||
| from authentik.lib.sync.outgoing.exceptions import StopSync | from authentik.lib.sync.outgoing.exceptions import StopSync | ||||||
| from authentik.lib.utils.reflection import class_to_path | from authentik.lib.utils.reflection import class_to_path | ||||||
| @ -56,8 +55,6 @@ class LDAPSyncTests(TestCase): | |||||||
|         connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) |         connection = MagicMock(return_value=mock_ad_connection(LDAP_PASSWORD)) | ||||||
|         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): |         with patch("authentik.sources.ldap.models.LDAPSource.connection", connection): | ||||||
|             ldap_sync_page.send(self.source.pk, class_to_path(UserLDAPSynchronizer), "foo") |             ldap_sync_page.send(self.source.pk, class_to_path(UserLDAPSynchronizer), "foo") | ||||||
|         task = SystemTask.objects.filter(name="ldap_sync", uid="ldap:users:foo").first() |  | ||||||
|         self.assertEqual(task.status, TaskStatus.ERROR) |  | ||||||
|  |  | ||||||
|     def test_sync_error(self): |     def test_sync_error(self): | ||||||
|         """Test user sync""" |         """Test user sync""" | ||||||
|  | |||||||
| @ -1,22 +0,0 @@ | |||||||
| """Tenant-aware Celery beat scheduler""" |  | ||||||
|  |  | ||||||
| from tenant_schemas_celery.scheduler import ( |  | ||||||
|     TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler, |  | ||||||
| ) |  | ||||||
| from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler): |  | ||||||
|     """Tenant-aware Celery beat scheduler""" |  | ||||||
|  |  | ||||||
|     @classmethod |  | ||||||
|     def get_queryset(cls): |  | ||||||
|         return super().get_queryset().filter(ready=True) |  | ||||||
|  |  | ||||||
|     def apply_entry(self, entry: TenantAwareScheduleEntry, producer=None): |  | ||||||
|         # https://github.com/maciej-gol/tenant-schemas-celery/blob/master/tenant_schemas_celery/scheduler.py#L85 |  | ||||||
|         # When (as by default) no tenant schemas are set, the public schema is excluded |  | ||||||
|         # so we need to explicitly include it here, otherwise the task is not executed |  | ||||||
|         if entry.tenant_schemas is None: |  | ||||||
|             entry.tenant_schemas = self.get_queryset().values_list("schema_name", flat=True) |  | ||||||
|         return super().apply_entry(entry, producer) |  | ||||||
| @ -79,10 +79,6 @@ elif [[ "$1" == "worker" ]]; then | |||||||
|     set_mode "worker" |     set_mode "worker" | ||||||
|     shift |     shift | ||||||
|     check_if_root "python -m manage worker $@" |     check_if_root "python -m manage worker $@" | ||||||
| elif [[ "$1" == "worker-status" ]]; then |  | ||||||
|     wait_for_db |  | ||||||
|     celery -A authentik.root.celery flower \ |  | ||||||
|         --port=9000 |  | ||||||
| elif [[ "$1" == "bash" ]]; then | elif [[ "$1" == "bash" ]]; then | ||||||
|     /bin/bash |     /bin/bash | ||||||
| elif [[ "$1" == "test-all" ]]; then | elif [[ "$1" == "test-all" ]]; then | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] | |||||||
| requires-python = "==3.13.*" | requires-python = "==3.13.*" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     "argon2-cffi==25.1.0", |     "argon2-cffi==25.1.0", | ||||||
|     "celery==5.5.3", |  | ||||||
|     "channels==4.2.2", |     "channels==4.2.2", | ||||||
|     "channels-redis==4.2.1", |     "channels-redis==4.2.1", | ||||||
|     "cron-converter==1.2.1", |     "cron-converter==1.2.1", | ||||||
| @ -35,7 +34,6 @@ dependencies = [ | |||||||
|     "dumb-init==1.2.5.post1", |     "dumb-init==1.2.5.post1", | ||||||
|     "duo-client==5.5.0", |     "duo-client==5.5.0", | ||||||
|     "fido2==2.0.0", |     "fido2==2.0.0", | ||||||
|     "flower==2.0.1", |  | ||||||
|     "geoip2==5.1.0", |     "geoip2==5.1.0", | ||||||
|     "geopy==2.4.1", |     "geopy==2.4.1", | ||||||
|     "google-api-python-client==2.171.0", |     "google-api-python-client==2.171.0", | ||||||
| @ -65,7 +63,6 @@ dependencies = [ | |||||||
|     "structlog==25.4.0", |     "structlog==25.4.0", | ||||||
|     "swagger-spec-validator==3.0.4", |     "swagger-spec-validator==3.0.4", | ||||||
|     "tenacity==9.1.2", |     "tenacity==9.1.2", | ||||||
|     "tenant-schemas-celery==3.0.0", |  | ||||||
|     "twilio==9.6.2", |     "twilio==9.6.2", | ||||||
|     "ua-parser==1.0.1", |     "ua-parser==1.0.1", | ||||||
|     "unidecode==1.4.0", |     "unidecode==1.4.0", | ||||||
| @ -230,7 +227,7 @@ show_missing = true | |||||||
| DJANGO_SETTINGS_MODULE = "authentik.root.settings" | DJANGO_SETTINGS_MODULE = "authentik.root.settings" | ||||||
| python_files = ["tests.py", "test_*.py", "*_tests.py"] | python_files = ["tests.py", "test_*.py", "*_tests.py"] | ||||||
| junit_family = "xunit2" | junit_family = "xunit2" | ||||||
| addopts = "-p no:celery -p authentik.root.test_plugin --junitxml=unittest.xml -vv --full-trace --doctest-modules --import-mode=importlib" | addopts = "-p authentik.root.test_plugin --junitxml=unittest.xml -vv --full-trace --doctest-modules --import-mode=importlib" | ||||||
| filterwarnings = [ | filterwarnings = [ | ||||||
|     "ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning", |     "ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning", | ||||||
|     "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning", |     "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning", | ||||||
|  | |||||||
							
								
								
									
										191
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										191
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @ -69,18 +69,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, |     { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "amqp" |  | ||||||
| version = "5.3.1" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "vine" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "annotated-types" | name = "annotated-types" | ||||||
| version = "0.7.0" | version = "0.7.0" | ||||||
| @ -169,7 +157,6 @@ version = "2025.6.0" | |||||||
| source = { editable = "." } | source = { editable = "." } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "argon2-cffi" }, |     { name = "argon2-cffi" }, | ||||||
|     { name = "celery" }, |  | ||||||
|     { name = "channels" }, |     { name = "channels" }, | ||||||
|     { name = "channels-redis" }, |     { name = "channels-redis" }, | ||||||
|     { name = "cron-converter" }, |     { name = "cron-converter" }, | ||||||
| @ -198,7 +185,6 @@ dependencies = [ | |||||||
|     { name = "dumb-init" }, |     { name = "dumb-init" }, | ||||||
|     { name = "duo-client" }, |     { name = "duo-client" }, | ||||||
|     { name = "fido2" }, |     { name = "fido2" }, | ||||||
|     { name = "flower" }, |  | ||||||
|     { name = "geoip2" }, |     { name = "geoip2" }, | ||||||
|     { name = "geopy" }, |     { name = "geopy" }, | ||||||
|     { name = "google-api-python-client" }, |     { name = "google-api-python-client" }, | ||||||
| @ -228,7 +214,6 @@ dependencies = [ | |||||||
|     { name = "structlog" }, |     { name = "structlog" }, | ||||||
|     { name = "swagger-spec-validator" }, |     { name = "swagger-spec-validator" }, | ||||||
|     { name = "tenacity" }, |     { name = "tenacity" }, | ||||||
|     { name = "tenant-schemas-celery" }, |  | ||||||
|     { name = "twilio" }, |     { name = "twilio" }, | ||||||
|     { name = "ua-parser" }, |     { name = "ua-parser" }, | ||||||
|     { name = "unidecode" }, |     { name = "unidecode" }, | ||||||
| @ -271,7 +256,6 @@ dev = [ | |||||||
| [package.metadata] | [package.metadata] | ||||||
| requires-dist = [ | requires-dist = [ | ||||||
|     { name = "argon2-cffi", specifier = "==25.1.0" }, |     { name = "argon2-cffi", specifier = "==25.1.0" }, | ||||||
|     { name = "celery", specifier = "==5.5.3" }, |  | ||||||
|     { name = "channels", specifier = "==4.2.2" }, |     { name = "channels", specifier = "==4.2.2" }, | ||||||
|     { name = "channels-redis", specifier = "==4.2.1" }, |     { name = "channels-redis", specifier = "==4.2.1" }, | ||||||
|     { name = "cron-converter", specifier = "==1.2.1" }, |     { name = "cron-converter", specifier = "==1.2.1" }, | ||||||
| @ -300,7 +284,6 @@ requires-dist = [ | |||||||
|     { name = "dumb-init", specifier = "==1.2.5.post1" }, |     { name = "dumb-init", specifier = "==1.2.5.post1" }, | ||||||
|     { name = "duo-client", specifier = "==5.5.0" }, |     { name = "duo-client", specifier = "==5.5.0" }, | ||||||
|     { name = "fido2", specifier = "==2.0.0" }, |     { name = "fido2", specifier = "==2.0.0" }, | ||||||
|     { name = "flower", specifier = "==2.0.1" }, |  | ||||||
|     { name = "geoip2", specifier = "==5.1.0" }, |     { name = "geoip2", specifier = "==5.1.0" }, | ||||||
|     { name = "geopy", specifier = "==2.4.1" }, |     { name = "geopy", specifier = "==2.4.1" }, | ||||||
|     { name = "google-api-python-client", specifier = "==2.171.0" }, |     { name = "google-api-python-client", specifier = "==2.171.0" }, | ||||||
| @ -330,7 +313,6 @@ requires-dist = [ | |||||||
|     { name = "structlog", specifier = "==25.4.0" }, |     { name = "structlog", specifier = "==25.4.0" }, | ||||||
|     { name = "swagger-spec-validator", specifier = "==3.0.4" }, |     { name = "swagger-spec-validator", specifier = "==3.0.4" }, | ||||||
|     { name = "tenacity", specifier = "==9.1.2" }, |     { name = "tenacity", specifier = "==9.1.2" }, | ||||||
|     { name = "tenant-schemas-celery", specifier = "==3.0.0" }, |  | ||||||
|     { name = "twilio", specifier = "==9.6.2" }, |     { name = "twilio", specifier = "==9.6.2" }, | ||||||
|     { name = "ua-parser", specifier = "==1.0.1" }, |     { name = "ua-parser", specifier = "==1.0.1" }, | ||||||
|     { name = "unidecode", specifier = "==1.4.0" }, |     { name = "unidecode", specifier = "==1.4.0" }, | ||||||
| @ -549,15 +531,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, |     { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "billiard" |  | ||||||
| version = "4.2.1" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/7c/58/1546c970afcd2a2428b1bfafecf2371d8951cc34b46701bea73f4280989e/billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", size = 155031, upload-time = "2024-09-21T13:40:22.491Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/30/da/43b15f28fe5f9e027b41c539abc5469052e9d48fd75f8ff094ba2a0ae767/billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb", size = 86766, upload-time = "2024-09-21T13:40:20.188Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "black" | name = "black" | ||||||
| version = "25.1.0" | version = "25.1.0" | ||||||
| @ -652,25 +625,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/9b/ef/1c4698cac96d792005ef0611832f38eaee477c275ab4b02cbfc4daba7ad3/cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468", size = 23752, upload-time = "2024-10-09T12:26:23.167Z" }, |     { url = "https://files.pythonhosted.org/packages/9b/ef/1c4698cac96d792005ef0611832f38eaee477c275ab4b02cbfc4daba7ad3/cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468", size = 23752, upload-time = "2024-10-09T12:26:23.167Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "celery" |  | ||||||
| version = "5.5.3" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "billiard" }, |  | ||||||
|     { name = "click" }, |  | ||||||
|     { name = "click-didyoumean" }, |  | ||||||
|     { name = "click-plugins" }, |  | ||||||
|     { name = "click-repl" }, |  | ||||||
|     { name = "kombu" }, |  | ||||||
|     { name = "python-dateutil" }, |  | ||||||
|     { name = "vine" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "certifi" | name = "certifi" | ||||||
| version = "2025.4.26" | version = "2025.4.26" | ||||||
| @ -769,43 +723,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, |     { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "click-didyoumean" |  | ||||||
| version = "0.3.1" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "click" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "click-plugins" |  | ||||||
| version = "1.1.1" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "click" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/45434f64ed749540af821fd7e42b8e4d23ac04b1eda7c26613288d6cd8a8/click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b", size = 8164, upload-time = "2019-04-04T04:27:04.82Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/e9/da/824b92d9942f4e472702488857914bdd50f73021efea15b4cad9aca8ecef/click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8", size = 7497, upload-time = "2019-04-04T04:27:03.36Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "click-repl" |  | ||||||
| version = "0.3.0" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "click" }, |  | ||||||
|     { name = "prompt-toolkit" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "codespell" | name = "codespell" | ||||||
| version = "2.4.1" | version = "2.4.1" | ||||||
| @ -1312,22 +1229,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/4c/7d/a1dba174d7ec4b6b8d6360eed0ac3a4a4e2aa45f234e903592d3184c6c3f/fido2-2.0.0-py3-none-any.whl", hash = "sha256:685f54a50a57e019c6156e2dd699802a603e3abf70bab334f26affdd4fb8d4f7", size = 224761, upload-time = "2025-05-20T09:44:59.029Z" }, |     { url = "https://files.pythonhosted.org/packages/4c/7d/a1dba174d7ec4b6b8d6360eed0ac3a4a4e2aa45f234e903592d3184c6c3f/fido2-2.0.0-py3-none-any.whl", hash = "sha256:685f54a50a57e019c6156e2dd699802a603e3abf70bab334f26affdd4fb8d4f7", size = 224761, upload-time = "2025-05-20T09:44:59.029Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "flower" |  | ||||||
| version = "2.0.1" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "celery" }, |  | ||||||
|     { name = "humanize" }, |  | ||||||
|     { name = "prometheus-client" }, |  | ||||||
|     { name = "pytz" }, |  | ||||||
|     { name = "tornado" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/09/a1/357f1b5d8946deafdcfdd604f51baae9de10aafa2908d0b7322597155f92/flower-2.0.1.tar.gz", hash = "sha256:5ab717b979530770c16afb48b50d2a98d23c3e9fe39851dcf6bc4d01845a02a0", size = 3220408, upload-time = "2023-08-13T14:37:46.073Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/a6/ff/ee2f67c0ff146ec98b5df1df637b2bc2d17beeb05df9f427a67bd7a7d79c/flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2", size = 383553, upload-time = "2023-08-13T14:37:41.552Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "freezegun" | name = "freezegun" | ||||||
| version = "1.5.1" | version = "1.5.1" | ||||||
| @ -1650,15 +1551,6 @@ http2 = [ | |||||||
|     { name = "h2" }, |     { name = "h2" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "humanize" |  | ||||||
| version = "4.12.3" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload-time = "2025-04-30T11:51:07.98Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload-time = "2025-04-30T11:51:06.468Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "hyperframe" | name = "hyperframe" | ||||||
| version = "6.1.0" | version = "6.1.0" | ||||||
| @ -1849,20 +1741,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/b5/ba/d86d465c589cbed8f872e187e90abbac73eb1453483477771e87e7ee8376/k5test-0.10.4-py2.py3-none-any.whl", hash = "sha256:33de7ff10bf99155fe8ee5d5976798ad1db6237214306dadf5a0ae9d6bb0ad03", size = 11954, upload-time = "2024-03-20T02:48:24.502Z" }, |     { url = "https://files.pythonhosted.org/packages/b5/ba/d86d465c589cbed8f872e187e90abbac73eb1453483477771e87e7ee8376/k5test-0.10.4-py2.py3-none-any.whl", hash = "sha256:33de7ff10bf99155fe8ee5d5976798ad1db6237214306dadf5a0ae9d6bb0ad03", size = 11954, upload-time = "2024-03-20T02:48:24.502Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "kombu" |  | ||||||
| version = "5.5.3" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "amqp" }, |  | ||||||
|     { name = "tzdata" }, |  | ||||||
|     { name = "vine" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/60/0a/128b65651ed8120460fc5af754241ad595eac74993115ec0de4f2d7bc459/kombu-5.5.3.tar.gz", hash = "sha256:021a0e11fcfcd9b0260ef1fb64088c0e92beb976eb59c1dfca7ddd4ad4562ea2", size = 461784, upload-time = "2025-04-16T12:46:17.014Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/5d/35/1407fb0b2f5b07b50cbaf97fce09ad87d3bfefbf64f7171a8651cd8d2f68/kombu-5.5.3-py3-none-any.whl", hash = "sha256:5b0dbceb4edee50aa464f59469d34b97864be09111338cfb224a10b6a163909b", size = 209921, upload-time = "2025-04-16T12:46:15.139Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "kubernetes" | name = "kubernetes" | ||||||
| version = "32.0.1" | version = "32.0.1" | ||||||
| @ -2398,18 +2276,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, |     { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "prompt-toolkit" |  | ||||||
| version = "3.0.51" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "wcwidth" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "propcache" | name = "propcache" | ||||||
| version = "0.3.1" | version = "0.3.1" | ||||||
| @ -2798,15 +2664,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/20/44/70f24d3cac3f5d13b2fbe24542a732e3f529e564615bb3656665eb7b78e4/python_kadmin_rs-0.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:80e3e0093dffe98f759b74ef25f6cdd8ab7b109f2af0f158572705ad31bb8699", size = 1617374, upload-time = "2025-04-02T11:34:07.595Z" }, |     { url = "https://files.pythonhosted.org/packages/20/44/70f24d3cac3f5d13b2fbe24542a732e3f529e564615bb3656665eb7b78e4/python_kadmin_rs-0.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:80e3e0093dffe98f759b74ef25f6cdd8ab7b109f2af0f158572705ad31bb8699", size = 1617374, upload-time = "2025-04-02T11:34:07.595Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "pytz" |  | ||||||
| version = "2025.2" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "pywin32" | name = "pywin32" | ||||||
| version = "310" | version = "310" | ||||||
| @ -3190,36 +3047,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, |     { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "tenant-schemas-celery" |  | ||||||
| version = "3.0.0" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| dependencies = [ |  | ||||||
|     { name = "celery" }, |  | ||||||
| ] |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/d0/fe/cfe19eb7cc3ad8e39d7df7b7c44414bf665b6ac6660c998eb498f89d16c6/tenant_schemas_celery-3.0.0.tar.gz", hash = "sha256:6be3ae1a5826f262f0f3dd343c6a85a34a1c59b89e04ae37de018f36562fed55", size = 15954, upload-time = "2024-05-19T11:16:41.837Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/db/2c/376e1e641ad08b374c75d896468a7be2e6906ce3621fd0c9f9dc09ff1963/tenant_schemas_celery-3.0.0-py3-none-any.whl", hash = "sha256:ca0f69e78ef698eb4813468231df5a0ab6a660c08e657b65f5ac92e16887eec8", size = 18108, upload-time = "2024-05-19T11:16:39.92Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "tornado" |  | ||||||
| version = "6.4.2" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/59/45/a0daf161f7d6f36c3ea5fc0c2de619746cc3dd4c76402e9db545bd920f63/tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b", size = 501135, upload-time = "2024-11-22T03:06:38.036Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/26/7e/71f604d8cea1b58f82ba3590290b66da1e72d840aeb37e0d5f7291bd30db/tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1", size = 436299, upload-time = "2024-11-22T03:06:20.162Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/96/44/87543a3b99016d0bf54fdaab30d24bf0af2e848f1d13d34a3a5380aabe16/tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803", size = 434253, upload-time = "2024-11-22T03:06:22.39Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/cb/fb/fdf679b4ce51bcb7210801ef4f11fdac96e9885daa402861751353beea6e/tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec", size = 437602, upload-time = "2024-11-22T03:06:24.214Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/4f/3b/e31aeffffc22b475a64dbeb273026a21b5b566f74dee48742817626c47dc/tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946", size = 436972, upload-time = "2024-11-22T03:06:25.559Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/22/55/b78a464de78051a30599ceb6983b01d8f732e6f69bf37b4ed07f642ac0fc/tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf", size = 437173, upload-time = "2024-11-22T03:06:27.584Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/79/5e/be4fb0d1684eb822c9a62fb18a3e44a06188f78aa466b2ad991d2ee31104/tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634", size = 437892, upload-time = "2024-11-22T03:06:28.933Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/f5/33/4f91fdd94ea36e1d796147003b490fe60a0215ac5737b6f9c65e160d4fe0/tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73", size = 437334, upload-time = "2024-11-22T03:06:30.428Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/2b/ae/c1b22d4524b0e10da2f29a176fb2890386f7bd1f63aacf186444873a88a0/tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c", size = 437261, upload-time = "2024-11-22T03:06:32.458Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/b5/25/36dbd49ab6d179bcfc4c6c093a51795a4f3bed380543a8242ac3517a1751/tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482", size = 438463, upload-time = "2024-11-22T03:06:34.71Z" }, |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907, upload-time = "2024-11-22T03:06:36.71Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "trio" | name = "trio" | ||||||
| version = "0.30.0" | version = "0.30.0" | ||||||
| @ -3429,15 +3256,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, |     { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "vine" |  | ||||||
| version = "5.1.0" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "watchdog" | name = "watchdog" | ||||||
| version = "6.0.0" | version = "6.0.0" | ||||||
| @ -3495,15 +3313,6 @@ wheels = [ | |||||||
|     { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload-time = "2025-04-08T10:35:52.458Z" }, |     { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload-time = "2025-04-08T10:35:52.458Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "wcwidth" |  | ||||||
| version = "0.2.13" |  | ||||||
| source = { registry = "https://pypi.org/simple" } |  | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } |  | ||||||
| wheels = [ |  | ||||||
|     { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "webauthn" | name = "webauthn" | ||||||
| version = "2.5.2" | version = "2.5.2" | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Marc 'risson' Schmitt
					Marc 'risson' Schmitt