diff --git a/.github/actions/docker-push-variables/push_vars.py b/.github/actions/docker-push-variables/push_vars.py index d20109e497..c3f91f9fd8 100644 --- a/.github/actions/docker-push-variables/push_vars.py +++ b/.github/actions/docker-push-variables/push_vars.py @@ -2,16 +2,28 @@ import os from json import dumps +from sys import exit as sysexit from time import time from authentik import authentik_version + +def must_or_fail(input: str | None, error: str) -> str: + if not input: + print(f"::error::{error}") + sysexit(1) + return input + + # Decide if we should push the image or not should_push = True if len(os.environ.get("DOCKER_USERNAME", "")) < 1: # Don't push if we don't have DOCKER_USERNAME, i.e. no secrets are available should_push = False -if os.environ.get("GITHUB_REPOSITORY").lower() == "goauthentik/authentik-internal": +if ( + must_or_fail(os.environ.get("GITHUB_REPOSITORY"), "Repo required").lower() + == "goauthentik/authentik-internal" +): # Don't push on the internal repo should_push = False @@ -20,13 +32,16 @@ if os.environ.get("GITHUB_HEAD_REF", "") != "": branch_name = os.environ["GITHUB_HEAD_REF"] safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-") -image_names = os.getenv("IMAGE_NAME").split(",") +image_names = must_or_fail(os.getenv("IMAGE_NAME"), "Image name required").split(",") image_arch = os.getenv("IMAGE_ARCH") or None is_pull_request = bool(os.getenv("PR_HEAD_SHA")) is_release = "dev" not in image_names[0] -sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA") +sha = must_or_fail( + os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA"), + "could not determine SHA", +) # 2042.1.0 or 2042.1.0-rc1 version = authentik_version() @@ -58,7 +73,7 @@ else: image_main_tag = image_tags[0].split(":")[-1] -def get_attest_image_names(image_with_tags: list[str]): +def get_attest_image_names(image_with_tags: list[str]) -> str: """Attestation only for GHCR""" image_tags = [] for image_name in set(name.split(":")[0] for name in image_with_tags): @@ -82,7 +97,6 @@ if os.getenv("RELEASE", "false").lower() == "true": image_build_args = [f"VERSION={os.getenv('REF')}"] else: image_build_args = [f"GIT_BUILD_HASH={sha}"] -image_build_args = "\n".join(image_build_args) with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: print(f"shouldPush={str(should_push).lower()}", file=_output) @@ -95,4 +109,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: print(f"imageMainTag={image_main_tag}", file=_output) print(f"imageMainName={image_tags[0]}", file=_output) print(f"cacheTo={cache_to}", file=_output) - print(f"imageBuildArgs={image_build_args}", file=_output) + print(f"imageBuildArgs={"\n".join(image_build_args)}", file=_output) diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index b6f37ebbfe..ebf4aa66f0 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -34,6 +34,7 @@ jobs: - codespell - pending-migrations - ruff + - mypy runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/Makefile b/Makefile index 5e1230ef99..94e90dc8b3 100644 --- a/Makefile +++ b/Makefile @@ -320,6 +320,9 @@ ci--meta-debug: python -V node --version +ci-mypy: ci--meta-debug + uv run mypy --strict $(PY_SOURCES) + ci-black: ci--meta-debug uv run black --check $(PY_SOURCES) diff --git a/authentik/core/management/commands/dev_server.py b/authentik/core/management/commands/dev_server.py index 417f2f2cf5..ee531c372a 100644 --- a/authentik/core/management/commands/dev_server.py +++ b/authentik/core/management/commands/dev_server.py @@ -1,6 +1,6 @@ """custom runserver command""" -from typing import TextIO +from io import StringIO from daphne.management.commands.runserver import Command as RunServer from daphne.server import Server @@ -33,4 +33,4 @@ class Command(RunServer): super().__init__(*args, **kwargs) # Redirect standard stdout banner from Daphne into the void # as there are a couple more steps that happen before startup is fully done - self.stdout = TextIO() + self.stdout = StringIO() diff --git a/authentik/core/signals.py b/authentik/core/signals.py index c4c639c9bc..cd76e07b51 100644 --- a/authentik/core/signals.py +++ b/authentik/core/signals.py @@ -2,10 +2,9 @@ from django.contrib.auth.signals import user_logged_in from django.core.cache import cache -from django.core.signals import Signal from django.db.models import Model from django.db.models.signals import post_delete, post_save, pre_save -from django.dispatch import receiver +from django.dispatch import Signal, receiver from django.http.request import HttpRequest from structlog.stdlib import get_logger diff --git a/authentik/events/context_processors/asn.py b/authentik/events/context_processors/asn.py index 6de0b9cbf6..e04748ef32 100644 --- a/authentik/events/context_processors/asn.py +++ b/authentik/events/context_processors/asn.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: class ASNDict(TypedDict): """ASN Details""" - asn: int + asn: int | None as_org: str | None network: str | None @@ -60,7 +60,7 @@ class ASNContextProcessor(MMDBContextProcessor): except (GeoIP2Error, ValueError): return None - def asn_to_dict(self, asn: ASN | None) -> ASNDict: + def asn_to_dict(self, asn: ASN | None) -> ASNDict | dict: """Convert ASN to dict""" if not asn: return {} diff --git a/authentik/events/context_processors/geoip.py b/authentik/events/context_processors/geoip.py index cfa8cb400d..d176bba9fe 100644 --- a/authentik/events/context_processors/geoip.py +++ b/authentik/events/context_processors/geoip.py @@ -19,10 +19,10 @@ if TYPE_CHECKING: class GeoIPDict(TypedDict): """GeoIP Details""" - continent: str - country: str - lat: float - long: float + continent: str | None + country: str | None + lat: float | None + long: float | None city: str @@ -61,7 +61,7 @@ class GeoIPContextProcessor(MMDBContextProcessor): except (GeoIP2Error, ValueError): return None - def city_to_dict(self, city: City | None) -> GeoIPDict: + def city_to_dict(self, city: City | None) -> GeoIPDict | dict: """Convert City to dict""" if not city: return {} diff --git a/authentik/providers/oauth2/views/userinfo.py b/authentik/providers/oauth2/views/userinfo.py index cf151cf6d6..53b66580fa 100644 --- a/authentik/providers/oauth2/views/userinfo.py +++ b/authentik/providers/oauth2/views/userinfo.py @@ -60,7 +60,7 @@ class UserInfoView(View): for scope in scopes: if scope in special_scope_map: scope_descriptions.append( - PermissionDict(id=scope, name=str(special_scope_map[scope])) + PermissionDict(id=str(scope), name=str(special_scope_map[scope])) ) return scope_descriptions diff --git a/authentik/providers/saml/processors/assertion.py b/authentik/providers/saml/processors/assertion.py index a3d45f2948..a5398a378d 100644 --- a/authentik/providers/saml/processors/assertion.py +++ b/authentik/providers/saml/processors/assertion.py @@ -239,32 +239,33 @@ class AssertionProcessor: ).from_http(self.http_request) LOGGER.warning("Failed to evaluate property mapping", exc=exc) return name_id - if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL: + if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_EMAIL: name_id.text = self.http_request.user.email return name_id - if name_id.attrib["Format"] in [ + if self.auth_n_request.name_id_policy in [ SAML_NAME_ID_FORMAT_PERSISTENT, SAML_NAME_ID_FORMAT_UNSPECIFIED, ]: name_id.text = persistent return name_id - if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509: + if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_X509: # This attribute is statically set by the LDAP source name_id.text = self.http_request.user.attributes.get( LDAP_DISTINGUISHED_NAME, persistent ) return name_id - if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_WINDOWS: + if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_WINDOWS: # This attribute is statically set by the LDAP source name_id.text = self.http_request.user.attributes.get("upn", persistent) return name_id - if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT: + if self.auth_n_request.name_id_policy == SAML_NAME_ID_FORMAT_TRANSIENT: # Use the hash of the user's session, which changes every session session_key: str = self.http_request.session.session_key name_id.text = sha256(session_key.encode()).hexdigest() return name_id raise UnsupportedNameIDFormat( - f"Assertion contains NameID with unsupported format {name_id.attrib['Format']}." + "Assertion contains NameID with unsupported " + f"format {self.auth_n_request.name_id_policy}." ) def get_assertion_subject(self) -> Element: diff --git a/authentik/root/signals.py b/authentik/root/signals.py index 51543dbd03..a178d5dee3 100644 --- a/authentik/root/signals.py +++ b/authentik/root/signals.py @@ -1,7 +1,6 @@ from datetime import timedelta -from django.core.signals import Signal -from django.dispatch import receiver +from django.dispatch import Signal, receiver from django.utils.timezone import now from structlog.stdlib import get_logger diff --git a/authentik/stages/identification/signals.py b/authentik/stages/identification/signals.py index 7eb44435f3..0ed403ecf6 100644 --- a/authentik/stages/identification/signals.py +++ b/authentik/stages/identification/signals.py @@ -1,6 +1,6 @@ """authentik identification signals""" -from django.core.signals import Signal +from django.dispatch import Signal # Arguments: request: HttpRequest, uid_field: Value entered by user identification_failed = Signal() diff --git a/authentik/stages/invitation/signals.py b/authentik/stages/invitation/signals.py index 34e91b4986..98345a7f09 100644 --- a/authentik/stages/invitation/signals.py +++ b/authentik/stages/invitation/signals.py @@ -1,6 +1,6 @@ """authentik invitation signals""" -from django.core.signals import Signal +from django.dispatch import Signal # Arguments: request: HttpRequest, invitation: Invitation invitation_used = Signal() diff --git a/authentik/stages/prompt/signals.py b/authentik/stages/prompt/signals.py index b2a05aaa54..d16c6a4b48 100644 --- a/authentik/stages/prompt/signals.py +++ b/authentik/stages/prompt/signals.py @@ -1,6 +1,6 @@ """authentik prompt stage signals""" -from django.core.signals import Signal +from django.dispatch import Signal # Arguments: password: str, plan_context: dict[str, Any] password_validate = Signal() diff --git a/authentik/stages/user_write/signals.py b/authentik/stages/user_write/signals.py index 988931006b..a3391d2c2d 100644 --- a/authentik/stages/user_write/signals.py +++ b/authentik/stages/user_write/signals.py @@ -1,6 +1,6 @@ """authentik user_write signals""" -from django.core.signals import Signal +from django.dispatch import Signal # Arguments: request: HttpRequest, user: User, data: dict[str, Any], created: bool user_write = Signal() diff --git a/pyproject.toml b/pyproject.toml index 1391c1ada4..f1f0d26338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,19 +85,24 @@ dev = [ "constructs==10.4.2", "coverage[toml]==7.8.0", "debugpy==1.8.14", + "django-stubs[compatible-mypy]==5.2.5", + "djangorestframework-stubs[compatible-mypy]==3.16.3", "drf-jsonschema-serializer==3.0.0", "freezegun==1.5.1", "importlib-metadata==8.6.1", "k5test==0.10.4", + "lxml-stubs==0.5.1", + "mypy==1.18.2", "pdoc==15.0.3", - "pytest==8.3.5", "pytest-django==4.11.1", "pytest-github-actions-annotate-failures==0.3.0", "pytest-randomly==3.16.0", "pytest-timeout==2.4.0", + "pytest==8.3.5", "requests-mock==1.12.1", "ruff==0.11.9", "selenium==4.32.0", + "types-ldap3==2.9.13.20250622", ] [tool.uv] @@ -172,20 +177,13 @@ exclude = ["**/migrations/**", "**/node_modules/**"] [tool.ruff.lint] select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # isort - "I", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # django - "DJ", - # pylint - "PL", + "E", # pycodestyle + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "DJ", # django + "PL", # pylint ] ignore = [ "DJ001", # Avoid using `null=True` on string-based fields, @@ -196,6 +194,82 @@ max-args = 7 max-branches = 18 max-returns = 10 +[tool.mypy] +plugins = ["mypy_django_plugin.main", "mypy_drf_plugin.main", "pydantic.mypy"] +exclude = ['^gen-py-api/'] + +[[tool.mypy.overrides]] +module = [ + "authentik.admin.*", + "authentik.api.*", + "authentik.blueprints.*", + "authentik.brands.*", + "authentik.core.*", + "authentik.crypto.*", + "authentik.enterprise.*", + "authentik.events.*", + "authentik.flows.*", + "authentik.lib.*", + "authentik.outposts.*", + "authentik.policies.*", + "authentik.policies.dummy.*", + "authentik.policies.event_matcher.*", + "authentik.policies.expiry.*", + "authentik.policies.expression.*", + "authentik.policies.geoip.*", + "authentik.policies.password.*", + "authentik.policies.reputation.*", + "authentik.providers.ldap.*", + "authentik.providers.oauth2.*", + "authentik.providers.proxy.*", + "authentik.providers.rac.*", + "authentik.providers.radius.*", + "authentik.providers.saml.*", + "authentik.providers.scim.*", + "authentik.rbac.*", + "authentik.recovery.*", + "authentik.root.*", + "authentik.sources.kerberos.*", + "authentik.sources.ldap.*", + "authentik.sources.oauth.*", + "authentik.sources.plex.*", + "authentik.sources.saml.*", + "authentik.sources.scim.*", + "authentik.stages.authenticator_duo.*", + "authentik.stages.authenticator_email.*", + "authentik.stages.authenticator_sms.*", + "authentik.stages.authenticator_static.*", + "authentik.stages.authenticator_totp.*", + "authentik.stages.authenticator_validate.*", + "authentik.stages.authenticator_webauthn.*", + "authentik.stages.authenticator.*", + "authentik.stages.captcha.*", + "authentik.stages.consent.*", + "authentik.stages.deny.*", + "authentik.stages.dummy.*", + "authentik.stages.email.*", + "authentik.stages.identification.*", + "authentik.stages.invitation.*", + "authentik.stages.password.*", + "authentik.stages.prompt.*", + "authentik.stages.redirect.*", + "authentik.stages.user_delete.*", + "authentik.stages.user_login.*", + "authentik.stages.user_logout.*", + "authentik.stages.user_write.*", + "authentik.tasks.*", + "authentik.tasks.schedules.*", + "authentik.tenants.*", + "django_dramatiq_postgres.*", + "lifecycle.*", + "tests.e2e.*", + "tests.integration.*", +] +ignore_errors = true + +[tool.django-stubs] +django_settings_module = "authentik.root.settings" + [tool.coverage.run] source = ["authentik"] relative_files = true diff --git a/scripts/generate_config.py b/scripts/generate_config.py index 6172dec9c6..37e265260c 100755 --- a/scripts/generate_config.py +++ b/scripts/generate_config.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 """Generate config for development""" +from typing import Any + from yaml import safe_dump from authentik.lib.generators import generate_id -def generate_local_config(): +def generate_local_config() -> dict[str, Any]: """Generate a local development configuration""" # TODO: This should be generated and validated against a schema, such as Pydantic. diff --git a/uv.lock b/uv.lock index ecc78bb010..6d9ca5112d 100644 --- a/uv.lock +++ b/uv.lock @@ -242,10 +242,14 @@ dev = [ { name = "constructs" }, { name = "coverage" }, { name = "debugpy" }, + { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "djangorestframework-stubs", extra = ["compatible-mypy"] }, { name = "drf-jsonschema-serializer" }, { name = "freezegun" }, { name = "importlib-metadata" }, { name = "k5test" }, + { name = "lxml-stubs" }, + { name = "mypy" }, { name = "pdoc" }, { name = "pytest" }, { name = "pytest-django" }, @@ -255,6 +259,7 @@ dev = [ { name = "requests-mock" }, { name = "ruff" }, { name = "selenium" }, + { name = "types-ldap3" }, ] [package.metadata] @@ -339,10 +344,14 @@ dev = [ { name = "constructs", specifier = "==10.4.2" }, { name = "coverage", extras = ["toml"], specifier = "==7.8.0" }, { name = "debugpy", specifier = "==1.8.14" }, + { name = "django-stubs", extras = ["compatible-mypy"], specifier = "==5.2.5" }, + { name = "djangorestframework-stubs", extras = ["compatible-mypy"], specifier = "==3.16.3" }, { name = "drf-jsonschema-serializer", specifier = "==3.0.0" }, { name = "freezegun", specifier = "==1.5.1" }, { name = "importlib-metadata", specifier = "==8.6.1" }, { name = "k5test", specifier = "==0.10.4" }, + { name = "lxml-stubs", specifier = "==0.5.1" }, + { name = "mypy", specifier = "==1.18.2" }, { name = "pdoc", specifier = "==15.0.3" }, { name = "pytest", specifier = "==8.3.5" }, { name = "pytest-django", specifier = "==4.11.1" }, @@ -352,6 +361,7 @@ dev = [ { name = "requests-mock", specifier = "==1.12.1" }, { name = "ruff", specifier = "==0.11.9" }, { name = "selenium", specifier = "==4.32.0" }, + { name = "types-ldap3", specifier = "==2.9.13.20250622" }, ] [[package]] @@ -1075,6 +1085,39 @@ s3 = [ { name = "boto3" }, ] +[[package]] +name = "django-stubs" +version = "5.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/8e/286150f593481c33f54d14efb58d72178f159d57d31043529d38bbc98e2f/django_stubs-5.2.5.tar.gz", hash = "sha256:fc78384e28d8c5292d60983075a5934f644f7c304c25ae2793fc57aa66d5018b", size = 247794, upload-time = "2025-09-12T19:29:49.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/02/cdbf7652ef2c9a7a1fed7c279484b7f3806f15b1bb34aec9fef8e8cfacbf/django_stubs-5.2.5-py3-none-any.whl", hash = "sha256:223c1a3324cd4873b7629dec6e9adbe224a94508284c1926b25fddff7a92252b", size = 490196, upload-time = "2025-09-12T19:29:47.954Z" }, +] + +[package.optional-dependencies] +compatible-mypy = [ + { name = "mypy" }, +] + +[[package]] +name = "django-stubs-ext" +version = "5.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/94/c9b8f4c47084a0fa666da9066c36771098101932688adf2c17a40fab79c2/django_stubs_ext-5.2.5.tar.gz", hash = "sha256:ecc628df29d36cede638567c4e33ff485dd7a99f1552ad0cece8c60e9c3a8872", size = 6489, upload-time = "2025-09-12T19:29:06.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/fe/a85a105fddffadb4a8d50e500caeee87d836b679d51a19d52dfa0cc6c660/django_stubs_ext-5.2.5-py3-none-any.whl", hash = "sha256:9b4b8ac9d32f7e6c304fd05477f8688fae6ed57f6a0f9f4d074f9e55b5a3da14", size = 9310, upload-time = "2025-09-12T19:29:04.62Z" }, +] + [[package]] name = "django-tenants" version = "3.8.0" @@ -1117,6 +1160,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/81/3d62f7ff71f7c45ec6664ebf03a4c736bf77f49481604361d40f8f4471e4/djangorestframework_guardian-0.4.0-py3-none-any.whl", hash = "sha256:30c2a349318c1cd603d6953d50d58159f9a0c833f5f8f5a811407d5984a39e14", size = 6064, upload-time = "2025-07-01T07:22:09.661Z" }, ] +[[package]] +name = "djangorestframework-stubs" +version = "3.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django-stubs" }, + { name = "requests" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/90/4512c9a464fec3fc6879f60c6ab1925da013bc5d6fa1a6951c750b02c89d/djangorestframework_stubs-3.16.3.tar.gz", hash = "sha256:200c620192da20a50a85b3e7d35cc7ec795aa9a58371a7f91368d4023e1b78f7", size = 34820, upload-time = "2025-09-18T18:06:01.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/64/00ff39218f80e7e9411ff1950c13b179d96e033a08dfb4048d5b90bc54fa/djangorestframework_stubs-3.16.3-py3-none-any.whl", hash = "sha256:d6e77922c2489aa6b5716d321b592b8aab047d5fd7ae9293a52c5cf1640e62c6", size = 54618, upload-time = "2025-09-18T18:06:00.265Z" }, +] + +[package.optional-dependencies] +compatible-mypy = [ + { name = "django-stubs", extra = ["compatible-mypy"] }, + { name = "mypy" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -1827,6 +1892,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, ] +[[package]] +name = "lxml-stubs" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -2112,6 +2186,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -3171,6 +3265,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/bb/d43e5c75054e53efce310e79d63df0ac3f25e34c926be5dffb7d283fb2a8/typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1", size = 17605, upload-time = "2021-12-10T21:09:37.844Z" }, ] +[[package]] +name = "types-ldap3" +version = "2.9.13.20250622" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/aa/238da23c20f48359c667b69c932a45bdcc325149042f677dc420a721844c/types_ldap3-2.9.13.20250622.tar.gz", hash = "sha256:4770a8701daddf1ee936d46414205a329211b6f6e655c2764f6b70c89b023c47", size = 33634, upload-time = "2025-06-22T03:19:16.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/fd/0339a618985d19d9b0630f78822d1becb0661be6abe8adbadd9569b875e1/types_ldap3-2.9.13.20250622-py3-none-any.whl", hash = "sha256:c18d0320327fa0017eb3d95acdf38921542d80939255e4ba130ca2d13ca3375f", size = 56498, upload-time = "2025-06-22T03:19:15.495Z" }, +] + +[[package]] +name = "types-pyasn1" +version = "0.6.0.20250914" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/92/bfe2385ee347c9d528adbd0fd8e5d7da6bcd18572cc42fd94e44c182dd69/types_pyasn1-0.6.0.20250914.tar.gz", hash = "sha256:236102553b76c938953037b7ae93d11d395d9413b7f2f8083d3b19d740f7eda6", size = 17109, upload-time = "2025-09-14T02:56:08.041Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/9d/5eb611d0db5b980cbb7d3eaca5baf187d5346f6371fdb6c708847539cea6/types_pyasn1-0.6.0.20250914-py3-none-any.whl", hash = "sha256:68ffeef3c28e1ed120b8b81a242f238f137543e68d466d84a97edcf3e4203b5b", size = 24052, upload-time = "2025-09-14T02:56:07.247Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"