From a2410b79cc9aa0dfbb2a39a4b32fa704a50139cb Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 9 Sep 2024 08:48:44 +0000 Subject: [PATCH 01/23] fix ambiguous variable names (E741) --- ruff.toml | 1 - src/eduid/webapp/support/app.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ruff.toml b/ruff.toml index 1eb98ea6f..e39b22d31 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,4 +3,3 @@ line-length = 120 target-version = "py310" [lint] -ignore = ["E741"] diff --git a/src/eduid/webapp/support/app.py b/src/eduid/webapp/support/app.py index 823be326e..91cba2620 100644 --- a/src/eduid/webapp/support/app.py +++ b/src/eduid/webapp/support/app.py @@ -44,21 +44,21 @@ def dateformat(value, format="%Y-%m-%d"): return value.strftime(format) @app.template_filter("multisort") - def sort_multi(l, *operators, **kwargs): + def sort_multi(items, *operators, **kwargs): # Don't try to sort on missing keys keys = list(operators) # operators is immutable for key in operators: - for item in l: + for item in items: if key not in item: app.logger.debug(f"Removed key {key} before sorting.") keys.remove(key) break reverse = kwargs.pop("reverse", False) try: - l.sort(key=operator.itemgetter(*keys), reverse=reverse) + items.sort(key=operator.itemgetter(*keys), reverse=reverse) except UndefinedError: # attribute did not exist - l = list() - return l + items = list() + return items @app.template_global() def static_url_for(f: str, version: Optional[str] = None) -> str: From d1a98235efe515a89439e7767916ce7822ba01e7 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 9 Sep 2024 08:49:52 +0000 Subject: [PATCH 02/23] enable W ruleset --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index e39b22d31..0c80ae16d 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,3 +3,4 @@ line-length = 120 target-version = "py310" [lint] +select = ["E4", "E7", "E9", "F", "W"] From 5c96a999a059207cb6dc1a300c6ddffcf62979dc Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 9 Sep 2024 08:50:16 +0000 Subject: [PATCH 03/23] remove trailing whitespace --- src/eduid/webapp/bankid/tests/test_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/webapp/bankid/tests/test_app.py b/src/eduid/webapp/bankid/tests/test_app.py index d33823dae..bd7283906 100644 --- a/src/eduid/webapp/bankid/tests/test_app.py +++ b/src/eduid/webapp/bankid/tests/test_app.py @@ -114,7 +114,7 @@ def setUp(self, *args: Any, **kwargs: Any) -> None: {sp_url}saml2-metadata - + http://id.swedenconnect.se/loa/1.0/uncertified-loa3 From 431412e27d0a12742426bf95b668bad2676643c2 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 9 Sep 2024 08:55:34 +0000 Subject: [PATCH 04/23] mark string as raw string literal --- src/eduid/common/models/scim_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/common/models/scim_base.py b/src/eduid/common/models/scim_base.py index 95e2fa3f0..8ba9f9f3c 100644 --- a/src/eduid/common/models/scim_base.py +++ b/src/eduid/common/models/scim_base.py @@ -23,7 +23,7 @@ # https://snipplr.com/view/11540/regex-for-tel-uris -PHONE_NUMBER_RFC_3966 = """^tel:((?:\+[\d().-]*\d[\d().-]*|[0-9A-F*#().-]*[0-9A-F*#][0-9A-F*#().-]*(?: +PHONE_NUMBER_RFC_3966 = r"""^tel:((?:\+[\d().-]*\d[\d().-]*|[0-9A-F*#().-]*[0-9A-F*#][0-9A-F*#().-]*(?: ;[a-z\d-]+(?:=(?:[a-z\d\[\]/:&+$_!~*'().-]|%[\dA-F]{2})+)?)*;phone-context=(?:\+[\d().-]*\d[\d().-]*| (?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*(?:[a-z]|[a-z][a-z0-9-]*[a-z0-9])))(?:;[a-z\d-]+(?:= (?:[a-z\d\[\]/:&+$_!~*'().-]|%[\dA-F]{2})+)?)*(?:,(?:\+[\d().-]*\d[\d().-]*|[0-9A-F*#().-]*[0-9A-F*#] From 74861588eb6cf0c791544835a22233d94b85ce53 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 08:32:28 +0000 Subject: [PATCH 05/23] add isort rules to ruff config --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 0c80ae16d..4bffb9086 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,4 +3,4 @@ line-length = 120 target-version = "py310" [lint] -select = ["E4", "E7", "E9", "F", "W"] +select = ["E4", "E7", "E9", "F", "W", "I"] From 7f6e4b6f3f51785f349df8ab46d1c1a2232acb4e Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 08:55:50 +0000 Subject: [PATCH 06/23] add ASYNC ruleset to ruff --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 4bffb9086..97e910dd1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,4 +3,4 @@ line-length = 120 target-version = "py310" [lint] -select = ["E4", "E7", "E9", "F", "W", "I"] +select = ["E4", "E7", "E9", "F", "W", "I", "ASYNC"] From 1cceafb890b5b3fe9796a916750c1aa46c17f499 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 09:00:58 +0000 Subject: [PATCH 07/23] do not use blocking sleep in async function --- src/eduid/queue/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/queue/testing.py b/src/eduid/queue/testing.py index 51bc6d745..5bf975e8e 100644 --- a/src/eduid/queue/testing.py +++ b/src/eduid/queue/testing.py @@ -173,7 +173,7 @@ async def _init_async_db(self): break except pymongo.errors.NotPrimaryError as e: db_init_try += 1 - time.sleep(db_init_try) + await asyncio.wait(db_init_try) if db_init_try >= 10: raise e continue From 5cc4cca2ddd53a5087bf1104ec867ee5f0fdb9ea Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 09:30:49 +0000 Subject: [PATCH 08/23] do not use blocking httpx call in async function --- src/eduid/queue/workers/scim_event.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/eduid/queue/workers/scim_event.py b/src/eduid/queue/workers/scim_event.py index cde00ed56..3f91a730a 100644 --- a/src/eduid/queue/workers/scim_event.py +++ b/src/eduid/queue/workers/scim_event.py @@ -48,8 +48,9 @@ async def handle_expired_item(self, queue_item: QueueItem) -> None: async def send_scim_notification(self, data: EduidSCIMAPINotification) -> Status: logger.debug(f"send_scim_notification: {data}") - r = httpx.post(data.post_url, json=json.loads(data.message)) - logger.debug(f"send_scim_notification: HTTPX result: {r}") + async with httpx.AsyncClient() as client: + r = client.post(data.post_url, json=json.loads(data.message)) + logger.debug(f"send_scim_notification: HTTPX result: {r}") return Status(success=True, message="OK") From 02252895990b16ab3e4c6b16e6d286194f3f0094 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 12:32:05 +0000 Subject: [PATCH 09/23] use the correct replacement --- src/eduid/queue/testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/queue/testing.py b/src/eduid/queue/testing.py index 5bf975e8e..4e85597f5 100644 --- a/src/eduid/queue/testing.py +++ b/src/eduid/queue/testing.py @@ -173,7 +173,7 @@ async def _init_async_db(self): break except pymongo.errors.NotPrimaryError as e: db_init_try += 1 - await asyncio.wait(db_init_try) + await asyncio.sleep(db_init_try) if db_init_try >= 10: raise e continue From ec49cee8fbf81d826af9179f315cdcdf5059f817 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 13:40:15 +0000 Subject: [PATCH 10/23] auto fix for deprecated imports (UP035) --- src/eduid/common/clients/gnap_client/base.py | 3 ++- src/eduid/common/config/base.py | 3 ++- src/eduid/common/config/parsers/__init__.py | 3 ++- src/eduid/common/config/parsers/base.py | 3 ++- src/eduid/common/config/parsers/decorators.py | 3 ++- src/eduid/common/config/parsers/yaml_parser.py | 3 ++- src/eduid/common/fastapi/api_router.py | 3 ++- src/eduid/common/fastapi/context_request.py | 3 ++- src/eduid/common/fastapi/utils.py | 3 ++- src/eduid/common/logging.py | 3 ++- src/eduid/common/models/bearer_token.py | 3 ++- src/eduid/common/rpc/tests/test_msg_relay.py | 3 ++- src/eduid/graphdb/db.py | 3 ++- src/eduid/graphdb/groupdb/group.py | 3 ++- src/eduid/graphdb/groupdb/user.py | 3 ++- src/eduid/graphdb/helpers.py | 3 ++- src/eduid/graphdb/testing.py | 3 ++- src/eduid/maccapi/routers/status.py | 2 +- src/eduid/maccapi/tests/test_maccapi.py | 3 ++- src/eduid/queue/config.py | 2 +- src/eduid/queue/db/change_event.py | 3 ++- src/eduid/queue/db/message/payload.py | 2 +- src/eduid/queue/db/payload.py | 3 ++- src/eduid/queue/db/queue_item.py | 3 ++- src/eduid/queue/db/worker.py | 3 ++- src/eduid/queue/helpers.py | 2 +- src/eduid/queue/testing.py | 3 ++- src/eduid/queue/workers/base.py | 2 +- src/eduid/queue/workers/mail.py | 3 ++- src/eduid/queue/workers/scim_event.py | 3 ++- src/eduid/queue/workers/sink.py | 3 ++- src/eduid/satosa/scimapi/accr.py | 3 ++- src/eduid/satosa/scimapi/common.py | 2 +- src/eduid/satosa/scimapi/pairwiseid.py | 3 ++- src/eduid/satosa/scimapi/scim_attributes.py | 3 ++- src/eduid/satosa/scimapi/static_attributes.py | 3 ++- src/eduid/satosa/scimapi/statsd.py | 3 ++- src/eduid/satosa/scimapi/stepup.py | 3 ++- src/eduid/scimapi/api_router.py | 3 ++- src/eduid/scimapi/routers/status.py | 2 +- src/eduid/scimapi/routers/utils/invites.py | 3 ++- src/eduid/scimapi/routers/utils/users.py | 3 ++- src/eduid/scimapi/test-scripts/scim-util.py | 3 ++- src/eduid/scimapi/testing.py | 3 ++- src/eduid/scimapi/tests/test_authn.py | 3 ++- src/eduid/scimapi/tests/test_scimevent.py | 3 ++- src/eduid/scimapi/tests/test_scimgroup.py | 3 ++- src/eduid/scimapi/tests/test_sciminvite.py | 3 ++- src/eduid/scimapi/tests/test_scimuser.py | 3 ++- src/eduid/scimapi/utils.py | 3 ++- src/eduid/userdb/admin/__init__.py | 3 ++- src/eduid/userdb/authninfo.py | 3 ++- src/eduid/userdb/credentials/external.py | 3 ++- src/eduid/userdb/db/async_db.py | 3 ++- src/eduid/userdb/db/base.py | 3 ++- src/eduid/userdb/db/sync_db.py | 3 ++- src/eduid/userdb/element.py | 3 ++- src/eduid/userdb/group_management/state.py | 3 ++- src/eduid/userdb/profile.py | 3 ++- src/eduid/userdb/proofing/db.py | 3 ++- src/eduid/userdb/proofing/state.py | 3 ++- src/eduid/userdb/reset_password/db.py | 3 ++- src/eduid/userdb/reset_password/element.py | 3 ++- src/eduid/userdb/scimapi/basedb.py | 3 ++- src/eduid/userdb/scimapi/common.py | 3 ++- src/eduid/userdb/scimapi/eventdb.py | 3 ++- src/eduid/userdb/scimapi/groupdb.py | 3 ++- src/eduid/userdb/scimapi/invitedb.py | 3 ++- src/eduid/userdb/scimapi/userdb.py | 3 ++- src/eduid/userdb/security/db.py | 3 ++- src/eduid/userdb/security/state.py | 3 ++- src/eduid/userdb/signup/invite.py | 3 ++- src/eduid/userdb/testing/__init__.py | 3 ++- src/eduid/userdb/testing/temp_instance.py | 3 ++- src/eduid/userdb/userdb.py | 3 ++- src/eduid/userdb/util.py | 3 ++- src/eduid/vccs/client/__init__.py | 3 ++- src/eduid/vccs/server/config.py | 3 ++- src/eduid/vccs/server/db.py | 3 ++- src/eduid/vccs/server/hasher.py | 3 ++- src/eduid/vccs/server/run.py | 3 ++- src/eduid/webapp/authn/app.py | 3 ++- src/eduid/webapp/authn/tests/test_authn.py | 3 ++- src/eduid/webapp/bankid/app.py | 3 ++- src/eduid/webapp/bankid/tests/test_app.py | 3 ++- src/eduid/webapp/common/api/debug.py | 3 ++- src/eduid/webapp/common/api/decorators.py | 4 ++-- src/eduid/webapp/common/api/exceptions.py | 3 ++- src/eduid/webapp/common/api/messages.py | 3 ++- src/eduid/webapp/common/api/oidc.py | 3 ++- src/eduid/webapp/common/api/testing.py | 3 ++- src/eduid/webapp/common/api/tests/test_backdoor.py | 3 ++- src/eduid/webapp/common/api/tests/test_decorators.py | 3 ++- src/eduid/webapp/common/api/tests/test_inputs.py | 3 ++- src/eduid/webapp/common/api/tests/test_logging.py | 3 ++- src/eduid/webapp/common/api/tests/test_nin_helpers.py | 3 ++- src/eduid/webapp/common/api/validation.py | 2 +- src/eduid/webapp/common/api/views/status.py | 3 ++- src/eduid/webapp/common/authn/acs_registry.py | 3 ++- src/eduid/webapp/common/authn/cache.py | 3 ++- src/eduid/webapp/common/authn/eduid_saml2.py | 3 ++- src/eduid/webapp/common/authn/fido_tokens.py | 3 ++- src/eduid/webapp/common/authn/middleware.py | 3 ++- src/eduid/webapp/common/authn/session_info.py | 3 ++- src/eduid/webapp/common/authn/tests/test_fido_tokens.py | 3 ++- src/eduid/webapp/common/authn/tests/test_middleware.py | 3 ++- src/eduid/webapp/common/authn/utils.py | 3 ++- src/eduid/webapp/common/session/eduid_session.py | 3 ++- src/eduid/webapp/common/session/namespaces.py | 3 ++- src/eduid/webapp/common/session/redis_session.py | 3 ++- src/eduid/webapp/common/session/testing.py | 2 +- src/eduid/webapp/common/session/tests/test_eduid_session.py | 3 ++- src/eduid/webapp/common/session/tests/test_namespaces.py | 3 ++- src/eduid/webapp/eidas/app.py | 3 ++- src/eduid/webapp/eidas/settings/common.py | 3 ++- src/eduid/webapp/eidas/tests/test_app.py | 3 ++- src/eduid/webapp/email/app.py | 3 ++- src/eduid/webapp/email/tests/test_app.py | 3 ++- src/eduid/webapp/freja_eid/app.py | 3 ++- src/eduid/webapp/group_management/app.py | 3 ++- src/eduid/webapp/group_management/tests/test_app.py | 3 ++- src/eduid/webapp/idp/app.py | 3 ++- src/eduid/webapp/idp/idp_authn.py | 3 ++- src/eduid/webapp/idp/idp_saml.py | 3 ++- src/eduid/webapp/idp/known_device.py | 3 ++- src/eduid/webapp/idp/login_context.py | 3 ++- src/eduid/webapp/idp/logout.py | 2 +- src/eduid/webapp/idp/mischttp.py | 3 ++- src/eduid/webapp/idp/other_device/db.py | 3 ++- src/eduid/webapp/idp/other_device/device1.py | 3 ++- src/eduid/webapp/idp/other_device/device2.py | 3 ++- src/eduid/webapp/idp/sso_cache.py | 3 ++- src/eduid/webapp/idp/sso_session.py | 3 ++- src/eduid/webapp/idp/tests/test_SSO.py | 3 ++- src/eduid/webapp/idp/tests/test_api.py | 3 ++- src/eduid/webapp/idp/views/mfa_auth.py | 3 ++- src/eduid/webapp/idp/views/tou.py | 3 ++- src/eduid/webapp/jsconfig/app.py | 3 ++- src/eduid/webapp/jsconfig/tests/test_app.py | 3 ++- src/eduid/webapp/ladok/app.py | 3 ++- src/eduid/webapp/ladok/client.py | 3 ++- src/eduid/webapp/ladok/tests/test_app.py | 3 ++- src/eduid/webapp/letter_proofing/app.py | 3 ++- src/eduid/webapp/letter_proofing/pdf.py | 2 +- src/eduid/webapp/letter_proofing/tests/test_app.py | 3 ++- src/eduid/webapp/lookup_mobile_proofing/app.py | 3 ++- src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py | 3 ++- src/eduid/webapp/oidc_proofing/app.py | 3 ++- src/eduid/webapp/oidc_proofing/helpers.py | 3 ++- src/eduid/webapp/oidc_proofing/views.py | 3 ++- src/eduid/webapp/orcid/app.py | 3 ++- src/eduid/webapp/orcid/tests/test_app.py | 3 ++- src/eduid/webapp/personal_data/app.py | 3 ++- src/eduid/webapp/personal_data/tests/test_app.py | 3 ++- src/eduid/webapp/phone/app.py | 3 ++- src/eduid/webapp/phone/tests/test_app.py | 3 ++- src/eduid/webapp/reset_password/app.py | 3 ++- src/eduid/webapp/reset_password/helpers.py | 3 ++- src/eduid/webapp/reset_password/tests/test_app.py | 3 ++- src/eduid/webapp/security/app.py | 3 ++- src/eduid/webapp/security/tests/test_app.py | 3 ++- src/eduid/webapp/security/tests/test_change_password.py | 3 ++- src/eduid/webapp/security/tests/test_webauthn.py | 3 ++- src/eduid/webapp/security/views/webauthn.py | 3 ++- src/eduid/webapp/signup/app.py | 3 ++- src/eduid/webapp/signup/tests/test_app.py | 3 ++- src/eduid/webapp/support/app.py | 3 ++- src/eduid/webapp/support/helpers.py | 3 ++- src/eduid/webapp/support/tests/test_app.py | 3 ++- src/eduid/webapp/support/views.py | 3 ++- src/eduid/webapp/svipe_id/app.py | 3 ++- src/eduid/workers/am/fetcher_registry.py | 2 +- src/eduid/workers/amapi/context_request.py | 3 ++- src/eduid/workers/amapi/routers/status.py | 2 +- src/eduid/workers/amapi/routers/utils/status.py | 3 ++- src/eduid/workers/job_runner/app.py | 3 ++- src/eduid/workers/job_runner/status.py | 2 +- src/eduid/workers/lookup_mobile/utilities.py | 3 ++- src/eduid/workers/msg/decorators.py | 3 ++- src/eduid/workers/msg/utils.py | 3 ++- 180 files changed, 346 insertions(+), 181 deletions(-) diff --git a/src/eduid/common/clients/gnap_client/base.py b/src/eduid/common/clients/gnap_client/base.py index b9863fa63..e88043886 100644 --- a/src/eduid/common/clients/gnap_client/base.py +++ b/src/eduid/common/clients/gnap_client/base.py @@ -1,7 +1,8 @@ import logging from abc import ABC +from collections.abc import Coroutine from datetime import datetime, timedelta -from typing import Annotated, Any, Coroutine, Optional, Union +from typing import Annotated, Any, Optional, Union from httpx import Request from jwcrypto.jwk import JWK diff --git a/src/eduid/common/config/base.py b/src/eduid/common/config/base.py index c92c7c0dc..7541d9ef4 100644 --- a/src/eduid/common/config/base.py +++ b/src/eduid/common/config/base.py @@ -4,11 +4,12 @@ from __future__ import annotations +from collections.abc import Iterable, Mapping, Sequence from datetime import timedelta from enum import Enum, unique from pathlib import Path from re import Pattern -from typing import IO, Annotated, Any, Iterable, Mapping, Optional, Sequence, TypeVar, Union +from typing import IO, Annotated, Any, Optional, TypeVar, Union import pkg_resources from pydantic import AfterValidator, BaseModel, ConfigDict, Field diff --git a/src/eduid/common/config/parsers/__init__.py b/src/eduid/common/config/parsers/__init__.py index fdd6ca528..dddb14dec 100644 --- a/src/eduid/common/config/parsers/__init__.py +++ b/src/eduid/common/config/parsers/__init__.py @@ -1,8 +1,9 @@ import json import os import sys +from collections.abc import Mapping from pathlib import Path -from typing import Any, Mapping, Optional +from typing import Any, Optional import yaml diff --git a/src/eduid/common/config/parsers/base.py b/src/eduid/common/config/parsers/base.py index d1d8e7b3f..983a68ba0 100644 --- a/src/eduid/common/config/parsers/base.py +++ b/src/eduid/common/config/parsers/base.py @@ -2,7 +2,8 @@ __author__ = "lundberg" -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any class BaseConfigParser(ABC): diff --git a/src/eduid/common/config/parsers/decorators.py b/src/eduid/common/config/parsers/decorators.py index 4bd7b1c0d..734171888 100644 --- a/src/eduid/common/config/parsers/decorators.py +++ b/src/eduid/common/config/parsers/decorators.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from functools import wraps from string import Template -from typing import Any, Mapping, Optional +from typing import Any, Optional from nacl import encoding, exceptions, secret diff --git a/src/eduid/common/config/parsers/yaml_parser.py b/src/eduid/common/config/parsers/yaml_parser.py index a13a70daa..de2a4768d 100644 --- a/src/eduid/common/config/parsers/yaml_parser.py +++ b/src/eduid/common/config/parsers/yaml_parser.py @@ -1,5 +1,6 @@ +from collections.abc import Mapping from pathlib import Path -from typing import Any, Mapping +from typing import Any from eduid.common.config.parsers import BaseConfigParser from eduid.common.config.parsers.decorators import decrypt, interpolate diff --git a/src/eduid/common/fastapi/api_router.py b/src/eduid/common/fastapi/api_router.py index e009a5729..a62a98668 100644 --- a/src/eduid/common/fastapi/api_router.py +++ b/src/eduid/common/fastapi/api_router.py @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from fastapi import APIRouter as FastAPIRouter from fastapi.types import DecoratedCallable diff --git a/src/eduid/common/fastapi/context_request.py b/src/eduid/common/fastapi/context_request.py index ce2754c18..a54a19926 100644 --- a/src/eduid/common/fastapi/context_request.py +++ b/src/eduid/common/fastapi/context_request.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from dataclasses import asdict, dataclass -from typing import Callable, Union +from typing import Union from fastapi import Request, Response from fastapi.routing import APIRoute diff --git a/src/eduid/common/fastapi/utils.py b/src/eduid/common/fastapi/utils.py index 773db9f00..f710d505d 100644 --- a/src/eduid/common/fastapi/utils.py +++ b/src/eduid/common/fastapi/utils.py @@ -2,9 +2,10 @@ import sys +from collections.abc import Mapping from dataclasses import dataclass, field, replace from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from fastapi import Response diff --git a/src/eduid/common/logging.py b/src/eduid/common/logging.py index 55aa0f568..a0ae99b39 100644 --- a/src/eduid/common/logging.py +++ b/src/eduid/common/logging.py @@ -3,10 +3,11 @@ import logging import logging.config import time +from collections.abc import Sequence from dataclasses import asdict, dataclass, field from os import environ from pprint import pformat -from typing import Any, Sequence +from typing import Any from eduid.common.config.base import LoggingConfigMixin, LoggingFilters from eduid.common.config.exceptions import BadConfiguration diff --git a/src/eduid/common/models/bearer_token.py b/src/eduid/common/models/bearer_token.py index f50365733..602184de0 100644 --- a/src/eduid/common/models/bearer_token.py +++ b/src/eduid/common/models/bearer_token.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from copy import copy from enum import Enum -from typing import Any, Mapping, Optional +from typing import Any, Optional from pydantic import BaseModel, Field, StrictInt, field_validator, model_validator from pydantic_core.core_schema import ValidationInfo diff --git a/src/eduid/common/rpc/tests/test_msg_relay.py b/src/eduid/common/rpc/tests/test_msg_relay.py index 48a1c1e48..65868273a 100644 --- a/src/eduid/common/rpc/tests/test_msg_relay.py +++ b/src/eduid/common/rpc/tests/test_msg_relay.py @@ -1,5 +1,6 @@ import unittest -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest diff --git a/src/eduid/graphdb/db.py b/src/eduid/graphdb/db.py index 0b7031981..08b4e600e 100644 --- a/src/eduid/graphdb/db.py +++ b/src/eduid/graphdb/db.py @@ -1,7 +1,8 @@ from __future__ import annotations from abc import ABC -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from urllib.parse import urlparse from neo4j import Driver, GraphDatabase, basic_auth diff --git a/src/eduid/graphdb/groupdb/group.py b/src/eduid/graphdb/groupdb/group.py index c79ed3683..b8eaa469f 100644 --- a/src/eduid/graphdb/groupdb/group.py +++ b/src/eduid/graphdb/groupdb/group.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime -from typing import Mapping, Optional, Union +from typing import Optional, Union from bson import ObjectId diff --git a/src/eduid/graphdb/groupdb/user.py b/src/eduid/graphdb/groupdb/user.py index e684df408..976f32f1a 100644 --- a/src/eduid/graphdb/groupdb/user.py +++ b/src/eduid/graphdb/groupdb/user.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from typing import Mapping, Optional +from typing import Optional from eduid.graphdb.helpers import neo4j_ts_to_dt diff --git a/src/eduid/graphdb/helpers.py b/src/eduid/graphdb/helpers.py index 07455baa7..e503cae33 100644 --- a/src/eduid/graphdb/helpers.py +++ b/src/eduid/graphdb/helpers.py @@ -1,5 +1,6 @@ +from collections.abc import Mapping from datetime import datetime, timezone -from typing import Mapping, Optional +from typing import Optional __author__ = "lundberg" diff --git a/src/eduid/graphdb/testing.py b/src/eduid/graphdb/testing.py index 34faf53d1..45d09603e 100644 --- a/src/eduid/graphdb/testing.py +++ b/src/eduid/graphdb/testing.py @@ -3,8 +3,9 @@ import logging import random import unittest +from collections.abc import Sequence from os import environ -from typing import Optional, Sequence, cast +from typing import Optional, cast from neo4j.exceptions import ServiceUnavailable diff --git a/src/eduid/maccapi/routers/status.py b/src/eduid/maccapi/routers/status.py index c43b63c98..2b49c3580 100644 --- a/src/eduid/maccapi/routers/status.py +++ b/src/eduid/maccapi/routers/status.py @@ -1,5 +1,5 @@ +from collections.abc import Mapping from os import environ -from typing import Mapping from fastapi import APIRouter, Response from pydantic import BaseModel diff --git a/src/eduid/maccapi/tests/test_maccapi.py b/src/eduid/maccapi/tests/test_maccapi.py index a7f951657..79c769938 100644 --- a/src/eduid/maccapi/tests/test_maccapi.py +++ b/src/eduid/maccapi/tests/test_maccapi.py @@ -1,5 +1,6 @@ import json -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from jwcrypto import jwt diff --git a/src/eduid/queue/config.py b/src/eduid/queue/config.py index a6adf1984..61c4428c7 100644 --- a/src/eduid/queue/config.py +++ b/src/eduid/queue/config.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from eduid.common.config.base import EduidEnvironment, LoggingConfigMixin, LoggingFilters, RootConfig diff --git a/src/eduid/queue/db/change_event.py b/src/eduid/queue/db/change_event.py index 51cf68a36..9da54ea0c 100644 --- a/src/eduid/queue/db/change_event.py +++ b/src/eduid/queue/db/change_event.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from enum import Enum -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union __author__ = "lundberg" diff --git a/src/eduid/queue/db/message/payload.py b/src/eduid/queue/db/message/payload.py index 2c0bfb8e4..1cb5a4a74 100644 --- a/src/eduid/queue/db/message/payload.py +++ b/src/eduid/queue/db/message/payload.py @@ -1,6 +1,6 @@ +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from typing import Mapping from eduid.queue.db import Payload diff --git a/src/eduid/queue/db/payload.py b/src/eduid/queue/db/payload.py index 9f69353dd..b26cd67f1 100644 --- a/src/eduid/queue/db/payload.py +++ b/src/eduid/queue/db/payload.py @@ -1,7 +1,8 @@ from abc import ABC +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Mapping, TypeVar +from typing import Any, TypeVar __author__ = "lundberg" diff --git a/src/eduid/queue/db/queue_item.py b/src/eduid/queue/db/queue_item.py index d55d3ffe2..cc54f7f92 100644 --- a/src/eduid/queue/db/queue_item.py +++ b/src/eduid/queue/db/queue_item.py @@ -1,6 +1,7 @@ +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Mapping, Optional +from typing import Any, Optional from bson import ObjectId diff --git a/src/eduid/queue/db/worker.py b/src/eduid/queue/db/worker.py index c740dacd8..5463d7279 100644 --- a/src/eduid/queue/db/worker.py +++ b/src/eduid/queue/db/worker.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from dataclasses import replace from datetime import datetime, timedelta, timezone -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union from bson import ObjectId from pymongo.results import UpdateResult diff --git a/src/eduid/queue/helpers.py b/src/eduid/queue/helpers.py index f16448fe7..76ad1c4c6 100644 --- a/src/eduid/queue/helpers.py +++ b/src/eduid/queue/helpers.py @@ -1,7 +1,7 @@ import logging +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path -from typing import Iterator import babel from babel.support import Translations diff --git a/src/eduid/queue/testing.py b/src/eduid/queue/testing.py index 4e85597f5..cd6c1ad27 100644 --- a/src/eduid/queue/testing.py +++ b/src/eduid/queue/testing.py @@ -4,8 +4,9 @@ import logging import time from asyncio import Task +from collections.abc import Sequence from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeAlias, cast +from typing import TYPE_CHECKING, Any, Optional, TypeAlias, cast from unittest import IsolatedAsyncioTestCase, TestCase from unittest.mock import patch diff --git a/src/eduid/queue/workers/base.py b/src/eduid/queue/workers/base.py index 2c5bac846..9ebab7f08 100644 --- a/src/eduid/queue/workers/base.py +++ b/src/eduid/queue/workers/base.py @@ -4,10 +4,10 @@ import signal from abc import ABC from asyncio import CancelledError, Task +from collections.abc import Sequence from dataclasses import replace from datetime import datetime from os import environ -from typing import Sequence from eduid.common.logging import init_logging from eduid.queue.config import QueueWorkerConfig diff --git a/src/eduid/queue/workers/mail.py b/src/eduid/queue/workers/mail.py index 7a0f945da..779acb370 100644 --- a/src/eduid/queue/workers/mail.py +++ b/src/eduid/queue/workers/mail.py @@ -1,9 +1,10 @@ import asyncio import logging +from collections.abc import Mapping, Sequence from dataclasses import asdict from email.message import EmailMessage from email.utils import formatdate, make_msgid -from typing import Any, Mapping, Optional, Sequence, cast +from typing import Any, Optional, cast from aiosmtplib import SMTP, SMTPException, SMTPResponse from jinja2 import Environment diff --git a/src/eduid/queue/workers/scim_event.py b/src/eduid/queue/workers/scim_event.py index 3f91a730a..33f9a318b 100644 --- a/src/eduid/queue/workers/scim_event.py +++ b/src/eduid/queue/workers/scim_event.py @@ -1,7 +1,8 @@ import asyncio import json import logging -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast import httpx diff --git a/src/eduid/queue/workers/sink.py b/src/eduid/queue/workers/sink.py index c6c7da494..8ede5859b 100644 --- a/src/eduid/queue/workers/sink.py +++ b/src/eduid/queue/workers/sink.py @@ -4,8 +4,9 @@ import logging import os from asyncio import Task +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from eduid.common.config.parsers import load_config from eduid.queue.config import QueueWorkerConfig diff --git a/src/eduid/satosa/scimapi/accr.py b/src/eduid/satosa/scimapi/accr.py index 9c3dd5743..a99089557 100644 --- a/src/eduid/satosa/scimapi/accr.py +++ b/src/eduid/satosa/scimapi/accr.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Mapping from copy import deepcopy -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union import satosa.internal from satosa.context import Context diff --git a/src/eduid/satosa/scimapi/common.py b/src/eduid/satosa/scimapi/common.py index 4a6fe798c..3f9a69004 100644 --- a/src/eduid/satosa/scimapi/common.py +++ b/src/eduid/satosa/scimapi/common.py @@ -1,5 +1,5 @@ import logging -from typing import Generator +from collections.abc import Generator import satosa.context import satosa.internal diff --git a/src/eduid/satosa/scimapi/pairwiseid.py b/src/eduid/satosa/scimapi/pairwiseid.py index 962d53de4..c0a9e4545 100644 --- a/src/eduid/satosa/scimapi/pairwiseid.py +++ b/src/eduid/satosa/scimapi/pairwiseid.py @@ -1,8 +1,9 @@ import hmac import logging +from collections.abc import Mapping from dataclasses import dataclass from hashlib import sha256 -from typing import Any, Mapping +from typing import Any import satosa.context import satosa.internal diff --git a/src/eduid/satosa/scimapi/scim_attributes.py b/src/eduid/satosa/scimapi/scim_attributes.py index b8f0bd02f..57951e69f 100644 --- a/src/eduid/satosa/scimapi/scim_attributes.py +++ b/src/eduid/satosa/scimapi/scim_attributes.py @@ -1,7 +1,8 @@ import logging import pprint +from collections.abc import Mapping from dataclasses import dataclass, field -from typing import Any, Mapping, Optional +from typing import Any, Optional import satosa.context import satosa.internal diff --git a/src/eduid/satosa/scimapi/static_attributes.py b/src/eduid/satosa/scimapi/static_attributes.py index 630a640a7..cb2f422c5 100644 --- a/src/eduid/satosa/scimapi/static_attributes.py +++ b/src/eduid/satosa/scimapi/static_attributes.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional import satosa.context import satosa.internal diff --git a/src/eduid/satosa/scimapi/statsd.py b/src/eduid/satosa/scimapi/statsd.py index 286743093..3528acdfd 100644 --- a/src/eduid/satosa/scimapi/statsd.py +++ b/src/eduid/satosa/scimapi/statsd.py @@ -1,6 +1,7 @@ import logging import re -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from satosa.context import Context from satosa.internal import InternalData diff --git a/src/eduid/satosa/scimapi/stepup.py b/src/eduid/satosa/scimapi/stepup.py index b4c33c939..80a7f88cd 100644 --- a/src/eduid/satosa/scimapi/stepup.py +++ b/src/eduid/satosa/scimapi/stepup.py @@ -3,7 +3,8 @@ import functools import json import logging -from typing import Any, Callable, Iterable, Mapping, NewType, Optional, TypeAlias, Union +from collections.abc import Callable, Iterable, Mapping +from typing import Any, NewType, Optional, TypeAlias, Union from urllib.parse import urlparse from pydantic import BaseModel, Field, ValidationError diff --git a/src/eduid/scimapi/api_router.py b/src/eduid/scimapi/api_router.py index e009a5729..a62a98668 100644 --- a/src/eduid/scimapi/api_router.py +++ b/src/eduid/scimapi/api_router.py @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from fastapi import APIRouter as FastAPIRouter from fastapi.types import DecoratedCallable diff --git a/src/eduid/scimapi/routers/status.py b/src/eduid/scimapi/routers/status.py index dbfa19440..d2dd7e1a4 100644 --- a/src/eduid/scimapi/routers/status.py +++ b/src/eduid/scimapi/routers/status.py @@ -1,5 +1,5 @@ +from collections.abc import Mapping from os import environ -from typing import Mapping from fastapi import Response diff --git a/src/eduid/scimapi/routers/utils/invites.py b/src/eduid/scimapi/routers/utils/invites.py index cec3f770b..82046bb87 100644 --- a/src/eduid/scimapi/routers/utils/invites.py +++ b/src/eduid/scimapi/routers/utils/invites.py @@ -1,7 +1,8 @@ +from collections.abc import Sequence from dataclasses import asdict from datetime import datetime, timedelta from os import environ -from typing import Any, Optional, Sequence +from typing import Any, Optional from fastapi import Request, Response from pymongo.errors import DuplicateKeyError diff --git a/src/eduid/scimapi/routers/utils/users.py b/src/eduid/scimapi/routers/utils/users.py index 11934d0af..e83790301 100644 --- a/src/eduid/scimapi/routers/utils/users.py +++ b/src/eduid/scimapi/routers/utils/users.py @@ -1,6 +1,7 @@ +from collections.abc import Sequence from dataclasses import asdict from datetime import datetime -from typing import Any, Optional, Sequence +from typing import Any, Optional from fastapi import Response from pymongo.errors import DuplicateKeyError diff --git a/src/eduid/scimapi/test-scripts/scim-util.py b/src/eduid/scimapi/test-scripts/scim-util.py index a46d73b87..19875eb7a 100755 --- a/src/eduid/scimapi/test-scripts/scim-util.py +++ b/src/eduid/scimapi/test-scripts/scim-util.py @@ -4,9 +4,10 @@ import json import logging import sys +from collections.abc import Callable, Mapping from dataclasses import dataclass from pprint import pformat -from typing import Any, Callable, Mapping, NewType, Optional, cast +from typing import Any, NewType, Optional, cast import requests import yaml diff --git a/src/eduid/scimapi/testing.py b/src/eduid/scimapi/testing.py index 8f7c91fbe..c44ae2f93 100644 --- a/src/eduid/scimapi/testing.py +++ b/src/eduid/scimapi/testing.py @@ -1,9 +1,10 @@ import os import unittest import uuid +from collections.abc import Mapping from dataclasses import asdict from json import JSONDecodeError -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union import pkg_resources from bson import ObjectId diff --git a/src/eduid/scimapi/tests/test_authn.py b/src/eduid/scimapi/tests/test_authn.py index c18821c18..7b53027e2 100644 --- a/src/eduid/scimapi/tests/test_authn.py +++ b/src/eduid/scimapi/tests/test_authn.py @@ -1,8 +1,9 @@ import logging import os +from collections.abc import Mapping from dataclasses import asdict from pathlib import PurePath -from typing import Any, Mapping, Optional +from typing import Any, Optional from uuid import uuid4 import pytest diff --git a/src/eduid/scimapi/tests/test_scimevent.py b/src/eduid/scimapi/tests/test_scimevent.py index 59154625d..d9f64935f 100644 --- a/src/eduid/scimapi/tests/test_scimevent.py +++ b/src/eduid/scimapi/tests/test_scimevent.py @@ -1,6 +1,7 @@ +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from uuid import UUID, uuid4 from httpx import Response diff --git a/src/eduid/scimapi/tests/test_scimgroup.py b/src/eduid/scimapi/tests/test_scimgroup.py index 4440ca0fe..99555aae4 100644 --- a/src/eduid/scimapi/tests/test_scimgroup.py +++ b/src/eduid/scimapi/tests/test_scimgroup.py @@ -1,8 +1,9 @@ __author__ = "lundberg" import logging +from collections.abc import Mapping from datetime import datetime -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union from uuid import UUID, uuid4 from bson import ObjectId diff --git a/src/eduid/scimapi/tests/test_sciminvite.py b/src/eduid/scimapi/tests/test_sciminvite.py index f26c2259a..e3444e223 100644 --- a/src/eduid/scimapi/tests/test_sciminvite.py +++ b/src/eduid/scimapi/tests/test_sciminvite.py @@ -1,10 +1,11 @@ import json import logging import unittest +from collections.abc import Mapping from copy import copy from dataclasses import asdict from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from bson import ObjectId diff --git a/src/eduid/scimapi/tests/test_scimuser.py b/src/eduid/scimapi/tests/test_scimuser.py index f5034687c..8bcf15754 100644 --- a/src/eduid/scimapi/tests/test_scimuser.py +++ b/src/eduid/scimapi/tests/test_scimuser.py @@ -2,9 +2,10 @@ import json import logging import unittest +from collections.abc import Mapping from dataclasses import asdict, dataclass from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest import IsolatedAsyncioTestCase from uuid import UUID, uuid4 diff --git a/src/eduid/scimapi/utils.py b/src/eduid/scimapi/utils.py index 6e777ae5c..14a04497e 100644 --- a/src/eduid/scimapi/utils.py +++ b/src/eduid/scimapi/utils.py @@ -2,7 +2,8 @@ import functools import logging import time -from typing import AnyStr, Callable, TypeVar +from collections.abc import Callable +from typing import AnyStr, TypeVar from uuid import uuid4 from jwcrypto import jwk diff --git a/src/eduid/userdb/admin/__init__.py b/src/eduid/userdb/admin/__init__.py index 921c4a58c..82014b827 100644 --- a/src/eduid/userdb/admin/__init__.py +++ b/src/eduid/userdb/admin/__init__.py @@ -7,8 +7,9 @@ import pprint import sys import time +from collections.abc import Generator from copy import deepcopy -from typing import Any, Generator, Optional +from typing import Any, Optional import bson import bson.json_util diff --git a/src/eduid/userdb/authninfo.py b/src/eduid/userdb/authninfo.py index 370334498..2e67619e4 100644 --- a/src/eduid/userdb/authninfo.py +++ b/src/eduid/userdb/authninfo.py @@ -1,8 +1,9 @@ import logging +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime from enum import Enum -from typing import Mapping, Optional +from typing import Optional from eduid.userdb import User from eduid.userdb.credentials import U2F, Password, Webauthn diff --git a/src/eduid/userdb/credentials/external.py b/src/eduid/userdb/credentials/external.py index 0ec08284c..73746b525 100644 --- a/src/eduid/userdb/credentials/external.py +++ b/src/eduid/userdb/credentials/external.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Mapping from enum import Enum -from typing import Any, Literal, Mapping, Optional +from typing import Any, Literal, Optional from bson import ObjectId from pydantic import Field, field_validator diff --git a/src/eduid/userdb/db/async_db.py b/src/eduid/userdb/db/async_db.py index 6a5394390..0163cfac8 100644 --- a/src/eduid/userdb/db/async_db.py +++ b/src/eduid/userdb/db/async_db.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union import pymongo from bson import ObjectId diff --git a/src/eduid/userdb/db/base.py b/src/eduid/userdb/db/base.py index 5a2e6bd65..785a5b5df 100644 --- a/src/eduid/userdb/db/base.py +++ b/src/eduid/userdb/db/base.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import copy import logging -from typing import Any, Mapping, NewType, Optional +from collections.abc import Mapping +from typing import Any, NewType, Optional from pymongo.uri_parser import parse_uri diff --git a/src/eduid/userdb/db/sync_db.py b/src/eduid/userdb/db/sync_db.py index 7271b9f4e..db860cf00 100644 --- a/src/eduid/userdb/db/sync_db.py +++ b/src/eduid/userdb/db/sync_db.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union import pymongo import pymongo.collection diff --git a/src/eduid/userdb/element.py b/src/eduid/userdb/element.py index b32a847fa..bf132f099 100644 --- a/src/eduid/userdb/element.py +++ b/src/eduid/userdb/element.py @@ -48,9 +48,10 @@ import copy from abc import ABC +from collections.abc import Mapping from datetime import datetime from enum import Enum -from typing import Any, Generic, Mapping, NewType, Optional, TypeVar, Union +from typing import Any, Generic, NewType, Optional, TypeVar, Union from pydantic import BaseModel, ConfigDict, Field, field_validator diff --git a/src/eduid/userdb/group_management/state.py b/src/eduid/userdb/group_management/state.py index 3e7499233..409f19169 100644 --- a/src/eduid/userdb/group_management/state.py +++ b/src/eduid/userdb/group_management/state.py @@ -2,9 +2,10 @@ import copy import datetime +from collections.abc import Mapping from dataclasses import asdict, dataclass, field, fields from enum import Enum, unique -from typing import Any, Mapping, Optional +from typing import Any, Optional import bson diff --git a/src/eduid/userdb/profile.py b/src/eduid/userdb/profile.py index c9e053665..cbcb31f1d 100644 --- a/src/eduid/userdb/profile.py +++ b/src/eduid/userdb/profile.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from eduid.userdb.element import Element, ElementKey, ElementList diff --git a/src/eduid/userdb/proofing/db.py b/src/eduid/userdb/proofing/db.py index 121023013..a8d836a30 100644 --- a/src/eduid/userdb/proofing/db.py +++ b/src/eduid/userdb/proofing/db.py @@ -1,7 +1,8 @@ import logging from abc import ABC +from collections.abc import Mapping from operator import itemgetter -from typing import Any, Generic, Mapping, Optional, TypeVar +from typing import Any, Generic, Optional, TypeVar from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.proofing.state import ( diff --git a/src/eduid/userdb/proofing/state.py b/src/eduid/userdb/proofing/state.py index cda7b82cf..dd7385ed3 100644 --- a/src/eduid/userdb/proofing/state.py +++ b/src/eduid/userdb/proofing/state.py @@ -3,8 +3,9 @@ import copy import datetime import logging +from collections.abc import Mapping from dataclasses import asdict, dataclass -from typing import Any, Mapping, Optional +from typing import Any, Optional import bson diff --git a/src/eduid/userdb/reset_password/db.py b/src/eduid/userdb/reset_password/db.py index 51e701be6..806339db7 100644 --- a/src/eduid/userdb/reset_password/db.py +++ b/src/eduid/userdb/reset_password/db.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.exceptions import MultipleDocumentsReturned diff --git a/src/eduid/userdb/reset_password/element.py b/src/eduid/userdb/reset_password/element.py index 27683aee4..44229d721 100644 --- a/src/eduid/userdb/reset_password/element.py +++ b/src/eduid/userdb/reset_password/element.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Union +from typing import Any, Union from eduid.userdb.element import Element, ElementKey from eduid.userdb.util import utc_now diff --git a/src/eduid/userdb/scimapi/basedb.py b/src/eduid/userdb/scimapi/basedb.py index 9450114c2..59ae3366d 100644 --- a/src/eduid/userdb/scimapi/basedb.py +++ b/src/eduid/userdb/scimapi/basedb.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from eduid.userdb.db import BaseDB, TUserDbDocument diff --git a/src/eduid/userdb/scimapi/common.py b/src/eduid/userdb/scimapi/common.py index 18e84f2b3..ddf0410f7 100644 --- a/src/eduid/userdb/scimapi/common.py +++ b/src/eduid/userdb/scimapi/common.py @@ -2,9 +2,10 @@ import uuid from abc import ABC +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Mapping, Optional, Type, Union +from typing import Any, Optional, Type, Union from uuid import UUID from eduid.common.models.scim_base import EmailType, PhoneNumberType, WeakVersion diff --git a/src/eduid/userdb/scimapi/eventdb.py b/src/eduid/userdb/scimapi/eventdb.py index bbd0ac0b7..92bb9a06c 100644 --- a/src/eduid/userdb/scimapi/eventdb.py +++ b/src/eduid/userdb/scimapi/eventdb.py @@ -1,10 +1,11 @@ from __future__ import annotations import logging +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Mapping, Optional +from typing import Any, Optional from uuid import UUID from bson import ObjectId diff --git a/src/eduid/userdb/scimapi/groupdb.py b/src/eduid/userdb/scimapi/groupdb.py index ba8296c95..cd715c36e 100644 --- a/src/eduid/userdb/scimapi/groupdb.py +++ b/src/eduid/userdb/scimapi/groupdb.py @@ -4,9 +4,10 @@ import logging import pprint import uuid +from collections.abc import Iterable, Mapping from dataclasses import asdict, dataclass, field, replace from datetime import datetime -from typing import Any, Iterable, Mapping, Optional, Union +from typing import Any, Optional, Union from uuid import UUID from bson import ObjectId diff --git a/src/eduid/userdb/scimapi/invitedb.py b/src/eduid/userdb/scimapi/invitedb.py index dd50fa1aa..5c9171b34 100644 --- a/src/eduid/userdb/scimapi/invitedb.py +++ b/src/eduid/userdb/scimapi/invitedb.py @@ -3,9 +3,10 @@ import copy import logging import uuid +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Mapping, Optional +from typing import Any, Optional from uuid import UUID from bson import ObjectId diff --git a/src/eduid/userdb/scimapi/userdb.py b/src/eduid/userdb/scimapi/userdb.py index 6a30a3b37..a9bf0e5d5 100644 --- a/src/eduid/userdb/scimapi/userdb.py +++ b/src/eduid/userdb/scimapi/userdb.py @@ -3,9 +3,10 @@ import copy import logging import uuid +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Mapping, Optional +from typing import Any, Optional from bson import ObjectId diff --git a/src/eduid/userdb/security/db.py b/src/eduid/userdb/security/db.py index 941066b06..05cb6ebb0 100644 --- a/src/eduid/userdb/security/db.py +++ b/src/eduid/userdb/security/db.py @@ -1,6 +1,7 @@ import copy import logging -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.deprecation import deprecated diff --git a/src/eduid/userdb/security/state.py b/src/eduid/userdb/security/state.py index 928128f35..0b283c7b3 100644 --- a/src/eduid/userdb/security/state.py +++ b/src/eduid/userdb/security/state.py @@ -2,7 +2,8 @@ import copy import datetime -from typing import Any, Mapping, Optional, TypeVar +from collections.abc import Mapping +from typing import Any, Optional, TypeVar import bson from pydantic import BaseModel, ConfigDict, Field diff --git a/src/eduid/userdb/signup/invite.py b/src/eduid/userdb/signup/invite.py index 4a0f64118..4194fd80e 100644 --- a/src/eduid/userdb/signup/invite.py +++ b/src/eduid/userdb/signup/invite.py @@ -1,9 +1,10 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Mapping, Optional +from typing import Any, Optional from uuid import UUID from bson import ObjectId diff --git a/src/eduid/userdb/testing/__init__.py b/src/eduid/userdb/testing/__init__.py index deaea52b4..564ca6aec 100644 --- a/src/eduid/userdb/testing/__init__.py +++ b/src/eduid/userdb/testing/__init__.py @@ -7,7 +7,8 @@ import logging import logging.config import unittest -from typing import Any, Optional, Sequence, cast +from collections.abc import Sequence +from typing import Any, Optional, cast import pymongo import pymongo.errors diff --git a/src/eduid/userdb/testing/temp_instance.py b/src/eduid/userdb/testing/temp_instance.py index f2eed6270..da11307be 100644 --- a/src/eduid/userdb/testing/temp_instance.py +++ b/src/eduid/userdb/testing/temp_instance.py @@ -8,7 +8,8 @@ import tempfile import time from abc import ABC, abstractmethod -from typing import Any, Optional, Sequence +from collections.abc import Sequence +from typing import Any, Optional from eduid.userdb.util import utc_now diff --git a/src/eduid/userdb/userdb.py b/src/eduid/userdb/userdb.py index 82f9cede2..c67cc8c90 100644 --- a/src/eduid/userdb/userdb.py +++ b/src/eduid/userdb/userdb.py @@ -1,7 +1,8 @@ import logging from abc import ABC +from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Generic, Mapping, Optional, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar, Union from bson import ObjectId from bson.errors import InvalidId diff --git a/src/eduid/userdb/util.py b/src/eduid/userdb/util.py index 10e4b916c..22d706fc5 100644 --- a/src/eduid/userdb/util.py +++ b/src/eduid/userdb/util.py @@ -3,7 +3,8 @@ import datetime import json import logging -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from bson import ObjectId diff --git a/src/eduid/vccs/client/__init__.py b/src/eduid/vccs/client/__init__.py index 87abdc40b..f3ec3e649 100644 --- a/src/eduid/vccs/client/__init__.py +++ b/src/eduid/vccs/client/__init__.py @@ -42,7 +42,8 @@ """ import os -from typing import Any, Optional, Sequence +from collections.abc import Sequence +from typing import Any, Optional from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen diff --git a/src/eduid/vccs/server/config.py b/src/eduid/vccs/server/config.py index 3758df39e..7d0be91fb 100644 --- a/src/eduid/vccs/server/config.py +++ b/src/eduid/vccs/server/config.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from eduid.common.config.base import RootConfig from eduid.common.config.parsers import load_config diff --git a/src/eduid/vccs/server/db.py b/src/eduid/vccs/server/db.py index cfbab7089..cbc6fffed 100644 --- a/src/eduid/vccs/server/db.py +++ b/src/eduid/vccs/server/db.py @@ -1,8 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import asdict, field from enum import Enum, unique -from typing import Any, Mapping, Optional, Type, Union, cast +from typing import Any, Optional, Type, Union, cast from bson import ObjectId from loguru import logger diff --git a/src/eduid/vccs/server/hasher.py b/src/eduid/vccs/server/hasher.py index 521b4cfbb..628412eb9 100644 --- a/src/eduid/vccs/server/hasher.py +++ b/src/eduid/vccs/server/hasher.py @@ -3,8 +3,9 @@ import stat from abc import ABC from binascii import unhexlify +from collections.abc import Mapping from hashlib import sha1 -from typing import Any, Mapping, Optional +from typing import Any, Optional import pyhsm import yaml diff --git a/src/eduid/vccs/server/run.py b/src/eduid/vccs/server/run.py index 53d1c1c74..5a3557a3a 100644 --- a/src/eduid/vccs/server/run.py +++ b/src/eduid/vccs/server/run.py @@ -1,6 +1,7 @@ import sys from asyncio import Lock -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from fastapi import FastAPI from fastapi.exceptions import RequestValidationError diff --git a/src/eduid/webapp/authn/app.py b/src/eduid/webapp/authn/app.py index d94f13fd0..09731e092 100644 --- a/src/eduid/webapp/authn/app.py +++ b/src/eduid/webapp/authn/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from flask import current_app diff --git a/src/eduid/webapp/authn/tests/test_authn.py b/src/eduid/webapp/authn/tests/test_authn.py index 70d644811..122808531 100644 --- a/src/eduid/webapp/authn/tests/test_authn.py +++ b/src/eduid/webapp/authn/tests/test_authn.py @@ -3,8 +3,9 @@ import logging import os import uuid +from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Mapping, Optional +from typing import Any, Optional from urllib.parse import quote_plus from flask import Blueprint diff --git a/src/eduid/webapp/bankid/app.py b/src/eduid/webapp/bankid/app.py index ef48d0932..cca9a6478 100644 --- a/src/eduid/webapp/bankid/app.py +++ b/src/eduid/webapp/bankid/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/bankid/tests/test_app.py b/src/eduid/webapp/bankid/tests/test_app.py index bd7283906..9770b9b01 100644 --- a/src/eduid/webapp/bankid/tests/test_app.py +++ b/src/eduid/webapp/bankid/tests/test_app.py @@ -3,7 +3,8 @@ import logging import os import unittest -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union from unittest.mock import MagicMock, patch from fido2.webauthn import AuthenticatorAttachment diff --git a/src/eduid/webapp/common/api/debug.py b/src/eduid/webapp/common/api/debug.py index ed02ae170..a881e7d88 100644 --- a/src/eduid/webapp/common/api/debug.py +++ b/src/eduid/webapp/common/api/debug.py @@ -1,8 +1,9 @@ import pprint import sys import warnings +from collections.abc import Callable from dataclasses import asdict -from typing import Any, Callable +from typing import Any from urllib import parse from flask import Flask, url_for diff --git a/src/eduid/webapp/common/api/decorators.py b/src/eduid/webapp/common/api/decorators.py index e38a75cec..d9f4b9dbc 100644 --- a/src/eduid/webapp/common/api/decorators.py +++ b/src/eduid/webapp/common/api/decorators.py @@ -1,8 +1,8 @@ import json import logging -from collections.abc import Awaitable, Mapping +from collections.abc import Awaitable, Callable, Mapping from functools import wraps -from typing import Any, Callable, Optional, TypeVar, Union, cast +from typing import Any, Optional, TypeVar, Union, cast from flask import abort, jsonify, request from flask.typing import ResponseReturnValue as FlaskResponseReturnValue diff --git a/src/eduid/webapp/common/api/exceptions.py b/src/eduid/webapp/common/api/exceptions.py index d4c9a38b3..5330bfb03 100644 --- a/src/eduid/webapp/common/api/exceptions.py +++ b/src/eduid/webapp/common/api/exceptions.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING, Any, Mapping, Optional +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Optional from flask import jsonify diff --git a/src/eduid/webapp/common/api/messages.py b/src/eduid/webapp/common/api/messages.py index f3b3efe7e..7770de663 100644 --- a/src/eduid/webapp/common/api/messages.py +++ b/src/eduid/webapp/common/api/messages.py @@ -1,7 +1,8 @@ +from collections.abc import Mapping from copy import copy from dataclasses import asdict, dataclass from enum import Enum, unique -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from flask import redirect diff --git a/src/eduid/webapp/common/api/oidc.py b/src/eduid/webapp/common/api/oidc.py index 5cf5af409..63ca828e6 100644 --- a/src/eduid/webapp/common/api/oidc.py +++ b/src/eduid/webapp/common/api/oidc.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from sys import exit from time import sleep -from typing import Any, Mapping +from typing import Any import requests from oic.oic import Client diff --git a/src/eduid/webapp/common/api/testing.py b/src/eduid/webapp/common/api/testing.py index 955b70a01..6f663065f 100644 --- a/src/eduid/webapp/common/api/testing.py +++ b/src/eduid/webapp/common/api/testing.py @@ -5,10 +5,11 @@ import pprint import sys import traceback +from collections.abc import Generator, Iterable, Mapping from contextlib import contextmanager from copy import deepcopy from datetime import timedelta -from typing import Any, Generator, Generic, Iterable, Mapping, Optional, TypeVar, cast +from typing import Any, Generic, Optional, TypeVar, cast from flask.testing import FlaskClient from werkzeug.test import TestResponse diff --git a/src/eduid/webapp/common/api/tests/test_backdoor.py b/src/eduid/webapp/common/api/tests/test_backdoor.py index 953a98856..20aa8c011 100644 --- a/src/eduid/webapp/common/api/tests/test_backdoor.py +++ b/src/eduid/webapp/common/api/tests/test_backdoor.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from flask import Blueprint, abort, current_app, request diff --git a/src/eduid/webapp/common/api/tests/test_decorators.py b/src/eduid/webapp/common/api/tests/test_decorators.py index 4231ac020..7817d8ee9 100644 --- a/src/eduid/webapp/common/api/tests/test_decorators.py +++ b/src/eduid/webapp/common/api/tests/test_decorators.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, cast +from collections.abc import Mapping +from typing import Any, cast import flask from flask.wrappers import Response as FlaskResponse diff --git a/src/eduid/webapp/common/api/tests/test_inputs.py b/src/eduid/webapp/common/api/tests/test_inputs.py index b9b330c1b..1185d6860 100644 --- a/src/eduid/webapp/common/api/tests/test_inputs.py +++ b/src/eduid/webapp/common/api/tests/test_inputs.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from urllib.parse import unquote from flask import Blueprint, make_response, request diff --git a/src/eduid/webapp/common/api/tests/test_logging.py b/src/eduid/webapp/common/api/tests/test_logging.py index 4b80faa2b..afd64b270 100644 --- a/src/eduid/webapp/common/api/tests/test_logging.py +++ b/src/eduid/webapp/common/api/tests/test_logging.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from eduid.common.config.base import EduIDBaseAppConfig from eduid.common.logging import merge_config diff --git a/src/eduid/webapp/common/api/tests/test_nin_helpers.py b/src/eduid/webapp/common/api/tests/test_nin_helpers.py index e37ec6f96..bc819f0a1 100644 --- a/src/eduid/webapp/common/api/tests/test_nin_helpers.py +++ b/src/eduid/webapp/common/api/tests/test_nin_helpers.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from unittest.mock import MagicMock, patch import pytest diff --git a/src/eduid/webapp/common/api/validation.py b/src/eduid/webapp/common/api/validation.py index ae837fe31..3df2cb2d2 100644 --- a/src/eduid/webapp/common/api/validation.py +++ b/src/eduid/webapp/common/api/validation.py @@ -1,6 +1,6 @@ import math import re -from typing import Sequence +from collections.abc import Sequence from zxcvbn import zxcvbn diff --git a/src/eduid/webapp/common/api/views/status.py b/src/eduid/webapp/common/api/views/status.py index 389a15ed5..d82567bf9 100644 --- a/src/eduid/webapp/common/api/views/status.py +++ b/src/eduid/webapp/common/api/views/status.py @@ -1,7 +1,8 @@ import logging +from collections.abc import Mapping from dataclasses import asdict, dataclass from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Literal, Mapping, Optional, cast, overload +from typing import TYPE_CHECKING, Any, Literal, Optional, cast, overload from flask import Blueprint, jsonify from flask import current_app as flask_current_app diff --git a/src/eduid/webapp/common/authn/acs_registry.py b/src/eduid/webapp/common/authn/acs_registry.py index ca3835f6b..f321c8524 100644 --- a/src/eduid/webapp/common/authn/acs_registry.py +++ b/src/eduid/webapp/common/authn/acs_registry.py @@ -10,9 +10,10 @@ * The user object """ +from collections.abc import Callable from dataclasses import dataclass from enum import Enum -from typing import Callable, Optional, Union +from typing import Optional, Union from flask import current_app from werkzeug.wrappers import Response as WerkzeugResponse diff --git a/src/eduid/webapp/common/authn/cache.py b/src/eduid/webapp/common/authn/cache.py index e09b40345..d9316ad44 100644 --- a/src/eduid/webapp/common/authn/cache.py +++ b/src/eduid/webapp/common/authn/cache.py @@ -1,4 +1,5 @@ -from typing import Any, Iterator, MutableMapping, TypeVar +from collections.abc import Iterator, MutableMapping +from typing import Any, TypeVar from saml2.cache import Cache diff --git a/src/eduid/webapp/common/authn/eduid_saml2.py b/src/eduid/webapp/common/authn/eduid_saml2.py index 16ea06412..d664de138 100644 --- a/src/eduid/webapp/common/authn/eduid_saml2.py +++ b/src/eduid/webapp/common/authn/eduid_saml2.py @@ -1,7 +1,8 @@ import logging import pprint +from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union from xml.etree.ElementTree import ParseError from dateutil.parser import parse as dt_parse diff --git a/src/eduid/webapp/common/authn/fido_tokens.py b/src/eduid/webapp/common/authn/fido_tokens.py index 118103d7f..84ecb0d51 100644 --- a/src/eduid/webapp/common/authn/fido_tokens.py +++ b/src/eduid/webapp/common/authn/fido_tokens.py @@ -2,7 +2,8 @@ import json import logging import pprint -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from fido2 import cbor from fido2.server import Fido2Server, U2FFido2Server diff --git a/src/eduid/webapp/common/authn/middleware.py b/src/eduid/webapp/common/authn/middleware.py index 487c2f510..40ddcefe5 100644 --- a/src/eduid/webapp/common/authn/middleware.py +++ b/src/eduid/webapp/common/authn/middleware.py @@ -2,7 +2,8 @@ import logging import re from abc import ABCMeta -from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional, cast +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from flask import Request, current_app diff --git a/src/eduid/webapp/common/authn/session_info.py b/src/eduid/webapp/common/authn/session_info.py index e8c951391..fc32f3ad8 100644 --- a/src/eduid/webapp/common/authn/session_info.py +++ b/src/eduid/webapp/common/authn/session_info.py @@ -1,5 +1,6 @@ # Solve circular imports of SessionInfo from all over the place by putting it in a 'leaf' file :/ # -from typing import Any, Mapping, NewType +from collections.abc import Mapping +from typing import Any, NewType SessionInfo = NewType("SessionInfo", Mapping[str, Any]) diff --git a/src/eduid/webapp/common/authn/tests/test_fido_tokens.py b/src/eduid/webapp/common/authn/tests/test_fido_tokens.py index cb519552b..b36486144 100644 --- a/src/eduid/webapp/common/authn/tests/test_fido_tokens.py +++ b/src/eduid/webapp/common/authn/tests/test_fido_tokens.py @@ -1,7 +1,8 @@ import base64 import json +from collections.abc import Mapping from copy import deepcopy -from typing import Any, Mapping +from typing import Any from unittest.mock import MagicMock, patch from flask import Blueprint, current_app, request diff --git a/src/eduid/webapp/common/authn/tests/test_middleware.py b/src/eduid/webapp/common/authn/tests/test_middleware.py index 77b9a2c22..5b2d33cc2 100644 --- a/src/eduid/webapp/common/authn/tests/test_middleware.py +++ b/src/eduid/webapp/common/authn/tests/test_middleware.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from werkzeug.exceptions import NotFound diff --git a/src/eduid/webapp/common/authn/utils.py b/src/eduid/webapp/common/authn/utils.py index 00a0dfff8..edc9e5772 100644 --- a/src/eduid/webapp/common/authn/utils.py +++ b/src/eduid/webapp/common/authn/utils.py @@ -2,7 +2,8 @@ import logging import os.path import sys -from typing import Optional, Sequence, Tuple +from collections.abc import Sequence +from typing import Optional, Tuple from saml2 import server from saml2.config import SPConfig diff --git a/src/eduid/webapp/common/session/eduid_session.py b/src/eduid/webapp/common/session/eduid_session.py index 339eeeade..373845bfe 100644 --- a/src/eduid/webapp/common/session/eduid_session.py +++ b/src/eduid/webapp/common/session/eduid_session.py @@ -4,8 +4,9 @@ import logging import os import pprint +from collections.abc import MutableMapping from datetime import datetime -from typing import TYPE_CHECKING, Any, MutableMapping, Optional +from typing import TYPE_CHECKING, Any, Optional from flask import Request as FlaskRequest from flask import Response as FlaskResponse diff --git a/src/eduid/webapp/common/session/namespaces.py b/src/eduid/webapp/common/session/namespaces.py index 1a4e2f745..9394fb8fe 100644 --- a/src/eduid/webapp/common/session/namespaces.py +++ b/src/eduid/webapp/common/session/namespaces.py @@ -2,10 +2,11 @@ import logging from abc import ABC +from collections.abc import Mapping from copy import deepcopy from datetime import datetime from enum import Enum, unique -from typing import Any, List, Mapping, NewType, Optional, TypeVar, Union, cast +from typing import Any, List, NewType, Optional, TypeVar, Union, cast from uuid import uuid4 from fido2.webauthn import AuthenticatorAttachment diff --git a/src/eduid/webapp/common/session/redis_session.py b/src/eduid/webapp/common/session/redis_session.py index 8a0da3621..ee02403d0 100644 --- a/src/eduid/webapp/common/session/redis_session.py +++ b/src/eduid/webapp/common/session/redis_session.py @@ -48,7 +48,8 @@ import json import logging import typing -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union import nacl.encoding import nacl.secret diff --git a/src/eduid/webapp/common/session/testing.py b/src/eduid/webapp/common/session/testing.py index 356dc4847..5d7384c40 100644 --- a/src/eduid/webapp/common/session/testing.py +++ b/src/eduid/webapp/common/session/testing.py @@ -1,5 +1,5 @@ import logging -from typing import Sequence +from collections.abc import Sequence import redis diff --git a/src/eduid/webapp/common/session/tests/test_eduid_session.py b/src/eduid/webapp/common/session/tests/test_eduid_session.py index 0f5293e31..be37a9c95 100644 --- a/src/eduid/webapp/common/session/tests/test_eduid_session.py +++ b/src/eduid/webapp/common/session/tests/test_eduid_session.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from eduid.common.config.base import EduIDBaseAppConfig from eduid.common.config.parsers import load_config diff --git a/src/eduid/webapp/common/session/tests/test_namespaces.py b/src/eduid/webapp/common/session/tests/test_namespaces.py index 53ddc80d4..1aa2768fe 100644 --- a/src/eduid/webapp/common/session/tests/test_namespaces.py +++ b/src/eduid/webapp/common/session/tests/test_namespaces.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Mapping from datetime import datetime -from typing import Any, Mapping +from typing import Any from eduid.common.config.base import FrontendAction from eduid.common.config.parsers import load_config diff --git a/src/eduid/webapp/eidas/app.py b/src/eduid/webapp/eidas/app.py index 2ae26182e..331766c5a 100644 --- a/src/eduid/webapp/eidas/app.py +++ b/src/eduid/webapp/eidas/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/eidas/settings/common.py b/src/eduid/webapp/eidas/settings/common.py index abb640476..84d558bbe 100644 --- a/src/eduid/webapp/eidas/settings/common.py +++ b/src/eduid/webapp/eidas/settings/common.py @@ -2,7 +2,8 @@ Configuration (file) handling for the eduID eidas app. """ -from typing import Mapping, Optional +from collections.abc import Mapping +from typing import Optional from pydantic import Field diff --git a/src/eduid/webapp/eidas/tests/test_app.py b/src/eduid/webapp/eidas/tests/test_app.py index fc459d7f2..e707718d4 100644 --- a/src/eduid/webapp/eidas/tests/test_app.py +++ b/src/eduid/webapp/eidas/tests/test_app.py @@ -2,7 +2,8 @@ import datetime import logging import os -from typing import Any, Mapping, Optional, Union +from collections.abc import Mapping +from typing import Any, Optional, Union from unittest import TestCase from unittest.mock import MagicMock, patch diff --git a/src/eduid/webapp/email/app.py b/src/eduid/webapp/email/app.py index 7df96dadc..b8e6faaf6 100644 --- a/src/eduid/webapp/email/app.py +++ b/src/eduid/webapp/email/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/email/tests/test_app.py b/src/eduid/webapp/email/tests/test_app.py index 3c4ff4a5f..8d07956ad 100644 --- a/src/eduid/webapp/email/tests/test_app.py +++ b/src/eduid/webapp/email/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import patch from eduid.common.config.base import EduidEnvironment diff --git a/src/eduid/webapp/freja_eid/app.py b/src/eduid/webapp/freja_eid/app.py index 74aaa17b1..cfacadb96 100644 --- a/src/eduid/webapp/freja_eid/app.py +++ b/src/eduid/webapp/freja_eid/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from authlib.integrations.flask_client import OAuth from flask import current_app diff --git a/src/eduid/webapp/group_management/app.py b/src/eduid/webapp/group_management/app.py index 64f18c0fd..79c7cab61 100644 --- a/src/eduid/webapp/group_management/app.py +++ b/src/eduid/webapp/group_management/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/group_management/tests/test_app.py b/src/eduid/webapp/group_management/tests/test_app.py index 4fcfb32ff..0149cd46f 100644 --- a/src/eduid/webapp/group_management/tests/test_app.py +++ b/src/eduid/webapp/group_management/tests/test_app.py @@ -1,5 +1,6 @@ import json -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from unittest.mock import patch from uuid import UUID diff --git a/src/eduid/webapp/idp/app.py b/src/eduid/webapp/idp/app.py index e1f375f62..6a973f6a2 100644 --- a/src/eduid/webapp/idp/app.py +++ b/src/eduid/webapp/idp/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/idp/idp_authn.py b/src/eduid/webapp/idp/idp_authn.py index 084c9a9e1..4b5e4f0d1 100644 --- a/src/eduid/webapp/idp/idp_authn.py +++ b/src/eduid/webapp/idp/idp_authn.py @@ -6,9 +6,10 @@ from __future__ import annotations import logging +from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Mapping, Optional, Sequence +from typing import Any, Optional from bson import ObjectId from pydantic import BaseModel, ConfigDict, Field diff --git a/src/eduid/webapp/idp/idp_saml.py b/src/eduid/webapp/idp/idp_saml.py index d2f1fb5d7..05f07455c 100644 --- a/src/eduid/webapp/idp/idp_saml.py +++ b/src/eduid/webapp/idp/idp_saml.py @@ -1,9 +1,10 @@ import logging import typing from base64 import b64encode +from collections.abc import Mapping from dataclasses import dataclass, field from hashlib import sha1 -from typing import Any, Mapping, NewType, Optional, Union +from typing import Any, NewType, Optional, Union import saml2.server from pydantic import BaseModel diff --git a/src/eduid/webapp/idp/known_device.py b/src/eduid/webapp/idp/known_device.py index 55ac864e2..953e3e6dd 100644 --- a/src/eduid/webapp/idp/known_device.py +++ b/src/eduid/webapp/idp/known_device.py @@ -2,8 +2,9 @@ import json import logging +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, NewType, Optional +from typing import Any, NewType, Optional from uuid import uuid4 import nacl diff --git a/src/eduid/webapp/idp/login_context.py b/src/eduid/webapp/idp/login_context.py index 0f3d272a3..822532b87 100644 --- a/src/eduid/webapp/idp/login_context.py +++ b/src/eduid/webapp/idp/login_context.py @@ -1,6 +1,7 @@ import logging from abc import ABC -from typing import Optional, Sequence, TypeVar +from collections.abc import Sequence +from typing import Optional, TypeVar from urllib.parse import urlencode from pydantic import BaseModel, ConfigDict diff --git a/src/eduid/webapp/idp/logout.py b/src/eduid/webapp/idp/logout.py index 94fbd239e..4ee2e4c01 100644 --- a/src/eduid/webapp/idp/logout.py +++ b/src/eduid/webapp/idp/logout.py @@ -59,7 +59,7 @@ Code handling Single Log Out requests. """ -from typing import Sequence +from collections.abc import Sequence import saml2 from flask import request diff --git a/src/eduid/webapp/idp/mischttp.py b/src/eduid/webapp/idp/mischttp.py index 14f6e9bb4..92d5db970 100644 --- a/src/eduid/webapp/idp/mischttp.py +++ b/src/eduid/webapp/idp/mischttp.py @@ -63,8 +63,9 @@ import logging import pprint +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from typing import Any, Mapping, Optional, Sequence +from typing import Any, Optional import user_agents from bleach import clean diff --git a/src/eduid/webapp/idp/other_device/db.py b/src/eduid/webapp/idp/other_device/db.py index 1f5a7b8f2..bc49c35cf 100644 --- a/src/eduid/webapp/idp/other_device/db.py +++ b/src/eduid/webapp/idp/other_device/db.py @@ -4,8 +4,9 @@ import logging import typing import uuid +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from bson import ObjectId from flask import request diff --git a/src/eduid/webapp/idp/other_device/device1.py b/src/eduid/webapp/idp/other_device/device1.py index ed2fab186..5bd841517 100644 --- a/src/eduid/webapp/idp/other_device/device1.py +++ b/src/eduid/webapp/idp/other_device/device1.py @@ -1,8 +1,9 @@ import base64 import logging +from collections.abc import Mapping from datetime import datetime from io import BytesIO -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union import nacl import nacl.encoding diff --git a/src/eduid/webapp/idp/other_device/device2.py b/src/eduid/webapp/idp/other_device/device2.py index ca10ed7c2..92b81b0b3 100644 --- a/src/eduid/webapp/idp/other_device/device2.py +++ b/src/eduid/webapp/idp/other_device/device2.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Mapping from datetime import datetime -from typing import Any, Mapping +from typing import Any from flask import request, url_for diff --git a/src/eduid/webapp/idp/sso_cache.py b/src/eduid/webapp/idp/sso_cache.py index a034bdde9..df722dd07 100644 --- a/src/eduid/webapp/idp/sso_cache.py +++ b/src/eduid/webapp/idp/sso_cache.py @@ -59,8 +59,9 @@ import time import warnings from collections import deque +from collections.abc import Mapping from threading import Lock -from typing import Any, Deque, Mapping, Optional, cast +from typing import Any, Deque, Optional, cast from eduid.userdb.db import BaseDB from eduid.userdb.exceptions import EduIDDBError diff --git a/src/eduid/webapp/idp/sso_session.py b/src/eduid/webapp/idp/sso_session.py index 39b4205c1..91cd6aeb8 100644 --- a/src/eduid/webapp/idp/sso_session.py +++ b/src/eduid/webapp/idp/sso_session.py @@ -3,8 +3,9 @@ import logging import typing import uuid +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, NewType, Optional +from typing import Any, NewType, Optional from bson import ObjectId from pydantic import BaseModel, ConfigDict, Field diff --git a/src/eduid/webapp/idp/tests/test_SSO.py b/src/eduid/webapp/idp/tests/test_SSO.py index 1cf2c152d..5d8f5a967 100644 --- a/src/eduid/webapp/idp/tests/test_SSO.py +++ b/src/eduid/webapp/idp/tests/test_SSO.py @@ -1,7 +1,8 @@ #!/usr/bin/python import logging -from typing import Mapping, Optional, Sequence, Union +from collections.abc import Mapping, Sequence +from typing import Optional, Union from uuid import uuid4 import saml2.server diff --git a/src/eduid/webapp/idp/tests/test_api.py b/src/eduid/webapp/idp/tests/test_api.py index 4d405c8df..f66bfb2d5 100644 --- a/src/eduid/webapp/idp/tests/test_api.py +++ b/src/eduid/webapp/idp/tests/test_api.py @@ -1,9 +1,10 @@ import json import logging import re +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import PurePath -from typing import Any, Mapping, Optional, Tuple, Union +from typing import Any, Optional, Tuple, Union from unittest.mock import MagicMock, patch from bson import ObjectId diff --git a/src/eduid/webapp/idp/views/mfa_auth.py b/src/eduid/webapp/idp/views/mfa_auth.py index 77e5e6ac6..645886848 100644 --- a/src/eduid/webapp/idp/views/mfa_auth.py +++ b/src/eduid/webapp/idp/views/mfa_auth.py @@ -1,6 +1,7 @@ +from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass -from typing import Any, Mapping, Optional +from typing import Any, Optional from flask import Blueprint diff --git a/src/eduid/webapp/idp/views/tou.py b/src/eduid/webapp/idp/views/tou.py index aba5b9051..5f7d62a89 100644 --- a/src/eduid/webapp/idp/views/tou.py +++ b/src/eduid/webapp/idp/views/tou.py @@ -1,4 +1,5 @@ -from typing import Optional, Sequence +from collections.abc import Sequence +from typing import Optional from bson import ObjectId from flask import Blueprint diff --git a/src/eduid/webapp/jsconfig/app.py b/src/eduid/webapp/jsconfig/app.py index fef66543a..ec2f91ca8 100644 --- a/src/eduid/webapp/jsconfig/app.py +++ b/src/eduid/webapp/jsconfig/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/jsconfig/tests/test_app.py b/src/eduid/webapp/jsconfig/tests/test_app.py index 11980dd1a..27aba72e9 100644 --- a/src/eduid/webapp/jsconfig/tests/test_app.py +++ b/src/eduid/webapp/jsconfig/tests/test_app.py @@ -1,7 +1,8 @@ import json import os +from collections.abc import Mapping from pathlib import PurePath -from typing import Any, Mapping, cast +from typing import Any, cast from eduid.common.config.parsers import load_config from eduid.common.testing_base import normalised_data diff --git a/src/eduid/webapp/ladok/app.py b/src/eduid/webapp/ladok/app.py index 950f45fb0..ede19178f 100644 --- a/src/eduid/webapp/ladok/app.py +++ b/src/eduid/webapp/ladok/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/ladok/client.py b/src/eduid/webapp/ladok/client.py index 746979eab..b84c54c62 100644 --- a/src/eduid/webapp/ladok/client.py +++ b/src/eduid/webapp/ladok/client.py @@ -1,6 +1,7 @@ import logging +from collections.abc import Mapping from datetime import datetime -from typing import Mapping, Optional +from typing import Optional import requests from pydantic import BaseModel, ConfigDict, Field, ValidationError diff --git a/src/eduid/webapp/ladok/tests/test_app.py b/src/eduid/webapp/ladok/tests/test_app.py index c26f46450..30b943061 100644 --- a/src/eduid/webapp/ladok/tests/test_app.py +++ b/src/eduid/webapp/ladok/tests/test_app.py @@ -1,5 +1,6 @@ import json -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from unittest.mock import MagicMock, patch from uuid import UUID, uuid4 diff --git a/src/eduid/webapp/letter_proofing/app.py b/src/eduid/webapp/letter_proofing/app.py index a079ed599..3245200b5 100644 --- a/src/eduid/webapp/letter_proofing/app.py +++ b/src/eduid/webapp/letter_proofing/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/letter_proofing/pdf.py b/src/eduid/webapp/letter_proofing/pdf.py index b9cb14313..5dec45a6a 100644 --- a/src/eduid/webapp/letter_proofing/pdf.py +++ b/src/eduid/webapp/letter_proofing/pdf.py @@ -1,8 +1,8 @@ import logging +from collections.abc import Mapping from datetime import datetime, timedelta from io import BytesIO, StringIO from pathlib import Path -from typing import Mapping from xhtml2pdf import pisa diff --git a/src/eduid/webapp/letter_proofing/tests/test_app.py b/src/eduid/webapp/letter_proofing/tests/test_app.py index e140da6f1..d61857ac3 100644 --- a/src/eduid/webapp/letter_proofing/tests/test_app.py +++ b/src/eduid/webapp/letter_proofing/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, AnyStr, Mapping, Optional +from typing import Any, AnyStr, Optional from unittest.mock import MagicMock, Mock, patch from werkzeug.test import TestResponse diff --git a/src/eduid/webapp/lookup_mobile_proofing/app.py b/src/eduid/webapp/lookup_mobile_proofing/app.py index 6cc26bf66..26d364d8a 100644 --- a/src/eduid/webapp/lookup_mobile_proofing/app.py +++ b/src/eduid/webapp/lookup_mobile_proofing/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py b/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py index aada3fa3e..19cfbe5af 100644 --- a/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py +++ b/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import MagicMock, patch from eduid.common.config.base import EduidEnvironment diff --git a/src/eduid/webapp/oidc_proofing/app.py b/src/eduid/webapp/oidc_proofing/app.py index d3334df6e..0ee37e236 100644 --- a/src/eduid/webapp/oidc_proofing/app.py +++ b/src/eduid/webapp/oidc_proofing/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/oidc_proofing/helpers.py b/src/eduid/webapp/oidc_proofing/helpers.py index 054576960..b35e227f0 100644 --- a/src/eduid/webapp/oidc_proofing/helpers.py +++ b/src/eduid/webapp/oidc_proofing/helpers.py @@ -1,7 +1,8 @@ import json +from collections.abc import Mapping from datetime import datetime, timedelta from enum import unique -from typing import Any, Mapping +from typing import Any import requests from flask import render_template diff --git a/src/eduid/webapp/oidc_proofing/views.py b/src/eduid/webapp/oidc_proofing/views.py index 9c2bc30a1..152d01dc3 100644 --- a/src/eduid/webapp/oidc_proofing/views.py +++ b/src/eduid/webapp/oidc_proofing/views.py @@ -1,7 +1,8 @@ import base64 import binascii +from collections.abc import Mapping from io import BytesIO -from typing import Any, Mapping, Union +from typing import Any, Union import qrcode import qrcode.image.svg diff --git a/src/eduid/webapp/orcid/app.py b/src/eduid/webapp/orcid/app.py index b4d61f0b7..d141905d6 100644 --- a/src/eduid/webapp/orcid/app.py +++ b/src/eduid/webapp/orcid/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/orcid/tests/test_app.py b/src/eduid/webapp/orcid/tests/test_app.py index df83b9f6e..4f5831a0e 100644 --- a/src/eduid/webapp/orcid/tests/test_app.py +++ b/src/eduid/webapp/orcid/tests/test_app.py @@ -1,5 +1,6 @@ import json -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from unittest.mock import MagicMock, patch from eduid.userdb.orcid import OidcAuthorization, OidcIdToken, Orcid diff --git a/src/eduid/webapp/personal_data/app.py b/src/eduid/webapp/personal_data/app.py index 73df43c0f..405340d52 100644 --- a/src/eduid/webapp/personal_data/app.py +++ b/src/eduid/webapp/personal_data/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/personal_data/tests/test_app.py b/src/eduid/webapp/personal_data/tests/test_app.py index c110f1232..6cf333c32 100644 --- a/src/eduid/webapp/personal_data/tests/test_app.py +++ b/src/eduid/webapp/personal_data/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import patch from werkzeug.test import TestResponse diff --git a/src/eduid/webapp/phone/app.py b/src/eduid/webapp/phone/app.py index ab31872ef..d5bf60905 100644 --- a/src/eduid/webapp/phone/app.py +++ b/src/eduid/webapp/phone/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/phone/tests/test_app.py b/src/eduid/webapp/phone/tests/test_app.py index 92025578f..4dae8bb07 100644 --- a/src/eduid/webapp/phone/tests/test_app.py +++ b/src/eduid/webapp/phone/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import MagicMock, patch from urllib.parse import quote_plus diff --git a/src/eduid/webapp/reset_password/app.py b/src/eduid/webapp/reset_password/app.py index 9b7701ef4..1ddcc97f6 100644 --- a/src/eduid/webapp/reset_password/app.py +++ b/src/eduid/webapp/reset_password/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/reset_password/helpers.py b/src/eduid/webapp/reset_password/helpers.py index b64e74da7..c4777aec3 100644 --- a/src/eduid/webapp/reset_password/helpers.py +++ b/src/eduid/webapp/reset_password/helpers.py @@ -1,8 +1,9 @@ import math +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta from enum import unique -from typing import Any, Mapping, Optional, Union +from typing import Any, Optional, Union from flask import render_template diff --git a/src/eduid/webapp/reset_password/tests/test_app.py b/src/eduid/webapp/reset_password/tests/test_app.py index f2357bbd8..3b072f754 100644 --- a/src/eduid/webapp/reset_password/tests/test_app.py +++ b/src/eduid/webapp/reset_password/tests/test_app.py @@ -1,6 +1,7 @@ import datetime import json -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from unittest.mock import Mock, patch from urllib.parse import quote_plus diff --git a/src/eduid/webapp/security/app.py b/src/eduid/webapp/security/app.py index e9ec97695..1d67c81d9 100644 --- a/src/eduid/webapp/security/app.py +++ b/src/eduid/webapp/security/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from fido_mds import FidoMetadataStore from flask import current_app diff --git a/src/eduid/webapp/security/tests/test_app.py b/src/eduid/webapp/security/tests/test_app.py index 3fa554133..4cd1b9c59 100644 --- a/src/eduid/webapp/security/tests/test_app.py +++ b/src/eduid/webapp/security/tests/test_app.py @@ -1,6 +1,7 @@ import json +from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import MagicMock, patch from eduid.common.config.base import FrontendAction diff --git a/src/eduid/webapp/security/tests/test_change_password.py b/src/eduid/webapp/security/tests/test_change_password.py index 38099ca82..6b5384088 100644 --- a/src/eduid/webapp/security/tests/test_change_password.py +++ b/src/eduid/webapp/security/tests/test_change_password.py @@ -1,5 +1,6 @@ import json -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from unittest.mock import patch from eduid.common.config.base import FrontendAction diff --git a/src/eduid/webapp/security/tests/test_webauthn.py b/src/eduid/webapp/security/tests/test_webauthn.py index b636c435d..da951e56d 100644 --- a/src/eduid/webapp/security/tests/test_webauthn.py +++ b/src/eduid/webapp/security/tests/test_webauthn.py @@ -1,6 +1,7 @@ import base64 import json -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional from unittest.mock import patch from fido2.webauthn import AttestationObject, AuthenticatorAttachment, CollectedClientData diff --git a/src/eduid/webapp/security/views/webauthn.py b/src/eduid/webapp/security/views/webauthn.py index d3a9304e6..833dce68f 100644 --- a/src/eduid/webapp/security/views/webauthn.py +++ b/src/eduid/webapp/security/views/webauthn.py @@ -1,5 +1,6 @@ import base64 -from typing import Optional, Sequence +from collections.abc import Sequence +from typing import Optional from fido2 import cbor from fido2.server import Fido2Server, PublicKeyCredentialRpEntity diff --git a/src/eduid/webapp/signup/app.py b/src/eduid/webapp/signup/app.py index ca56665c2..13201503f 100644 --- a/src/eduid/webapp/signup/app.py +++ b/src/eduid/webapp/signup/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app diff --git a/src/eduid/webapp/signup/tests/test_app.py b/src/eduid/webapp/signup/tests/test_app.py index 27a70a1d4..ac855df9d 100644 --- a/src/eduid/webapp/signup/tests/test_app.py +++ b/src/eduid/webapp/signup/tests/test_app.py @@ -1,9 +1,10 @@ import json import logging +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Mapping, Optional +from typing import Any, Optional from unittest.mock import MagicMock, patch from uuid import uuid4 diff --git a/src/eduid/webapp/support/app.py b/src/eduid/webapp/support/app.py index 91cba2620..ae38a32f0 100644 --- a/src/eduid/webapp/support/app.py +++ b/src/eduid/webapp/support/app.py @@ -1,5 +1,6 @@ import operator -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from flask import current_app from jinja2.exceptions import UndefinedError diff --git a/src/eduid/webapp/support/helpers.py b/src/eduid/webapp/support/helpers.py index 7beeb16c1..49e96426d 100644 --- a/src/eduid/webapp/support/helpers.py +++ b/src/eduid/webapp/support/helpers.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from functools import wraps -from typing import Any, Callable, TypeVar +from typing import Any, TypeVar from flask import abort diff --git a/src/eduid/webapp/support/tests/test_app.py b/src/eduid/webapp/support/tests/test_app.py index f7b9e8fb3..f1265086d 100644 --- a/src/eduid/webapp/support/tests/test_app.py +++ b/src/eduid/webapp/support/tests/test_app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from eduid.webapp.common.api.testing import EduidAPITestCase from eduid.webapp.support.app import SupportApp, support_init_app diff --git a/src/eduid/webapp/support/views.py b/src/eduid/webapp/support/views.py index dda2c35cd..f17be9a3f 100644 --- a/src/eduid/webapp/support/views.py +++ b/src/eduid/webapp/support/views.py @@ -1,4 +1,5 @@ -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from flask import Blueprint, render_template, request diff --git a/src/eduid/webapp/svipe_id/app.py b/src/eduid/webapp/svipe_id/app.py index 40a72c329..5ff5ad6b3 100644 --- a/src/eduid/webapp/svipe_id/app.py +++ b/src/eduid/webapp/svipe_id/app.py @@ -1,4 +1,5 @@ -from typing import Any, Mapping, Optional, cast +from collections.abc import Mapping +from typing import Any, Optional, cast from authlib.integrations.flask_client import OAuth from flask import current_app diff --git a/src/eduid/workers/am/fetcher_registry.py b/src/eduid/workers/am/fetcher_registry.py index 5659aacfa..3508f8dd7 100644 --- a/src/eduid/workers/am/fetcher_registry.py +++ b/src/eduid/workers/am/fetcher_registry.py @@ -5,7 +5,7 @@ See the file LICENSE.txt for full license statement. """ -from typing import Iterable +from collections.abc import Iterable import eduid.workers.am.ams from eduid.workers.am.ams import AttributeFetcher diff --git a/src/eduid/workers/amapi/context_request.py b/src/eduid/workers/amapi/context_request.py index f5f2ff701..e1b36d87a 100644 --- a/src/eduid/workers/amapi/context_request.py +++ b/src/eduid/workers/amapi/context_request.py @@ -1,7 +1,8 @@ __author__ = "masv" +from collections.abc import Callable from dataclasses import asdict, dataclass -from typing import Callable, Union +from typing import Union from fastapi import Request, Response from fastapi.routing import APIRoute diff --git a/src/eduid/workers/amapi/routers/status.py b/src/eduid/workers/amapi/routers/status.py index 4172f0929..b6bf7efcc 100644 --- a/src/eduid/workers/amapi/routers/status.py +++ b/src/eduid/workers/amapi/routers/status.py @@ -1,5 +1,5 @@ +from collections.abc import Mapping from os import environ -from typing import Mapping from fastapi import Response diff --git a/src/eduid/workers/amapi/routers/utils/status.py b/src/eduid/workers/amapi/routers/utils/status.py index 292f49ce9..d2dad6f2c 100644 --- a/src/eduid/workers/amapi/routers/utils/status.py +++ b/src/eduid/workers/amapi/routers/utils/status.py @@ -1,7 +1,8 @@ import sys +from collections.abc import Mapping from dataclasses import dataclass, field, replace from datetime import datetime, timedelta -from typing import Any, Mapping, Optional +from typing import Any, Optional from fastapi import Response diff --git a/src/eduid/workers/job_runner/app.py b/src/eduid/workers/job_runner/app.py index 07c6e60c3..cfc51fc53 100644 --- a/src/eduid/workers/job_runner/app.py +++ b/src/eduid/workers/job_runner/app.py @@ -1,5 +1,6 @@ +from collections.abc import Callable from contextlib import asynccontextmanager -from typing import Callable, Optional +from typing import Optional from fastapi import FastAPI diff --git a/src/eduid/workers/job_runner/status.py b/src/eduid/workers/job_runner/status.py index 7cc7a0642..0aa0900fd 100644 --- a/src/eduid/workers/job_runner/status.py +++ b/src/eduid/workers/job_runner/status.py @@ -1,5 +1,5 @@ +from collections.abc import Mapping from os import environ -from typing import Mapping from fastapi import APIRouter, Response from pydantic import BaseModel diff --git a/src/eduid/workers/lookup_mobile/utilities.py b/src/eduid/workers/lookup_mobile/utilities.py index d1e14b13d..a7b7c02ec 100644 --- a/src/eduid/workers/lookup_mobile/utilities.py +++ b/src/eduid/workers/lookup_mobile/utilities.py @@ -1,7 +1,8 @@ __author__ = "mathiashedstrom" import re -from typing import Optional, Sequence +from collections.abc import Sequence +from typing import Optional import phonenumbers diff --git a/src/eduid/workers/msg/decorators.py b/src/eduid/workers/msg/decorators.py index ab32a05c4..5f75032b1 100644 --- a/src/eduid/workers/msg/decorators.py +++ b/src/eduid/workers/msg/decorators.py @@ -1,6 +1,7 @@ +from collections.abc import Callable from datetime import datetime from inspect import isclass -from typing import Any, Callable, Optional +from typing import Any, Optional from eduid.userdb.db import MongoDB diff --git a/src/eduid/workers/msg/utils.py b/src/eduid/workers/msg/utils.py index 0a4b7ecef..e4a704ae5 100644 --- a/src/eduid/workers/msg/utils.py +++ b/src/eduid/workers/msg/utils.py @@ -4,7 +4,8 @@ import os from collections import OrderedDict -from typing import Any, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Optional def is_deregistered(person: Optional[dict[str, Any]]) -> bool: From db2cb5848bc555ee1f68567593d07c4f2ed0c372 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 14:15:48 +0000 Subject: [PATCH 11/23] fix some more imports Some typing imports are unnecessary with our current python version. Specifically use builtins.type for those classes that have a variable named type --- src/eduid/maccapi/helpers.py | 3 +-- src/eduid/maccapi/model/api.py | 4 ++-- src/eduid/satosa/entity_category/auth_server.py | 4 ++-- src/eduid/userdb/maccapi/userdb.py | 4 ++-- src/eduid/userdb/scimapi/common.py | 13 +++++++------ src/eduid/vccs/server/db.py | 9 +++++---- src/eduid/webapp/common/api/helpers.py | 8 ++++---- src/eduid/webapp/common/authn/utils.py | 4 ++-- src/eduid/webapp/common/session/namespaces.py | 4 ++-- src/eduid/webapp/idp/sso_cache.py | 4 ++-- src/eduid/webapp/idp/tests/test_api.py | 4 ++-- src/eduid/webapp/security/helpers.py | 8 ++++---- 12 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/eduid/maccapi/helpers.py b/src/eduid/maccapi/helpers.py index 90e9205a7..db955ce9c 100644 --- a/src/eduid/maccapi/helpers.py +++ b/src/eduid/maccapi/helpers.py @@ -1,5 +1,4 @@ from datetime import datetime, timedelta -from typing import List from bson import ObjectId @@ -22,7 +21,7 @@ class UnableToAddPassword(Exception): def list_users(context: Context, data_owner: str): - managed_accounts: List[ManagedAccount] = context.db.get_users(data_owner=data_owner) + managed_accounts: list[ManagedAccount] = context.db.get_users(data_owner=data_owner) context.logger.info(f"Listing {managed_accounts.__len__()} users") return managed_accounts diff --git a/src/eduid/maccapi/model/api.py b/src/eduid/maccapi/model/api.py index 5cfbffbac..6e527bb15 100644 --- a/src/eduid/maccapi/model/api.py +++ b/src/eduid/maccapi/model/api.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from pydantic import BaseModel @@ -16,7 +16,7 @@ class ApiResponseBaseModel(BaseModel): class UserListResponse(ApiResponseBaseModel): - users: List[ApiUser] + users: list[ApiUser] class UserCreateRequest(BaseModel): diff --git a/src/eduid/satosa/entity_category/auth_server.py b/src/eduid/satosa/entity_category/auth_server.py index 9fb8a62ea..e9327729a 100644 --- a/src/eduid/satosa/entity_category/auth_server.py +++ b/src/eduid/satosa/entity_category/auth_server.py @@ -1,6 +1,6 @@ __author__ = "lundberg" -from typing import Any, Dict, List +from typing import Any SUNET_AUTH_SERVER = [ "eduPersonPrincipalName", @@ -24,7 +24,7 @@ } # These restrictions are parsed (and validated) into a list of saml2.assertion.EntityCategoryRule instances. -RESTRICTIONS: List[Dict[str, Any]] = [ +RESTRICTIONS: list[dict[str, Any]] = [ # Example of conversion of some of the rules in RELEASE to this new format: # # { diff --git a/src/eduid/userdb/maccapi/userdb.py b/src/eduid/userdb/maccapi/userdb.py index 1cec553fc..2067c5417 100644 --- a/src/eduid/userdb/maccapi/userdb.py +++ b/src/eduid/userdb/maccapi/userdb.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import List, Optional +from typing import Optional from pydantic import field_validator @@ -54,7 +54,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_managed_accounts", collect def user_from_dict(cls, data: TUserDbDocument) -> ManagedAccount: return ManagedAccount.from_dict(data) - def get_users(self, data_owner: str) -> List[ManagedAccount]: + def get_users(self, data_owner: str) -> list[ManagedAccount]: """ :return: A list of users with the given organization """ diff --git a/src/eduid/userdb/scimapi/common.py b/src/eduid/userdb/scimapi/common.py index ddf0410f7..24860f18e 100644 --- a/src/eduid/userdb/scimapi/common.py +++ b/src/eduid/userdb/scimapi/common.py @@ -1,11 +1,12 @@ from __future__ import annotations +import builtins import uuid from abc import ABC from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional, Type, Union +from typing import Any, Optional, Union from uuid import UUID from eduid.common.models.scim_base import EmailType, PhoneNumberType, WeakVersion @@ -34,7 +35,7 @@ def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod - def from_dict(cls: Type[ScimApiProfile], data: Mapping[str, Any]) -> ScimApiProfile: + def from_dict(cls: type[ScimApiProfile], data: Mapping[str, Any]) -> ScimApiProfile: _attributes = data.get("attributes", {}) _data = data.get("data", {}) return cls(attributes=_attributes, data=_data) @@ -50,7 +51,7 @@ def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod - def from_dict(cls: Type[ScimApiLinkedAccount], data: Mapping[str, Any]) -> ScimApiLinkedAccount: + def from_dict(cls: type[ScimApiLinkedAccount], data: Mapping[str, Any]) -> ScimApiLinkedAccount: _issuer = data.get("issuer") if not isinstance(_issuer, str): raise ValueError("ScimApiLinkedAccount.issuer must be a string") @@ -76,7 +77,7 @@ def to_dict(self) -> dict[str, Optional[str]]: return asdict(self) @classmethod - def from_dict(cls: Type[ScimApiName], data: Mapping[str, Optional[str]]) -> ScimApiName: + def from_dict(cls: type[ScimApiName], data: Mapping[str, Optional[str]]) -> ScimApiName: return cls(**data) @@ -94,7 +95,7 @@ def to_dict(self) -> dict[str, Union[Optional[str], bool]]: return res @classmethod - def from_dict(cls: Type[ScimApiEmail], data: Mapping[str, Any]) -> ScimApiEmail: + def from_dict(cls: builtins.type[ScimApiEmail], data: Mapping[str, Any]) -> ScimApiEmail: email_type = None if data.get("type") is not None: email_type = EmailType(data["type"]) @@ -115,7 +116,7 @@ def to_dict(self) -> dict[str, Union[Optional[str], bool]]: return res @classmethod - def from_dict(cls: Type[ScimApiPhoneNumber], data: Mapping[str, Any]) -> ScimApiPhoneNumber: + def from_dict(cls: builtins.type[ScimApiPhoneNumber], data: Mapping[str, Any]) -> ScimApiPhoneNumber: number_type = None if data.get("type") is not None: number_type = PhoneNumberType(data["type"]) diff --git a/src/eduid/vccs/server/db.py b/src/eduid/vccs/server/db.py index cbc6fffed..7bcf8a277 100644 --- a/src/eduid/vccs/server/db.py +++ b/src/eduid/vccs/server/db.py @@ -1,9 +1,10 @@ from __future__ import annotations +import builtins from collections.abc import Mapping from dataclasses import asdict, field from enum import Enum, unique -from typing import Any, Optional, Type, Union, cast +from typing import Any, Optional, Union, cast from bson import ObjectId from loguru import logger @@ -48,7 +49,7 @@ class Credential: obj_id: ObjectId = field(default_factory=ObjectId) @classmethod - def _from_dict(cls: Type[Credential], data: Mapping[str, Any]) -> Credential: + def _from_dict(cls: builtins.type[Credential], data: Mapping[str, Any]) -> Credential: """Construct element from a data dict in database format.""" _data = dict(data) # to not modify callers data @@ -114,7 +115,7 @@ class _PasswordCredentialRequired: @dataclass(config=CredentialPydanticConfig) class PasswordCredential(Credential, _PasswordCredentialRequired): @classmethod - def from_dict(cls: Type[PasswordCredential], data: Mapping[str, Any]) -> PasswordCredential: + def from_dict(cls: type[PasswordCredential], data: Mapping[str, Any]) -> PasswordCredential: # This indirection provides the correct return type for this subclass return cast(PasswordCredential, cls._from_dict(data)) @@ -128,7 +129,7 @@ class _RevokedCredentialRequired: @dataclass(config=CredentialPydanticConfig) class RevokedCredential(Credential, _RevokedCredentialRequired): @classmethod - def from_dict(cls: Type[RevokedCredential], data: Mapping[str, Any]) -> RevokedCredential: + def from_dict(cls: type[RevokedCredential], data: Mapping[str, Any]) -> RevokedCredential: # This indirection provides the correct return type for this subclass return cast(RevokedCredential, cls._from_dict(data)) diff --git a/src/eduid/webapp/common/api/helpers.py b/src/eduid/webapp/common/api/helpers.py index 3a557e088..671be574d 100644 --- a/src/eduid/webapp/common/api/helpers.py +++ b/src/eduid/webapp/common/api/helpers.py @@ -1,6 +1,6 @@ import warnings from dataclasses import dataclass -from typing import Any, List, Optional, TypeVar, Union, cast, overload +from typing import Any, Optional, TypeVar, Union, cast, overload from flask import current_app, render_template, request @@ -55,7 +55,7 @@ def get_marked_given_name(given_name: str, given_name_marking: Optional[str]) -> return given_name # cheating with indexing - _given_names: List[Optional[str]] = [None] + _given_names: list[Optional[str]] = [None] for name in given_name.split(): if "-" in name: # hyphenated names are counted separately @@ -63,12 +63,12 @@ def get_marked_given_name(given_name: str, given_name_marking: Optional[str]) -> else: _given_names.append(name) - _optional_marked_names: List[Optional[str]] = [] + _optional_marked_names: list[Optional[str]] = [] for i in given_name_marking: _optional_marked_names.append(_given_names[int(i)]) # remove None values # i.e. 0 index and hyphenated names second part placeholder - _marked_names: List[str] = [name for name in _optional_marked_names if name is not None] + _marked_names: list[str] = [name for name in _optional_marked_names if name is not None] if "-".join(_marked_names) in given_name: return "-".join(_marked_names) else: diff --git a/src/eduid/webapp/common/authn/utils.py b/src/eduid/webapp/common/authn/utils.py index edc9e5772..01b50f569 100644 --- a/src/eduid/webapp/common/authn/utils.py +++ b/src/eduid/webapp/common/authn/utils.py @@ -3,7 +3,7 @@ import os.path import sys from collections.abc import Sequence -from typing import Optional, Tuple +from typing import Optional from saml2 import server from saml2.config import SPConfig @@ -113,7 +113,7 @@ def init_pysaml2(cfgfile: str) -> server.Server: def get_authn_for_action( config: FrontendActionMixin, frontend_action: FrontendAction -) -> Tuple[Optional[SP_AuthnRequest], AuthnParameters]: +) -> tuple[Optional[SP_AuthnRequest], AuthnParameters]: authn_params = config.frontend_action_authn_parameters.get(frontend_action) if authn_params is None: raise BadConfiguration(f"No authn parameters for frontend action {frontend_action}") diff --git a/src/eduid/webapp/common/session/namespaces.py b/src/eduid/webapp/common/session/namespaces.py index 9394fb8fe..dae114ca8 100644 --- a/src/eduid/webapp/common/session/namespaces.py +++ b/src/eduid/webapp/common/session/namespaces.py @@ -6,7 +6,7 @@ from copy import deepcopy from datetime import datetime from enum import Enum, unique -from typing import Any, List, NewType, Optional, TypeVar, Union, cast +from typing import Any, NewType, Optional, TypeVar, Union, cast from uuid import uuid4 from fido2.webauthn import AuthenticatorAttachment @@ -263,7 +263,7 @@ class SP_AuthnRequest(BaseAuthnRequest): credentials_used: list[ElementKey] = Field(default_factory=list) # proofing_credential_id is the credential being person-proofed, when doing that proofing_credential_id: Optional[ElementKey] = None - req_authn_ctx: List[str] = Field( + req_authn_ctx: list[str] = Field( default_factory=list ) # the authentication contexts requested for this authentication asserted_authn_ctx: Optional[str] = None # the authentication contexts asserted for this authentication diff --git a/src/eduid/webapp/idp/sso_cache.py b/src/eduid/webapp/idp/sso_cache.py index df722dd07..b39bdc758 100644 --- a/src/eduid/webapp/idp/sso_cache.py +++ b/src/eduid/webapp/idp/sso_cache.py @@ -61,7 +61,7 @@ from collections import deque from collections.abc import Mapping from threading import Lock -from typing import Any, Deque, Optional, cast +from typing import Any, Optional, cast from eduid.userdb.db import BaseDB from eduid.userdb.exceptions import EduIDDBError @@ -114,7 +114,7 @@ def __init__(self, name: str, logger: Optional[logging.Logger], ttl: int, lock: self.ttl = ttl self.name = name self._data: dict[SSOSessionId, Any] = {} - self._ages: Deque[tuple[float, SSOSessionId]] = deque() + self._ages: deque[tuple[float, SSOSessionId]] = deque() self.lock = lock if self.lock is None: self.lock = cast(Lock, NoOpLock()) # intentionally lie to mypy diff --git a/src/eduid/webapp/idp/tests/test_api.py b/src/eduid/webapp/idp/tests/test_api.py index f66bfb2d5..82958e80e 100644 --- a/src/eduid/webapp/idp/tests/test_api.py +++ b/src/eduid/webapp/idp/tests/test_api.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import PurePath -from typing import Any, Optional, Tuple, Union +from typing import Any, Optional, Union from unittest.mock import MagicMock, patch from bson import ObjectId @@ -379,7 +379,7 @@ def get_sso_session(self, sso_cookie_val: str) -> Optional[SSOSession]: return None return self.app.sso_sessions.get_session(SSOSessionId(sso_cookie_val)) - def add_test_user_tou(self, eppn: Optional[str] = None, version: Optional[str] = None) -> Tuple[IdPUser, ToUEvent]: + def add_test_user_tou(self, eppn: Optional[str] = None, version: Optional[str] = None) -> tuple[IdPUser, ToUEvent]: """Utility function to add a valid ToU to the default test user""" if version is None: version = self.app.conf.tou_version diff --git a/src/eduid/webapp/security/helpers.py b/src/eduid/webapp/security/helpers.py index 08f0c4db0..4f66595fe 100644 --- a/src/eduid/webapp/security/helpers.py +++ b/src/eduid/webapp/security/helpers.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from enum import unique from functools import cache -from typing import Any, List, Optional +from typing import Any, Optional from fido_mds.models.webauthn import AttestationFormat @@ -252,7 +252,7 @@ def update_user_official_name(security_user: SecurityUser, navet_data: NavetData @cache def get_approved_security_keys() -> dict[str, Any]: # a way to reuse is_authenticator_mfa_approved() from security app - parsed_entries: List[AuthenticatorInformation] = [] + parsed_entries: list[AuthenticatorInformation] = [] for metadata_entry in current_app.fido_mds.metadata.entries: user_verification_methods = [ detail.user_verification_method @@ -278,14 +278,14 @@ def get_approved_security_keys() -> dict[str, Any]: ) parsed_entries.append(authenticator_info) - approved_keys_list: List[str] = [] + approved_keys_list: list[str] = [] for entry in parsed_entries: if entry.description and is_authenticator_mfa_approved(entry): approved_keys_list.append(entry.description) # remove case-insensitive duplicates from list, while maintaining the original case marker = set() - unique_approved_keys_list: List[str] = [] + unique_approved_keys_list: list[str] = [] for key in approved_keys_list: lower_case_key = key.lower() From 6d0d9855c6932db7aef77291d80229e4d46f327c Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 14:36:02 +0000 Subject: [PATCH 12/23] update type annotations for union types Use the updated | operator for union types in python 3.10 and later PEP604 --- .../common/clients/amapi_client/testing.py | 4 +- src/eduid/common/clients/gnap_client/base.py | 10 +- .../common/clients/gnap_client/testing.py | 4 +- .../common/clients/scim_client/scim_client.py | 9 +- src/eduid/common/config/base.py | 76 ++++++------ src/eduid/common/config/parsers/__init__.py | 8 +- src/eduid/common/config/parsers/decorators.py | 6 +- src/eduid/common/fastapi/context_request.py | 5 +- src/eduid/common/fastapi/exceptions.py | 13 +-- src/eduid/common/fastapi/utils.py | 12 +- src/eduid/common/misc/encoders.py | 4 +- src/eduid/common/models/amapi_user.py | 13 +-- src/eduid/common/models/bearer_token.py | 18 +-- src/eduid/common/models/gnap_models.py | 50 ++++---- src/eduid/common/models/jose_models.py | 85 +++++++------- src/eduid/common/models/saml_models.py | 3 +- src/eduid/common/models/scim_base.py | 30 ++--- src/eduid/common/models/scim_invite.py | 19 ++- src/eduid/common/models/scim_user.py | 6 +- src/eduid/common/rpc/am_relay.py | 3 +- src/eduid/common/rpc/lookup_mobile_relay.py | 4 +- src/eduid/common/rpc/mail_relay.py | 7 +- src/eduid/common/rpc/msg_relay.py | 53 +++++---- src/eduid/common/testing_base.py | 6 +- src/eduid/graphdb/db.py | 8 +- src/eduid/graphdb/groupdb/db.py | 24 ++-- src/eduid/graphdb/groupdb/group.py | 23 ++-- src/eduid/graphdb/groupdb/user.py | 5 +- src/eduid/graphdb/helpers.py | 3 +- src/eduid/graphdb/testing.py | 4 +- src/eduid/graphdb/tests/test_group.py | 5 +- src/eduid/graphdb/tests/test_groupdb.py | 5 +- src/eduid/maccapi/app.py | 10 +- src/eduid/maccapi/config.py | 3 +- src/eduid/maccapi/context.py | 3 +- src/eduid/maccapi/context_request.py | 6 +- src/eduid/maccapi/model/api.py | 8 +- src/eduid/queue/db/change_event.py | 14 +-- src/eduid/queue/db/client.py | 3 +- src/eduid/queue/db/queue_item.py | 8 +- src/eduid/queue/db/worker.py | 8 +- src/eduid/queue/testing.py | 4 +- src/eduid/queue/workers/mail.py | 6 +- src/eduid/queue/workers/scim_event.py | 4 +- src/eduid/queue/workers/sink.py | 8 +- src/eduid/satosa/scimapi/accr.py | 9 +- src/eduid/satosa/scimapi/scim_attributes.py | 20 ++-- src/eduid/satosa/scimapi/static_attributes.py | 6 +- src/eduid/satosa/scimapi/stepup.py | 32 +++--- src/eduid/scimapi/app.py | 6 +- src/eduid/scimapi/config.py | 9 +- src/eduid/scimapi/context.py | 11 +- src/eduid/scimapi/context_request.py | 11 +- src/eduid/scimapi/exceptions.py | 17 ++- src/eduid/scimapi/models/event.py | 12 +- src/eduid/scimapi/models/group.py | 4 +- src/eduid/scimapi/routers/events.py | 3 +- src/eduid/scimapi/routers/invites.py | 3 +- src/eduid/scimapi/routers/users.py | 3 +- src/eduid/scimapi/routers/utils/events.py | 4 +- src/eduid/scimapi/routers/utils/groups.py | 11 +- src/eduid/scimapi/routers/utils/invites.py | 4 +- src/eduid/scimapi/routers/utils/users.py | 8 +- src/eduid/scimapi/search.py | 5 +- src/eduid/scimapi/test-scripts/scim-util.py | 36 +++--- src/eduid/scimapi/testing.py | 22 ++-- src/eduid/scimapi/tests/test_authn.py | 4 +- src/eduid/scimapi/tests/test_groupdb.py | 3 +- src/eduid/scimapi/tests/test_scimevent.py | 4 +- src/eduid/scimapi/tests/test_scimgroup.py | 14 +-- src/eduid/scimapi/tests/test_sciminvite.py | 10 +- src/eduid/scimapi/tests/test_scimuser.py | 14 +-- src/eduid/userdb/admin/__init__.py | 8 +- src/eduid/userdb/authninfo.py | 3 +- src/eduid/userdb/credentials/base.py | 4 +- src/eduid/userdb/credentials/external.py | 4 +- src/eduid/userdb/credentials/fido.py | 10 +- src/eduid/userdb/credentials/list.py | 4 +- src/eduid/userdb/db/async_db.py | 22 ++-- src/eduid/userdb/db/base.py | 8 +- src/eduid/userdb/db/sync_db.py | 28 ++--- src/eduid/userdb/element.py | 18 +-- src/eduid/userdb/event.py | 6 +- src/eduid/userdb/group_management/db.py | 4 +- src/eduid/userdb/group_management/state.py | 4 +- src/eduid/userdb/identity.py | 26 ++--- src/eduid/userdb/idp/db.py | 3 +- src/eduid/userdb/idp/user.py | 8 +- src/eduid/userdb/logs/db.py | 3 +- src/eduid/userdb/logs/element.py | 24 ++-- src/eduid/userdb/maccapi/userdb.py | 3 +- src/eduid/userdb/meta.py | 8 +- src/eduid/userdb/orcid.py | 22 ++-- src/eduid/userdb/proofing/db.py | 12 +- src/eduid/userdb/proofing/element.py | 12 +- src/eduid/userdb/proofing/state.py | 6 +- src/eduid/userdb/reset_password/db.py | 8 +- src/eduid/userdb/reset_password/element.py | 6 +- src/eduid/userdb/reset_password/state.py | 8 +- src/eduid/userdb/scimapi/basedb.py | 8 +- src/eduid/userdb/scimapi/common.py | 38 +++--- src/eduid/userdb/scimapi/eventdb.py | 8 +- src/eduid/userdb/scimapi/groupdb.py | 30 +++-- src/eduid/userdb/scimapi/invitedb.py | 12 +- src/eduid/userdb/scimapi/userdb.py | 14 +-- src/eduid/userdb/security/db.py | 8 +- src/eduid/userdb/security/state.py | 8 +- src/eduid/userdb/signup/invite.py | 16 +-- src/eduid/userdb/signup/invitedb.py | 6 +- src/eduid/userdb/signup/user.py | 8 +- src/eduid/userdb/signup/userdb.py | 7 +- src/eduid/userdb/support/db.py | 6 +- src/eduid/userdb/support/models.py | 5 +- src/eduid/userdb/testing/__init__.py | 4 +- src/eduid/userdb/testing/temp_instance.py | 4 +- src/eduid/userdb/user.py | 30 ++--- src/eduid/userdb/user_cleaner/db.py | 3 +- src/eduid/userdb/userdb.py | 18 +-- src/eduid/userdb/util.py | 4 +- src/eduid/vccs/client/__init__.py | 6 +- src/eduid/vccs/server/config.py | 6 +- src/eduid/vccs/server/db.py | 4 +- src/eduid/vccs/server/endpoints/misc.py | 3 +- src/eduid/vccs/server/hasher.py | 8 +- src/eduid/vccs/server/password.py | 3 +- src/eduid/vccs/server/run.py | 4 +- src/eduid/webapp/authn/app.py | 4 +- src/eduid/webapp/authn/tests/test_authn.py | 8 +- src/eduid/webapp/authn/views.py | 13 +-- src/eduid/webapp/bankid/acs_actions.py | 4 +- src/eduid/webapp/bankid/app.py | 4 +- src/eduid/webapp/bankid/helpers.py | 6 +- src/eduid/webapp/bankid/proofing.py | 9 +- src/eduid/webapp/bankid/saml_session_info.py | 4 +- src/eduid/webapp/bankid/settings/common.py | 6 +- src/eduid/webapp/bankid/tests/test_app.py | 48 ++++---- src/eduid/webapp/bankid/views.py | 19 ++- src/eduid/webapp/common/api/app.py | 4 +- src/eduid/webapp/common/api/checks.py | 22 ++-- src/eduid/webapp/common/api/decorators.py | 15 +-- src/eduid/webapp/common/api/errors.py | 3 +- src/eduid/webapp/common/api/exceptions.py | 6 +- src/eduid/webapp/common/api/helpers.py | 16 +-- src/eduid/webapp/common/api/messages.py | 16 ++- src/eduid/webapp/common/api/request.py | 6 +- src/eduid/webapp/common/api/sanitation.py | 6 +- .../webapp/common/api/schemas/password.py | 8 +- src/eduid/webapp/common/api/testing.py | 68 +++++------ .../webapp/common/api/tests/test_backdoor.py | 4 +- .../common/api/tests/test_nin_helpers.py | 4 +- src/eduid/webapp/common/api/translation.py | 6 +- src/eduid/webapp/common/api/utils.py | 8 +- src/eduid/webapp/common/api/views/status.py | 6 +- src/eduid/webapp/common/authn/acs_registry.py | 15 +-- src/eduid/webapp/common/authn/eduid_saml2.py | 22 ++-- src/eduid/webapp/common/authn/middleware.py | 4 +- .../webapp/common/authn/tests/responses.py | 5 +- src/eduid/webapp/common/authn/utils.py | 9 +- src/eduid/webapp/common/authn/vccs.py | 42 +++---- src/eduid/webapp/common/proofing/base.py | 22 ++-- src/eduid/webapp/common/proofing/methods.py | 29 +++-- .../webapp/common/proofing/saml_helpers.py | 7 +- src/eduid/webapp/common/proofing/testing.py | 16 +-- .../webapp/common/session/eduid_session.py | 28 ++--- src/eduid/webapp/common/session/logindata.py | 3 +- src/eduid/webapp/common/session/namespaces.py | 108 +++++++++--------- .../webapp/common/session/redis_session.py | 12 +- src/eduid/webapp/eidas/acs_actions.py | 4 +- src/eduid/webapp/eidas/app.py | 4 +- src/eduid/webapp/eidas/helpers.py | 10 +- src/eduid/webapp/eidas/proofing.py | 26 ++--- src/eduid/webapp/eidas/saml_session_info.py | 7 +- src/eduid/webapp/eidas/settings/common.py | 5 +- src/eduid/webapp/eidas/tests/test_app.py | 52 ++++----- src/eduid/webapp/eidas/views.py | 19 ++- src/eduid/webapp/email/app.py | 4 +- src/eduid/webapp/email/tests/test_app.py | 18 +-- src/eduid/webapp/freja_eid/app.py | 4 +- src/eduid/webapp/freja_eid/helpers.py | 6 +- src/eduid/webapp/freja_eid/proofing.py | 5 +- src/eduid/webapp/freja_eid/settings/common.py | 4 +- src/eduid/webapp/freja_eid/tests/test_app.py | 8 +- src/eduid/webapp/freja_eid/views.py | 15 ++- src/eduid/webapp/group_management/app.py | 4 +- src/eduid/webapp/group_management/helpers.py | 8 +- .../group_management/settings/common.py | 4 +- .../webapp/group_management/tests/test_app.py | 4 +- src/eduid/webapp/idp/app.py | 6 +- src/eduid/webapp/idp/exceptions.py | 4 +- src/eduid/webapp/idp/helpers.py | 3 +- src/eduid/webapp/idp/idp_authn.py | 22 ++-- src/eduid/webapp/idp/idp_saml.py | 14 +-- src/eduid/webapp/idp/known_device.py | 16 +-- src/eduid/webapp/idp/login.py | 16 +-- src/eduid/webapp/idp/login_context.py | 44 +++---- src/eduid/webapp/idp/mfa_action.py | 3 +- src/eduid/webapp/idp/mischttp.py | 10 +- src/eduid/webapp/idp/other_device/db.py | 32 +++--- src/eduid/webapp/idp/other_device/device1.py | 8 +- src/eduid/webapp/idp/other_device/helpers.py | 7 +- src/eduid/webapp/idp/schemas.py | 4 +- src/eduid/webapp/idp/service.py | 10 +- src/eduid/webapp/idp/settings/common.py | 13 +-- src/eduid/webapp/idp/sso_cache.py | 10 +- src/eduid/webapp/idp/sso_session.py | 12 +- src/eduid/webapp/idp/tests/test_SSO.py | 21 ++-- src/eduid/webapp/idp/tests/test_api.py | 66 +++++------ src/eduid/webapp/idp/util.py | 5 +- src/eduid/webapp/idp/views/mfa_auth.py | 16 +-- src/eduid/webapp/idp/views/misc.py | 6 +- src/eduid/webapp/idp/views/next.py | 18 +-- src/eduid/webapp/idp/views/pw_auth.py | 4 +- src/eduid/webapp/idp/views/saml.py | 12 +- src/eduid/webapp/idp/views/tou.py | 7 +- src/eduid/webapp/idp/views/use_other.py | 19 ++- src/eduid/webapp/jsconfig/app.py | 4 +- src/eduid/webapp/jsconfig/settings/jsapps.py | 16 ++- src/eduid/webapp/ladok/app.py | 4 +- src/eduid/webapp/ladok/client.py | 27 +++-- src/eduid/webapp/letter_proofing/app.py | 4 +- .../webapp/letter_proofing/settings/common.py | 4 +- .../webapp/letter_proofing/tests/test_app.py | 20 ++-- .../webapp/lookup_mobile_proofing/app.py | 4 +- .../webapp/lookup_mobile_proofing/helpers.py | 5 +- .../lookup_mobile_proofing/tests/test_app.py | 6 +- src/eduid/webapp/oidc_proofing/app.py | 4 +- src/eduid/webapp/oidc_proofing/views.py | 6 +- src/eduid/webapp/orcid/app.py | 4 +- src/eduid/webapp/orcid/helpers.py | 3 +- src/eduid/webapp/personal_data/app.py | 4 +- src/eduid/webapp/personal_data/helpers.py | 5 +- .../webapp/personal_data/tests/test_app.py | 12 +- src/eduid/webapp/personal_data/views.py | 4 +- src/eduid/webapp/phone/app.py | 4 +- src/eduid/webapp/phone/tests/test_app.py | 16 +-- src/eduid/webapp/phone/views.py | 4 +- src/eduid/webapp/reset_password/app.py | 4 +- src/eduid/webapp/reset_password/helpers.py | 14 +-- .../webapp/reset_password/tests/test_app.py | 46 ++++---- .../reset_password/views/reset_password.py | 12 +- src/eduid/webapp/security/app.py | 4 +- src/eduid/webapp/security/helpers.py | 10 +- src/eduid/webapp/security/settings/common.py | 3 +- src/eduid/webapp/security/tests/test_app.py | 12 +- .../security/tests/test_change_password.py | 8 +- .../webapp/security/tests/test_webauthn.py | 10 +- .../webapp/security/views/change_password.py | 4 +- src/eduid/webapp/security/views/webauthn.py | 3 +- .../webapp/security/webauthn_proofing.py | 11 +- src/eduid/webapp/signup/app.py | 4 +- src/eduid/webapp/signup/helpers.py | 9 +- src/eduid/webapp/signup/settings/common.py | 7 +- src/eduid/webapp/signup/tests/test_app.py | 72 ++++++------ src/eduid/webapp/signup/views.py | 5 +- src/eduid/webapp/support/app.py | 6 +- src/eduid/webapp/svipe_id/app.py | 4 +- src/eduid/webapp/svipe_id/helpers.py | 10 +- src/eduid/webapp/svipe_id/proofing.py | 5 +- src/eduid/webapp/svipe_id/settings/common.py | 4 +- src/eduid/webapp/svipe_id/tests/test_app.py | 8 +- src/eduid/webapp/svipe_id/views.py | 15 ++- src/eduid/workers/am/ams/common.py | 4 +- src/eduid/workers/am/consistency_checks.py | 4 +- src/eduid/workers/am/tasks.py | 6 +- src/eduid/workers/am/testing.py | 8 +- src/eduid/workers/amapi/app.py | 6 +- src/eduid/workers/amapi/config.py | 4 +- src/eduid/workers/amapi/context_request.py | 5 +- .../workers/amapi/routers/utils/status.py | 12 +- .../workers/amapi/routers/utils/users.py | 16 +-- src/eduid/workers/amapi/tests/test_user.py | 6 +- src/eduid/workers/job_runner/app.py | 7 +- src/eduid/workers/job_runner/config.py | 28 ++--- .../client/mobile_lookup_client.py | 8 +- src/eduid/workers/lookup_mobile/tasks.py | 6 +- src/eduid/workers/lookup_mobile/utilities.py | 7 +- src/eduid/workers/msg/cache.py | 4 +- src/eduid/workers/msg/decorators.py | 6 +- src/eduid/workers/msg/tasks.py | 26 ++--- src/eduid/workers/msg/utils.py | 12 +- 280 files changed, 1586 insertions(+), 1741 deletions(-) diff --git a/src/eduid/common/clients/amapi_client/testing.py b/src/eduid/common/clients/amapi_client/testing.py index 1ea55fc25..d6b911dad 100644 --- a/src/eduid/common/clients/amapi_client/testing.py +++ b/src/eduid/common/clients/amapi_client/testing.py @@ -1,12 +1,10 @@ -from typing import Optional - import respx from eduid.common.clients.gnap_client.testing import MockedSyncAuthAPIMixin class MockedAMAPIMixin(MockedSyncAuthAPIMixin): - def start_mock_amapi(self, access_token_value: Optional[str] = None): + def start_mock_amapi(self, access_token_value: str | None = None): self.start_mock_auth_api(access_token_value=access_token_value) self.mocked_users = respx.mock(base_url="http://localhost", assert_all_called=False) diff --git a/src/eduid/common/clients/gnap_client/base.py b/src/eduid/common/clients/gnap_client/base.py index e88043886..5aac156d0 100644 --- a/src/eduid/common/clients/gnap_client/base.py +++ b/src/eduid/common/clients/gnap_client/base.py @@ -2,7 +2,7 @@ from abc import ABC from collections.abc import Coroutine from datetime import datetime, timedelta -from typing import Annotated, Any, Optional, Union +from typing import Annotated, Any from httpx import Request from jwcrypto.jwk import JWK @@ -40,13 +40,13 @@ class GNAPClientAuthData(BaseModel): authn_server_verify: bool = True key_name: str client_jwk: ClientJWK - access: list[Union[str, Access]] = Field(default_factory=list) + access: list[str | Access] = Field(default_factory=list) default_access_token_expires_in: timedelta = timedelta(hours=1) class GNAPBearerTokenMixin(ABC): _auth_data: GNAPClientAuthData - _bearer_token: Optional[str] = None + _bearer_token: str | None = None _bearer_token_expires_at: datetime = utc_now() @property @@ -87,8 +87,8 @@ def _set_bearer_token(self, grant_response: GrantResponse) -> None: def _has_bearer_token(self) -> bool: return self._bearer_token is not None and self._bearer_token_expires_at > utc_now() - def _request_bearer_token(self) -> Union[GrantResponse, Coroutine[Any, Any, GrantResponse]]: + def _request_bearer_token(self) -> GrantResponse | Coroutine[Any, Any, GrantResponse]: raise NotImplementedError() - def _add_authz_header(self, request: Request) -> Union[None, Coroutine[Any, Any, None]]: + def _add_authz_header(self, request: Request) -> None | Coroutine[Any, Any, None]: raise NotImplementedError() diff --git a/src/eduid/common/clients/gnap_client/testing.py b/src/eduid/common/clients/gnap_client/testing.py index b0438ae68..62cf913ce 100644 --- a/src/eduid/common/clients/gnap_client/testing.py +++ b/src/eduid/common/clients/gnap_client/testing.py @@ -1,5 +1,3 @@ -from typing import Optional - import respx from httpx import Response @@ -9,7 +7,7 @@ class MockedSyncAuthAPIMixin: - def start_mock_auth_api(self, access_token_value: Optional[str] = None): + def start_mock_auth_api(self, access_token_value: str | None = None): if access_token_value is None: access_token_value = "mock_jwt" self.mocked_auth_api = respx.mock(base_url="http://localhost/auth", assert_all_called=False) diff --git a/src/eduid/common/clients/scim_client/scim_client.py b/src/eduid/common/clients/scim_client/scim_client.py index 92bda7cff..d66eaa2a7 100644 --- a/src/eduid/common/clients/scim_client/scim_client.py +++ b/src/eduid/common/clients/scim_client/scim_client.py @@ -1,5 +1,4 @@ import logging -from typing import Optional, Union from uuid import UUID import httpx @@ -56,7 +55,7 @@ def users_endpoint(self) -> str: def invites_endpoint(self) -> str: return urlappend(self.scim_api_url, "Invites") - def _get(self, endpoint: str, obj_id: Union[UUID, str]) -> httpx.Response: + def _get(self, endpoint: str, obj_id: UUID | str) -> httpx.Response: if isinstance(obj_id, UUID): obj_id = str(obj_id) return self.get(urlappend(endpoint, obj_id)) @@ -74,7 +73,7 @@ def _search(self, endpoint: str, _filter: str, start_index: int = 1, count: int ret = self.post(search_endpoint, content=search_req.json()) return ListResponse.parse_raw(ret.text) - def get_user(self, user_id: Union[UUID, str]) -> UserResponse: + def get_user(self, user_id: UUID | str) -> UserResponse: ret = self._get(self.users_endpoint, obj_id=user_id) return UserResponse.parse_raw(ret.text) @@ -86,7 +85,7 @@ def update_user(self, user: UserUpdateRequest, version: WeakVersion) -> UserResp ret = self._update(self.users_endpoint, update_request=user, version=version) return UserResponse.parse_raw(ret.text) - def get_user_by_external_id(self, external_id: Optional[str]) -> Optional[UserResponse]: + def get_user_by_external_id(self, external_id: str | None) -> UserResponse | None: if external_id is None: return None @@ -98,7 +97,7 @@ def get_user_by_external_id(self, external_id: Optional[str]) -> Optional[UserRe raise SCIMError(f'More than one user with external_id "{external_id}"') return self.get_user(user_id=ret.resources[0]["id"]) - def get_invite(self, invite_id: Union[UUID, str]) -> InviteResponse: + def get_invite(self, invite_id: UUID | str) -> InviteResponse: ret = self._get(self.invites_endpoint, obj_id=invite_id) return InviteResponse.parse_raw(ret.text) diff --git a/src/eduid/common/config/base.py b/src/eduid/common/config/base.py index 7541d9ef4..2c86bc401 100644 --- a/src/eduid/common/config/base.py +++ b/src/eduid/common/config/base.py @@ -9,7 +9,7 @@ from enum import Enum, unique from pathlib import Path from re import Pattern -from typing import IO, Annotated, Any, Optional, TypeVar, Union +from typing import IO, Annotated, Any, TypeVar import pkg_resources from pydantic import AfterValidator, BaseModel, ConfigDict, Field @@ -45,25 +45,25 @@ class CeleryConfig(BaseModel): "eduid_lookup_mobile.tasks.*": {"queue": "lookup_mobile"}, } ) - mongo_uri: Optional[str] = None + mongo_uri: str | None = None class RedisConfig(BaseModel): port: int = 6379 db: int = 0 - host: Optional[str] = None - sentinel_hosts: Optional[Sequence[str]] = None - sentinel_service_name: Optional[str] = None + host: str | None = None + sentinel_hosts: Sequence[str] | None = None + sentinel_service_name: str | None = None class CookieConfig(BaseModel): key: str - domain: Optional[str] = None + domain: str | None = None path: str = "/" secure: bool = True httponly: bool = True - samesite: Optional[str] = None - max_age_seconds: Optional[int] = None # None means this is a session cookie + samesite: str | None = None + max_age_seconds: int | None = None # None means this is a session cookie TRootConfigSubclass = TypeVar("TRootConfigSubclass", bound="RootConfig") @@ -105,21 +105,21 @@ class WorkerConfig(RootConfig): audit: bool = False celery: CeleryConfig = Field(default_factory=CeleryConfig) - mongo_uri: Optional[str] = None + mongo_uri: str | None = None transaction_audit: bool = False class CORSMixin(BaseModel): - cors_allow_headers: Union[str, list[str]] = "*" + cors_allow_headers: str | list[str] = "*" cors_always_send: bool = True cors_automatic_options: bool = True - cors_expose_headers: Optional[Union[str, list[str]]] = None + cors_expose_headers: str | list[str] | None = None cors_intercept_exceptions: bool = True - cors_max_age: Optional[Union[timedelta, int, str]] = None - cors_methods: Union[str, list[str]] = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"] + cors_max_age: timedelta | int | str | None = None + cors_methods: str | list[str] = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"] # The origin(s) to allow requests from. An origin configured here that matches the value of the Origin header in a # preflight OPTIONS request is returned as the value of the Access-Control-Allow-Origin response header. - cors_origins: Union[str, list[str], Pattern] = [r"^eduid\.se$", r".*\.eduid\.se$"] + cors_origins: str | list[str] | Pattern = [r"^eduid\.se$", r".*\.eduid\.se$"] # The series of regular expression and (optionally) associated CORS options to be applied to the given resource # path. # If the value is a dictionary, it’s keys must be regular expressions matching resources, and the values must be @@ -128,7 +128,7 @@ class CORSMixin(BaseModel): # app-wide configured options are applied. # If the argument is a string, it is expected to be a regular expression matching resources for which the app-wide # configured options are applied. - cors_resources: Union[dict[Union[str, Pattern], CORSMixin], list[Union[str, Pattern]], Union[str, Pattern]] = r"/*" + cors_resources: dict[str | Pattern, CORSMixin] | list[str | Pattern] | str | Pattern = r"/*" cors_send_wildcard: bool = False cors_supports_credentials: bool = True cors_vary_header: bool = True @@ -149,13 +149,13 @@ class FlaskConfig(CORSMixin): # explicitly enable or disable the propagation of exceptions. # If not set or explicitly set to None this is implicitly true if either # TESTING or DEBUG is true. - propagate_exceptions: Optional[bool] = None + propagate_exceptions: bool | None = None # By default if the application is in debug mode the request context is not # popped on exceptions to enable debuggers to introspect the data. This can be # disabled by this key. You can also use this setting to force-enable it for non # debug execution which might be useful to debug production applications (but # also very risky). - preserve_context_on_exception: Optional[bool] = None + preserve_context_on_exception: bool | None = None # If this is set to True Flask will not execute the error handlers of HTTP # exceptions but instead treat the exception like any other and bubble it through # the exception stack. This is helpful for hairy debugging situations where you @@ -167,15 +167,15 @@ class FlaskConfig(CORSMixin): # consistency. Since it’s nice for debugging to know why exactly it failed this # flag can be used to debug those situations. If this config is set to True you # will get a regular traceback instead. - trap_bad_request_errors: Optional[bool] = None - secret_key: Optional[str] = None + trap_bad_request_errors: bool | None = None + secret_key: str | None = None # the name of the session cookie session_cookie_name: str = "sessid" # Sets a cookie with legacy SameSite=None, the SameSite key and value is omitted cookies_samesite_compat: list = Field(default=[("sessid", "sessid_samesite_compat")]) # the domain for the session cookie. If this is not set, the cookie will # be valid for all subdomains of SERVER_NAME. - session_cookie_domain: Optional[str] = None + session_cookie_domain: str | None = None # the path for the session cookie. If this is not set the cookie will be valid # for all of APPLICATION_ROOT or if that is not set for '/'. session_cookie_path: str = "/" @@ -186,7 +186,7 @@ class FlaskConfig(CORSMixin): # Restrict how cookies are sent with requests from external sites. # Can be set to None (samesite key omitted), 'None', 'Lax' (recommended) or 'Strict'. # Defaults to None - session_cookie_samesite: Optional[str] = None + session_cookie_samesite: str | None = None # the lifetime of a permanent session as datetime.timedelta object. # Starting with Flask 0.8 this can also be an integer representing seconds. permanent_session_lifetime: int = 14400 # 4 hours @@ -200,7 +200,7 @@ class FlaskConfig(CORSMixin): # the name and port number of the server. Required for subdomain support (e.g.: 'myapp.dev:5000') Note that localhost # does not support subdomains so setting this to “localhost” does not help. Setting a SERVER_NAME also by default # enables URL generation without a request context but with an application context. - server_name: Optional[str] = None + server_name: str | None = None # If the application does not occupy a whole domain or subdomain this can be set to the path where the application is # configured to live. This is for session cookie as path value. If domains are used, this should be None. application_root: str = "/" @@ -209,8 +209,8 @@ class FlaskConfig(CORSMixin): preferred_url_scheme: str = "http" # If set to a value in bytes, Flask will reject incoming requests with a # content length greater than this by returning a 413 status code. - max_content_length: Optional[int] = None - templates_auto_reload: Optional[bool] = None + max_content_length: int | None = None + templates_auto_reload: bool | None = None explain_template_loading: bool = False max_cookie_size: int = 4093 babel_translation_directories: list[str] = ["translations"] @@ -232,10 +232,10 @@ class ProfilingConfig(BaseModel): """ model_config = ConfigDict(arbitrary_types_allowed=True) # allow IO type - stream: Optional[IO] = None + stream: IO | None = None sort_by: Iterable[str] = Field(default_factory=lambda: ("time", "calls")) - restrictions: Iterable[Union[str, int, float]] = Field(default_factory=tuple) - profile_dir: Optional[str] = None + restrictions: Iterable[str | int | float] = Field(default_factory=tuple) + profile_dir: str | None = None filename_format: str = "{method}.{path}.{elapsed:.0f}ms.{time:.0f}.prof" @@ -247,9 +247,9 @@ class WebauthnConfigMixin2(BaseModel): class MagicCookieMixin(BaseModel): environment: EduidEnvironment = EduidEnvironment.production # code to set in a "magic" cookie to bypass various verifications in test environments. - magic_cookie: Optional[str] = None + magic_cookie: str | None = None # name of the magic cookie - magic_cookie_name: Optional[str] = None + magic_cookie_name: str | None = None class CeleryConfigMixin(BaseModel): @@ -271,7 +271,7 @@ class LoggingConfigMixin(BaseModel): class StatsConfigMixin(BaseModel): app_name: str - stats_host: Optional[str] = None + stats_host: str | None = None stats_port: int = 8125 @@ -308,7 +308,7 @@ class CaptchaConfigMixin(BaseModel): class AmConfigMixin(CeleryConfigMixin): """Config used by AmRelay""" - am_relay_for_override: Optional[str] = None # only set this if f'eduid_{app_name}' is not right + am_relay_for_override: str | None = None # only set this if f'eduid_{app_name}' is not right class MailConfigMixin(CeleryConfigMixin): @@ -337,7 +337,7 @@ class PasswordConfigMixin(BaseModel): class ErrorsConfigMixin(BaseModel): - errors_url_template: Optional[str] = None + errors_url_template: str | None = None class Pysaml2SPConfigMixin(BaseModel): @@ -454,17 +454,17 @@ class ProofingConfigMixin(FrontendActionMixin): # sweden connect trust_framework: TrustFramework = TrustFramework.SWECONN required_loa: list[str] = Field(default=["loa3"]) - freja_idp: Optional[str] = None + freja_idp: str | None = None # eidas foreign_trust_framework: TrustFramework = TrustFramework.EIDAS foreign_required_loa: list[str] = Field(default=["eidas-nf-low", "eidas-nf-sub", "eidas-nf-high"]) - foreign_identity_idp: Optional[str] = None + foreign_identity_idp: str | None = None # bankid bankid_trust_framework: TrustFramework = TrustFramework.BANKID bankid_required_loa: list[str] = Field(default=["uncertified-loa3"]) - bankid_idp: Optional[str] = None + bankid_idp: str | None = None # identity proofing freja_proofing_version: str = Field(default="2023v1") @@ -483,7 +483,7 @@ class EduIDBaseAppConfig(RootConfig, LoggingConfigMixin, StatsConfigMixin, Redis available_languages: Mapping[str, str] = Field(default={"en": "English", "sv": "Svenska"}) flask: FlaskConfig = Field(default_factory=FlaskConfig) # settings for optional profiling of the application - profiling: Optional[ProfilingConfig] = None + profiling: ProfilingConfig | None = None mongo_uri: str # Allow list of URLs that do not need authentication. Unauthenticated requests # for these URLs will be served, rather than redirected to the authn service. @@ -503,7 +503,7 @@ class EduIDBaseAppConfig(RootConfig, LoggingConfigMixin, StatsConfigMixin, Redis class DataOwnerConfig(BaseModel): - db_name: Optional[str] = None + db_name: str | None = None notify: list[str] = [] @@ -523,7 +523,7 @@ class AuthnBearerTokenConfig(RootConfig): scope_mapping: dict[ScopeName, DataOwnerName] = Field(default={}) # Allow someone with scope x to sudo to scope y scope_sudo: dict[ScopeName, set[ScopeName]] = Field(default={}) - requested_access_type: Optional[str] = None + requested_access_type: str | None = None required_saml_assurance_level: list[str] = Field(default=["http://www.swamid.se/policy/assurance/al3"]) # group name to match saml entitlement for authorization account_manager_default_group: str = "Account Managers" diff --git a/src/eduid/common/config/parsers/__init__.py b/src/eduid/common/config/parsers/__init__.py index dddb14dec..ea1d8a428 100644 --- a/src/eduid/common/config/parsers/__init__.py +++ b/src/eduid/common/config/parsers/__init__.py @@ -3,7 +3,7 @@ import sys from collections.abc import Mapping from pathlib import Path -from typing import Any, Optional +from typing import Any import yaml @@ -19,7 +19,7 @@ def load_config( typ: type[TRootConfigSubclass], ns: str, app_name: str, - test_config: Optional[Mapping[str, Any]] = None, + test_config: Mapping[str, Any] | None = None, ) -> TRootConfigSubclass: """Figure out where to load configuration from, and do it.""" print("loading config...", file=sys.stderr) @@ -64,7 +64,7 @@ def load_config( return res -def _choose_parser() -> Optional[BaseConfigParser]: +def _choose_parser() -> BaseConfigParser | None: """ Choose a parser to use for this app. @@ -73,7 +73,7 @@ def _choose_parser() -> Optional[BaseConfigParser]: :return: Config parser instance """ - parser: Optional[BaseConfigParser] = None + parser: BaseConfigParser | None = None yaml_file = os.environ.get("EDUID_CONFIG_YAML") if yaml_file: try: diff --git a/src/eduid/common/config/parsers/decorators.py b/src/eduid/common/config/parsers/decorators.py index 734171888..ce686111f 100644 --- a/src/eduid/common/config/parsers/decorators.py +++ b/src/eduid/common/config/parsers/decorators.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from functools import wraps from string import Template -from typing import Any, Optional +from typing import Any from nacl import encoding, exceptions, secret @@ -32,7 +32,7 @@ def read_secret_key(key_name: str) -> bytes: return encoding.URLSafeBase64Encoder.decode(f.readline()) -def init_secret_box(key_name: Optional[str] = None, secret_key: Optional[bytes] = None) -> secret.SecretBox: +def init_secret_box(key_name: str | None = None, secret_key: bytes | None = None) -> secret.SecretBox: """ :param key_name: Key file name :param secret_key: 32 bytes of secret data @@ -118,7 +118,7 @@ def interpolate_list(config_dict: dict[str, Any], sub_list: list) -> list: return sub_list -def interpolate_config(config_dict: dict[str, Any], sub_dict: Optional[dict[str, Any]] = None) -> dict[str, Any]: +def interpolate_config(config_dict: dict[str, Any], sub_dict: dict[str, Any] | None = None) -> dict[str, Any]: """ :param config_dict: Configuration dictionary :param sub_dict: Sub configuration dictionary diff --git a/src/eduid/common/fastapi/context_request.py b/src/eduid/common/fastapi/context_request.py index a54a19926..c4953b007 100644 --- a/src/eduid/common/fastapi/context_request.py +++ b/src/eduid/common/fastapi/context_request.py @@ -1,6 +1,5 @@ from collections.abc import Callable from dataclasses import asdict, dataclass -from typing import Union from fastapi import Request, Response from fastapi.routing import APIRoute @@ -33,7 +32,7 @@ def context(self, context: Context): class ContextRequestMixin: @staticmethod - def make_context_request(request: Union[Request, ContextRequest], context_class: type[Context]) -> ContextRequest: + def make_context_request(request: Request | ContextRequest, context_class: type[Context]) -> ContextRequest: if not isinstance(request, ContextRequest): request = ContextRequest(context_class=context_class, scope=request.scope, receive=request.receive) return request @@ -50,7 +49,7 @@ class ContextRequestRoute(APIRoute, ContextRequestMixin): def get_route_handler(self) -> Callable: original_route_handler = super().get_route_handler() - async def context_route_handler(request: Union[Request, ContextRequest]) -> Response: + async def context_route_handler(request: Request | ContextRequest) -> Response: request = self.make_context_request(request=request, context_class=self.contextClass) return await original_route_handler(request) diff --git a/src/eduid/common/fastapi/exceptions.py b/src/eduid/common/fastapi/exceptions.py index 92d7c2c82..e150698a9 100644 --- a/src/eduid/common/fastapi/exceptions.py +++ b/src/eduid/common/fastapi/exceptions.py @@ -2,7 +2,6 @@ import logging import uuid -from typing import Optional, Union from fastapi import Request, status from fastapi.exception_handlers import http_exception_handler @@ -15,15 +14,15 @@ class ErrorDetail(BaseModel): - detail: Optional[Union[str, dict, list]] = None - status: Optional[int] = None + detail: str | dict | list | None = None + status: int | None = None class ErrorResponse(JSONResponse): media_type = "application/json" -async def unexpected_error_handler(req: Request, exc: Exception) -> Union[Response, JSONResponse]: +async def unexpected_error_handler(req: Request, exc: Exception) -> Response | JSONResponse: error_id = uuid.uuid4() logger.error(f"unexpected error {error_id}: {req.method} {req.url.path} - {exc}") http_exception = StarletteHTTPException( @@ -56,17 +55,17 @@ class HTTPErrorDetail(Exception): def __init__( self, status_code: int, - detail: Optional[str] = None, + detail: str | None = None, ): self._error_detail = ErrorDetail(detail=detail, status=status_code) - self._extra_headers: Optional[dict] = None + self._extra_headers: dict | None = None @property def error_detail(self) -> ErrorDetail: return self._error_detail @property - def extra_headers(self) -> Optional[dict]: + def extra_headers(self) -> dict | None: return self._extra_headers @extra_headers.setter diff --git a/src/eduid/common/fastapi/utils.py b/src/eduid/common/fastapi/utils.py index f710d505d..820878ab8 100644 --- a/src/eduid/common/fastapi/utils.py +++ b/src/eduid/common/fastapi/utils.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field, replace from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from fastapi import Response @@ -21,9 +21,9 @@ class SimpleCacheItem: @dataclass class FailCountItem: first_failure: datetime = field(repr=False) - restart_at: Optional[datetime] = None - restart_interval: Optional[int] = None - exit_at: Optional[datetime] = None + restart_at: datetime | None = None + restart_interval: int | None = None + exit_at: datetime | None = None count: int = 0 def __str__(self): @@ -34,7 +34,7 @@ def __str__(self): FAILURE_INFO: dict[str, FailCountItem] = dict() -def log_failure_info(req: ContextRequest, key: str, msg: str, exc: Optional[Exception] = None) -> None: +def log_failure_info(req: ContextRequest, key: str, msg: str, exc: Exception | None = None) -> None: if key not in FAILURE_INFO: FAILURE_INFO[key] = FailCountItem(first_failure=datetime.utcnow()) FAILURE_INFO[key].count += 1 @@ -68,7 +68,7 @@ def check_restart(key, restart: int, terminate: int) -> bool: return res -def get_cached_response(req: ContextRequest, resp: Response, key: str) -> Optional[Mapping[str, Any]]: +def get_cached_response(req: ContextRequest, resp: Response, key: str) -> Mapping[str, Any] | None: cache_for_seconds = req.app.context.config.status_cache_seconds resp.headers["Cache-Control"] = f"public,max-age={cache_for_seconds}" diff --git a/src/eduid/common/misc/encoders.py b/src/eduid/common/misc/encoders.py index f3a8e09e7..96f659469 100644 --- a/src/eduid/common/misc/encoders.py +++ b/src/eduid/common/misc/encoders.py @@ -1,7 +1,7 @@ import json from datetime import datetime, timedelta from enum import Enum -from typing import Any, Union +from typing import Any from bson import ObjectId from pydantic import TypeAdapter @@ -11,7 +11,7 @@ class EduidJSONEncoder(json.JSONEncoder): # TODO: This enables us to serialise NameIDs into the stored sessions, # but we don't seem to de-serialise them on load - def default(self, o: Any) -> Union[str, Any]: + def default(self, o: Any) -> str | Any: if isinstance(o, datetime): return o.isoformat() if isinstance(o, timedelta): diff --git a/src/eduid/common/models/amapi_user.py b/src/eduid/common/models/amapi_user.py index 2f8284fe2..176305e32 100644 --- a/src/eduid/common/models/amapi_user.py +++ b/src/eduid/common/models/amapi_user.py @@ -1,6 +1,5 @@ from datetime import datetime from enum import Enum -from typing import Optional from pydantic import BaseModel, Field @@ -33,14 +32,14 @@ class UserBaseRequest(BaseModel): class UserUpdateResponse(BaseModel): status: bool - diff: Optional[str] = None + diff: str | None = None class UserUpdateNameRequest(UserBaseRequest): - given_name: Optional[str] = None - chosen_given_name: Optional[str] = None - surname: Optional[str] = None - legal_name: Optional[str] = None + given_name: str | None = None + chosen_given_name: str | None = None + surname: str | None = None + legal_name: str | None = None class UserUpdateMetaRequest(UserBaseRequest): @@ -56,7 +55,7 @@ class UserUpdatePhoneRequest(UserBaseRequest): class UserUpdateLanguageRequest(UserBaseRequest): - language: Optional[str] = None + language: str | None = None class UserUpdateMetaCleanedRequest(UserBaseRequest): diff --git a/src/eduid/common/models/bearer_token.py b/src/eduid/common/models/bearer_token.py index 602184de0..206186bfa 100644 --- a/src/eduid/common/models/bearer_token.py +++ b/src/eduid/common/models/bearer_token.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from copy import copy from enum import Enum -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field, StrictInt, field_validator, model_validator from pydantic_core.core_schema import ValidationInfo @@ -20,7 +20,7 @@ class AuthSource(str, Enum): class RequestedAccess(BaseModel): type: str - scope: Optional[ScopeName] = None + scope: ScopeName | None = None class AuthenticationError(Exception): @@ -47,11 +47,11 @@ class AuthnBearerToken(BaseModel): requested_access: list[RequestedAccess] = Field(default=[]) scopes: set[ScopeName] = Field(default=set()) # saml interaction claims - saml_issuer: Optional[str] = None - saml_assurance: Optional[list[str]] = None - saml_entitlement: Optional[list[str]] = None - saml_eppn: Optional[str] = None - saml_unique_id: Optional[str] = None + saml_issuer: str | None = None + saml_assurance: list[str] | None = None + saml_entitlement: list[str] | None = None + saml_eppn: str | None = None + saml_unique_id: str | None = None def __str__(self): return f"<{self.__class__.__name__}: scopes={self.scopes}, requested_access={self.requested_access}>" @@ -134,7 +134,7 @@ def validate_auth_source(self) -> None: raise AuthenticationError(f"Unsupported authentication source: {self.auth_source}") - def validate_saml_entitlements(self, data_owner: DataOwnerName, groupdb: Optional[ScimApiGroupDB] = None) -> None: + def validate_saml_entitlements(self, data_owner: DataOwnerName, groupdb: ScimApiGroupDB | None = None) -> None: if groupdb is None: raise AuthenticationError("No groupdb provided, cannot validate saml entitlements.") @@ -158,7 +158,7 @@ def validate_saml_entitlements(self, data_owner: DataOwnerName, groupdb: Optiona logger.error(f"{saml_group_id} NOT in {entitlements}") raise AuthorizationError(f"Not authorized: {saml_group_id} not in saml entitlements") - def get_data_owner(self) -> Optional[DataOwnerName]: + def get_data_owner(self) -> DataOwnerName | None: """ Get the data owner to use. diff --git a/src/eduid/common/models/gnap_models.py b/src/eduid/common/models/gnap_models.py index 7d1a2caae..115739bba 100644 --- a/src/eduid/common/models/gnap_models.py +++ b/src/eduid/common/models/gnap_models.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Any, Optional, Union +from typing import Any from pydantic import AnyUrl, BaseModel, ConfigDict, Field, field_validator @@ -37,13 +37,13 @@ class Proof(GnapBaseModel): class Key(GnapBaseModel): proof: Proof - jwk: Optional[Union[ECJWK, RSAJWK, SymmetricJWK]] = None - cert: Optional[str] = None - cert_S256: Optional[str] = Field(default=None, alias="cert#S256") + jwk: ECJWK | RSAJWK | SymmetricJWK | None = None + cert: str | None = None + cert_S256: str | None = Field(default=None, alias="cert#S256") @field_validator("proof", mode="before") @classmethod - def expand_proof(cls, v: Union[str, dict[str, Any]]) -> dict[str, Any]: + def expand_proof(cls, v: str | dict[str, Any]) -> dict[str, Any]: # If additional parameters are not required or used for a specific method, # the method MAY be passed as a string instead of an object. if isinstance(v, str): @@ -69,54 +69,54 @@ class Access(GnapBaseModel): # The types of actions the client instance will take at the RS as an # array of strings. For example, a client instance asking for a # combination of "read" and "write" access. - actions: Optional[list[str]] = None + actions: list[str] | None = None # The location of the RS as an array of strings. These strings are # typically URIs identifying the location of the RS. - locations: Optional[list[str]] = None + locations: list[str] | None = None # The kinds of data available to the client instance at the RS's API # as an array of strings. For example, a client instance asking for # access to raw "image" data and "metadata" at a photograph API. - datatypes: Optional[list[str]] = None + datatypes: list[str] | None = None # A string identifier indicating a specific resource at the RS. For # example, a patient identifier for a medical API or a bank account # number for a financial API. - identifier: Optional[str] = None + identifier: str | None = None # The types or levels of privilege being requested at the resource. # For example, a client instance asking for administrative level # access, or access when the resource owner is no longer online. - privileges: Optional[list[str]] = None + privileges: list[str] | None = None # Sunet addition for requesting access to a specified scope - scope: Optional[str] = None + scope: str | None = None class AccessTokenRequest(GnapBaseModel): - access: Optional[list[Union[str, Access]]] = None + access: list[str | Access] | None = None # TODO: label is REQUIRED if used as part of a multiple access token request - label: Optional[str] = None - flags: Optional[list[AccessTokenFlags]] = None + label: str | None = None + flags: list[AccessTokenFlags] | None = None class Client(GnapBaseModel): - key: Union[str, Key] + key: str | Key class GrantRequest(GnapBaseModel): - access_token: Union[AccessTokenRequest, list[AccessTokenRequest]] - client: Union[str, Client] + access_token: AccessTokenRequest | list[AccessTokenRequest] + client: str | Client class AccessTokenResponse(GnapBaseModel): value: str - label: Optional[str] = None - manage: Optional[AnyUrl] = None - access: Optional[list[Union[str, Access]]] = None - expires_in: Optional[int] = Field(default=None, description="seconds until expiry") - key: Optional[Union[str, Key]] = None - flags: Optional[list[AccessTokenFlags]] = None + label: str | None = None + manage: AnyUrl | None = None + access: list[str | Access] | None = None + expires_in: int | None = Field(default=None, description="seconds until expiry") + key: str | Key | None = None + flags: list[AccessTokenFlags] | None = None class GrantResponse(GnapBaseModel): - access_token: Optional[AccessTokenResponse] = None + access_token: AccessTokenResponse | None = None class GNAPJOSEHeader(JOSEHeader): @@ -131,4 +131,4 @@ class GNAPJOSEHeader(JOSEHeader): # When a request is bound to an access token, the access token hash value. The value MUST be the result of # Base64url encoding (with no padding) the SHA-256 digest of the ASCII encoding of the associated access # token's value. REQUIRED if the request protects an access token. - ath: Optional[str] = None + ath: str | None = None diff --git a/src/eduid/common/models/jose_models.py b/src/eduid/common/models/jose_models.py index feb8f2ef6..7d2ecd67a 100644 --- a/src/eduid/common/models/jose_models.py +++ b/src/eduid/common/models/jose_models.py @@ -1,6 +1,5 @@ import datetime from enum import Enum -from typing import Optional, Union from pydantic import AnyUrl, BaseModel, Field @@ -43,45 +42,45 @@ class SupportedHTTPMethods(str, Enum): class JWK(BaseModel): kty: KeyType - use: Optional[KeyUse] = None - key_opts: Optional[list[KeyOptions]] = None - alg: Optional[str] = None - kid: Optional[str] = None - x5u: Optional[str] = None - x5c: Optional[str] = None - x5t: Optional[str] = None - x5tS256: Optional[str] = Field(None, alias="x5t#S256") + use: KeyUse | None = None + key_opts: list[KeyOptions] | None = None + alg: str | None = None + kid: str | None = None + x5u: str | None = None + x5c: str | None = None + x5t: str | None = None + x5tS256: str | None = Field(None, alias="x5t#S256") class ECJWK(JWK): - crv: Optional[str] = None - x: Optional[str] = None - y: Optional[str] = None - d: Optional[str] = None - n: Optional[str] = None - e: Optional[str] = None + crv: str | None = None + x: str | None = None + y: str | None = None + d: str | None = None + n: str | None = None + e: str | None = None class RSAJWK(JWK): - d: Optional[str] = None - n: Optional[str] = None - e: Optional[str] = None - p: Optional[str] = None - q: Optional[str] = None - dp: Optional[str] = None - dq: Optional[str] = None - qi: Optional[str] = None - oth: Optional[str] = None - r: Optional[str] = None - t: Optional[str] = None + d: str | None = None + n: str | None = None + e: str | None = None + p: str | None = None + q: str | None = None + dp: str | None = None + dq: str | None = None + qi: str | None = None + oth: str | None = None + r: str | None = None + t: str | None = None class SymmetricJWK(JWK): - k: Optional[str] = None + k: str | None = None class JWKS(BaseModel): - keys: list[Union[ECJWK, RSAJWK, SymmetricJWK]] + keys: list[ECJWK | RSAJWK | SymmetricJWK] class SupportedJWSType(str, Enum): @@ -90,17 +89,17 @@ class SupportedJWSType(str, Enum): class JOSEHeader(BaseModel): - kid: Optional[str] = None + kid: str | None = None alg: SupportedAlgorithms - jku: Optional[AnyUrl] = None - jwk: Optional[Union[ECJWK, RSAJWK, SymmetricJWK]] = None - x5u: Optional[str] = None - x5c: Optional[str] = None - x5t: Optional[str] = None - x5tS256: Optional[str] = Field(default=None, alias="x5t#S256") - typ: Optional[str] = None - cty: Optional[str] = None - crit: Optional[list] = None + jku: AnyUrl | None = None + jwk: ECJWK | RSAJWK | SymmetricJWK | None = None + x5u: str | None = None + x5c: str | None = None + x5t: str | None = None + x5tS256: str | None = Field(default=None, alias="x5t#S256") + typ: str | None = None + cty: str | None = None + crit: list | None = None class RegisteredClaims(BaseModel): @@ -108,13 +107,13 @@ class RegisteredClaims(BaseModel): https://tools.ietf.org/html/rfc7519#section-4.1 """ - iss: Optional[str] = None # Issuer - sub: Optional[str] = None # Subject - aud: Optional[str] = None # Audience - exp: Optional[datetime.timedelta] = None # Expiration Time + iss: str | None = None # Issuer + sub: str | None = None # Subject + aud: str | None = None # Audience + exp: datetime.timedelta | None = None # Expiration Time nbf: datetime.datetime = Field(default_factory=utc_now) # Not Before iat: datetime.datetime = Field(default_factory=utc_now) # Issued At - jti: Optional[str] = None # JWT ID + jti: str | None = None # JWT ID def to_rfc7519(self): d = self.dict(exclude_none=True) diff --git a/src/eduid/common/models/saml_models.py b/src/eduid/common/models/saml_models.py index cf0abd773..b413532c0 100644 --- a/src/eduid/common/models/saml_models.py +++ b/src/eduid/common/models/saml_models.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import logging from datetime import datetime -from typing import Optional from dateutil.parser import parse as dt_parse from pydantic import BaseModel, model_validator @@ -17,7 +16,7 @@ class BaseSessionInfo(BaseModel): authn_info: list[tuple[str, list[str], str]] @property - def authn_context(self) -> Optional[str]: + def authn_context(self) -> str | None: try: return self.authn_info[0][0] except KeyError: diff --git a/src/eduid/common/models/scim_base.py b/src/eduid/common/models/scim_base.py index 8ba9f9f3c..243dcc6be 100644 --- a/src/eduid/common/models/scim_base.py +++ b/src/eduid/common/models/scim_base.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Annotated, Any, Optional +from typing import Annotated, Any from uuid import UUID from bson import ObjectId @@ -125,25 +125,25 @@ class Meta(EduidBaseModel): class Name(EduidBaseModel): - family_name: Optional[str] = Field(default=None, alias="familyName") - given_name: Optional[str] = Field(default=None, alias="givenName") - formatted: Optional[str] = None - middle_name: Optional[str] = Field(default=None, alias="middleName") - honorific_prefix: Optional[str] = Field(default=None, alias="honorificPrefix") - honorific_suffix: Optional[str] = Field(default=None, alias="honorificSuffix") + family_name: str | None = Field(default=None, alias="familyName") + given_name: str | None = Field(default=None, alias="givenName") + formatted: str | None = None + middle_name: str | None = Field(default=None, alias="middleName") + honorific_prefix: str | None = Field(default=None, alias="honorificPrefix") + honorific_suffix: str | None = Field(default=None, alias="honorificSuffix") class Email(EduidBaseModel): value: LowerEmailStr - display: Optional[str] = None - type: Optional[EmailType] = None + display: str | None = None + type: EmailType | None = None primary: bool = True class PhoneNumber(EduidBaseModel): value: PhoneNumberStr - display: Optional[str] = None - type: Optional[PhoneNumberType] = None + display: str | None = None + type: PhoneNumberType | None = None primary: bool = True @@ -153,18 +153,18 @@ class BaseResponse(EduidBaseModel): id: UUID meta: Meta schemas: list[SCIMSchema] = Field(min_length=1) - external_id: Optional[str] = Field(default=None, alias="externalId") + external_id: str | None = Field(default=None, alias="externalId") class BaseCreateRequest(EduidBaseModel): schemas: list[SCIMSchema] = Field(min_length=1) - external_id: Optional[str] = Field(default=None, alias="externalId") + external_id: str | None = Field(default=None, alias="externalId") class BaseUpdateRequest(EduidBaseModel): id: UUID schemas: list[SCIMSchema] = Field(min_length=1) - external_id: Optional[str] = Field(default=None, alias="externalId") + external_id: str | None = Field(default=None, alias="externalId") class SearchRequest(EduidBaseModel): @@ -172,7 +172,7 @@ class SearchRequest(EduidBaseModel): filter: str start_index: int = Field(default=1, alias="startIndex", ge=1) # Greater or equal to 1 count: int = Field(default=100, ge=1) # Greater or equal to 1 - attributes: Optional[list[str]] = None + attributes: list[str] | None = None class ListResponse(EduidBaseModel): diff --git a/src/eduid/common/models/scim_invite.py b/src/eduid/common/models/scim_invite.py index dda913414..a3164f78c 100644 --- a/src/eduid/common/models/scim_invite.py +++ b/src/eduid/common/models/scim_invite.py @@ -1,4 +1,3 @@ -from typing import Optional from uuid import UUID from pydantic import Field, model_validator @@ -26,20 +25,20 @@ class NutidInviteExtensionV1(EduidBaseModel): name: Name = Field(default_factory=Name) emails: list[Email] = Field(default_factory=list) phone_numbers: list[PhoneNumber] = Field(default_factory=list, alias="phoneNumbers") - national_identity_number: Optional[str] = Field( + national_identity_number: str | None = Field( default=None, alias="nationalIdentityNumber", pattern=nin_re_str, ) - preferred_language: Optional[LanguageTag] = Field(default=None, alias="preferredLanguage") + preferred_language: LanguageTag | None = Field(default=None, alias="preferredLanguage") groups: list[UUID] = Field(default_factory=list) - inviter_name: Optional[str] = Field(default=None, alias="inviterName") - send_email: Optional[bool] = Field(default=None, alias="sendEmail") - finish_url: Optional[str] = Field(default=None, alias="finishURL") - invite_url: Optional[str] = Field(default=None, alias="inviteURL") - enable_mfa_stepup: Optional[bool] = Field(default=None, alias="enableMfaStepup") - completed: Optional[ScimDatetime] = None - expires_at: Optional[ScimDatetime] = Field(default=None, alias="expiresAt") + inviter_name: str | None = Field(default=None, alias="inviterName") + send_email: bool | None = Field(default=None, alias="sendEmail") + finish_url: str | None = Field(default=None, alias="finishURL") + invite_url: str | None = Field(default=None, alias="inviteURL") + enable_mfa_stepup: bool | None = Field(default=None, alias="enableMfaStepup") + completed: ScimDatetime | None = None + expires_at: ScimDatetime | None = Field(default=None, alias="expiresAt") @model_validator(mode="after") def validate_schema(self) -> Self: diff --git a/src/eduid/common/models/scim_user.py b/src/eduid/common/models/scim_user.py index 018290b52..dee4b1c04 100644 --- a/src/eduid/common/models/scim_user.py +++ b/src/eduid/common/models/scim_user.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import Field @@ -48,9 +48,9 @@ class User(EduidBaseModel): name: Name = Field(default_factory=Name) emails: list[Email] = Field(default_factory=list) phone_numbers: list[PhoneNumber] = Field(default_factory=list, alias="phoneNumbers") - preferred_language: Optional[LanguageTag] = Field(default=None, alias="preferredLanguage") + preferred_language: LanguageTag | None = Field(default=None, alias="preferredLanguage") groups: list[Group] = Field(default_factory=list) - nutid_user_v1: Optional[NutidUserExtensionV1] = Field( + nutid_user_v1: NutidUserExtensionV1 | None = Field( default_factory=NutidUserExtensionV1, alias=SCIMSchema.NUTID_USER_V1.value ) diff --git a/src/eduid/common/rpc/am_relay.py b/src/eduid/common/rpc/am_relay.py index 34429ea35..6e7f6da24 100644 --- a/src/eduid/common/rpc/am_relay.py +++ b/src/eduid/common/rpc/am_relay.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import eduid.workers.am from eduid.common.config.base import AmConfigMixin @@ -33,7 +32,7 @@ def __init__(self, config: AmConfigMixin): self._update_attrs = update_attributes_keep_result self._pong = pong - def request_user_sync(self, user: User, timeout: int = 25, app_name_override: Optional[str] = None) -> bool: + def request_user_sync(self, user: User, timeout: int = 25, app_name_override: str | None = None) -> bool: """ Use Celery to ask eduid-am worker to propagate changes from our private UserDB into the central UserDB. diff --git a/src/eduid/common/rpc/lookup_mobile_relay.py b/src/eduid/common/rpc/lookup_mobile_relay.py index 5ac2c1c85..302162d36 100644 --- a/src/eduid/common/rpc/lookup_mobile_relay.py +++ b/src/eduid/common/rpc/lookup_mobile_relay.py @@ -1,5 +1,3 @@ -from typing import Optional - import eduid.workers.lookup_mobile __author__ = "mathiashedstrom" @@ -19,7 +17,7 @@ def __init__(self, config: CeleryConfigMixin): self._find_NIN_by_mobile = find_NIN_by_mobile self._pong = pong - def find_nin_by_mobile(self, mobile_number: str) -> Optional[str]: + def find_nin_by_mobile(self, mobile_number: str) -> str | None: try: result = self._find_NIN_by_mobile.delay(mobile_number) result = result.get(timeout=10) # Lower timeout than standard gunicorn worker timeout (25) diff --git a/src/eduid/common/rpc/mail_relay.py b/src/eduid/common/rpc/mail_relay.py index b5da427fc..0dd84de58 100644 --- a/src/eduid/common/rpc/mail_relay.py +++ b/src/eduid/common/rpc/mail_relay.py @@ -1,7 +1,6 @@ import logging from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import Optional import eduid.workers.msg from eduid.common.config.base import MailConfigMixin @@ -29,9 +28,9 @@ def sendmail( self, subject: str, recipients: list[str], - text: Optional[str] = None, - html: Optional[str] = None, - reference: Optional[str] = None, + text: str | None = None, + html: str | None = None, + reference: str | None = None, timeout: int = 25, ) -> None: """ diff --git a/src/eduid/common/rpc/msg_relay.py b/src/eduid/common/rpc/msg_relay.py index 53b1d9ff7..9a24b21a0 100644 --- a/src/eduid/common/rpc/msg_relay.py +++ b/src/eduid/common/rpc/msg_relay.py @@ -1,6 +1,5 @@ import logging from enum import Enum -from typing import Optional from pydantic import BaseModel, ConfigDict, Field, ValidationError @@ -35,40 +34,40 @@ class CaseInformation(NavetModelConfig): class Name(NavetModelConfig): - given_name_marking: Optional[str] = Field(default=None, alias="GivenNameMarking") - given_name: Optional[str] = Field(default=None, alias="GivenName") - middle_name: Optional[str] = Field(default=None, alias="MiddleName") - surname: Optional[str] = Field(default=None, alias="Surname") - notification_name: Optional[str] = Field(default=None, alias="NotificationName") + given_name_marking: str | None = Field(default=None, alias="GivenNameMarking") + given_name: str | None = Field(default=None, alias="GivenName") + middle_name: str | None = Field(default=None, alias="MiddleName") + surname: str | None = Field(default=None, alias="Surname") + notification_name: str | None = Field(default=None, alias="NotificationName") class RelationName(NavetModelConfig): - given_name_marking: Optional[str] = Field(default=None, alias="GivenNameMarking") - given_name: Optional[str] = Field(default=None, alias="GivenName") - middle_name: Optional[str] = Field(default=None, alias="MiddleName") - surname: Optional[str] = Field(default=None, alias="Surname") - notification_name: Optional[str] = Field(default=None, alias="NotificationName") + given_name_marking: str | None = Field(default=None, alias="GivenNameMarking") + given_name: str | None = Field(default=None, alias="GivenName") + middle_name: str | None = Field(default=None, alias="MiddleName") + surname: str | None = Field(default=None, alias="Surname") + notification_name: str | None = Field(default=None, alias="NotificationName") class PersonId(NavetModelConfig): - national_identity_number: Optional[str] = Field(default=None, alias="NationalIdentityNumber") - co_ordination_number: Optional[str] = Field(default=None, alias="CoOrdinationNumber") + national_identity_number: str | None = Field(default=None, alias="NationalIdentityNumber") + co_ordination_number: str | None = Field(default=None, alias="CoOrdinationNumber") class OfficialAddress(NavetModelConfig): - care_of: Optional[str] = Field(default=None, alias="CareOf") + care_of: str | None = Field(default=None, alias="CareOf") # From Skatteverket's documentation it is not clear why Address1 # is needed. In practice, it is rarely used, but when actually # used it has been seen to often contains apartment numbers. - address1: Optional[str] = Field(default=None, alias="Address1") - address2: Optional[str] = Field(default=None, alias="Address2") - postal_code: Optional[str] = Field(default=None, alias="PostalCode") - city: Optional[str] = Field(default=None, alias="City") + address1: str | None = Field(default=None, alias="Address1") + address2: str | None = Field(default=None, alias="Address2") + postal_code: str | None = Field(default=None, alias="PostalCode") + city: str | None = Field(default=None, alias="City") class RelationId(NavetModelConfig): - national_identity_number: Optional[str] = Field(default=None, alias="NationalIdentityNumber") - birth_time_number: Optional[str] = Field(default=None, alias="BirthTimeNumber") + national_identity_number: str | None = Field(default=None, alias="NationalIdentityNumber") + birth_time_number: str | None = Field(default=None, alias="BirthTimeNumber") class RelationType(str, Enum): @@ -85,10 +84,10 @@ class RelationType(str, Enum): class Relation(NavetModelConfig): name: RelationName = Field(default_factory=RelationName, alias="Name") relation_id: RelationId = Field(alias="RelationId") - relation_type: Optional[RelationType] = Field(default=None, alias="RelationType") - relation_start_date: Optional[str] = Field(default=None, alias="RelationStartDate") - relation_end_date: Optional[str] = Field(default=None, alias="RelationEndDate") - status: Optional[str] = Field(default=None, alias="Status") + relation_type: RelationType | None = Field(default=None, alias="RelationType") + relation_start_date: str | None = Field(default=None, alias="RelationStartDate") + relation_end_date: str | None = Field(default=None, alias="RelationEndDate") + status: str | None = Field(default=None, alias="Status") class PostalAddresses(NavetModelConfig): @@ -111,15 +110,15 @@ class DeregisteredCauseCode(str, Enum): class DeregistrationInformation(NavetModelConfig): - date: Optional[str] = None - cause_code: Optional[DeregisteredCauseCode] = Field(default=None, alias="causeCode") + date: str | None = None + cause_code: DeregisteredCauseCode | None = Field(default=None, alias="causeCode") class Person(NavetModelConfig): name: Name = Field(default_factory=Name, alias="Name") person_id: PersonId = Field(alias="PersonId") deregistration_information: DeregistrationInformation = Field(alias="DeregistrationInformation") - reference_national_identity_number: Optional[str] = Field(default=None, alias="ReferenceNationalIdentityNumber") + reference_national_identity_number: str | None = Field(default=None, alias="ReferenceNationalIdentityNumber") postal_addresses: PostalAddresses = Field(alias="PostalAddresses") relations: list[Relation] = Field(default_factory=list, alias="Relations") diff --git a/src/eduid/common/testing_base.py b/src/eduid/common/testing_base.py index fbf471760..92f2504ea 100644 --- a/src/eduid/common/testing_base.py +++ b/src/eduid/common/testing_base.py @@ -6,7 +6,7 @@ import uuid from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Optional, TypeVar, Union +from typing import Any, TypeVar from bson import ObjectId @@ -32,12 +32,12 @@ def setUp(self, *args: Any, **kwargs: Any) -> None: def normalised_data( - data: SomeData, replace_datetime: Optional[str] = None, exclude_keys: Optional[list[str]] = None + data: SomeData, replace_datetime: str | None = None, exclude_keys: list[str] | None = None ) -> SomeData: """Utility function for normalising data before comparisons in test cases.""" class NormaliseEncoder(json.JSONEncoder): - def default(self, o: Any) -> Union[str, Any]: + def default(self, o: Any) -> str | Any: if isinstance(o, datetime): if replace_datetime is not None: return replace_datetime diff --git a/src/eduid/graphdb/db.py b/src/eduid/graphdb/db.py index 08b4e600e..bb5fbf868 100644 --- a/src/eduid/graphdb/db.py +++ b/src/eduid/graphdb/db.py @@ -2,7 +2,7 @@ from abc import ABC from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from urllib.parse import urlparse from neo4j import Driver, GraphDatabase, basic_auth @@ -13,7 +13,7 @@ class Neo4jDB: """Simple wrapper to allow us to define the api""" - def __init__(self, db_uri: str, config: Optional[Mapping[str, Any]] = None): + def __init__(self, db_uri: str, config: Mapping[str, Any] | None = None): if not db_uri: raise ValueError("db_uri not supplied") @@ -43,7 +43,7 @@ def __init__(self, db_uri: str, config: Optional[Mapping[str, Any]] = None): def __repr__(self) -> str: return f'' - def count_nodes(self, label: Optional[str] = None) -> Optional[int]: + def count_nodes(self, label: str | None = None) -> int | None: match_statement = "MATCH ()" if label: match_statement = f"MATCH(:{label})" @@ -76,7 +76,7 @@ def close(self): class BaseGraphDB(ABC): """Base class for common db operations""" - def __init__(self, db_uri: str, config: Optional[dict[str, Any]] = None): + def __init__(self, db_uri: str, config: dict[str, Any] | None = None): self._db_uri = db_uri self._db = Neo4jDB(db_uri=self._db_uri, config=config) self.db_setup() diff --git a/src/eduid/graphdb/groupdb/db.py b/src/eduid/graphdb/groupdb/db.py index 6a1e89fc7..b887550b1 100644 --- a/src/eduid/graphdb/groupdb/db.py +++ b/src/eduid/graphdb/groupdb/db.py @@ -1,7 +1,7 @@ import enum import logging from dataclasses import replace -from typing import Any, Optional, Union +from typing import Any from bson import ObjectId from neo4j import READ_ACCESS, WRITE_ACCESS, Record, Transaction @@ -33,7 +33,7 @@ class Role(enum.Enum): class GroupDB(BaseGraphDB): - def __init__(self, db_uri: str, scope: str, config: Optional[dict[str, Any]] = None): + def __init__(self, db_uri: str, scope: str, config: dict[str, Any] | None = None): super().__init__(db_uri=db_uri, config=config) self._scope = scope @@ -95,9 +95,9 @@ def _create_or_update_group(self, tx: Transaction, group: Group) -> Group: def _add_or_update_users_and_groups( self, tx: Transaction, group: Group - ) -> tuple[set[Union[User, Group]], set[Union[User, Group]]]: - members: set[Union[User, Group]] = set() - owners: set[Union[User, Group]] = set() + ) -> tuple[set[User | Group], set[User | Group]]: + members: set[User | Group] = set() + owners: set[User | Group] = set() for user_member in group.member_users: res = self._add_user_to_group(tx, group=group, member=user_member, role=Role.MEMBER) @@ -198,8 +198,8 @@ def _add_user_to_group(self, tx, group: Group, member: User, role: Role) -> Reco display_name=member.display_name, ).single() - def get_users_and_groups_by_role(self, identifier: str, role: Role) -> list[Union[User, Group]]: - res: list[Union[User, Group]] = [] + def get_users_and_groups_by_role(self, identifier: str, role: Role) -> list[User | Group]: + res: list[User | Group] = [] q = f""" MATCH (g: Group {{scope: $scope, identifier: $identifier}})<-[r:{role.value}]-(m) RETURN r.display_name as display_name, r.created_ts as created_ts, r.modified_ts as modified_ts, @@ -214,7 +214,7 @@ def get_users_and_groups_by_role(self, identifier: str, role: Role) -> list[Unio res.append(self._load_group(record.data())) return res - def get_group(self, identifier: str) -> Optional[Group]: + def get_group(self, identifier: str) -> Group | None: q = """ MATCH (g: Group {scope: $scope, identifier: $identifier}) OPTIONAL MATCH (g)<-[r]-(m) @@ -227,7 +227,7 @@ def get_group(self, identifier: str) -> Optional[Group]: # group did not exist return None - group_data: Optional[Node] # please mypy + group_data: Node | None # please mypy if len(group_graph.relationships) == 0: # Just a group with no owners or members group_data = [node_data for node_data in group_graph.nodes][0] @@ -382,17 +382,17 @@ def save(self, group: Group) -> Group: saved_group = replace(saved_group, members=saved_members, owners=saved_owners) return saved_group - def _load_node(self, data: Union[dict, Node]) -> Union[User, Group]: + def _load_node(self, data: dict | Node) -> User | Group: if data.get("scope"): return self._load_group(data=data) return self._load_user(data=data) @staticmethod - def _load_group(data: Union[dict, Node]) -> Group: + def _load_group(data: dict | Node) -> Group: """Method meant to be overridden by subclasses wanting to annotate the group.""" return Group.from_mapping(data) @staticmethod - def _load_user(data: Union[dict, Node]) -> User: + def _load_user(data: dict | Node) -> User: """Method meant to be overridden by subclasses wanting to annotate the user.""" return User.from_mapping(data) diff --git a/src/eduid/graphdb/groupdb/group.py b/src/eduid/graphdb/groupdb/group.py index b8eaa469f..6fddddb47 100644 --- a/src/eduid/graphdb/groupdb/group.py +++ b/src/eduid/graphdb/groupdb/group.py @@ -3,7 +3,6 @@ from collections.abc import Mapping from dataclasses import dataclass, field from datetime import datetime -from typing import Optional, Union from bson import ObjectId @@ -18,14 +17,14 @@ class Group: identifier: str display_name: str - version: Optional[ObjectId] = None - created_ts: Optional[datetime] = None - modified_ts: Optional[datetime] = None - owners: set[Union[User, Group]] = field(compare=False, default_factory=set) - members: set[Union[User, Group]] = field(compare=False, default_factory=set) + version: ObjectId | None = None + created_ts: datetime | None = None + modified_ts: datetime | None = None + owners: set[User | Group] = field(compare=False, default_factory=set) + members: set[User | Group] = field(compare=False, default_factory=set) @staticmethod - def _get_user(it: list[User], identifier: str) -> Optional[User]: + def _get_user(it: list[User], identifier: str) -> User | None: res = [user for user in it if user.identifier == identifier] if not res: return None @@ -34,7 +33,7 @@ def _get_user(it: list[User], identifier: str) -> Optional[User]: return res[0] @staticmethod - def _get_group(it: list[Group], identifier: str) -> Optional[Group]: + def _get_group(it: list[Group], identifier: str) -> Group | None: res = [group for group in it if group.identifier == identifier] if not res: return None @@ -58,16 +57,16 @@ def owner_users(self) -> list[User]: def owner_groups(self) -> list[Group]: return [item for item in self.owners if isinstance(item, Group)] - def get_member_user(self, identifier: str) -> Optional[User]: + def get_member_user(self, identifier: str) -> User | None: return self._get_user(self.member_users, identifier=identifier) - def get_owner_user(self, identifier: str) -> Optional[User]: + def get_owner_user(self, identifier: str) -> User | None: return self._get_user(self.owner_users, identifier=identifier) - def get_member_group(self, identifier: str) -> Optional[Group]: + def get_member_group(self, identifier: str) -> Group | None: return self._get_group(self.member_groups, identifier=identifier) - def get_owner_group(self, identifier: str) -> Optional[Group]: + def get_owner_group(self, identifier: str) -> Group | None: return self._get_group(self.owner_groups, identifier=identifier) def has_member(self, identifier: str) -> bool: diff --git a/src/eduid/graphdb/groupdb/user.py b/src/eduid/graphdb/groupdb/user.py index 976f32f1a..e498cb238 100644 --- a/src/eduid/graphdb/groupdb/user.py +++ b/src/eduid/graphdb/groupdb/user.py @@ -3,7 +3,6 @@ from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from typing import Optional from eduid.graphdb.helpers import neo4j_ts_to_dt @@ -14,8 +13,8 @@ class User: identifier: str display_name: str - created_ts: Optional[datetime] = None - modified_ts: Optional[datetime] = None + created_ts: datetime | None = None + modified_ts: datetime | None = None @classmethod def from_mapping(cls, data: Mapping) -> User: diff --git a/src/eduid/graphdb/helpers.py b/src/eduid/graphdb/helpers.py index e503cae33..e76e548bb 100644 --- a/src/eduid/graphdb/helpers.py +++ b/src/eduid/graphdb/helpers.py @@ -1,11 +1,10 @@ from collections.abc import Mapping from datetime import datetime, timezone -from typing import Optional __author__ = "lundberg" -def neo4j_ts_to_dt(data: Mapping) -> Mapping[str, Optional[datetime]]: +def neo4j_ts_to_dt(data: Mapping) -> Mapping[str, datetime | None]: created_ts = data.get("created_ts") if isinstance(created_ts, int): created_ts = datetime.fromtimestamp(created_ts / 1000) # Milliseconds since 1970 diff --git a/src/eduid/graphdb/testing.py b/src/eduid/graphdb/testing.py index 45d09603e..c5021c0c9 100644 --- a/src/eduid/graphdb/testing.py +++ b/src/eduid/graphdb/testing.py @@ -5,7 +5,7 @@ import unittest from collections.abc import Sequence from os import environ -from typing import Optional, cast +from typing import cast from neo4j.exceptions import ServiceUnavailable @@ -31,7 +31,7 @@ class Neo4jTemporaryInstance(EduidTemporaryInstance): """ - _instance: Optional[Neo4jTemporaryInstance] = None + _instance: Neo4jTemporaryInstance | None = None _http_port: int _https_port: int _bolt_port: int diff --git a/src/eduid/graphdb/tests/test_group.py b/src/eduid/graphdb/tests/test_group.py index 2027c30c3..2fa36bf17 100644 --- a/src/eduid/graphdb/tests/test_group.py +++ b/src/eduid/graphdb/tests/test_group.py @@ -1,4 +1,3 @@ -from typing import Union from unittest import TestCase from eduid.graphdb.groupdb import Group, User @@ -8,11 +7,11 @@ class TestGroup(TestCase): def setUp(self) -> None: - self.group1: dict[str, Union[str, list]] = { + self.group1: dict[str, str | list] = { "identifier": "test1", "display_name": "Test Group 1", } - self.group2: dict[str, Union[str, list]] = { + self.group2: dict[str, str | list] = { "identifier": "test2", "display_name": "Test Group 2", } diff --git a/src/eduid/graphdb/tests/test_groupdb.py b/src/eduid/graphdb/tests/test_groupdb.py index 0d9c1bd9a..e5bf168de 100644 --- a/src/eduid/graphdb/tests/test_groupdb.py +++ b/src/eduid/graphdb/tests/test_groupdb.py @@ -1,5 +1,4 @@ from dataclasses import replace -from typing import Union from bson import ObjectId from neo4j import basic_auth @@ -16,12 +15,12 @@ class TestGroupDB(Neo4jTestCase): def setUp(self) -> None: self.db_config = {"encrypted": False, "auth": basic_auth("neo4j", "testing")} self.group_db = GroupDB(db_uri=self.neo4jdb.db_uri, scope="__testing__", config=self.db_config) - self.group1: dict[str, Union[str, list, None]] = { + self.group1: dict[str, str | list | None] = { "identifier": "test1", "version": None, "display_name": "Test Group 1", } - self.group2: dict[str, Union[str, list, None]] = { + self.group2: dict[str, str | list | None] = { "identifier": "test2", "version": None, "display_name": "Test Group 2", diff --git a/src/eduid/maccapi/app.py b/src/eduid/maccapi/app.py index 91a6dca64..d42e52e4c 100644 --- a/src/eduid/maccapi/app.py +++ b/src/eduid/maccapi/app.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware @@ -14,18 +12,14 @@ class MAccAPI(FastAPI): - def __init__( - self, name: str = "maccapi", test_config: Optional[dict] = None, vccs_client: Optional[VCCSClient] = None - ): + def __init__(self, name: str = "maccapi", test_config: dict | None = None, vccs_client: VCCSClient | None = None): self.config = load_config(typ=MAccApiConfig, app_name=name, ns="api", test_config=test_config) super().__init__(root_path=self.config.application_root) self.context = Context(config=self.config, vccs_client=vccs_client) self.context.logger.info(f"Starting {name} app") -def init_api( - name: str = "maccapi", test_config: Optional[dict] = None, vccs_client: Optional[VCCSClient] = None -) -> MAccAPI: +def init_api(name: str = "maccapi", test_config: dict | None = None, vccs_client: VCCSClient | None = None) -> MAccAPI: """ Initialize the API. """ diff --git a/src/eduid/maccapi/config.py b/src/eduid/maccapi/config.py index 03c367054..908fff0c9 100644 --- a/src/eduid/maccapi/config.py +++ b/src/eduid/maccapi/config.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from pydantic import field_validator @@ -16,7 +15,7 @@ class MAccApiConfig(AuthnBearerTokenConfig, LoggingConfigMixin, StatsConfigMixin vccs_url: str = "http://vccs:8080/" # The expected value of the authn JWT claims['requested_access']['type'] - requested_access_type: Optional[str] = "maccapi" + requested_access_type: str | None = "maccapi" status_cache_seconds: int = 10 diff --git a/src/eduid/maccapi/context.py b/src/eduid/maccapi/context.py index e30039c64..82a1fc925 100644 --- a/src/eduid/maccapi/context.py +++ b/src/eduid/maccapi/context.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from eduid.common.fastapi.log import init_logging from eduid.common.stats import init_app_stats @@ -11,7 +10,7 @@ class Context: - def __init__(self, config: MAccApiConfig, vccs_client: Optional[VCCSClient] = None): + def __init__(self, config: MAccApiConfig, vccs_client: VCCSClient | None = None): self.name = config.app_name self.config = config diff --git a/src/eduid/maccapi/context_request.py b/src/eduid/maccapi/context_request.py index 552b68155..bd5f77473 100644 --- a/src/eduid/maccapi/context_request.py +++ b/src/eduid/maccapi/context_request.py @@ -1,11 +1,9 @@ -from typing import Optional - from eduid.common.fastapi.context_request import Context, ContextRequestRoute class MaccAPIContext(Context): - manager_eppn: Optional[str] = None - data_owner: Optional[str] = None + manager_eppn: str | None = None + data_owner: str | None = None class MaccAPIRoute(ContextRequestRoute): diff --git a/src/eduid/maccapi/model/api.py b/src/eduid/maccapi/model/api.py index 6e527bb15..dde5256fb 100644 --- a/src/eduid/maccapi/model/api.py +++ b/src/eduid/maccapi/model/api.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel @@ -7,7 +5,7 @@ class ApiUser(BaseModel): eppn: str given_name: str surname: str - password: Optional[str] = None + password: str | None = None class ApiResponseBaseModel(BaseModel): @@ -33,7 +31,7 @@ class UserRemoveRequest(BaseModel): class UserRemovedResponse(ApiResponseBaseModel): - user: Optional[ApiUser] = None + user: ApiUser | None = None class UserResetPasswordRequest(BaseModel): @@ -41,4 +39,4 @@ class UserResetPasswordRequest(BaseModel): class UserResetPasswordResponse(ApiResponseBaseModel): - user: Optional[ApiUser] = None + user: ApiUser | None = None diff --git a/src/eduid/queue/db/change_event.py b/src/eduid/queue/db/change_event.py index 9da54ea0c..bc4894c15 100644 --- a/src/eduid/queue/db/change_event.py +++ b/src/eduid/queue/db/change_event.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, Union +from typing import Any __author__ = "lundberg" @@ -22,7 +22,7 @@ class OperationType(Enum): @dataclass class ResumeToken: - data: Union[str, bytes] + data: str | bytes @dataclass @@ -38,8 +38,8 @@ class DocumentKey: @dataclass class UpdateDescription: - updated_fields: Optional[dict[str, Any]] - removed_fields: Optional[list[str]] + updated_fields: dict[str, Any] | None + removed_fields: list[str] | None @dataclass(frozen=True) @@ -52,9 +52,9 @@ class ChangeEvent: operation_type: OperationType ns: NS document_key: DocumentKey - full_document: Optional[dict[str, Any]] = None - to: Optional[NS] = None - update_description: Optional[UpdateDescription] = None + full_document: dict[str, Any] | None = None + to: NS | None = None + update_description: UpdateDescription | None = None # Available in MongoDB >=4 # clusterTime # txnNumber diff --git a/src/eduid/queue/db/client.py b/src/eduid/queue/db/client.py index da4d53542..82034a7dc 100644 --- a/src/eduid/queue/db/client.py +++ b/src/eduid/queue/db/client.py @@ -1,6 +1,5 @@ import logging from dataclasses import replace -from typing import Optional, Union from bson import ObjectId @@ -46,7 +45,7 @@ def __init__(self, db_uri: str, collection: str, db_name: str = "eduid_queue"): } self.setup_indexes(indexes) - def get_item_by_id(self, message_id: Union[str, ObjectId], parse_payload: bool = True) -> Optional[QueueItem]: + def get_item_by_id(self, message_id: str | ObjectId, parse_payload: bool = True) -> QueueItem | None: if isinstance(message_id, str): message_id = ObjectId(message_id) diff --git a/src/eduid/queue/db/queue_item.py b/src/eduid/queue/db/queue_item.py index cc54f7f92..1779ee17d 100644 --- a/src/eduid/queue/db/queue_item.py +++ b/src/eduid/queue/db/queue_item.py @@ -1,7 +1,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any from bson import ObjectId @@ -15,7 +15,7 @@ class Status: success: bool retry: bool = False - message: Optional[str] = None + message: str | None = None @dataclass(frozen=True) @@ -39,8 +39,8 @@ class QueueItem: payload: Payload item_id: ObjectId = field(default_factory=ObjectId) created_ts: datetime = field(default_factory=datetime.utcnow) - processed_by: Optional[str] = None - processed_ts: Optional[datetime] = None + processed_by: str | None = None + processed_ts: datetime | None = None retries: int = 0 def to_dict(self) -> TUserDbDocument: diff --git a/src/eduid/queue/db/worker.py b/src/eduid/queue/db/worker.py index 5463d7279..a4e7357e0 100644 --- a/src/eduid/queue/db/worker.py +++ b/src/eduid/queue/db/worker.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from dataclasses import replace from datetime import datetime, timedelta, timezone -from typing import Any, Optional, Union +from typing import Any from bson import ObjectId from pymongo.results import UpdateResult @@ -40,7 +40,7 @@ def parse_queue_item(self, doc: Mapping, parse_payload: bool = True): return item return replace(item, payload=self._load_payload(item)) - async def grab_item(self, item_id: Union[str, ObjectId], worker_name: str, regrab=False) -> Optional[QueueItem]: + async def grab_item(self, item_id: str | ObjectId, worker_name: str, regrab=False) -> QueueItem | None: """ :param item_id: document id :param worker_name: current workers name @@ -93,7 +93,7 @@ async def grab_item(self, item_id: Union[str, ObjectId], worker_name: str, regra return item async def find_items( - self, processed: bool, min_age_in_seconds: Optional[int] = None, expired: Optional[bool] = None + self, processed: bool, min_age_in_seconds: int | None = None, expired: bool | None = None ) -> list: # TODO: Add registered payload types to spec spec: dict[str, Any] = {} @@ -117,7 +117,7 @@ async def find_items( logger.debug(f"spec: {spec}") return [doc for doc in await self.collection.find(spec).to_list(length=100)] - async def remove_item(self, item_id: Union[str, ObjectId]) -> bool: + async def remove_item(self, item_id: str | ObjectId) -> bool: """ Remove a document in the db given the _id. diff --git a/src/eduid/queue/testing.py b/src/eduid/queue/testing.py index cd6c1ad27..7c68c66eb 100644 --- a/src/eduid/queue/testing.py +++ b/src/eduid/queue/testing.py @@ -6,7 +6,7 @@ from asyncio import Task from collections.abc import Sequence from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Optional, TypeAlias, cast +from typing import TYPE_CHECKING, Any, TypeAlias, cast from unittest import IsolatedAsyncioTestCase, TestCase from unittest.mock import patch @@ -193,7 +193,7 @@ def create_queue_item(expires_at: datetime, discard_at: datetime, payload: Paylo async def _assert_item_gets_processed(self, queue_item: QueueItem, retry: bool = False): end_time = utc_now() + timedelta(seconds=10) - fetched: Optional[QueueItem] = None + fetched: QueueItem | None = None while utc_now() < end_time: await asyncio.sleep(0.5) # Allow worker to run fetched = self.client_db.get_item_by_id(queue_item.item_id) diff --git a/src/eduid/queue/workers/mail.py b/src/eduid/queue/workers/mail.py index 779acb370..d92eb9bd2 100644 --- a/src/eduid/queue/workers/mail.py +++ b/src/eduid/queue/workers/mail.py @@ -4,7 +4,7 @@ from dataclasses import asdict from email.message import EmailMessage from email.utils import formatdate, make_msgid -from typing import Any, Optional, cast +from typing import Any, cast from aiosmtplib import SMTP, SMTPException, SMTPResponse from jinja2 import Environment @@ -42,7 +42,7 @@ def __init__(self, config: QueueWorkerConfig): ] super().__init__(config=config, handle_payloads=payloads) - self._smtp: Optional[SMTP] = None + self._smtp: SMTP | None = None self._jinja2 = Jinja2Env() @property @@ -288,7 +288,7 @@ async def send_eduid_termination_mail(self, data: EduidTerminationEmail) -> Stat ) -def init_mail_worker(name: str = "mail_worker", test_config: Optional[Mapping[str, Any]] = None) -> MailQueueWorker: +def init_mail_worker(name: str = "mail_worker", test_config: Mapping[str, Any] | None = None) -> MailQueueWorker: config = load_config(typ=QueueWorkerConfig, app_name=name, ns="queue", test_config=test_config) return MailQueueWorker(config=config) diff --git a/src/eduid/queue/workers/scim_event.py b/src/eduid/queue/workers/scim_event.py index 33f9a318b..31d7fbf4c 100644 --- a/src/eduid/queue/workers/scim_event.py +++ b/src/eduid/queue/workers/scim_event.py @@ -2,7 +2,7 @@ import json import logging from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast import httpx @@ -56,7 +56,7 @@ async def send_scim_notification(self, data: EduidSCIMAPINotification) -> Status def init_scim_event_worker( - name: str = "scim_event_worker", test_config: Optional[Mapping[str, Any]] = None + name: str = "scim_event_worker", test_config: Mapping[str, Any] | None = None ) -> ScimEventQueueWorker: config = load_config(typ=QueueWorkerConfig, app_name=name, ns="queue", test_config=test_config) return ScimEventQueueWorker(config=config) diff --git a/src/eduid/queue/workers/sink.py b/src/eduid/queue/workers/sink.py index 8ede5859b..5643b5b5e 100644 --- a/src/eduid/queue/workers/sink.py +++ b/src/eduid/queue/workers/sink.py @@ -6,7 +6,7 @@ from asyncio import Task from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from eduid.common.config.parsers import load_config from eduid.queue.config import QueueWorkerConfig @@ -28,8 +28,8 @@ def __init__(self, config: QueueWorkerConfig): self._receiving = False self._counter = 0 - self._first_ts: Optional[datetime] = None - self._last_ts: Optional[datetime] = None + self._first_ts: datetime | None = None + self._last_ts: datetime | None = None hostname = os.environ.get("HOSTNAME") or "localhost" self._sender_info = SenderInfo(hostname=hostname, node_id="sink_worker") @@ -86,7 +86,7 @@ async def periodic_stats_publishing(self) -> None: self._receiving = False -def init_sink_worker(name: str = "sink_worker", test_config: Optional[Mapping[str, Any]] = None) -> SinkQueueWorker: +def init_sink_worker(name: str = "sink_worker", test_config: Mapping[str, Any] | None = None) -> SinkQueueWorker: config = load_config(typ=QueueWorkerConfig, app_name=name, ns="queue", test_config=test_config) return SinkQueueWorker(config=config) diff --git a/src/eduid/satosa/scimapi/accr.py b/src/eduid/satosa/scimapi/accr.py index a99089557..4c35955ce 100644 --- a/src/eduid/satosa/scimapi/accr.py +++ b/src/eduid/satosa/scimapi/accr.py @@ -1,9 +1,10 @@ import logging from collections.abc import Mapping from copy import deepcopy -from typing import Any, Optional, Union +from typing import Any, TypeAlias import satosa.internal +import satosa.response from satosa.context import Context from satosa.exception import SATOSAAuthenticationError, SATOSAConfigurationError from satosa.micro_services.base import RequestMicroService, ResponseMicroService @@ -14,7 +15,7 @@ SupportedACCRsSortedByPrioConfig = list[str] LowestAcceptedACCRForVirtualIdpConfig = dict[str, str] InternalACCRRewriteMap = Mapping[str, str] -ProcessReturnType = Union[satosa.internal.InternalData, satosa.response.Response] +ProcessReturnType: TypeAlias = satosa.internal.InternalData | satosa.response.Response class request(RequestMicroService): @@ -35,13 +36,13 @@ class request(RequestMicroService): """ def __init__(self, config: Mapping[str, Any], internal_attributes: dict[str, Any], *args: Any, **kwargs: Any): - self.lowest_accepted_accr_for_virtual_idp: Optional[LowestAcceptedACCRForVirtualIdpConfig] = config.get( + self.lowest_accepted_accr_for_virtual_idp: LowestAcceptedACCRForVirtualIdpConfig | None = config.get( "lowest_accepted_accr_for_virtual_idp" ) self.supported_accr_sorted_by_prio: SupportedACCRsSortedByPrioConfig = config.get( "supported_accr_sorted_by_prio", [] ) - self.internal_accr_rewrite_map: Optional[InternalACCRRewriteMap] = config.get("internal_accr_rewrite_map") + self.internal_accr_rewrite_map: InternalACCRRewriteMap | None = config.get("internal_accr_rewrite_map") if self.lowest_accepted_accr_for_virtual_idp: for idp, minimum_accr in self.lowest_accepted_accr_for_virtual_idp.items(): diff --git a/src/eduid/satosa/scimapi/scim_attributes.py b/src/eduid/satosa/scimapi/scim_attributes.py index 57951e69f..488f8319d 100644 --- a/src/eduid/satosa/scimapi/scim_attributes.py +++ b/src/eduid/satosa/scimapi/scim_attributes.py @@ -2,7 +2,7 @@ import pprint from collections.abc import Mapping from dataclasses import dataclass, field -from typing import Any, Optional +from typing import Any import satosa.context import satosa.internal @@ -21,10 +21,10 @@ @dataclass class Config: mongo_uri: str - neo4j_uri: Optional[str] = None + neo4j_uri: str | None = None neo4j_config: dict = field(default_factory=dict) allow_users_not_in_database: Mapping[str, bool] = field(default_factory=lambda: {"default": False}) - fallback_data_owner: Optional[str] = None + fallback_data_owner: str | None = None idp_to_data_owner: Mapping[str, str] = field(default_factory=dict) mfa_stepup_issuer_to_entity_id: Mapping[str, str] = field(default_factory=dict) scope_to_data_owner: Mapping[str, str] = field(default_factory=dict) @@ -71,7 +71,7 @@ def get_userdb_for_data_owner(self, data_owner: str) -> ScimApiUserDB: ) return self._userdbs[data_owner] - def get_groupdb_for_data_owner(self, data_owner: str) -> Optional[ScimApiGroupDB]: + def get_groupdb_for_data_owner(self, data_owner: str) -> ScimApiGroupDB | None: if self.config.neo4j_uri is None: # be able to turn off group lookups by unsetting neo4j_uri logger.info("No neo4j_uri set in config, group lookups will be turned off.") @@ -159,7 +159,7 @@ def _process_user(self, user: ScimApiUser, data: satosa.internal.InternalData) - return data def _process_groups( - self, data_owner: Optional[str], user_groups: UserGroups, data: satosa.internal.InternalData + self, data_owner: str | None, user_groups: UserGroups, data: satosa.internal.InternalData ) -> satosa.internal.InternalData: if data_owner is None: return data @@ -191,12 +191,10 @@ def _get_scopes_for_idp(self, context: satosa.context.Context, entity_id: str) - res.update(_extract_saml_scope(idpsso)) return res - def _get_data_owner( - self, data: satosa.internal.InternalData, scopes: set[str], frontend_name: str - ) -> Optional[str]: + def _get_data_owner(self, data: satosa.internal.InternalData, scopes: set[str], frontend_name: str) -> str | None: # Look for explicit information about what data owner to use for this IdP issuer = frontend_name - data_owner: Optional[str] = self.config.virt_idp_to_data_owner.get(issuer) + data_owner: str | None = self.config.virt_idp_to_data_owner.get(issuer) # Fallback to issuer. E.g Skolverkets idpproxy if not data_owner: issuer = data.auth_info.issuer @@ -224,7 +222,7 @@ def _get_data_owner( return data_owner - def _get_user(self, data: satosa.internal.InternalData, data_owner: Optional[str]) -> Optional[ScimApiUser]: + def _get_user(self, data: satosa.internal.InternalData, data_owner: str | None) -> ScimApiUser | None: if data_owner is None: return None @@ -243,7 +241,7 @@ def _get_user(self, data: satosa.internal.InternalData, data_owner: Optional[str logger.info(f"No user found using {self.ext_id_attr} {ext_id}") return user - def _get_user_groups(self, user: Optional[ScimApiUser], data_owner: Optional[str]) -> Optional[UserGroups]: + def _get_user_groups(self, user: ScimApiUser | None, data_owner: str | None) -> UserGroups | None: if user is None or data_owner is None: return None diff --git a/src/eduid/satosa/scimapi/static_attributes.py b/src/eduid/satosa/scimapi/static_attributes.py index cb2f422c5..19493e842 100644 --- a/src/eduid/satosa/scimapi/static_attributes.py +++ b/src/eduid/satosa/scimapi/static_attributes.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from typing import Any, Optional +from typing import Any import satosa.context import satosa.internal @@ -51,8 +51,8 @@ class AddStaticAttributesForVirtualIdp(ResponseMicroService): def __init__(self, config: Mapping[str, Any], *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.static_attributes: Optional[StaticAttributesConfig] = config.get("static_attributes_for_virtual_idp") - self.static_appended_attributes: Optional[StaticAppendedAttributesConfig] = config.get( + self.static_attributes: StaticAttributesConfig | None = config.get("static_attributes_for_virtual_idp") + self.static_appended_attributes: StaticAppendedAttributesConfig | None = config.get( "static_appended_attributes_for_virtual_idp" ) diff --git a/src/eduid/satosa/scimapi/stepup.py b/src/eduid/satosa/scimapi/stepup.py index 80a7f88cd..acfedfbb4 100644 --- a/src/eduid/satosa/scimapi/stepup.py +++ b/src/eduid/satosa/scimapi/stepup.py @@ -4,7 +4,7 @@ import json import logging from collections.abc import Callable, Iterable, Mapping -from typing import Any, NewType, Optional, TypeAlias, Union +from typing import Any, NewType, TypeAlias from urllib.parse import urlparse from pydantic import BaseModel, Field, ValidationError @@ -45,7 +45,7 @@ logger = logging.getLogger(__name__) # TODO: Remove when https://github.com/IdentityPython/SATOSA/pull/435 has been accepted -ProcessReturnType = Union[satosa.internal.InternalData, satosa.response.Response] +ProcessReturnType: TypeAlias = satosa.internal.InternalData | satosa.response.Response CallbackReturnType: TypeAlias = satosa.response.Response CallbackCallSignature = Callable[[satosa.context.Context, Any], CallbackReturnType] # end todo @@ -65,7 +65,7 @@ class StepUpError(SATOSAError): class LoaSettings(BaseModel): requested: list[str] # LoA that the StepUp-provider understands extra_accepted: list[str] = Field(default=[]) # (aliased) LoAs that satisfy the requester - returned: Optional[str] = ( + returned: str | None = ( None # LoA that should be returned to the requester, if we get any of the requested + extra_accepted ) @@ -77,8 +77,8 @@ class LoaSettings(BaseModel): class MfaConfig(BaseModel): by_entity_id: Mapping[EntityId, LoaSettings] = Field(default={}) - by_entity_category: Optional[Mapping[EntityCategory, LoaSettings]] = Field(default={}) - by_assurance_certification: Optional[Mapping[AssuranceCertification, LoaSettings]] = Field(default={}) + by_entity_category: Mapping[EntityCategory, LoaSettings] | None = Field(default={}) + by_assurance_certification: Mapping[AssuranceCertification, LoaSettings] | None = Field(default={}) class MFA(BaseModel): @@ -88,13 +88,13 @@ class MFA(BaseModel): class StepupPluginConfig(BaseModel): mfa: MfaConfig sp_config: Mapping[str, Any] - sign_alg: Optional[str] = None - digest_alg: Optional[str] = None + sign_alg: str | None = None + digest_alg: str | None = None class StepupParams(BaseModel): issuer: str - issuer_loa: Optional[str] = None # LoA that the IdP released - as requested through the acr_mapping configuration + issuer_loa: str | None = None # LoA that the IdP released - as requested through the acr_mapping configuration requester: EntityId requester_loas: list[str] # (original) LoAs required by the requester loa_settings: LoaSettings # LoA settings to use. Either from the configuration or derived using entity attributes in the metadata. @@ -208,7 +208,7 @@ def __init__(self, config: Mapping[str, Any], internal_attributes: dict[str, Any logger.info("StepUp Authentication is active") def _get_params(self, context: satosa.context.Context, data: satosa.internal.InternalData) -> StepupParams: - _requester: Optional[EntityId] = EntityId(data.requester) if isinstance(data.requester, str) else None + _requester: EntityId | None = EntityId(data.requester) if isinstance(data.requester, str) else None _loa_settings = get_loa_settings_for_entity_id(_requester, get_metadata(context), self.mfa) if not _loa_settings: sp_requested = AuthnContext.get_from_state(context=context, state_key=STATE_KEY_MFA) @@ -559,8 +559,8 @@ def idp_sent_loa(context: Context, state_key: str = STATE_KEY_LOA) -> bool: def get_loa_settings_for_entity_id( - entity_id: Optional[EntityId], metadata: Iterable[MetaData], mfa: Optional[MfaConfig] -) -> Optional[LoaSettings]: + entity_id: EntityId | None, metadata: Iterable[MetaData], mfa: MfaConfig | None +) -> LoaSettings | None: """ SP: Return setting from by_entity_id or by_entity_category. IDP: Return settings from by_entity_id or by_assurance_certification. @@ -610,7 +610,7 @@ class StepupSAMLBackend(SAMLBackend): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.mfa: Optional[MfaConfig] = None + self.mfa: MfaConfig | None = None try: parsed_config = StepupPluginConfig.model_validate(self.config) @@ -642,7 +642,7 @@ class RewriteAuthnContextClass(ResponseMicroService): def __init__(self, config: Mapping[str, Any], internal_attributes: dict[str, Any], *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.mfa: Optional[MfaConfig] = None + self.mfa: MfaConfig | None = None try: parsed_config = MFA.model_validate(config) @@ -668,7 +668,7 @@ def process( logger.debug(f"LoA settings for {_issuer}: {_loa_settings}") if _loa_settings and _loa_settings.returned: - _asserted_loa: Optional[str] = data.auth_info.auth_class_ref + _asserted_loa: str | None = data.auth_info.auth_class_ref if _asserted_loa in _loa_settings.requested or _asserted_loa in _loa_settings.extra_accepted: logger.info( "Rewriting authnContextClassRef in response from " @@ -683,7 +683,7 @@ def process( return super().process(context, data) -def is_loa_requirements_satisfied(settings: Optional[LoaSettings], loa: Optional[str]) -> bool: +def is_loa_requirements_satisfied(settings: LoaSettings | None, loa: str | None) -> bool: if not settings: return False satisfied = loa in settings.requested or loa in settings.extra_accepted @@ -696,7 +696,7 @@ def store_params(data: satosa.internal.InternalData, params: StepupParams) -> No data.stepup_params = params.dict() -def fetch_params(data: satosa.internal.InternalData) -> Optional[StepupParams]: +def fetch_params(data: satosa.internal.InternalData) -> StepupParams | None: """Retrieve the LoA settings from the internal data""" if not hasattr(data, "stepup_params") or not isinstance(data.stepup_params, dict): return None diff --git a/src/eduid/scimapi/app.py b/src/eduid/scimapi/app.py index a2dbf03e7..5809e7eff 100644 --- a/src/eduid/scimapi/app.py +++ b/src/eduid/scimapi/app.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from fastapi.exceptions import RequestValidationError from starlette.middleware.cors import CORSMiddleware @@ -24,14 +22,14 @@ class ScimAPI(FastAPI): - def __init__(self, name: str = "scimapi", test_config: Optional[dict] = None): + def __init__(self, name: str = "scimapi", test_config: dict | None = None): self.config = load_config(typ=ScimApiConfig, app_name=name, ns="api", test_config=test_config) super().__init__(root_path=self.config.application_root) self.context = Context(config=self.config) self.context.logger.info(f"Starting {name} app") -def init_api(name: str = "scimapi", test_config: Optional[dict] = None) -> ScimAPI: +def init_api(name: str = "scimapi", test_config: dict | None = None) -> ScimAPI: app = ScimAPI(name=name, test_config=test_config) app.router.route_class = ScimApiRoute diff --git a/src/eduid/scimapi/config.py b/src/eduid/scimapi/config.py index 33a6dc89b..784af4116 100644 --- a/src/eduid/scimapi/config.py +++ b/src/eduid/scimapi/config.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from pydantic import BaseModel, Field, field_validator @@ -10,9 +9,9 @@ class AWSMixin(BaseModel): - aws_access_key_id: Optional[str] = None - aws_secret_access_key: Optional[str] = None - aws_region: Optional[str] = None + aws_access_key_id: str | None = None + aws_secret_access_key: str | None = None + aws_region: str | None = None class ScimApiConfig(AuthnBearerTokenConfig, LoggingConfigMixin, AWSMixin): @@ -27,7 +26,7 @@ class ScimApiConfig(AuthnBearerTokenConfig, LoggingConfigMixin, AWSMixin): no_authn_urls: list[str] = Field(default=["^/login/?$", "^/status/healthy$", "^/docs/?$", "^/openapi.json"]) status_cache_seconds: int = 10 # The expected value of the authn JWT claims['requested_access']['type'] - requested_access_type: Optional[str] = "scim-api" + requested_access_type: str | None = "scim-api" # Invite config invite_url: str = "" invite_expire: int = 180 * 86400 # 180 days diff --git a/src/eduid/scimapi/context.py b/src/eduid/scimapi/context.py index 616ad9fb5..a16b07945 100644 --- a/src/eduid/scimapi/context.py +++ b/src/eduid/scimapi/context.py @@ -2,7 +2,6 @@ import logging.config from dataclasses import dataclass, field from datetime import datetime -from typing import Optional, Union from uuid import UUID from eduid.common.config.base import DataOwnerConfig, DataOwnerName @@ -95,16 +94,16 @@ def _get_data_owner_dbs(self, data_owner: DataOwnerName) -> DataOwnerDatabases: data_owner_dbs.last_accessed = utc_now() return data_owner_dbs - def get_userdb(self, data_owner: DataOwnerName) -> Optional[ScimApiUserDB]: + def get_userdb(self, data_owner: DataOwnerName) -> ScimApiUserDB | None: return self._get_data_owner_dbs(data_owner=data_owner).userdb - def get_groupdb(self, data_owner: DataOwnerName) -> Optional[ScimApiGroupDB]: + def get_groupdb(self, data_owner: DataOwnerName) -> ScimApiGroupDB | None: return self._get_data_owner_dbs(data_owner=data_owner).groupdb - def get_invitedb(self, data_owner: DataOwnerName) -> Optional[ScimApiInviteDB]: + def get_invitedb(self, data_owner: DataOwnerName) -> ScimApiInviteDB | None: return self._get_data_owner_dbs(data_owner=data_owner).invitedb - def get_eventdb(self, data_owner: DataOwnerName) -> Optional[ScimApiEventDB]: + def get_eventdb(self, data_owner: DataOwnerName) -> ScimApiEventDB | None: return self._get_data_owner_dbs(data_owner=data_owner).eventdb def url_for(self, *args) -> str: @@ -116,7 +115,7 @@ def url_for(self, *args) -> str: def resource_url(self, resource_type: SCIMResourceType, scim_id: UUID) -> str: return self.url_for(resource_type.value + "s", str(scim_id)) - def check_version(self, req: ContextRequest, db_obj: Union[ScimApiGroup, ScimApiUser, ScimApiInvite]) -> bool: + def check_version(self, req: ContextRequest, db_obj: ScimApiGroup | ScimApiUser | ScimApiInvite) -> bool: if req.headers.get("IF-MATCH") == make_etag(db_obj.version): return True self.logger.error("Version mismatch") diff --git a/src/eduid/scimapi/context_request.py b/src/eduid/scimapi/context_request.py index 8c4f34889..00c2e91cc 100644 --- a/src/eduid/scimapi/context_request.py +++ b/src/eduid/scimapi/context_request.py @@ -1,6 +1,5 @@ __author__ = "lundberg" -from typing import Optional from eduid.common.config.base import DataOwnerName from eduid.common.fastapi.context_request import Context, ContextRequestRoute @@ -10,11 +9,11 @@ class ScimApiContext(Context): - data_owner: Optional[DataOwnerName] = None - userdb: Optional[ScimApiUserDB] = None - groupdb: Optional[ScimApiGroupDB] = None - invitedb: Optional[ScimApiInviteDB] = None - eventdb: Optional[ScimApiEventDB] = None + data_owner: DataOwnerName | None = None + userdb: ScimApiUserDB | None = None + groupdb: ScimApiGroupDB | None = None + invitedb: ScimApiInviteDB | None = None + eventdb: ScimApiEventDB | None = None class ScimApiRoute(ContextRequestRoute): diff --git a/src/eduid/scimapi/exceptions.py b/src/eduid/scimapi/exceptions.py index 5a3eda36d..562378f2e 100644 --- a/src/eduid/scimapi/exceptions.py +++ b/src/eduid/scimapi/exceptions.py @@ -2,7 +2,6 @@ import logging import uuid -from typing import Optional, Union from fastapi import Request, status from fastapi.encoders import jsonable_encoder @@ -22,10 +21,10 @@ class MaxRetriesReached(Exception): class ErrorDetail(BaseModel): - scimType: Optional[str] = None + scimType: str | None = None schemas: list[str] = [SCIMSchema.ERROR.value] - detail: Optional[Union[str, dict, list]] = None - status: Optional[int] = None + detail: str | dict | list | None = None + status: int | None = None class SCIMErrorResponse(JSONResponse): @@ -67,22 +66,22 @@ class HTTPErrorDetail(Exception): def __init__( self, status_code: int, - detail: Optional[str] = None, - schemas: Optional[list[str]] = None, - scim_type: Optional[str] = None, + detail: str | None = None, + schemas: list[str] | None = None, + scim_type: str | None = None, ): if schemas is None: schemas = [SCIMSchema.ERROR.value] self._error_detail = ErrorDetail(scimType=scim_type, schemas=schemas, detail=detail, status=status_code) - self._extra_headers: Optional[dict] = None + self._extra_headers: dict | None = None @property def error_detail(self) -> ErrorDetail: return self._error_detail @property - def extra_headers(self) -> Optional[dict]: + def extra_headers(self) -> dict | None: return self._extra_headers @extra_headers.setter diff --git a/src/eduid/scimapi/models/event.py b/src/eduid/scimapi/models/event.py index 5a634a7ea..e6a06b472 100644 --- a/src/eduid/scimapi/models/event.py +++ b/src/eduid/scimapi/models/event.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from uuid import UUID from pydantic import Field @@ -22,8 +22,8 @@ class NutidEventResource(EduidBaseModel): scim_id: UUID = Field(alias="id") last_modified: ScimDatetime = Field(alias="lastModified") version: WeakVersion - external_id: Optional[str] = Field(default=None, alias="externalId") - location: Optional[str] = None + external_id: str | None = Field(default=None, alias="externalId") + location: str | None = None class NutidEventExtensionV1(EduidBaseModel): @@ -35,9 +35,9 @@ class NutidEventExtensionV1(EduidBaseModel): resource: NutidEventResource level: EventLevel = Field(default=EventLevel.INFO) data: dict[str, Any] = Field(default_factory=dict) - expires_at: Optional[ScimDatetime] = Field(default=None, alias="expiresAt") - timestamp: Optional[ScimDatetime] = None - source: Optional[str] = None + expires_at: ScimDatetime | None = Field(default=None, alias="expiresAt") + timestamp: ScimDatetime | None = None + source: str | None = None class NutidEventV1(EduidBaseModel): diff --git a/src/eduid/scimapi/models/group.py b/src/eduid/scimapi/models/group.py index 8d71c317f..15197987f 100644 --- a/src/eduid/scimapi/models/group.py +++ b/src/eduid/scimapi/models/group.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import Field @@ -25,7 +25,7 @@ class GroupMember(SubResource): class Group(EduidBaseModel): display_name: str = Field(alias="displayName") members: list[GroupMember] = Field(default_factory=list) - nutid_group_v1: Optional[NutidGroupExtensionV1] = Field( + nutid_group_v1: NutidGroupExtensionV1 | None = Field( default_factory=NutidGroupExtensionV1, alias=SCIMSchema.NUTID_GROUP_V1.value ) diff --git a/src/eduid/scimapi/routers/events.py b/src/eduid/scimapi/routers/events.py index 9f1d7fced..62948a6fd 100644 --- a/src/eduid/scimapi/routers/events.py +++ b/src/eduid/scimapi/routers/events.py @@ -1,5 +1,4 @@ from datetime import timedelta -from typing import Optional from fastapi import Response @@ -28,7 +27,7 @@ @events_router.get("/{scim_id}", response_model=EventResponse, response_model_exclude_none=True) -async def on_get(req: ContextRequest, resp: Response, scim_id: Optional[str] = None) -> EventResponse: +async def on_get(req: ContextRequest, resp: Response, scim_id: str | None = None) -> EventResponse: if scim_id is None: raise BadRequest(detail="Not implemented") req.app.context.logger.info(f"Fetching event {scim_id}") diff --git a/src/eduid/scimapi/routers/invites.py b/src/eduid/scimapi/routers/invites.py index d5a6189f5..19732beb5 100644 --- a/src/eduid/scimapi/routers/invites.py +++ b/src/eduid/scimapi/routers/invites.py @@ -1,5 +1,4 @@ from dataclasses import replace -from typing import Optional from fastapi import Response @@ -37,7 +36,7 @@ @invites_router.get("/{scim_id}", response_model=InviteResponse, response_model_exclude_none=True) -async def on_get(req: ContextRequest, resp: Response, scim_id: Optional[str] = None) -> InviteResponse: +async def on_get(req: ContextRequest, resp: Response, scim_id: str | None = None) -> InviteResponse: if scim_id is None: raise BadRequest(detail="Not implemented") req.app.context.logger.info(f"Fetching invite {scim_id}") diff --git a/src/eduid/scimapi/routers/users.py b/src/eduid/scimapi/routers/users.py index a1ac99734..7aac70c58 100644 --- a/src/eduid/scimapi/routers/users.py +++ b/src/eduid/scimapi/routers/users.py @@ -1,7 +1,6 @@ import pprint import re from dataclasses import replace -from typing import Optional from fastapi import Response @@ -45,7 +44,7 @@ @users_router.get("/{scim_id}", response_model=UserResponse, response_model_exclude_none=True) -async def on_get(req: ContextRequest, resp: Response, scim_id: Optional[str] = None) -> UserResponse: +async def on_get(req: ContextRequest, resp: Response, scim_id: str | None = None) -> UserResponse: if scim_id is None: raise BadRequest(detail="Not implemented") req.app.context.logger.info(f"Fetching user {scim_id}") diff --git a/src/eduid/scimapi/routers/utils/events.py b/src/eduid/scimapi/routers/utils/events.py index e48a43555..1a7ecdbc6 100644 --- a/src/eduid/scimapi/routers/utils/events.py +++ b/src/eduid/scimapi/routers/utils/events.py @@ -1,5 +1,5 @@ from datetime import timedelta -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from uuid import uuid4 from fastapi import Response @@ -60,7 +60,7 @@ def db_event_to_response(req: ContextRequest, resp: Response, db_event: ScimApiE return event_response -def get_scim_referenced(req: ContextRequest, resource: NutidEventResource) -> Optional[ScimApiResourceBase]: +def get_scim_referenced(req: ContextRequest, resource: NutidEventResource) -> ScimApiResourceBase | None: if resource.resource_type == SCIMResourceType.USER: return req.context.userdb.get_user_by_scim_id(str(resource.scim_id)) elif resource.resource_type == SCIMResourceType.GROUP: diff --git a/src/eduid/scimapi/routers/utils/groups.py b/src/eduid/scimapi/routers/utils/groups.py index 1c2c09c02..b92709791 100644 --- a/src/eduid/scimapi/routers/utils/groups.py +++ b/src/eduid/scimapi/routers/utils/groups.py @@ -1,6 +1,5 @@ import re from datetime import datetime -from typing import Optional from uuid import UUID from fastapi import Request, Response @@ -65,8 +64,8 @@ def db_group_to_response(req: ContextRequest, resp: Response, db_group: ScimApiG def filter_display_name( req: ContextRequest, filter: SearchFilter, - skip: Optional[int] = None, - limit: Optional[int] = None, + skip: int | None = None, + limit: int | None = None, ) -> tuple[list[ScimApiGroup], int]: if filter.op != "eq": raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") @@ -85,7 +84,7 @@ def filter_display_name( def filter_lastmodified( - req: ContextRequest, filter: SearchFilter, skip: Optional[int] = None, limit: Optional[int] = None + req: ContextRequest, filter: SearchFilter, skip: int | None = None, limit: int | None = None ) -> tuple[list[ScimApiGroup], int]: if filter.op not in ["gt", "ge"]: raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") @@ -101,8 +100,8 @@ def filter_lastmodified( def filter_extensions_data( req: ContextRequest, filter: SearchFilter, - skip: Optional[int] = None, - limit: Optional[int] = None, + skip: int | None = None, + limit: int | None = None, ) -> tuple[list[ScimApiGroup], int]: if filter.op != "eq": raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") diff --git a/src/eduid/scimapi/routers/utils/invites.py b/src/eduid/scimapi/routers/utils/invites.py index 82046bb87..7fc405f23 100644 --- a/src/eduid/scimapi/routers/utils/invites.py +++ b/src/eduid/scimapi/routers/utils/invites.py @@ -2,7 +2,7 @@ from dataclasses import asdict from datetime import datetime, timedelta from os import environ -from typing import Any, Optional +from typing import Any from fastapi import Request, Response from pymongo.errors import DuplicateKeyError @@ -181,7 +181,7 @@ def save_invite( def filter_lastmodified( - req: ContextRequest, filter: SearchFilter, skip: Optional[int] = None, limit: Optional[int] = None + req: ContextRequest, filter: SearchFilter, skip: int | None = None, limit: int | None = None ) -> tuple[list[ScimApiInvite], int]: if filter.op not in ["gt", "ge"]: raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") diff --git a/src/eduid/scimapi/routers/utils/users.py b/src/eduid/scimapi/routers/utils/users.py index e83790301..c3a68422c 100644 --- a/src/eduid/scimapi/routers/utils/users.py +++ b/src/eduid/scimapi/routers/utils/users.py @@ -1,7 +1,7 @@ from collections.abc import Sequence from dataclasses import asdict from datetime import datetime -from typing import Any, Optional +from typing import Any from fastapi import Response from pymongo.errors import DuplicateKeyError @@ -170,7 +170,7 @@ def filter_externalid(req: ContextRequest, search_filter: SearchFilter) -> list[ def filter_lastmodified( - req: ContextRequest, search_filter: SearchFilter, skip: Optional[int] = None, limit: Optional[int] = None + req: ContextRequest, search_filter: SearchFilter, skip: int | None = None, limit: int | None = None ) -> tuple[list[ScimApiUser], int]: if search_filter.op not in ["gt", "ge"]: raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") @@ -186,8 +186,8 @@ def filter_profile_data( search_filter: SearchFilter, profile: str, key: str, - skip: Optional[int] = None, - limit: Optional[int] = None, + skip: int | None = None, + limit: int | None = None, ) -> tuple[list[ScimApiUser], int]: if search_filter.op != "eq": raise BadRequest(scim_type="invalidFilter", detail="Unsupported operator") diff --git a/src/eduid/scimapi/search.py b/src/eduid/scimapi/search.py index c5b14f0bb..6d0f1d2d7 100644 --- a/src/eduid/scimapi/search.py +++ b/src/eduid/scimapi/search.py @@ -1,7 +1,6 @@ import logging import re from dataclasses import dataclass -from typing import Union from eduid.scimapi.exceptions import BadRequest @@ -12,7 +11,7 @@ class SearchFilter: attr: str op: str - val: Union[str, int] + val: str | int def parse_search_filter(filter: str) -> SearchFilter: @@ -25,7 +24,7 @@ def parse_search_filter(filter: str) -> SearchFilter: logger.debug(f"Unrecognised filter: {filter}") raise BadRequest(scim_type="invalidFilter", detail="Unrecognised filter") - val: Union[str, int] + val: str | int attr, op, val = match.groups() if len(val) and val[0] == '"' and val[-1] == '"': diff --git a/src/eduid/scimapi/test-scripts/scim-util.py b/src/eduid/scimapi/test-scripts/scim-util.py index 19875eb7a..f1df093fe 100755 --- a/src/eduid/scimapi/test-scripts/scim-util.py +++ b/src/eduid/scimapi/test-scripts/scim-util.py @@ -7,7 +7,7 @@ from collections.abc import Callable, Mapping from dataclasses import dataclass from pprint import pformat -from typing import Any, NewType, Optional, cast +from typing import Any, NewType, cast import requests import yaml @@ -25,7 +25,7 @@ class Api: url: str verify: bool - token: Optional[str] = None + token: str | None = None def parse_args() -> Args: @@ -42,11 +42,11 @@ def parse_args() -> Args: def scim_request( func: Callable, url: str, - data: Optional[dict] = None, - headers: Optional[dict] = None, - token: Optional[str] = None, + data: dict | None = None, + headers: dict | None = None, + token: str | None = None, verify: bool = True, -) -> Optional[dict[str, Any]]: +) -> dict[str, Any] | None: if not headers: headers = {"content-type": "application/scim+json"} if token is not None: @@ -66,10 +66,10 @@ def scim_request( def _make_request( func: Callable, url: str, - data: Optional[dict] = None, - headers: Optional[dict] = None, + data: dict | None = None, + headers: dict | None = None, verify: bool = True, -) -> Optional[requests.Response]: +) -> requests.Response | None: r = func(url, json=data, headers=headers, verify=verify) logger.debug(f"Response from server: {r}\n{r.text}") @@ -85,7 +85,7 @@ def _make_request( return r -def search_user(api: Api, filter: str) -> Optional[dict[str, Any]]: +def search_user(api: Api, filter: str) -> dict[str, Any] | None: logger.info(f"Searching for user with filter {filter}") query = { "schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"], @@ -100,7 +100,7 @@ def search_user(api: Api, filter: str) -> Optional[dict[str, Any]]: return res -def search_group(api: Api, filter: str) -> Optional[dict[str, Any]]: +def search_group(api: Api, filter: str) -> dict[str, Any] | None: logger.info(f"Searching for group with filter {filter}") query = { "schemas": ["urn:ietf:params:scim:api:messages:2.0:SearchRequest"], @@ -115,7 +115,7 @@ def search_group(api: Api, filter: str) -> Optional[dict[str, Any]]: return res -def create_user(api: Api, external_id: str) -> Optional[dict[str, Any]]: +def create_user(api: Api, external_id: str) -> dict[str, Any] | None: logger.info(f"Creating user with externalId {external_id}") query = {"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "externalId": external_id} logger.debug(f"Sending user create query:\n{pformat(json.dumps(query, sort_keys=True, indent=4))}") @@ -124,7 +124,7 @@ def create_user(api: Api, external_id: str) -> Optional[dict[str, Any]]: return res -def create_group(api: Api, display_name: str, token: Optional[str] = None) -> Optional[dict[str, Any]]: +def create_group(api: Api, display_name: str, token: str | None = None) -> dict[str, Any] | None: logger.info(f"Creating group with displayName {display_name}") query = {"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": display_name, "members": []} logger.debug(f"Sending group create query:\n{pformat(json.dumps(query, sort_keys=True, indent=4))}") @@ -133,7 +133,7 @@ def create_group(api: Api, display_name: str, token: Optional[str] = None) -> Op return res -def get_user_resource(api: Api, scim_id: str) -> Optional[dict[str, Any]]: +def get_user_resource(api: Api, scim_id: str) -> dict[str, Any] | None: logger.debug(f"Fetching SCIM user resource {scim_id}") if "@" in scim_id: @@ -147,7 +147,7 @@ def get_user_resource(api: Api, scim_id: str) -> Optional[dict[str, Any]]: return scim_request(requests.get, f"{api.url}/Users/{scim_id}", token=api.token, verify=api.verify) -def get_group_resource(api: Api, scim_id: str) -> Optional[dict[str, Any]]: +def get_group_resource(api: Api, scim_id: str) -> dict[str, Any] | None: logger.debug(f"Fetching SCIM group resource {scim_id}") return scim_request(requests.get, f"{api.url}/Groups/{scim_id}", token=api.token, verify=api.verify) @@ -183,7 +183,7 @@ def put_user(api: Api, scim_id: str, nutid_data: Mapping[str, Any]) -> None: return None -def put_group(api: Api, scim_id: str, data: dict[str, Any], token: Optional[str] = None) -> None: +def put_group(api: Api, scim_id: str, data: dict[str, Any], token: str | None = None) -> None: scim = get_group_resource(api, scim_id) if not scim: return @@ -219,7 +219,7 @@ def post_event( resource_scim_id: str, resource_type: str, level: str = "info", - data: Optional[dict[str, Any]] = None, + data: dict[str, Any] | None = None, ) -> None: if resource_type == "User": resource = get_user_resource(api=api, scim_id=resource_scim_id) @@ -256,7 +256,7 @@ def post_event( logger.info(f"Update result:\n{json.dumps(res, sort_keys=True, indent=4)}") -def process_login(api: Api, params: Mapping[str, Any]) -> Optional[str]: +def process_login(api: Api, params: Mapping[str, Any]) -> str | None: url = f"{api.url}/login" data_owner = params["data_owner"] logger.debug(f"Login URL: {url}") diff --git a/src/eduid/scimapi/testing.py b/src/eduid/scimapi/testing.py index c44ae2f93..84d7ced98 100644 --- a/src/eduid/scimapi/testing.py +++ b/src/eduid/scimapi/testing.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import asdict from json import JSONDecodeError -from typing import Any, Optional, Union +from typing import Any import pkg_resources from bson import ObjectId @@ -130,9 +130,9 @@ def _get_config(self) -> dict: def add_user( self, identifier: str, - external_id: Optional[str] = None, - profiles: Optional[dict[str, ScimApiProfile]] = None, - linked_accounts: Optional[list[ScimApiLinkedAccount]] = None, + external_id: str | None = None, + profiles: dict[str, ScimApiProfile] | None = None, + linked_accounts: list[ScimApiLinkedAccount] | None = None, ) -> ScimApiUser: user = ScimApiUser(user_id=ObjectId(), scim_id=uuid.UUID(identifier), external_id=external_id) if profiles: @@ -155,7 +155,7 @@ def add_group_with_member(self, group_identifier: str, display_name: str, user_i assert saved_group is not None return saved_group - def add_member_to_group(self, group_identifier: str, user_identifier: str) -> Optional[ScimApiGroup]: + def add_member_to_group(self, group_identifier: str, user_identifier: str) -> ScimApiGroup | None: assert self.groupdb group = self.groupdb.get_group_by_scim_id(scim_id=group_identifier) assert group is not None # please mypy @@ -164,7 +164,7 @@ def add_member_to_group(self, group_identifier: str, user_identifier: str) -> Op self.groupdb.save(group) return self.groupdb.get_group_by_scim_id(scim_id=group_identifier) - def add_owner_to_group(self, group_identifier: str, user_identifier: str) -> Optional[ScimApiGroup]: + def add_owner_to_group(self, group_identifier: str, user_identifier: str) -> ScimApiGroup | None: assert self.groupdb group = self.groupdb.get_group_by_scim_id(scim_id=group_identifier) assert group is not None # please mypy @@ -192,11 +192,11 @@ def tearDown(self): def _assertScimError( self, json: Mapping[str, Any], - schemas: Optional[list[str]] = None, + schemas: list[str] | None = None, status: int = 400, - scim_type: Optional[str] = None, - detail: Optional[Any] = None, - exclude_keys: Optional[list[str]] = None, + scim_type: str | None = None, + detail: Any | None = None, + exclude_keys: list[str] | None = None, ): if schemas is None: schemas = [SCIMSchema.ERROR.value] @@ -212,7 +212,7 @@ def _assertScimError( def _assertScimResponseProperties( self, response: Response, - resource: Union[ScimApiGroup, ScimApiUser, ScimApiInvite, ScimApiEvent], + resource: ScimApiGroup | ScimApiUser | ScimApiInvite | ScimApiEvent, expected_schemas: list[str], ): if SCIMSchema.NUTID_USER_V1.value in response.json(): diff --git a/src/eduid/scimapi/tests/test_authn.py b/src/eduid/scimapi/tests/test_authn.py index 7b53027e2..f0f74fdbc 100644 --- a/src/eduid/scimapi/tests/test_authn.py +++ b/src/eduid/scimapi/tests/test_authn.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from dataclasses import asdict from pathlib import PurePath -from typing import Any, Optional +from typing import Any from uuid import uuid4 import pytest @@ -419,7 +419,7 @@ def _get_config(self) -> dict: config["authorization_mandatory"] = True return config - def _get_user_from_api(self, user: ScimApiUser, bearer_token: Optional[str] = None) -> Response: + def _get_user_from_api(self, user: ScimApiUser, bearer_token: str | None = None) -> Response: headers = self.headers if bearer_token: headers["Authorization"] = f"Bearer {bearer_token}" diff --git a/src/eduid/scimapi/tests/test_groupdb.py b/src/eduid/scimapi/tests/test_groupdb.py index d2d6cccc1..6a6ce1f36 100644 --- a/src/eduid/scimapi/tests/test_groupdb.py +++ b/src/eduid/scimapi/tests/test_groupdb.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from uuid import UUID, uuid4 from eduid.common.config.base import DataOwnerName @@ -28,7 +27,7 @@ def tearDown(self): super().tearDown() self.groupdb._drop_whole_collection() - def add_group(self, scim_id: UUID, display_name: str, extensions: Optional[GroupExtensions] = None) -> ScimApiGroup: + def add_group(self, scim_id: UUID, display_name: str, extensions: GroupExtensions | None = None) -> ScimApiGroup: if extensions is None: extensions = GroupExtensions() group = ScimApiGroup(scim_id=scim_id, display_name=display_name, extensions=extensions) diff --git a/src/eduid/scimapi/tests/test_scimevent.py b/src/eduid/scimapi/tests/test_scimevent.py index d9f64935f..27fcbb12b 100644 --- a/src/eduid/scimapi/tests/test_scimevent.py +++ b/src/eduid/scimapi/tests/test_scimevent.py @@ -1,7 +1,7 @@ from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from typing import Any, Optional +from typing import Any from uuid import UUID, uuid4 from httpx import Response @@ -19,7 +19,7 @@ class EventApiResult: response: Response event: NutidEventExtensionV1 parsed_response: EventResponse - request: Optional[Mapping] = None + request: Mapping | None = None class TestEventResource(ScimApiTestCase): diff --git a/src/eduid/scimapi/tests/test_scimgroup.py b/src/eduid/scimapi/tests/test_scimgroup.py index 99555aae4..38864cd65 100644 --- a/src/eduid/scimapi/tests/test_scimgroup.py +++ b/src/eduid/scimapi/tests/test_scimgroup.py @@ -3,7 +3,7 @@ import logging from collections.abc import Mapping from datetime import datetime -from typing import Any, Optional, Union +from typing import Any from uuid import UUID, uuid4 from bson import ObjectId @@ -59,7 +59,7 @@ def tearDown(self): super().tearDown() self.groupdb._drop_whole_collection() - def add_group(self, scim_id: UUID, display_name: str, extensions: Optional[GroupExtensions] = None) -> ScimApiGroup: + def add_group(self, scim_id: UUID, display_name: str, extensions: GroupExtensions | None = None) -> ScimApiGroup: if extensions is None: extensions = GroupExtensions() group = ScimApiGroup(scim_id=scim_id, display_name=display_name, extensions=extensions) @@ -68,9 +68,7 @@ def add_group(self, scim_id: UUID, display_name: str, extensions: Optional[Group self.groupdb.save(group) return group - def add_member( - self, group: ScimApiGroup, member: Union[ScimApiUser, ScimApiGroup], display_name: str - ) -> ScimApiGroup: + def add_member(self, group: ScimApiGroup, member: ScimApiUser | ScimApiGroup, display_name: str) -> ScimApiGroup: if isinstance(member, ScimApiUser): user_member = GraphUser(identifier=str(member.scim_id), display_name=display_name) group.add_member(user_member) @@ -87,9 +85,9 @@ def _perform_search( start: int = 1, count: int = 10, return_json: bool = False, - expected_group: Optional[ScimApiGroup] = None, - expected_num_resources: Optional[int] = None, - expected_total_results: Optional[int] = None, + expected_group: ScimApiGroup | None = None, + expected_num_resources: int | None = None, + expected_total_results: int | None = None, ): logger.info(f"Searching for group(s) using filter {repr(filter)}") req = { diff --git a/src/eduid/scimapi/tests/test_sciminvite.py b/src/eduid/scimapi/tests/test_sciminvite.py index e3444e223..ae1e1ad05 100644 --- a/src/eduid/scimapi/tests/test_sciminvite.py +++ b/src/eduid/scimapi/tests/test_sciminvite.py @@ -5,7 +5,7 @@ from copy import copy from dataclasses import asdict from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from bson import ObjectId @@ -190,7 +190,7 @@ def setUp(self) -> None: "profiles": {"student": {"attributes": {"displayName": "Test"}, "data": {}}}, } - def add_invite(self, data: Optional[dict[str, Any]] = None, update: bool = False) -> ScimApiInvite: + def add_invite(self, data: dict[str, Any] | None = None, update: bool = False) -> ScimApiInvite: invite_data = self.invite_data if data: invite_data = data @@ -285,9 +285,9 @@ def _perform_search( start: int = 1, count: int = 10, return_json: bool = False, - expected_invite: Optional[ScimApiInvite] = None, - expected_num_resources: Optional[int] = None, - expected_total_results: Optional[int] = None, + expected_invite: ScimApiInvite | None = None, + expected_num_resources: int | None = None, + expected_total_results: int | None = None, ): logger.info(f"Searching for group(s) using filter {repr(filter)}") req = { diff --git a/src/eduid/scimapi/tests/test_scimuser.py b/src/eduid/scimapi/tests/test_scimuser.py index 8bcf15754..dc275d718 100644 --- a/src/eduid/scimapi/tests/test_scimuser.py +++ b/src/eduid/scimapi/tests/test_scimuser.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest import IsolatedAsyncioTestCase from uuid import UUID, uuid4 @@ -177,8 +177,8 @@ def test_bson_serialization(self): class UserApiResult: request: Mapping[str, Any] response: Response - nutid_user: Optional[NutidUserExtensionV1] - parsed_response: Optional[UserResponse] + nutid_user: NutidUserExtensionV1 | None + parsed_response: UserResponse | None class ScimApiTestUserResourceBase(ScimApiTestCase): @@ -250,7 +250,7 @@ def _create_user(self, req: dict[str, Any], expect_success: bool = True) -> User return UserApiResult(request=req, nutid_user=nutid_user, response=response, parsed_response=user_response) def _update_user( - self, req: dict[str, Any], scim_id: UUID, version: Optional[ObjectId], expect_success: bool = True + self, req: dict[str, Any], scim_id: UUID, version: ObjectId | None, expect_success: bool = True ) -> UserApiResult: if "schemas" not in req: _schemas = [SCIMSchema.CORE_20_USER.value] @@ -726,9 +726,9 @@ def _perform_search( start: int = 1, count: int = 10, return_json: bool = False, - expected_user: Optional[ScimApiUser] = None, - expected_num_resources: Optional[int] = None, - expected_total_results: Optional[int] = None, + expected_user: ScimApiUser | None = None, + expected_num_resources: int | None = None, + expected_total_results: int | None = None, ): logger.info(f"Searching for user(s) using filter {repr(search_filter)}") req = { diff --git a/src/eduid/userdb/admin/__init__.py b/src/eduid/userdb/admin/__init__.py index 82014b827..48a649cdd 100644 --- a/src/eduid/userdb/admin/__init__.py +++ b/src/eduid/userdb/admin/__init__.py @@ -9,7 +9,7 @@ import time from collections.abc import Generator from copy import deepcopy -from typing import Any, Optional +from typing import Any import bson import bson.json_util @@ -44,10 +44,10 @@ class RawDb: log detailing all the changes. """ - def __init__(self, myname: Optional[str] = None, backupbase: str = "/root/raw_db_changes"): + def __init__(self, myname: str | None = None, backupbase: str = "/root/raw_db_changes"): self._client = get_client() self._start_time: str = datetime.datetime.fromtimestamp(int(time.time())).isoformat(sep="_").replace(":", "") - self._myname: Optional[str] = myname + self._myname: str | None = myname self._backupbase: str = backupbase self._file_num: int = 0 @@ -318,7 +318,7 @@ def get_client() -> MongoClient[TUserDbDocument]: ) -def get_argparser(description: Optional[str] = None, eppn: bool = False) -> argparse.ArgumentParser: +def get_argparser(description: str | None = None, eppn: bool = False) -> argparse.ArgumentParser: """ Get a standard argparser for raw db scripts. diff --git a/src/eduid/userdb/authninfo.py b/src/eduid/userdb/authninfo.py index 2e67619e4..1b3f92cb3 100644 --- a/src/eduid/userdb/authninfo.py +++ b/src/eduid/userdb/authninfo.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from datetime import datetime from enum import Enum -from typing import Optional from eduid.userdb import User from eduid.userdb.credentials import U2F, Password, Webauthn @@ -26,7 +25,7 @@ class AuthnCredType(str, Enum): class AuthnInfoElement: credential_type: AuthnCredType created_ts: datetime - success_ts: Optional[datetime] + success_ts: datetime | None class AuthnInfoDB(BaseDB): diff --git a/src/eduid/userdb/credentials/base.py b/src/eduid/userdb/credentials/base.py index 932d582e0..b2a8acdf6 100644 --- a/src/eduid/userdb/credentials/base.py +++ b/src/eduid/userdb/credentials/base.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Optional +from typing import Any from eduid.userdb.element import TVerifiedElementSubclass, VerifiedElement @@ -27,7 +27,7 @@ class Credential(VerifiedElement): so we are making them hashable. """ - proofing_method: Optional[CredentialProofingMethod] = None + proofing_method: CredentialProofingMethod | None = None def __str__(self): if len(self.key) == 24: diff --git a/src/eduid/userdb/credentials/external.py b/src/eduid/userdb/credentials/external.py index 73746b525..044c3bce0 100644 --- a/src/eduid/userdb/credentials/external.py +++ b/src/eduid/userdb/credentials/external.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from enum import Enum -from typing import Any, Literal, Optional +from typing import Any, Literal from bson import ObjectId from pydantic import Field, field_validator @@ -69,7 +69,7 @@ class BankIDCredential(ExternalCredential): level: str # a value like "loa3", "eidas_sub", ... -def external_credential_from_dict(data: Mapping[str, Any]) -> Optional[ExternalCredential]: +def external_credential_from_dict(data: Mapping[str, Any]) -> ExternalCredential | None: if data["framework"] == TrustFramework.SWECONN.value: return SwedenConnectCredential.from_dict(data) if data["framework"] == TrustFramework.EIDAS.value: diff --git a/src/eduid/userdb/credentials/fido.py b/src/eduid/userdb/credentials/fido.py index 2f207d199..ca56cd52f 100644 --- a/src/eduid/userdb/credentials/fido.py +++ b/src/eduid/userdb/credentials/fido.py @@ -1,7 +1,7 @@ from __future__ import annotations from hashlib import sha256 -from typing import Any, Optional, Union +from typing import Any from uuid import UUID from fido2.webauthn import AuthenticatorAttachment @@ -32,7 +32,7 @@ class U2F(FidoCredential): version: str public_key: str - attest_cert: Optional[str] = None + attest_cert: str | None = None @property def key(self) -> ElementKey: @@ -48,11 +48,11 @@ class Webauthn(FidoCredential): Webauthn token authentication credential """ - authenticator_id: Optional[Union[UUID, str]] = None + authenticator_id: UUID | str | None = None credential_data: str authenticator: AuthenticatorAttachment - webauthn_proofing_version: Optional[str] = None - attestation_format: Optional[AttestationFormat] = None + webauthn_proofing_version: str | None = None + attestation_format: AttestationFormat | None = None mfa_approved: bool = False @property diff --git a/src/eduid/userdb/credentials/list.py b/src/eduid/userdb/credentials/list.py index 810406242..59abf18d2 100644 --- a/src/eduid/userdb/credentials/list.py +++ b/src/eduid/userdb/credentials/list.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from eduid.userdb.credentials.base import Credential from eduid.userdb.credentials.external import external_credential_from_dict @@ -25,7 +25,7 @@ def from_list_of_dicts(cls: type[CredentialList], items: list[dict[str, Any]]) - if not isinstance(this, dict): raise UserHasUnknownData(f"Unknown credential data (type {type(this)}): {repr(this)}") - credential: Optional[Credential] = None + credential: Credential | None = None if "salt" in this: credential = Password.from_dict(this) diff --git a/src/eduid/userdb/db/async_db.py b/src/eduid/userdb/db/async_db.py index 0163cfac8..8d565bec2 100644 --- a/src/eduid/userdb/db/async_db.py +++ b/src/eduid/userdb/db/async_db.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import logging from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any import pymongo from bson import ObjectId @@ -41,7 +41,7 @@ class AsyncMongoDB(BaseMongoDB): def __init__( self, db_uri: str, - db_name: Optional[str] = None, + db_name: str | None = None, **kwargs: Any, ): super().__init__(db_uri=db_uri, db_name=db_name, **kwargs) @@ -57,7 +57,7 @@ def get_connection(self) -> AsyncIOMotorClient: """ return self._client - def get_database(self, database_name: Optional[str] = None) -> AsyncIOMotorDatabase: + def get_database(self, database_name: str | None = None) -> AsyncIOMotorDatabase: """ Get a pymongo database handle. @@ -70,7 +70,7 @@ def get_database(self, database_name: Optional[str] = None) -> AsyncIOMotorDatab raise ValueError("No database_name supplied, and no default provided to __init__") return self._client[database_name] - def get_collection(self, collection: str, database_name: Optional[str] = None) -> AsyncIOMotorCollection: + def get_collection(self, collection: str, database_name: str | None = None) -> AsyncIOMotorCollection: """ Get a pymongo collection handle. @@ -158,7 +158,7 @@ async def _drop_whole_collection(self): logger.warning(f"{self!s} Dropping collection {self._coll_name!r}") return await self._coll.drop() - async def _get_document_by_attr(self, attr: str, value: Any) -> Optional[Mapping[str, Any]]: + async def _get_document_by_attr(self, attr: str, value: Any) -> Mapping[str, Any] | None: """ Return the document in the MongoDB matching field=value @@ -193,7 +193,7 @@ async def _get_documents_by_attr(self, attr: str, value: str) -> list[Mapping[st return docs async def _get_documents_by_aggregate( - self, match: Mapping[str, Any], sort: Optional[Mapping[str, Any]] = None, limit: Optional[int] = None + self, match: Mapping[str, Any], sort: Mapping[str, Any] | None = None, limit: int | None = None ) -> list[Mapping[str, Any]]: pipeline: list[dict[str, Any]] = [{"$match": match}] @@ -208,9 +208,9 @@ async def _get_documents_by_aggregate( async def _get_documents_by_filter( self, spec: Mapping[str, Any], - fields: Optional[Mapping[str, Any]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, + fields: Mapping[str, Any] | None = None, + skip: int | None = None, + limit: int | None = None, ) -> list[Mapping[str, Any]]: """ Locate documents in the db using a custom search filter. @@ -237,7 +237,7 @@ async def _get_documents_by_filter( return [] return docs - async def db_count(self, spec: Optional[Mapping[str, Any]] = None, limit: Optional[int] = None) -> int: + async def db_count(self, spec: Mapping[str, Any] | None = None, limit: int | None = None) -> int: """ Return number of entries in the collection. @@ -252,7 +252,7 @@ async def db_count(self, spec: Optional[Mapping[str, Any]] = None, limit: Option args["limit"] = limit return await self._coll.count_documents(filter=_filter, **args) - async def remove_document(self, spec_or_id: Union[Mapping[str, Any], ObjectId]) -> bool: + async def remove_document(self, spec_or_id: Mapping[str, Any] | ObjectId) -> bool: """ Remove a document in the db given the _id or dict spec. diff --git a/src/eduid/userdb/db/base.py b/src/eduid/userdb/db/base.py index 785a5b5df..ccff66eb1 100644 --- a/src/eduid/userdb/db/base.py +++ b/src/eduid/userdb/db/base.py @@ -2,7 +2,7 @@ import copy import logging from collections.abc import Mapping -from typing import Any, NewType, Optional +from typing import Any, NewType from pymongo.uri_parser import parse_uri @@ -20,15 +20,15 @@ class BaseMongoDB: def __init__( self, db_uri: str, - db_name: Optional[str] = None, + db_name: str | None = None, **kwargs: Any, ): if db_uri is None: raise ValueError("db_uri not supplied") self._db_uri: str = db_uri - self._database_name: Optional[str] = db_name - self._sanitized_uri: Optional[str] = None + self._database_name: str | None = db_name + self._sanitized_uri: str | None = None self._parsed_uri = parse_uri(db_uri) diff --git a/src/eduid/userdb/db/sync_db.py b/src/eduid/userdb/db/sync_db.py index db860cf00..6393a9026 100644 --- a/src/eduid/userdb/db/sync_db.py +++ b/src/eduid/userdb/db/sync_db.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime -from typing import Any, Optional, Union +from typing import Any import pymongo import pymongo.collection @@ -45,7 +45,7 @@ class MongoDB(BaseMongoDB): def __init__( self, db_uri: str, - db_name: Optional[str] = None, + db_name: str | None = None, **kwargs: Any, ): super().__init__(db_uri=db_uri, db_name=db_name, **kwargs) @@ -62,7 +62,7 @@ def get_connection(self) -> pymongo.MongoClient[TUserDbDocument]: """ return self._client - def get_database(self, database_name: Optional[str] = None) -> Database[TUserDbDocument]: + def get_database(self, database_name: str | None = None) -> Database[TUserDbDocument]: """ Get a pymongo database handle. @@ -76,7 +76,7 @@ def get_database(self, database_name: Optional[str] = None) -> Database[TUserDbD return self._client[database_name] def get_collection( - self, collection: str, database_name: Optional[str] = None + self, collection: str, database_name: str | None = None ) -> pymongo.collection.Collection[TUserDbDocument]: """ Get a pymongo collection handle. @@ -136,7 +136,7 @@ class SaveResult: ts: datetime inserted: int = 0 updated: int = 0 - doc_id: Optional[ObjectId] = None + doc_id: ObjectId | None = None def __bool__(self): return bool(self.inserted or self.updated) @@ -182,7 +182,7 @@ def _get_all_docs(self) -> pymongo.cursor.Cursor[TUserDbDocument]: """ return self._coll.find({}) - def _get_document_by_attr(self, attr: str, value: Any) -> Optional[TUserDbDocument]: + def _get_document_by_attr(self, attr: str, value: Any) -> TUserDbDocument | None: """ Return the document in the MongoDB matching field=value @@ -217,7 +217,7 @@ def _get_documents_by_attr(self, attr: str, value: str) -> list[TUserDbDocument] return docs def _get_documents_by_aggregate( - self, match: Mapping[str, Any], sort: Optional[Mapping[str, Any]] = None, limit: Optional[int] = None + self, match: Mapping[str, Any], sort: Mapping[str, Any] | None = None, limit: int | None = None ) -> list[TUserDbDocument]: pipeline: list[dict[str, Any]] = [{"$match": match}] @@ -232,9 +232,9 @@ def _get_documents_by_aggregate( def _get_documents_by_filter( self, spec: Mapping[str, Any], - fields: Optional[Mapping[str, Any]] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, + fields: Mapping[str, Any] | None = None, + skip: int | None = None, + limit: int | None = None, ) -> list[TUserDbDocument]: """ Locate documents in the db using a custom search filter. @@ -261,7 +261,7 @@ def _get_documents_by_filter( return [] return docs - def db_count(self, spec: Optional[Mapping[str, Any]] = None, limit: Optional[int] = None) -> int: + def db_count(self, spec: Mapping[str, Any] | None = None, limit: int | None = None) -> int: """ Return number of entries in the collection. @@ -276,7 +276,7 @@ def db_count(self, spec: Optional[Mapping[str, Any]] = None, limit: Optional[int args["limit"] = limit return self._coll.count_documents(filter=_filter, **args) - def remove_document(self, spec_or_id: Union[Mapping[str, Any], ObjectId]) -> bool: + def remove_document(self, spec_or_id: Mapping[str, Any] | ObjectId) -> bool: """ Remove a document in the db given the _id or dict spec. @@ -331,7 +331,7 @@ def _save( data: TUserDbDocument, spec: Mapping[str, Any], is_in_database: bool, - meta: Optional[Meta] = None, + meta: Meta | None = None, ) -> SaveResult: """ Save a document in the db. @@ -413,7 +413,7 @@ def _save( logger.debug(f"{self} Updated document {replace_spec} (ts {now.isoformat()}): {save_result}") return save_result - def _get_and_format_existing_doc_for_logging(self, spec: Mapping[str, Any]) -> Optional[str]: + def _get_and_format_existing_doc_for_logging(self, spec: Mapping[str, Any]) -> str | None: db_doc = {} db_state = self._coll.find_one(spec) if not db_state: diff --git a/src/eduid/userdb/element.py b/src/eduid/userdb/element.py index bf132f099..758de0024 100644 --- a/src/eduid/userdb/element.py +++ b/src/eduid/userdb/element.py @@ -51,7 +51,7 @@ from collections.abc import Mapping from datetime import datetime from enum import Enum -from typing import Any, Generic, NewType, Optional, TypeVar, Union +from typing import Any, Generic, NewType, TypeVar from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -104,7 +104,7 @@ class Element(BaseModel): EventElement """ - created_by: Optional[str] = Field(default=None, alias="source") + created_by: str | None = Field(default=None, alias="source") created_ts: datetime = Field(default_factory=utc_now, alias="added_timestamp") modified_ts: datetime = Field(default_factory=utc_now) # This is a short-term hack to deploy new dataclass based elements without @@ -200,10 +200,10 @@ class VerifiedElement(Element, ABC): """ is_verified: bool = Field(default=False, alias="verified") - verified_by: Optional[str] = None - verified_ts: Optional[datetime] = None - proofing_method: Optional[Enum] = None - proofing_version: Optional[str] = None + verified_by: str | None = None + verified_ts: datetime | None = None + proofing_method: Enum | None = None + proofing_version: str | None = None def __str__(self): return f"" @@ -332,7 +332,7 @@ def to_list_of_dicts(self) -> list[dict[str, Any]]: """ return [this.to_dict() for this in self.elements if isinstance(this, Element)] - def find(self, key: Optional[Union[ElementKey, str]]) -> Optional[ListElement]: + def find(self, key: ElementKey | str | None) -> ListElement | None: """ Find an Element from the element list, using the key. @@ -433,7 +433,7 @@ def _validate_elements(cls, values: list[ListElement]): return values @property - def primary(self) -> Optional[ListElement]: + def primary(self) -> ListElement | None: """ :return: Return the primary Element. @@ -494,7 +494,7 @@ def set_primary(self, key: ElementKey) -> None: self.elements = new # type: ignore @classmethod - def _get_primary(cls, elements: list[ListElement]) -> Optional[ListElement]: + def _get_primary(cls, elements: list[ListElement]) -> ListElement | None: """ Find the primary element in a list, and ensure there is exactly one (unless there are no confirmed elements, in which case, ensure there are exactly zero). diff --git a/src/eduid/userdb/event.py b/src/eduid/userdb/event.py index 9793b9863..b995839a7 100644 --- a/src/eduid/userdb/event.py +++ b/src/eduid/userdb/event.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC -from typing import Any, Generic, Optional, TypeVar +from typing import Any, Generic, TypeVar from uuid import uuid4 from pydantic import Field @@ -15,8 +15,8 @@ class Event(Element): """ """ - data: Optional[dict[str, Any]] = None - event_type: Optional[str] = None + data: dict[str, Any] | None = None + event_type: str | None = None event_id: str = Field(default_factory=lambda: str(uuid4()), alias="id") # This is a short-term hack to deploy new dataclass based events without # any changes to data in the production database. Remove after a burn-in period. diff --git a/src/eduid/userdb/group_management/db.py b/src/eduid/userdb/group_management/db.py index 42b504dcb..c8889cce5 100644 --- a/src/eduid/userdb/group_management/db.py +++ b/src/eduid/userdb/group_management/db.py @@ -1,6 +1,6 @@ import logging from dataclasses import replace -from typing import Any, Optional +from typing import Any from eduid.userdb.db import BaseDB, SaveResult from eduid.userdb.group_management.state import GroupInviteState, GroupRole @@ -20,7 +20,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_group_management", collect } self.setup_indexes(indexes) - def get_state(self, group_scim_id: str, email_address: str, role: GroupRole) -> Optional[GroupInviteState]: + def get_state(self, group_scim_id: str, email_address: str, role: GroupRole) -> GroupInviteState | None: """ :param group_scim_id: Groups unique identifier :param email_address: Invited email address diff --git a/src/eduid/userdb/group_management/state.py b/src/eduid/userdb/group_management/state.py index 409f19169..b15bd9045 100644 --- a/src/eduid/userdb/group_management/state.py +++ b/src/eduid/userdb/group_management/state.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, field, fields from enum import Enum, unique -from typing import Any, Optional +from typing import Any import bson @@ -30,7 +30,7 @@ class GroupInviteState: id: bson.ObjectId = field(default_factory=bson.ObjectId) # Timestamp of last modification in the database. # None if GroupInviteState has never been written to the database. - modified_ts: Optional[datetime.datetime] = None + modified_ts: datetime.datetime | None = None @classmethod def from_dict(cls, data: Mapping[str, Any]) -> GroupInviteState: diff --git a/src/eduid/userdb/identity.py b/src/eduid/userdb/identity.py index c595a4093..a9ea2fe0f 100644 --- a/src/eduid/userdb/identity.py +++ b/src/eduid/userdb/identity.py @@ -4,7 +4,7 @@ from abc import ABC from datetime import datetime from enum import Enum -from typing import Any, Literal, Optional, Union +from typing import Any, Literal from pydantic import Field @@ -43,7 +43,7 @@ class IdentityElement(VerifiedElement, ABC): """ identity_type: IdentityType - proofing_method: Optional[IdentityProofingMethod] = None + proofing_method: IdentityProofingMethod | None = None @property def key(self) -> ElementKey: @@ -69,7 +69,7 @@ def unique_value(self) -> str: def to_frontend_format(self) -> dict[str, Any]: return super().to_dict() - def get_missing_proofing_method(self) -> Optional[IdentityProofingMethod]: + def get_missing_proofing_method(self) -> IdentityProofingMethod | None: """ Returns the proofing method that is missing for this identity """ @@ -104,7 +104,7 @@ class NinIdentity(IdentityElement): identity_type: Literal[IdentityType.NIN] = IdentityType.NIN number: str - date_of_birth: Optional[datetime] = None + date_of_birth: datetime | None = None @property def unique_key_name(self) -> str: @@ -114,7 +114,7 @@ def unique_key_name(self) -> str: def unique_value(self) -> str: return self.number - def to_old_nin(self) -> dict[str, Union[str, bool]]: + def to_old_nin(self) -> dict[str, str | bool]: # TODO: remove nins after frontend stops using it return {"number": self.number, "verified": self.is_verified, "primary": True} @@ -177,7 +177,7 @@ class SvipeIdentity(ForeignIdentityElement): # A globally unique identifier issued by Svipe to the user. Under normal conditions, a given person will retain # the same Svipe ID even after renewing the underlying identity document. svipe_id: str - administrative_number: Optional[str] = None + administrative_number: str | None = None @property def unique_key_name(self) -> str: @@ -209,7 +209,7 @@ class FrejaIdentity(ForeignIdentityElement): # claim: https://frejaeid.com/oidc/scopes/relyingPartyUserId # A unique, user-specific value that allows the Relying Party to identify the same user across multiple sessions user_id: str - personal_identity_number: Optional[str] = None + personal_identity_number: str | None = None registration_level: FrejaRegistrationLevel @property @@ -255,35 +255,35 @@ def is_verified(self) -> bool: return any([element.is_verified for element in self.elements if isinstance(element, IdentityElement)]) @property - def nin(self) -> Optional[NinIdentity]: + def nin(self) -> NinIdentity | None: _nin = self.filter(NinIdentity) if _nin: return _nin[0] return None @property - def eidas(self) -> Optional[EIDASIdentity]: + def eidas(self) -> EIDASIdentity | None: _eidas = self.filter(EIDASIdentity) if _eidas: return _eidas[0] return None @property - def svipe(self) -> Optional[SvipeIdentity]: + def svipe(self) -> SvipeIdentity | None: _svipe = self.filter(SvipeIdentity) if _svipe: return _svipe[0] return None @property - def freja(self) -> Optional[FrejaIdentity]: + def freja(self) -> FrejaIdentity | None: _freja = self.filter(FrejaIdentity) if _freja: return _freja[0] return None @property - def date_of_birth(self) -> Optional[datetime]: + def date_of_birth(self) -> datetime | None: if not self.is_verified: return None # NIN @@ -315,7 +315,7 @@ def date_of_birth(self) -> Optional[datetime]: return None def to_frontend_format(self) -> dict[str, Any]: - res: dict[str, Union[bool, dict[str, Any]]] = { + res: dict[str, bool | dict[str, Any]] = { item.identity_type.value: item.to_frontend_format() for item in self.to_list() } res["is_verified"] = self.is_verified diff --git a/src/eduid/userdb/idp/db.py b/src/eduid/userdb/idp/db.py index ffa76ab65..8dd243fde 100644 --- a/src/eduid/userdb/idp/db.py +++ b/src/eduid/userdb/idp/db.py @@ -3,7 +3,6 @@ """ import logging -from typing import Optional from eduid.userdb import UserDB from eduid.userdb.db import TUserDbDocument @@ -21,7 +20,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_am", collection: str = "at def user_from_dict(cls, data: TUserDbDocument) -> IdPUser: return IdPUser.from_dict(data) - def lookup_user(self, username: str) -> Optional[IdPUser]: + def lookup_user(self, username: str) -> IdPUser | None: """ Load IdPUser from userdb. diff --git a/src/eduid/userdb/idp/user.py b/src/eduid/userdb/idp/user.py index f2f80917b..f3dfda78a 100644 --- a/src/eduid/userdb/idp/user.py +++ b/src/eduid/userdb/idp/user.py @@ -5,7 +5,7 @@ import logging from dataclasses import dataclass from enum import Enum, unique -from typing import Any, Optional +from typing import Any from eduid.common.models.saml2 import EduidAuthnContextClass from eduid.userdb import User @@ -48,7 +48,7 @@ class SAMLAttributeSettings: sp_subject_id_request: list[str] esi_ladok_prefix: str authn_context_class: EduidAuthnContextClass - pairwise_id: Optional[str] = None + pairwise_id: str | None = None @unique @@ -76,7 +76,7 @@ def _to_dict_transform(self, data: dict[str, Any]) -> dict[str, Any]: def to_saml_attributes( self, settings: SAMLAttributeSettings, - filter_attributes: Optional[list[str]] = None, + filter_attributes: list[str] | None = None, ) -> dict[str, Any]: """ Return a dict of SAML attributes for a user. @@ -259,7 +259,7 @@ def make_eduperson_orcid(attributes: dict[str, Any], user: IdPUser) -> dict[str, return attributes -def _make_user_esi(user: IdPUser, settings: SAMLAttributeSettings) -> Optional[str]: +def _make_user_esi(user: IdPUser, settings: SAMLAttributeSettings) -> str | None: # do not release Ladok ESI for an unverified user as you need to be verified to connect to Ladok if user.identities.is_verified: if user.ladok is not None and user.ladok.is_verified: diff --git a/src/eduid/userdb/logs/db.py b/src/eduid/userdb/logs/db.py index 16fadf39a..871e9c0f7 100644 --- a/src/eduid/userdb/logs/db.py +++ b/src/eduid/userdb/logs/db.py @@ -1,6 +1,5 @@ import logging from datetime import datetime -from typing import Union from uuid import UUID from eduid.userdb.db import BaseDB, TUserDbDocument @@ -43,7 +42,7 @@ def __init__(self, db_uri: str, collection: str = "fido_metadata_log") -> None: } self.setup_indexes(indexes) - def exists(self, authenticator_id: Union[str, UUID], last_status_change: datetime) -> bool: + def exists(self, authenticator_id: str | UUID, last_status_change: datetime) -> bool: return bool( self.db_count( spec={"authenticator_id": authenticator_id, "last_status_change": last_status_change}, diff --git a/src/eduid/userdb/logs/element.py b/src/eduid/userdb/logs/element.py index d5cd6024e..a946fce25 100644 --- a/src/eduid/userdb/logs/element.py +++ b/src/eduid/userdb/logs/element.py @@ -3,7 +3,7 @@ # import logging from datetime import datetime -from typing import Any, Optional, TypeVar, Union +from typing import Any, TypeVar from uuid import UUID import bson @@ -87,7 +87,7 @@ class NinNavetProofingLogElement(NinProofingLogElement): # Navet response for users official address user_postal_address: FullPostalAddress # Navet response for users deregistration information (used if official address is missing) - deregistration_information: Optional[DeregistrationInformation] = None + deregistration_information: DeregistrationInformation | None = None class ForeignIdProofingLogElement(ProofingLogElement): @@ -191,7 +191,7 @@ class TeleAdressProofingRelation(TeleAdressProofing): # Navet response for mobile phone subscriber registered_postal_address: FullPostalAddress # Navet response for mobile phone subscriber deregistration information (used if official address is missing) - registered_deregistration_information: Optional[DeregistrationInformation] = None + registered_deregistration_information: DeregistrationInformation | None = None # Proofing method name proofing_method: str = IdentityProofingMethod.TELEADRESS.value @@ -348,8 +348,8 @@ class SwedenConnectEIDASProofing(ForeignIdProofingLogElement): # Transaction identifier transaction_identifier: str # if and when a nin can be mapped to a person these will be used - mapped_personal_identity_number: Optional[str] = None - personal_identity_number_binding: Optional[str] = None + mapped_personal_identity_number: str | None = None + personal_identity_number_binding: str | None = None # Proofing method name proofing_method: str = IdentityProofingMethod.SWEDEN_CONNECT.value @@ -407,7 +407,7 @@ class SvipeIDForeignProofing(ForeignIdProofingLogElement): # transaction id transaction_id: str # document administrative number - administrative_number: Optional[str] + administrative_number: str | None # document type (standardized english) document_type: str # document number @@ -494,7 +494,7 @@ class FrejaEIDForeignProofing(ForeignIdProofingLogElement): # transaction id transaction_id: str # document administrative number - administrative_number: Optional[str] + administrative_number: str | None # document type (standardized english) document_type: str # document number @@ -592,9 +592,9 @@ class NameUpdateProofing(NinNavetProofingLogElement): """ # Previous given name - previous_given_name: Optional[str] + previous_given_name: str | None # Previous surname - previous_surname: Optional[str] + previous_surname: str | None # Proofing method name proofing_method: str = "Navet name update" @@ -636,14 +636,14 @@ class WebauthnMfaCapabilityProofingLog(ProofingLogElement): } """ - authenticator_id: Union[UUID, str] + authenticator_id: UUID | str attestation_format: AttestationFormat user_verification_methods: list[str] key_protection: list[str] class FidoMetadataLogElement(LogElement): - authenticator_id: Union[UUID, str] + authenticator_id: UUID | str last_status_change: datetime metadata_entry: FidoMetadataEntry @@ -651,7 +651,7 @@ class FidoMetadataLogElement(LogElement): class UserChangeLogElement(LogElement): eppn: str = Field(alias="eduPersonPrincipalName") diff: str - log_element_id: Optional[bson.ObjectId] = Field(alias="_id", default=None) + log_element_id: bson.ObjectId | None = Field(alias="_id", default=None) reason: Reason source: Source diff --git a/src/eduid/userdb/maccapi/userdb.py b/src/eduid/userdb/maccapi/userdb.py index 2067c5417..f7ddb5936 100644 --- a/src/eduid/userdb/maccapi/userdb.py +++ b/src/eduid/userdb/maccapi/userdb.py @@ -1,6 +1,5 @@ import logging from datetime import datetime -from typing import Optional from pydantic import field_validator @@ -61,7 +60,7 @@ def get_users(self, data_owner: str) -> list[ManagedAccount]: users = self._get_documents_by_aggregate({"data_owner": data_owner, "terminated": {"$exists": False}}) return self._users_from_documents(users) - def get_account_as_idp_user(self, username: str) -> Optional[IdPUser]: + def get_account_as_idp_user(self, username: str) -> IdPUser | None: """ Get ManagedAccount from the db """ diff --git a/src/eduid/userdb/meta.py b/src/eduid/userdb/meta.py index f9417fa68..dd076aab2 100644 --- a/src/eduid/userdb/meta.py +++ b/src/eduid/userdb/meta.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Annotated, Optional +from typing import Annotated from bson import ObjectId from pydantic import BaseModel, ConfigDict, Field @@ -17,10 +17,10 @@ class CleanerType(str, Enum): class Meta(BaseModel): - version: Optional[ObjectId] = None + version: ObjectId | None = None created_ts: datetime = Field(default_factory=utc_now) - modified_ts: Optional[datetime] = None - cleaned: Optional[dict[CleanerType, datetime]] = None + modified_ts: datetime | None = None + cleaned: dict[CleanerType, datetime] | None = None is_in_database: Annotated[bool, Field(exclude=True)] = False # this is set to True when userdb loads the object model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/src/eduid/userdb/orcid.py b/src/eduid/userdb/orcid.py index 4234c80ec..d74dba3ed 100644 --- a/src/eduid/userdb/orcid.py +++ b/src/eduid/userdb/orcid.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from eduid.userdb.element import Element, ElementKey, VerifiedElement @@ -24,15 +24,15 @@ class OidcIdToken(Element): # the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. iat: int # Nonce used to associate a Client session with an ID Token, and to mitigate replay attacks. - nonce: Optional[str] = None + nonce: str | None = None # Time when the End-User authentication occurred. - auth_time: Optional[int] = None + auth_time: int | None = None # Authentication Context Class Reference - acr: Optional[str] = None + acr: str | None = None # Authentication Methods References - amr: Optional[list[str]] = None + amr: list[str] | None = None # Authorized party - azp: Optional[str] = None + azp: str | None = None @property def key(self) -> ElementKey: @@ -64,8 +64,8 @@ class OidcAuthorization(Element): access_token: str token_type: str id_token: OidcIdToken - expires_in: Optional[int] = None - refresh_token: Optional[str] = None + expires_in: int | None = None + refresh_token: str | None = None @property def key(self) -> ElementKey: @@ -105,9 +105,9 @@ class Orcid(VerifiedElement): # User's ORCID id: str oidc_authz: OidcAuthorization - name: Optional[str] = None - given_name: Optional[str] = None - family_name: Optional[str] = None + name: str | None = None + given_name: str | None = None + family_name: str | None = None @property def key(self) -> ElementKey: diff --git a/src/eduid/userdb/proofing/db.py b/src/eduid/userdb/proofing/db.py index a8d836a30..6b3c705ea 100644 --- a/src/eduid/userdb/proofing/db.py +++ b/src/eduid/userdb/proofing/db.py @@ -2,7 +2,7 @@ from abc import ABC from collections.abc import Mapping from operator import itemgetter -from typing import Any, Generic, Optional, TypeVar +from typing import Any, Generic, TypeVar from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.proofing.state import ( @@ -34,7 +34,7 @@ def state_from_dict(cls, data: Mapping[str, Any]) -> ProofingStateVar: # must be implemented by subclass to get correct type information raise NotImplementedError() - def get_state_by_eppn(self, eppn: str) -> Optional[ProofingStateVar]: + def get_state_by_eppn(self, eppn: str) -> ProofingStateVar | None: """ Locate a state in the db given the state user's eppn. @@ -51,7 +51,7 @@ def get_state_by_eppn(self, eppn: str) -> Optional[ProofingStateVar]: return None return self.state_from_dict(data) - def get_latest_state_by_spec(self, spec: Mapping[str, Any]) -> Optional[ProofingStateVar]: + def get_latest_state_by_spec(self, spec: Mapping[str, Any]) -> ProofingStateVar | None: """ Returns the latest inserted state and __removes any other state found__ defined by the spec . @@ -113,7 +113,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_email"): def state_from_dict(cls, data: Mapping[str, Any]) -> EmailProofingState: return EmailProofingState.from_dict(data) - def get_state_by_eppn_and_email(self, eppn: str, email: str) -> Optional[EmailProofingState]: + def get_state_by_eppn_and_email(self, eppn: str, email: str) -> EmailProofingState | None: """ Locate a state in the db given the eppn of the user and the email to be verified. @@ -143,7 +143,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_phone"): def state_from_dict(cls, data: Mapping[str, Any]) -> PhoneProofingState: return PhoneProofingState.from_dict(data) - def get_state_by_eppn_and_mobile(self, eppn: str, number: str) -> Optional[PhoneProofingState]: + def get_state_by_eppn_and_mobile(self, eppn: str, number: str) -> PhoneProofingState | None: """ Locate a state in the db given the eppn of the user and the mobile to be verified. @@ -171,7 +171,7 @@ def remove_state(self, state: ProofingStateVar) -> None: class OidcStateDB(ProofingStateDB[ProofingStateVar], Generic[ProofingStateVar], ABC): - def get_state_by_oidc_state(self, oidc_state: str) -> Optional[ProofingStateVar]: + def get_state_by_oidc_state(self, oidc_state: str) -> ProofingStateVar | None: """ Locate a state in the db given the user's OIDC state. diff --git a/src/eduid/userdb/proofing/element.py b/src/eduid/userdb/proofing/element.py index 2cb0a9399..4ce26a902 100644 --- a/src/eduid/userdb/proofing/element.py +++ b/src/eduid/userdb/proofing/element.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Optional, TypeVar +from typing import Any, TypeVar from pydantic import field_validator @@ -29,7 +29,7 @@ class ProofingElement(VerifiedElement): verification_code """ - verification_code: Optional[str] = None + verification_code: str | None = None @classmethod def _from_dict_transform(cls: type[TProofingElementSubclass], data: dict[str, Any]) -> dict[str, Any]: @@ -64,7 +64,7 @@ class NinProofingElement(ProofingElement): """ number: str - date_of_birth: Optional[datetime] = None + date_of_birth: datetime | None = None class EmailProofingElement(ProofingElement): @@ -123,6 +123,6 @@ class SentLetterElement(Element): """ is_sent: bool = False - sent_ts: Optional[datetime] = None - transaction_id: Optional[str] = None - address: Optional[FullPostalAddress] = None + sent_ts: datetime | None = None + transaction_id: str | None = None + address: FullPostalAddress | None = None diff --git a/src/eduid/userdb/proofing/state.py b/src/eduid/userdb/proofing/state.py index dd7385ed3..310cf3512 100644 --- a/src/eduid/userdb/proofing/state.py +++ b/src/eduid/userdb/proofing/state.py @@ -5,7 +5,7 @@ import logging from collections.abc import Mapping from dataclasses import asdict, dataclass -from typing import Any, Optional +from typing import Any import bson @@ -27,11 +27,11 @@ @dataclass() class ProofingState: # __post_init__ will mint a new ObjectId if `id' is None - id: Optional[bson.ObjectId] + id: bson.ObjectId | None eppn: str # Timestamp of last modification in the database. # None if ProofingState has never been written to the database. - modified_ts: Optional[datetime.datetime] + modified_ts: datetime.datetime | None def __post_init__(self): if self.id is None: diff --git a/src/eduid/userdb/reset_password/db.py b/src/eduid/userdb/reset_password/db.py index 806339db7..659296b9e 100644 --- a/src/eduid/userdb/reset_password/db.py +++ b/src/eduid/userdb/reset_password/db.py @@ -1,6 +1,6 @@ import logging from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.exceptions import MultipleDocumentsReturned @@ -30,7 +30,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_reset_password", collectio def get_state_by_email_code( self, email_code: str - ) -> Optional[Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState]]: + ) -> ResetPasswordEmailState | ResetPasswordEmailAndPhoneState | None: """ Locate a state in the db given the state's email code. @@ -53,7 +53,7 @@ def get_state_by_email_code( return self.init_state(states[0]) - def get_state_by_eppn(self, eppn: str) -> Optional[Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState]]: + def get_state_by_eppn(self, eppn: str) -> ResetPasswordEmailState | ResetPasswordEmailAndPhoneState | None: """ Locate a state in the db given the users eppn. @@ -69,7 +69,7 @@ def get_state_by_eppn(self, eppn: str) -> Optional[Union[ResetPasswordEmailState return None @staticmethod - def init_state(state_mapping: Mapping) -> Optional[Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState]]: + def init_state(state_mapping: Mapping) -> ResetPasswordEmailState | ResetPasswordEmailAndPhoneState | None: state = dict(state_mapping) if state.get("method") == "email": return ResetPasswordEmailState.from_dict(data=state) diff --git a/src/eduid/userdb/reset_password/element.py b/src/eduid/userdb/reset_password/element.py index 44229d721..1dce0da05 100644 --- a/src/eduid/userdb/reset_password/element.py +++ b/src/eduid/userdb/reset_password/element.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Union +from typing import Any from eduid.userdb.element import Element, ElementKey from eduid.userdb.util import utc_now @@ -53,9 +53,7 @@ def is_expired(self, timeout: timedelta) -> bool: return expiry_date < now @classmethod - def parse( - cls: type[CodeElement], code_or_element: Union[Mapping, CodeElement, str], application: str - ) -> CodeElement: + def parse(cls: type[CodeElement], code_or_element: Mapping | CodeElement | str, application: str) -> CodeElement: if isinstance(code_or_element, str): return cls(created_by=application, code=code_or_element, is_verified=False) if isinstance(code_or_element, dict): diff --git a/src/eduid/userdb/reset_password/state.py b/src/eduid/userdb/reset_password/state.py index d0c31076a..695e23d3d 100644 --- a/src/eduid/userdb/reset_password/state.py +++ b/src/eduid/userdb/reset_password/state.py @@ -3,7 +3,7 @@ import datetime import logging from dataclasses import asdict, dataclass, field -from typing import Any, Optional, TypeVar +from typing import Any, TypeVar from uuid import uuid4 import bson @@ -25,10 +25,10 @@ class ResetPasswordState: eppn: str id: bson.ObjectId = field(default_factory=lambda: bson.ObjectId()) reference: str = field(init=False) - method: Optional[str] = None + method: str | None = None created_ts: datetime.datetime = field(default_factory=utc_now) - modified_ts: Optional[datetime.datetime] = None - extra_security: Optional[dict[str, Any]] = None + modified_ts: datetime.datetime | None = None + extra_security: dict[str, Any] | None = None generated_password: bool = False def __post_init__(self): diff --git a/src/eduid/userdb/scimapi/basedb.py b/src/eduid/userdb/scimapi/basedb.py index 59ae3366d..ddf23cf35 100644 --- a/src/eduid/userdb/scimapi/basedb.py +++ b/src/eduid/userdb/scimapi/basedb.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from eduid.userdb.db import BaseDB, TUserDbDocument @@ -10,9 +10,9 @@ class ScimApiBaseDB(BaseDB): def _get_documents_and_count_by_filter( self, spec: Mapping[str, Any], - fields: Optional[dict[str, Any]] = None, - limit: Optional[int] = None, - skip: Optional[int] = None, + fields: dict[str, Any] | None = None, + limit: int | None = None, + skip: int | None = None, ) -> tuple[list[TUserDbDocument], int]: """ Locate and count documents in the db using a custom search filter. diff --git a/src/eduid/userdb/scimapi/common.py b/src/eduid/userdb/scimapi/common.py index 24860f18e..e3d48ceeb 100644 --- a/src/eduid/userdb/scimapi/common.py +++ b/src/eduid/userdb/scimapi/common.py @@ -6,7 +6,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional, Union +from typing import Any from uuid import UUID from eduid.common.models.scim_base import EmailType, PhoneNumberType, WeakVersion @@ -20,7 +20,7 @@ class ScimApiResourceBase(ABC): """The elements common to all SCIM resource database objects""" scim_id: UUID = field(default_factory=lambda: uuid.uuid4()) - external_id: Optional[str] = None + external_id: str | None = None version: WeakVersion = field(default_factory=lambda: WeakVersion()) created: datetime = field(default_factory=lambda: utc_now()) last_modified: datetime = field(default_factory=lambda: utc_now()) @@ -66,29 +66,29 @@ def from_dict(cls: type[ScimApiLinkedAccount], data: Mapping[str, Any]) -> ScimA @dataclass(frozen=True) class ScimApiName: - family_name: Optional[str] = None - given_name: Optional[str] = None - formatted: Optional[str] = None - middle_name: Optional[str] = None - honorific_prefix: Optional[str] = None - honorific_suffix: Optional[str] = None - - def to_dict(self) -> dict[str, Optional[str]]: + family_name: str | None = None + given_name: str | None = None + formatted: str | None = None + middle_name: str | None = None + honorific_prefix: str | None = None + honorific_suffix: str | None = None + + def to_dict(self) -> dict[str, str | None]: return asdict(self) @classmethod - def from_dict(cls: type[ScimApiName], data: Mapping[str, Optional[str]]) -> ScimApiName: + def from_dict(cls: type[ScimApiName], data: Mapping[str, str | None]) -> ScimApiName: return cls(**data) @dataclass(frozen=True) class ScimApiEmail: value: str - display: Optional[str] = None - type: Optional[EmailType] = None - primary: Optional[bool] = None + display: str | None = None + type: EmailType | None = None + primary: bool | None = None - def to_dict(self) -> dict[str, Union[Optional[str], bool]]: + def to_dict(self) -> dict[str, str | None | bool]: res = asdict(self) if self.type is not None: res["type"] = self.type.value @@ -105,11 +105,11 @@ def from_dict(cls: builtins.type[ScimApiEmail], data: Mapping[str, Any]) -> Scim @dataclass(frozen=True) class ScimApiPhoneNumber: value: str - display: Optional[str] = None - type: Optional[PhoneNumberType] = None - primary: Optional[bool] = None + display: str | None = None + type: PhoneNumberType | None = None + primary: bool | None = None - def to_dict(self) -> dict[str, Union[Optional[str], bool]]: + def to_dict(self) -> dict[str, str | None | bool]: res = asdict(self) if self.type is not None: res["type"] = self.type.value diff --git a/src/eduid/userdb/scimapi/eventdb.py b/src/eduid/userdb/scimapi/eventdb.py index 92bb9a06c..3daf0f4ad 100644 --- a/src/eduid/userdb/scimapi/eventdb.py +++ b/src/eduid/userdb/scimapi/eventdb.py @@ -5,7 +5,7 @@ from dataclasses import asdict, dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Optional +from typing import Any from uuid import UUID from bson import ObjectId @@ -22,7 +22,7 @@ class ScimApiEventResource: resource_type: SCIMResourceType scim_id: UUID - external_id: Optional[str] + external_id: str | None version: ObjectId last_modified: datetime @@ -111,7 +111,7 @@ def save(self, event: ScimApiEvent) -> bool: return result.acknowledged def get_events_by_resource( - self, resource_type: SCIMResourceType, scim_id: Optional[UUID] = None, external_id: Optional[str] = None + self, resource_type: SCIMResourceType, scim_id: UUID | None = None, external_id: str | None = None ) -> list[ScimApiEvent]: filter = { "resource.resource_type": resource_type.value, @@ -126,7 +126,7 @@ def get_events_by_resource( return [ScimApiEvent.from_dict(this) for this in docs] return [] - def get_event_by_scim_id(self, scim_id: str) -> Optional[ScimApiEvent]: + def get_event_by_scim_id(self, scim_id: str) -> ScimApiEvent | None: doc = self._get_document_by_attr("scim_id", scim_id) if not doc: return None diff --git a/src/eduid/userdb/scimapi/groupdb.py b/src/eduid/userdb/scimapi/groupdb.py index cd715c36e..3b65792e9 100644 --- a/src/eduid/userdb/scimapi/groupdb.py +++ b/src/eduid/userdb/scimapi/groupdb.py @@ -7,7 +7,7 @@ from collections.abc import Iterable, Mapping from dataclasses import asdict, dataclass, field, replace from datetime import datetime -from typing import Any, Optional, Union +from typing import Any from uuid import UUID from bson import ObjectId @@ -55,27 +55,27 @@ def __post_init__(self): self.graph = GraphGroup(identifier=str(self.scim_id), display_name=self.display_name) @property - def members(self) -> set[Union[GraphGroup, GraphUser]]: + def members(self) -> set[GraphGroup | GraphUser]: return self.graph.members @members.setter - def members(self, members: Iterable[Union[GraphGroup, GraphUser]]): + def members(self, members: Iterable[GraphGroup | GraphUser]): members = set(members) self.graph = replace(self.graph, members=members) - def add_member(self, member: Union[GraphGroup, GraphUser]) -> None: + def add_member(self, member: GraphGroup | GraphUser) -> None: self.graph.members.add(member) @property - def owners(self) -> set[Union[GraphGroup, GraphUser]]: + def owners(self) -> set[GraphGroup | GraphUser]: return self.graph.owners @owners.setter - def owners(self, owners: Iterable[Union[GraphGroup, GraphUser]]): + def owners(self, owners: Iterable[GraphGroup | GraphUser]): owners = set(owners) self.graph = replace(self.graph, owners=owners) - def add_owner(self, owner: Union[GraphGroup, GraphUser]) -> None: + def add_owner(self, owner: GraphGroup | GraphUser) -> None: self.graph.owners.add(owner) def has_member(self, identifier: UUID) -> bool: @@ -108,7 +108,7 @@ def __init__( mongo_uri: str, mongo_dbname: str, mongo_collection: str, - neo4j_config: Optional[dict[str, Any]] = None, + neo4j_config: dict[str, Any] | None = None, setup_indexes: bool = True, ): super().__init__(mongo_uri, mongo_dbname, collection=mongo_collection) @@ -179,8 +179,8 @@ def update_group(self, update_request: GroupUpdateRequest, db_group: ScimApiGrou updated_members = set() logger.info(f"Updating group {str(db_group.scim_id)}") # please mypy - _member: Optional[Union[GraphUser, GraphGroup]] - _new_member: Optional[Union[GraphUser, GraphGroup]] + _member: GraphUser | GraphGroup | None + _new_member: GraphUser | GraphGroup | None for this in update_request.members: if this.is_user: @@ -252,7 +252,7 @@ def get_groups(self) -> list[ScimApiGroup]: res += [group] return res - def get_group_by_scim_id(self, scim_id: str) -> Optional[ScimApiGroup]: + def get_group_by_scim_id(self, scim_id: str) -> ScimApiGroup | None: doc = self._get_document_by_attr("scim_id", scim_id) if doc: group = ScimApiGroup.from_dict(doc) @@ -260,7 +260,7 @@ def get_group_by_scim_id(self, scim_id: str) -> Optional[ScimApiGroup]: return group return None - def get_group_by_display_name(self, display_name: str) -> Optional[ScimApiGroup]: + def get_group_by_display_name(self, display_name: str) -> ScimApiGroup | None: doc = self._get_document_by_attr("display_name", display_name) if doc: group = ScimApiGroup.from_dict(doc) @@ -268,9 +268,7 @@ def get_group_by_display_name(self, display_name: str) -> Optional[ScimApiGroup] return group return None - def get_groups_by_property( - self, key: str, value: Union[str, int], skip=0, limit=100 - ) -> tuple[list[ScimApiGroup], int]: + def get_groups_by_property(self, key: str, value: str | int, skip=0, limit=100) -> tuple[list[ScimApiGroup], int]: docs, count = self._get_documents_and_count_by_filter({key: value}, skip=skip, limit=limit) if not docs: return [], 0 @@ -304,7 +302,7 @@ def get_groups_owned_by_user_identifier(self, owner_identifier: UUID) -> list[Sc return res def get_groups_by_last_modified( - self, operator: str, value: datetime, limit: Optional[int] = None, skip: Optional[int] = None + self, operator: str, value: datetime, limit: int | None = None, skip: int | None = None ) -> tuple[list[ScimApiGroup], int]: mongo_operator = self._get_mongo_operator(operator) spec = {"last_modified": {mongo_operator: value}} diff --git a/src/eduid/userdb/scimapi/invitedb.py b/src/eduid/userdb/scimapi/invitedb.py index 5c9171b34..b72e58475 100644 --- a/src/eduid/userdb/scimapi/invitedb.py +++ b/src/eduid/userdb/scimapi/invitedb.py @@ -6,7 +6,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any from uuid import UUID from bson import ObjectId @@ -35,9 +35,9 @@ class ScimApiInvite(ScimApiResourceBase): emails: list[ScimApiEmail] = field(default_factory=list) phone_numbers: list[ScimApiPhoneNumber] = field(default_factory=list) groups: list[UUID] = field(default_factory=list) - nin: Optional[str] = field(default=None) - preferred_language: Optional[str] = field(default=None) - completed: Optional[datetime] = field(default=None) + nin: str | None = field(default=None) + preferred_language: str | None = field(default=None) + completed: datetime | None = field(default=None) profiles: dict[str, ScimApiProfile] = field(default_factory=lambda: {}) def to_dict(self) -> TUserDbDocument: @@ -110,14 +110,14 @@ def save(self, invite: ScimApiInvite) -> bool: def remove(self, invite: ScimApiInvite): return self.remove_document(invite.invite_id) - def get_invite_by_scim_id(self, scim_id: str) -> Optional[ScimApiInvite]: + def get_invite_by_scim_id(self, scim_id: str) -> ScimApiInvite | None: docs = self._get_document_by_attr("scim_id", scim_id) if docs: return ScimApiInvite.from_dict(docs) return None def get_invites_by_last_modified( - self, operator: str, value: datetime, limit: Optional[int] = None, skip: Optional[int] = None + self, operator: str, value: datetime, limit: int | None = None, skip: int | None = None ) -> tuple[list[ScimApiInvite], int]: # map SCIM filter operators to mongodb filter mongo_operator = {"gt": "$gt", "ge": "$gte"}.get(operator) diff --git a/src/eduid/userdb/scimapi/userdb.py b/src/eduid/userdb/scimapi/userdb.py index a9bf0e5d5..396904a85 100644 --- a/src/eduid/userdb/scimapi/userdb.py +++ b/src/eduid/userdb/scimapi/userdb.py @@ -6,7 +6,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any from bson import ObjectId @@ -35,7 +35,7 @@ class ScimApiUser(ScimApiResourceBase): name: ScimApiName = field(default_factory=lambda: ScimApiName()) emails: list[ScimApiEmail] = field(default_factory=list) phone_numbers: list[ScimApiPhoneNumber] = field(default_factory=list) - preferred_language: Optional[str] = None + preferred_language: str | None = None profiles: dict[str, ScimApiProfile] = field(default_factory=dict) linked_accounts: list[ScimApiLinkedAccount] = field(default_factory=list) @@ -131,20 +131,20 @@ def save(self, user: ScimApiUser) -> None: def remove(self, user: ScimApiUser): return self.remove_document(user.user_id) - def get_user_by_scim_id(self, scim_id: str) -> Optional[ScimApiUser]: + def get_user_by_scim_id(self, scim_id: str) -> ScimApiUser | None: doc = self._get_document_by_attr("scim_id", scim_id) if doc: return ScimApiUser.from_dict(doc) return None - def get_user_by_external_id(self, external_id: str) -> Optional[ScimApiUser]: + def get_user_by_external_id(self, external_id: str) -> ScimApiUser | None: doc = self._get_document_by_attr("external_id", external_id) if doc: return ScimApiUser.from_dict(doc) return None def get_users_by_last_modified( - self, operator: str, value: datetime, limit: Optional[int] = None, skip: Optional[int] = None + self, operator: str, value: datetime, limit: int | None = None, skip: int | None = None ) -> tuple[list[ScimApiUser], int]: mongo_operator = self._get_mongo_operator(operator) spec = {"last_modified": {mongo_operator: value}} @@ -158,8 +158,8 @@ def get_user_by_profile_data( key: str, operator: str, value: datetime, - limit: Optional[int] = None, - skip: Optional[int] = None, + limit: int | None = None, + skip: int | None = None, ) -> tuple[list[ScimApiUser], int]: mongo_operator = self._get_mongo_operator(operator) spec = {f"profiles.{profile}.data.{key}": {mongo_operator: value}} diff --git a/src/eduid/userdb/security/db.py b/src/eduid/userdb/security/db.py index 05cb6ebb0..d1017a893 100644 --- a/src/eduid/userdb/security/db.py +++ b/src/eduid/userdb/security/db.py @@ -1,7 +1,7 @@ import copy import logging from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.deprecation import deprecated @@ -30,7 +30,7 @@ class PasswordResetStateDB(BaseDB): def __init__(self, db_uri, db_name="eduid_security", collection="password_reset_data"): super().__init__(db_uri, db_name, collection=collection) - def get_state_by_email_code(self, email_code: str) -> Optional[PasswordResetState]: + def get_state_by_email_code(self, email_code: str) -> PasswordResetState | None: """ Locate a state in the db given the state's email code. @@ -51,7 +51,7 @@ def get_state_by_email_code(self, email_code: str) -> Optional[PasswordResetStat return self.init_state(states[0]) - def get_state_by_eppn(self, eppn: str) -> Optional[PasswordResetState]: + def get_state_by_eppn(self, eppn: str) -> PasswordResetState | None: """ Locate a state in the db given the users eppn. @@ -69,7 +69,7 @@ def get_state_by_eppn(self, eppn: str) -> Optional[PasswordResetState]: @staticmethod def init_state( data: Mapping[str, Any], - ) -> Optional[Union[PasswordResetEmailState, PasswordResetEmailAndPhoneState]]: + ) -> PasswordResetEmailState | PasswordResetEmailAndPhoneState | None: _data = dict(copy.deepcopy(data)) # to not modify callers data method = _data.pop("method", None) if method == "email": diff --git a/src/eduid/userdb/security/state.py b/src/eduid/userdb/security/state.py index 0b283c7b3..b2f1cc600 100644 --- a/src/eduid/userdb/security/state.py +++ b/src/eduid/userdb/security/state.py @@ -3,7 +3,7 @@ import copy import datetime from collections.abc import Mapping -from typing import Any, Optional, TypeVar +from typing import Any, TypeVar import bson from pydantic import BaseModel, ConfigDict, Field @@ -22,9 +22,9 @@ class PasswordResetState(BaseModel): eppn: str = Field(alias="eduPersonPrincipalName") state_id: bson.ObjectId = Field(default_factory=lambda: bson.ObjectId(), alias="_id") created_ts: datetime.datetime = Field(default_factory=lambda: utc_now()) - modified_ts: Optional[datetime.datetime] = None - extra_security: Optional[dict[str, Any]] = None - generated_password: Optional[str] = None + modified_ts: datetime.datetime | None = None + extra_security: dict[str, Any] | None = None + generated_password: str | None = None model_config = ConfigDict( populate_by_name=True, validate_assignment=True, extra="forbid", arbitrary_types_allowed=True ) diff --git a/src/eduid/userdb/signup/invite.py b/src/eduid/userdb/signup/invite.py index 4194fd80e..50e7d2b28 100644 --- a/src/eduid/userdb/signup/invite.py +++ b/src/eduid/userdb/signup/invite.py @@ -4,7 +4,7 @@ from dataclasses import asdict, dataclass, field from datetime import datetime from enum import Enum -from typing import Any, Optional +from typing import Any from uuid import UUID from bson import ObjectId @@ -58,18 +58,18 @@ class _InviteRequired: @dataclass(frozen=True) class Invite(_InviteRequired): invite_id: ObjectId = field(default_factory=ObjectId) - given_name: Optional[str] = field(default=None) - surname: Optional[str] = field(default=None) + given_name: str | None = field(default=None) + surname: str | None = field(default=None) mail_addresses: list[InviteMailAddress] = field(default_factory=list) phone_numbers: list[InvitePhoneNumber] = field(default_factory=list) - nin: Optional[str] = field(default=None) + nin: str | None = field(default=None) preferred_language: str = field(default="sv") - finish_url: Optional[str] = field(default=None) - completed_ts: Optional[datetime] = field(default=None) + finish_url: str | None = field(default=None) + completed_ts: datetime | None = field(default=None) created_ts: datetime = field(default_factory=datetime.utcnow) - modified_ts: Optional[datetime] = field(default=None) + modified_ts: datetime | None = field(default=None) - def get_primary_mail_address(self) -> Optional[str]: + def get_primary_mail_address(self) -> str | None: # there can be only one primary mail address set primary_mail_address = [item.email for item in self.mail_addresses if item.primary is True] if not primary_mail_address: diff --git a/src/eduid/userdb/signup/invitedb.py b/src/eduid/userdb/signup/invitedb.py index e30252b31..151cdd1b7 100644 --- a/src/eduid/userdb/signup/invitedb.py +++ b/src/eduid/userdb/signup/invitedb.py @@ -1,7 +1,7 @@ import datetime import logging from dataclasses import replace -from typing import Any, Optional +from typing import Any from eduid.userdb.db import SaveResult from eduid.userdb.exceptions import MultipleDocumentsReturned @@ -27,13 +27,13 @@ def __init__(self, db_uri: str, db_name: str = "eduid_signup", collection: str = } self.setup_indexes(indexes) - def get_invite_by_invite_code(self, code: str) -> Optional[Invite]: + def get_invite_by_invite_code(self, code: str) -> Invite | None: doc = self._get_document_by_attr("invite_code", code) if doc: return Invite.from_dict(doc) return None - def get_invite_by_reference(self, reference: InviteReference) -> Optional[Invite]: + def get_invite_by_reference(self, reference: InviteReference) -> Invite | None: if isinstance(reference, SCIMReference): spec = {"invite_reference.scim_id": reference.scim_id, "invite_reference.data_owner": reference.data_owner} else: diff --git a/src/eduid/userdb/signup/user.py b/src/eduid/userdb/signup/user.py index 1bf8fa697..36e26f81c 100644 --- a/src/eduid/userdb/signup/user.py +++ b/src/eduid/userdb/signup/user.py @@ -1,6 +1,6 @@ __author__ = "ft" -from typing import Any, Optional +from typing import Any import bson from pydantic import Field @@ -15,10 +15,10 @@ class SignupUser(User): Subclass of eduid.userdb.User with eduid Signup application specific data. """ - social_network: Optional[str] = None - social_network_id: Optional[str] = None + social_network: str | None = None + social_network_id: str | None = None # The user's pending (unconfirmed) mail address. - pending_mail_address: Optional[EmailProofingElement] = None + pending_mail_address: EmailProofingElement | None = None # Holds a reference id that is used for connecting msg tasks with proofing log statements. proofing_reference: str = Field(default_factory=lambda: str(bson.ObjectId())) diff --git a/src/eduid/userdb/signup/userdb.py b/src/eduid/userdb/signup/userdb.py index 7b8fb257f..f060458f3 100644 --- a/src/eduid/userdb/signup/userdb.py +++ b/src/eduid/userdb/signup/userdb.py @@ -1,5 +1,4 @@ from datetime import timedelta -from typing import Optional from eduid.userdb.db import TUserDbDocument from eduid.userdb.signup import SignupUser @@ -14,7 +13,7 @@ def __init__( db_uri: str, db_name: str = "eduid_signup", collection: str = "registered", - auto_expire: Optional[timedelta] = None, + auto_expire: timedelta | None = None, ): super().__init__(db_uri, db_name, collection=collection) @@ -32,9 +31,9 @@ def __init__( def user_from_dict(cls, data: TUserDbDocument) -> SignupUser: return SignupUser.from_dict(data) - def get_user_by_mail_verification_code(self, code: str) -> Optional[SignupUser]: + def get_user_by_mail_verification_code(self, code: str) -> SignupUser | None: return self._get_user_by_attr("pending_mail_address.verification_code", code) - def get_user_by_pending_mail_address(self, mail: str) -> Optional[SignupUser]: + def get_user_by_pending_mail_address(self, mail: str) -> SignupUser | None: mail = mail.lower() return self._get_user_by_attr("pending_mail_address.email", mail) diff --git a/src/eduid/userdb/support/db.py b/src/eduid/userdb/support/db.py index 590484207..37fb3fe40 100644 --- a/src/eduid/userdb/support/db.py +++ b/src/eduid/userdb/support/db.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any from bson import ObjectId @@ -32,7 +32,7 @@ def search_users(self, query: str) -> list[SupportUser]: :param query: search query, can be a user eppn, nin, mail address or phone number :return: A list of user docs """ - results: list[Optional[SupportUser]] = list() + results: list[SupportUser | None] = list() # We could do this with a custom filter (and one db call) but it is better to lean on existing methods # if the way we find users change in the future try: @@ -58,7 +58,7 @@ def __init__(self, db_uri: str): collection = "authn_info" super().__init__(db_uri, db_name, collection) - def get_authn_info(self, user_id: Union[str, ObjectId]) -> dict[str, Any]: + def get_authn_info(self, user_id: str | ObjectId) -> dict[str, Any]: """ :param user_id: User objects user_id property :type user_id: ObjectId | str | unicode diff --git a/src/eduid/userdb/support/models.py b/src/eduid/userdb/support/models.py index 32a324262..ed5158eab 100644 --- a/src/eduid/userdb/support/models.py +++ b/src/eduid/userdb/support/models.py @@ -1,13 +1,12 @@ from copy import deepcopy -from typing import Optional __author__ = "lundberg" # Models for filtering out unneeded or unwanted data from eduID database objects class GenericFilterDict(dict): - add_keys: Optional[list[str]] = None - remove_keys: Optional[list[str]] = None + add_keys: list[str] | None = None + remove_keys: list[str] | None = None def __init__(self, data): """ diff --git a/src/eduid/userdb/testing/__init__.py b/src/eduid/userdb/testing/__init__.py index 564ca6aec..7627d0b24 100644 --- a/src/eduid/userdb/testing/__init__.py +++ b/src/eduid/userdb/testing/__init__.py @@ -8,7 +8,7 @@ import logging.config import unittest from collections.abc import Sequence -from typing import Any, Optional, cast +from typing import Any, cast import pymongo import pymongo.errors @@ -82,7 +82,7 @@ class MongoTestCase(unittest.TestCase): A test can access the port using the attribute `port` """ - def setUp(self, *args: list[Any], am_users: Optional[list[User]] = None, **kwargs: dict[str, Any]): + def setUp(self, *args: list[Any], am_users: list[User] | None = None, **kwargs: dict[str, Any]): """ Test case initialization. :return: diff --git a/src/eduid/userdb/testing/temp_instance.py b/src/eduid/userdb/testing/temp_instance.py index da11307be..faa69f3aa 100644 --- a/src/eduid/userdb/testing/temp_instance.py +++ b/src/eduid/userdb/testing/temp_instance.py @@ -9,7 +9,7 @@ import time from abc import ABC, abstractmethod from collections.abc import Sequence -from typing import Any, Optional +from typing import Any from eduid.userdb.util import utc_now @@ -26,7 +26,7 @@ class EduidTemporaryInstance(ABC): _instance = None def __init__(self, max_retry_seconds: int): - self._conn: Optional[Any] = None # self._conn should be initialised by subclasses in `setup_conn' + self._conn: Any | None = None # self._conn should be initialised by subclasses in `setup_conn' self._tmpdir = tempfile.mkdtemp() self._port = random.randint(40000, 65535) self._logfile = open(f"/tmp/{self.__class__.__name__}-{self.port}.log", "w") diff --git a/src/eduid/userdb/user.py b/src/eduid/userdb/user.py index 4815aa7ce..7f12fbe96 100644 --- a/src/eduid/userdb/user.py +++ b/src/eduid/userdb/user.py @@ -5,7 +5,7 @@ from datetime import datetime from enum import Enum, unique from operator import itemgetter -from typing import Any, Optional, TypeVar, Union, cast +from typing import Any, TypeVar, cast import bson from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator @@ -48,27 +48,27 @@ class User(BaseModel): meta: Meta = Field(default_factory=Meta) eppn: str = Field(alias="eduPersonPrincipalName") user_id: bson.ObjectId = Field(default_factory=bson.ObjectId, alias="_id") - given_name: Optional[str] = Field(default=None, alias="givenName") - chosen_given_name: Optional[str] = None - surname: Optional[str] = None - legal_name: Optional[str] = None - subject: Optional[SubjectType] = None + given_name: str | None = Field(default=None, alias="givenName") + chosen_given_name: str | None = None + surname: str | None = None + legal_name: str | None = None + subject: SubjectType | None = None # TODO: Move language in to settings and set the initial value in signup flow based in browser language - language: Optional[str] = Field(default=None, alias="preferredLanguage") + language: str | None = Field(default=None, alias="preferredLanguage") mail_addresses: MailAddressList = Field(default_factory=MailAddressList, alias="mailAliases") phone_numbers: PhoneNumberList = Field(default_factory=PhoneNumberList, alias="phone") credentials: CredentialList = Field(default_factory=CredentialList, alias="passwords") identities: IdentityList = Field(default_factory=IdentityList) - modified_ts: Optional[datetime] = None # TODO: remove after meta.modified_ts is used + modified_ts: datetime | None = None # TODO: remove after meta.modified_ts is used entitlements: list[str] = Field(default_factory=list, alias="eduPersonEntitlement") tou: ToUList = Field(default_factory=ToUList) - terminated: Optional[datetime] = None + terminated: datetime | None = None locked_identity: LockedIdentityList = Field(default_factory=LockedIdentityList) - orcid: Optional[Orcid] = None - ladok: Optional[Ladok] = None + orcid: Orcid | None = None + ladok: Ladok | None = None profiles: ProfileList = Field(default_factory=ProfileList) - letter_proofing_data: Optional[Union[list, dict]] = None # remove dict after a full load-save-users - revoked_ts: Optional[datetime] = None + letter_proofing_data: list | dict | None = None # remove dict after a full load-save-users + revoked_ts: datetime | None = None preferences: UserPreferences = Field(default_factory=UserPreferences) model_config = ConfigDict( populate_by_name=True, validate_assignment=True, extra="forbid", arbitrary_types_allowed=True @@ -384,7 +384,7 @@ def _parse_locked_identity(cls, data: dict[str, Any]) -> LockedIdentityList: return LockedIdentityList.from_list_of_dicts(_locked_identity) @classmethod - def _parse_orcid(cls, data: dict[str, Any]) -> Optional[Orcid]: + def _parse_orcid(cls, data: dict[str, Any]) -> Orcid | None: """ Parse the Orcid element. """ @@ -394,7 +394,7 @@ def _parse_orcid(cls, data: dict[str, Any]) -> Optional[Orcid]: return None @classmethod - def _parse_ladok(cls, data: dict[str, Any]) -> Optional[Ladok]: + def _parse_ladok(cls, data: dict[str, Any]) -> Ladok | None: """ Parse the Ladok element. """ diff --git a/src/eduid/userdb/user_cleaner/db.py b/src/eduid/userdb/user_cleaner/db.py index 41e72fcb8..fe06f1091 100644 --- a/src/eduid/userdb/user_cleaner/db.py +++ b/src/eduid/userdb/user_cleaner/db.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import pymongo @@ -35,7 +34,7 @@ def __init__(self, db_uri: str, db_name: str = "eduid_user_cleaner", collection: def user_from_dict(cls, data: TUserDbDocument) -> CleanerQueueUser: return CleanerQueueUser.from_dict(data) - def get_next_user(self, cleaner_type: CleanerType) -> Optional[CleanerQueueUser]: + def get_next_user(self, cleaner_type: CleanerType) -> CleanerQueueUser | None: doc = self._coll.find_one_and_delete( filter={"cleaner_type": cleaner_type}, sort=[("meta.created_ts", pymongo.ASCENDING)] ) diff --git a/src/eduid/userdb/userdb.py b/src/eduid/userdb/userdb.py index c67cc8c90..de5d8fea6 100644 --- a/src/eduid/userdb/userdb.py +++ b/src/eduid/userdb/userdb.py @@ -2,7 +2,7 @@ from abc import ABC from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Generic, Optional, TypeVar, Union +from typing import Any, Generic, TypeVar from bson import ObjectId from bson.errors import InvalidId @@ -82,7 +82,7 @@ def user_from_dict(cls, data: TUserDbDocument) -> UserVar: # must be implemented by subclass to get correct type information raise NotImplementedError(f"user_from_dict not implemented in UserDB subclass {cls}") - def get_user_by_id(self, user_id: Union[str, ObjectId]) -> Optional[UserVar]: + def get_user_by_id(self, user_id: str | ObjectId) -> UserVar | None: """ Locate a user in the userdb given the user's _id. @@ -97,7 +97,7 @@ def get_user_by_id(self, user_id: Union[str, ObjectId]) -> Optional[UserVar]: return None return self._get_user_by_attr("_id", user_id) - def get_verified_users_count(self, identity_type: Optional[IdentityType] = None) -> int: + def get_verified_users_count(self, identity_type: IdentityType | None = None) -> int: spec: dict[str, Any] spec = { "identities": { @@ -126,7 +126,7 @@ def _get_user_by_filter(self, filter: Mapping[str, Any]) -> list[UserVar]: return self._users_from_documents(users) - def get_user_by_mail(self, email: str) -> Optional[UserVar]: + def get_user_by_mail(self, email: str) -> UserVar | None: """Locate a user with a (confirmed) e-mail address""" res = self.get_users_by_mail(email=email) if not res: @@ -153,7 +153,7 @@ def get_users_by_mail(self, email: str, include_unconfirmed: bool = False) -> li filter = {"$or": [{"mail": email}, {"mailAliases": {"$elemMatch": elemmatch}}]} return self._get_user_by_filter(filter) - def get_user_by_nin(self, nin: str) -> Optional[UserVar]: + def get_user_by_nin(self, nin: str) -> UserVar | None: """Locate a user with a (confirmed) NIN""" res = self.get_users_by_nin(nin=nin) if not res: @@ -189,7 +189,7 @@ def get_users_by_identity( _filter = {"identities": {"$elemMatch": match}} return self._get_user_by_filter(_filter) - def get_user_by_phone(self, phone: str) -> Optional[UserVar]: + def get_user_by_phone(self, phone: str) -> UserVar | None: """Locate a user with a (confirmed) phone number""" res = self.get_users_by_phone(phone=phone) if not res: @@ -220,7 +220,7 @@ def get_users_by_phone(self, phone: str, include_unconfirmed: bool = False) -> l filter = {"$or": [old_filter, new_filter]} return self._get_user_by_filter(filter) - def get_user_by_eppn(self, eppn: Optional[str]) -> UserVar: + def get_user_by_eppn(self, eppn: str | None) -> UserVar: """ Look for a user using the eduPersonPrincipalName. @@ -234,7 +234,7 @@ def get_user_by_eppn(self, eppn: Optional[str]) -> UserVar: raise UserDoesNotExist(f"No user with eppn {repr(eppn)}") return res - def _get_user_by_attr(self, attr: str, value: Any) -> Optional[UserVar]: + def _get_user_by_attr(self, attr: str, value: Any) -> UserVar | None: """ Locate a user in the userdb using any attribute and value. @@ -364,7 +364,7 @@ def get_unterminated_users_with_nin(self) -> list[User]: users = self._get_documents_by_aggregate(match=match) return self._users_from_documents(users) - def unverify_mail_aliases(self, user_id: ObjectId, mail_aliases: Optional[list[dict[str, Any]]]) -> int: + def unverify_mail_aliases(self, user_id: ObjectId, mail_aliases: list[dict[str, Any]] | None) -> int: count = 0 if mail_aliases is None: logger.debug(f"No mailAliases to check duplicates against for user {user_id}.") diff --git a/src/eduid/userdb/util.py b/src/eduid/userdb/util.py index 22d706fc5..7da2da2e3 100644 --- a/src/eduid/userdb/util.py +++ b/src/eduid/userdb/util.py @@ -4,7 +4,7 @@ import json import logging from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from bson import ObjectId @@ -34,7 +34,7 @@ def objectid_str() -> str: return str(ObjectId()) -def format_dict_for_debug(data: Optional[Mapping[str, Any]]) -> Optional[str]: +def format_dict_for_debug(data: Mapping[str, Any] | None) -> str | None: """ Format a dict for logging. diff --git a/src/eduid/vccs/client/__init__.py b/src/eduid/vccs/client/__init__.py index f3ec3e649..60a8123ab 100644 --- a/src/eduid/vccs/client/__init__.py +++ b/src/eduid/vccs/client/__init__.py @@ -43,7 +43,7 @@ import os from collections.abc import Sequence -from typing import Any, Optional +from typing import Any from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen @@ -106,7 +106,7 @@ class VCCSPasswordFactor(VCCSFactor): Object representing an ordinary password authentication factor. """ - def __init__(self, password: str, credential_id: str, salt: Optional[str] = None, strip_whitespace: bool = True): + def __init__(self, password: str, credential_id: str, salt: str | None = None, strip_whitespace: bool = True): """ :param password: string, password as plaintext :param credential_id: unique id of credential in the authentication backend database @@ -307,7 +307,7 @@ class VCCSClient: credentials (authentication factors). """ - def __init__(self, base_url: Optional[str] = None): + def __init__(self, base_url: str | None = None): self.base_url = base_url if base_url else "http://localhost:8550/" def authenticate(self, user_id: str, factors: Sequence[VCCSFactor]) -> bool: diff --git a/src/eduid/vccs/server/config.py b/src/eduid/vccs/server/config.py index 7d0be91fb..48a91692e 100644 --- a/src/eduid/vccs/server/config.py +++ b/src/eduid/vccs/server/config.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from eduid.common.config.base import RootConfig from eduid.common.config.parsers import load_config @@ -16,10 +16,10 @@ class VCCSConfig(RootConfig): kdf_min_iterations: int = 20000 yhsm_debug: bool = False yhsm_device: str = "/dev/ttyACM0" - yhsm_unlock_password: Optional[str] = None + yhsm_unlock_password: str | None = None -def init_config(ns: str, app_name: str, test_config: Optional[Mapping[str, Any]] = None) -> VCCSConfig: +def init_config(ns: str, app_name: str, test_config: Mapping[str, Any] | None = None) -> VCCSConfig: config = load_config(typ=VCCSConfig, app_name=app_name, ns=ns, test_config=test_config) assert isinstance(config, VCCSConfig) # convince mypy return config diff --git a/src/eduid/vccs/server/db.py b/src/eduid/vccs/server/db.py index 7bcf8a277..1e9a80185 100644 --- a/src/eduid/vccs/server/db.py +++ b/src/eduid/vccs/server/db.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import asdict, field from enum import Enum, unique -from typing import Any, Optional, Union, cast +from typing import Any, cast from bson import ObjectId from loguru import logger @@ -217,7 +217,7 @@ def save(self, credential: Credential) -> bool: credential.revision -= 1 return False - def get_credential(self, credential_id: str) -> Optional[Union[PasswordCredential, RevokedCredential]]: + def get_credential(self, credential_id: str) -> PasswordCredential | RevokedCredential | None: """ Lookup an credential using the credential id. diff --git a/src/eduid/vccs/server/endpoints/misc.py b/src/eduid/vccs/server/endpoints/misc.py index 89007de6b..3345c5836 100644 --- a/src/eduid/vccs/server/endpoints/misc.py +++ b/src/eduid/vccs/server/endpoints/misc.py @@ -1,5 +1,4 @@ from enum import Enum, unique -from typing import Optional from fastapi import APIRouter, Request from pydantic.main import BaseModel @@ -16,7 +15,7 @@ class Status(str, Enum): class StatusResponse(BaseModel): status: Status - add_creds_hmac: Optional[Status] = None + add_creds_hmac: Status | None = None version: int = 1 diff --git a/src/eduid/vccs/server/hasher.py b/src/eduid/vccs/server/hasher.py index 628412eb9..68b460fe7 100644 --- a/src/eduid/vccs/server/hasher.py +++ b/src/eduid/vccs/server/hasher.py @@ -5,7 +5,7 @@ from binascii import unhexlify from collections.abc import Mapping from hashlib import sha1 -from typing import Any, Optional +from typing import Any import pyhsm import yaml @@ -99,7 +99,7 @@ def __init__(self, keys: Mapping[int, str], lock, debug=False): self.debug = debug # Covert keys from strings to bytes when loading self.keys: dict[int, bytes] = {} - self._temp_key: Optional[bytes] = None + self._temp_key: bytes | None = None for k, v in keys.items(): self.keys[k] = unhexlify(v) @@ -109,7 +109,7 @@ def unlock(self, password: str) -> None: def info(self) -> Any: return f"key handles loaded: {list(self.keys.keys())}" - async def hmac_sha1(self, key_handle: Optional[int], data: bytes) -> bytes: + async def hmac_sha1(self, key_handle: int | None, data: bytes) -> bytes: """ Perform HMAC-SHA-1 operation using YubiHSM. @@ -121,7 +121,7 @@ async def hmac_sha1(self, key_handle: Optional[int], data: bytes) -> bytes: finally: await self.lock_release() - def unsafe_hmac_sha1(self, key_handle: Optional[int], data: bytes) -> bytes: + def unsafe_hmac_sha1(self, key_handle: int | None, data: bytes) -> bytes: if key_handle is None: if not self._temp_key: raise RuntimeError("No key handle provided, and no temp key loaded") diff --git a/src/eduid/vccs/server/password.py b/src/eduid/vccs/server/password.py index 621347c82..bbc8c20b0 100644 --- a/src/eduid/vccs/server/password.py +++ b/src/eduid/vccs/server/password.py @@ -1,5 +1,4 @@ from binascii import unhexlify -from typing import Union from ndnkdf import NDNKDF @@ -46,7 +45,7 @@ async def calculate_cred_hash( """ # Lock down key usage & credential to auth T1 = b"" - _components: list[Union[str, bytes]] = ["A", user_id, cred.credential_id, unhexlify(H1)] + _components: list[str | bytes] = ["A", user_id, cred.credential_id, unhexlify(H1)] for this in _components: # Turn strings into bytes if isinstance(this, str): diff --git a/src/eduid/vccs/server/run.py b/src/eduid/vccs/server/run.py index 5a3557a3a..e56683c0a 100644 --- a/src/eduid/vccs/server/run.py +++ b/src/eduid/vccs/server/run.py @@ -1,7 +1,7 @@ import sys from asyncio import Lock from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from fastapi import FastAPI from fastapi.exceptions import RequestValidationError @@ -20,7 +20,7 @@ class VCCS_API(FastAPI): - def __init__(self, test_config: Optional[Mapping[str, Any]] = None) -> None: + def __init__(self, test_config: Mapping[str, Any] | None = None) -> None: print("vccs_api is starting", file=sys.stderr) super().__init__() diff --git a/src/eduid/webapp/authn/app.py b/src/eduid/webapp/authn/app.py index 09731e092..eb27a3fd5 100644 --- a/src/eduid/webapp/authn/app.py +++ b/src/eduid/webapp/authn/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from flask import current_app @@ -26,7 +26,7 @@ def get_current_app() -> AuthnApp: current_authn_app = get_current_app() -def authn_init_app(name: str = "authn", test_config: Optional[Mapping[str, Any]] = None) -> AuthnApp: +def authn_init_app(name: str = "authn", test_config: Mapping[str, Any] | None = None) -> AuthnApp: """ Create an instance of an authentication app. diff --git a/src/eduid/webapp/authn/tests/test_authn.py b/src/eduid/webapp/authn/tests/test_authn.py index 122808531..1c2666cdc 100644 --- a/src/eduid/webapp/authn/tests/test_authn.py +++ b/src/eduid/webapp/authn/tests/test_authn.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from urllib.parse import quote_plus from flask import Blueprint @@ -49,7 +49,7 @@ class AuthnAPITestBase(EduidAPITestCase): def setUp( # type: ignore[override] self, *args: list[Any], - users: Optional[list[str]] = None, + users: list[str] | None = None, copy_user_to_private: bool = False, **kwargs: dict[str, Any], ) -> None: @@ -183,8 +183,8 @@ def acs( url: str, eppn: str, frontend_action: FrontendAction, - frontend_state: Optional[str] = None, - accr: Optional[EduidAuthnContextClass] = None, + frontend_state: str | None = None, + accr: EduidAuthnContextClass | None = None, ) -> AcsResult: """ common code for the tests that need to access the assertion consumer service diff --git a/src/eduid/webapp/authn/views.py b/src/eduid/webapp/authn/views.py index 6ed121186..528298a65 100644 --- a/src/eduid/webapp/authn/views.py +++ b/src/eduid/webapp/authn/views.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from flask import Blueprint, abort, make_response, redirect, request from saml2 import BINDING_HTTP_REDIRECT @@ -108,8 +107,8 @@ def terminate() -> WerkzeugResponse: @MarshalWith(AuthnCommonResponseSchema) def authenticate( frontend_action: str, - frontend_state: Optional[str] = None, - method: Optional[str] = None, + frontend_state: str | None = None, + method: str | None = None, ) -> FluxData: current_app.logger.debug(f"authenticate called with frontend_action: {frontend_action}") try: @@ -209,9 +208,9 @@ def _old_authn(action: AuthnAcsAction, force_authn: bool = False, same_user: boo @dataclass class AuthnResult: - authn_id: Optional[AuthnRequestRef] = None - error: Optional[TranslatableMsg] = None - url: Optional[str] = None + authn_id: AuthnRequestRef | None = None + error: TranslatableMsg | None = None + url: str | None = None def _authn(sp_authn: SP_AuthnRequest, idp: str, authn_params: AuthnParameters) -> AuthnResult: @@ -291,7 +290,7 @@ def assertion_consumer_service() -> WerkzeugResponse: return redirect(formatted_finish_url) -def _get_authn_name_id(session: EduidSession) -> Optional[NameID]: +def _get_authn_name_id(session: EduidSession) -> NameID | None: """ Get the SAML2 NameID of the currently logged-in user. :param session: The current session object diff --git a/src/eduid/webapp/bankid/acs_actions.py b/src/eduid/webapp/bankid/acs_actions.py index 44c6f431f..6096ae2a4 100644 --- a/src/eduid/webapp/bankid/acs_actions.py +++ b/src/eduid/webapp/bankid/acs_actions.py @@ -1,5 +1,3 @@ -from typing import Optional - from eduid.userdb import User from eduid.userdb.credentials.fido import FidoCredential from eduid.webapp.bankid.app import current_bankid_app as current_app @@ -20,7 +18,7 @@ from eduid.webapp.bankid.saml_session_info import BaseSessionInfo -def common_saml_checks(args: ACSArgs) -> Optional[ACSResult]: +def common_saml_checks(args: ACSArgs) -> ACSResult | None: """ Perform common checks for SAML ACS actions. """ diff --git a/src/eduid/webapp/bankid/app.py b/src/eduid/webapp/bankid/app.py index cca9a6478..f03ca21c1 100644 --- a/src/eduid/webapp/bankid/app.py +++ b/src/eduid/webapp/bankid/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -33,7 +33,7 @@ def __init__(self, config: BankIDConfig, **kwargs: Any): current_bankid_app: BankIDApp = cast(BankIDApp, current_app) -def init_bankid_app(name: str = "bankid", test_config: Optional[Mapping[str, Any]] = None) -> BankIDApp: +def init_bankid_app(name: str = "bankid", test_config: Mapping[str, Any] | None = None) -> BankIDApp: """ Create an instance of an bankid app. diff --git a/src/eduid/webapp/bankid/helpers.py b/src/eduid/webapp/bankid/helpers.py index e0fec615e..a06234da9 100644 --- a/src/eduid/webapp/bankid/helpers.py +++ b/src/eduid/webapp/bankid/helpers.py @@ -1,6 +1,6 @@ import logging from enum import unique -from typing import Any, Optional +from typing import Any from saml2 import BINDING_HTTP_REDIRECT from saml2.client import Saml2Client @@ -97,8 +97,8 @@ def create_authn_info( def check_reauthn( - frontend_action: FrontendAction, user: User, credential_used: Optional[Credential] = None -) -> Optional[AuthnActionStatus]: + frontend_action: FrontendAction, user: User, credential_used: Credential | None = None +) -> AuthnActionStatus | None: """Check if a re-authentication has been performed recently enough for this action""" authn_status = validate_authn_for_action( diff --git a/src/eduid/webapp/bankid/proofing.py b/src/eduid/webapp/bankid/proofing.py index 16532e28d..6abbdb581 100644 --- a/src/eduid/webapp/bankid/proofing.py +++ b/src/eduid/webapp/bankid/proofing.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from eduid.common.config.base import ProofingConfigMixin from eduid.common.models.saml_models import BaseSessionInfo @@ -31,7 +30,7 @@ @dataclass class BankIDProofingFunctions(ProofingFunctions[BankIDSessionInfo]): - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: return user.identities.nin def verify_identity(self, user: User) -> VerifyUserResult: @@ -177,7 +176,7 @@ def _match_identity_for_mfa( return MatchResult(matched=mfa_success, credential_used=credential_used) - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: if loa != "uncertified-loa3": return VerifyCredentialResult(error=BankIDMsg.authn_context_mismatch) @@ -188,9 +187,7 @@ def mark_credential_as_verified(self, credential: Credential, loa: Optional[str] return VerifyCredentialResult(credential=credential) -def _find_or_add_credential( - user: User, framework: Optional[TrustFramework], required_loa: list[str] -) -> Optional[ElementKey]: +def _find_or_add_credential(user: User, framework: TrustFramework | None, required_loa: list[str]) -> ElementKey | None: if not required_loa: # mainly keep mypy calm current_app.logger.debug("Not recording credential used without required_loa") diff --git a/src/eduid/webapp/bankid/saml_session_info.py b/src/eduid/webapp/bankid/saml_session_info.py index 4fea8e266..c72e61ee8 100644 --- a/src/eduid/webapp/bankid/saml_session_info.py +++ b/src/eduid/webapp/bankid/saml_session_info.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from eduid.common.models.saml_models import BaseSessionInfo, SAMLAttributes @@ -11,7 +9,7 @@ class NinAttributes(SAMLAttributes): nin: str = Field(alias="personalIdentityNumber") given_name: str = Field(alias="givenName") surname: str = Field(alias="sn") - display_name: Optional[str] = Field(default=None, alias="displayName") + display_name: str | None = Field(default=None, alias="displayName") auth_context_params: str = Field(alias="authContextParams") transaction_id: str = Field(alias="transactionIdentifier") diff --git a/src/eduid/webapp/bankid/settings/common.py b/src/eduid/webapp/bankid/settings/common.py index bebabe5b3..75b201f1f 100644 --- a/src/eduid/webapp/bankid/settings/common.py +++ b/src/eduid/webapp/bankid/settings/common.py @@ -2,8 +2,6 @@ Configuration (file) handling for the eduID eidas app. """ -from typing import Optional - from pydantic import Field from eduid.common.config.base import ( @@ -41,5 +39,5 @@ class BankIDConfig( } ) # magic cookie IdP is used for integration tests when magic cookie is set - magic_cookie_idp: Optional[str] = None - magic_cookie_foreign_id_idp: Optional[str] = None + magic_cookie_idp: str | None = None + magic_cookie_foreign_id_idp: str | None = None diff --git a/src/eduid/webapp/bankid/tests/test_app.py b/src/eduid/webapp/bankid/tests/test_app.py index 9770b9b01..43f62321b 100644 --- a/src/eduid/webapp/bankid/tests/test_app.py +++ b/src/eduid/webapp/bankid/tests/test_app.py @@ -4,7 +4,7 @@ import os import unittest from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any from unittest.mock import MagicMock, patch from fido2.webauthn import AuthenticatorAttachment @@ -208,9 +208,9 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: ) return config - def add_token_to_user(self, eppn: str, credential_id: str, token_type: str) -> Union[U2F, Webauthn]: + def add_token_to_user(self, eppn: str, credential_id: str, token_type: str) -> U2F | Webauthn: user = self.app.central_userdb.get_user_by_eppn(eppn) - mfa_token: Union[U2F, Webauthn] + mfa_token: U2F | Webauthn if token_type == "u2f": mfa_token = U2F( version="test", @@ -246,9 +246,9 @@ def generate_auth_response( request_id: str, saml_response_tpl: str, asserted_identity: str, - date_of_birth: Optional[datetime.datetime] = None, + date_of_birth: datetime.datetime | None = None, age: int = 10, - credentials_used: Optional[list[ElementKey]] = None, + credentials_used: list[ElementKey] | None = None, ) -> bytes: """ Generates a fresh signed authentication response @@ -323,13 +323,13 @@ def reauthn( frontend_action: FrontendAction, expect_msg: TranslatableMsg, age: int = 10, - browser: Optional[CSRFTestClient] = None, - eppn: Optional[str] = None, + browser: CSRFTestClient | None = None, + eppn: str | None = None, expect_error: bool = False, - identity: Optional[NinIdentity] = None, + identity: NinIdentity | None = None, logged_in: bool = True, method: str = "bankid", - response_template: Optional[str] = None, + response_template: str | None = None, ) -> None: return self._call_endpoint_and_saml_acs( age=age, @@ -351,15 +351,15 @@ def verify_token( frontend_action: FrontendAction, expect_msg: TranslatableMsg, age: int = 10, - browser: Optional[CSRFTestClient] = None, - credentials_used: Optional[list[ElementKey]] = None, - eppn: Optional[str] = None, + browser: CSRFTestClient | None = None, + credentials_used: list[ElementKey] | None = None, + eppn: str | None = None, expect_error: bool = False, expect_saml_error: bool = False, - identity: Optional[NinIdentity] = None, + identity: NinIdentity | None = None, method: str = "bankid", - response_template: Optional[str] = None, - verify_credential: Optional[ElementKey] = None, + response_template: str | None = None, + verify_credential: ElementKey | None = None, ) -> None: return self._call_endpoint_and_saml_acs( age=age, @@ -383,8 +383,8 @@ def _get_authn_redirect_url( endpoint: str, method: str, frontend_action: FrontendAction, - verify_credential: Optional[ElementKey] = None, - frontend_state: Optional[str] = None, + verify_credential: ElementKey | None = None, + frontend_state: str | None = None, ) -> str: with browser.session_transaction() as sess: csrf_token = sess.get_csrf_token() @@ -412,18 +412,18 @@ def _call_endpoint_and_saml_acs( endpoint: str, method: str, frontend_action: FrontendAction, - eppn: Optional[str], + eppn: str | None, expect_msg: TranslatableMsg, age: int = 10, - browser: Optional[CSRFTestClient] = None, - credentials_used: Optional[list[ElementKey]] = None, + browser: CSRFTestClient | None = None, + credentials_used: list[ElementKey] | None = None, expect_error: bool = False, expect_saml_error: bool = False, - identity: Optional[NinIdentity] = None, + identity: NinIdentity | None = None, logged_in: bool = True, - response_template: Optional[str] = None, - verify_credential: Optional[ElementKey] = None, - frontend_state: Optional[str] = "This is a unit test", + response_template: str | None = None, + verify_credential: ElementKey | None = None, + frontend_state: str | None = "This is a unit test", ) -> None: if eppn is None: eppn = self.test_user_eppn diff --git a/src/eduid/webapp/bankid/views.py b/src/eduid/webapp/bankid/views.py index 6a43de84f..dbdc98a36 100644 --- a/src/eduid/webapp/bankid/views.py +++ b/src/eduid/webapp/bankid/views.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from flask import Blueprint, make_response, redirect, request from werkzeug.wrappers import Response as WerkzeugResponse @@ -79,7 +78,7 @@ def get_status(authn_id: AuthnRequestRef) -> FluxData: @MarshalWith(BankIDCommonResponseSchema) @require_user def verify_credential( - user: User, method: str, credential_id: ElementKey, frontend_action: str, frontend_state: Optional[str] = None + user: User, method: str, credential_id: ElementKey, frontend_action: str, frontend_state: str | None = None ) -> FluxData: current_app.logger.debug(f"verify-credential called with credential_id: {credential_id}") @@ -122,7 +121,7 @@ def verify_credential( @UnmarshalWith(BankIDCommonRequestSchema) @MarshalWith(BankIDCommonResponseSchema) @require_user -def verify_identity(user: User, method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def verify_identity(user: User, method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: current_app.logger.debug(f"verify-identity called for method {method}") result = _authn( @@ -141,7 +140,7 @@ def verify_identity(user: User, method: str, frontend_action: str, frontend_stat @bankid_views.route("/mfa-authenticate", methods=["POST"]) @UnmarshalWith(BankIDCommonRequestSchema) @MarshalWith(BankIDCommonResponseSchema) -def mfa_authentication(method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def mfa_authentication(method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: current_app.logger.debug("mfa-authenticate called") result = _authn( @@ -159,18 +158,18 @@ def mfa_authentication(method: str, frontend_action: str, frontend_state: Option @dataclass class AuthnResult: - authn_req: Optional[SAMLHttpArgs] = None - authn_id: Optional[AuthnRequestRef] = None - error: Optional[TranslatableMsg] = None - url: Optional[str] = None + authn_req: SAMLHttpArgs | None = None + authn_id: AuthnRequestRef | None = None + error: TranslatableMsg | None = None + url: str | None = None def _authn( action: BankIDAcsAction, method: str, frontend_action: str, - frontend_state: Optional[str] = None, - proofing_credential_id: Optional[ElementKey] = None, + frontend_state: str | None = None, + proofing_credential_id: ElementKey | None = None, ) -> AuthnResult: current_app.logger.debug(f"Requested method: {method}, frontend action: {frontend_action}") try: diff --git a/src/eduid/webapp/common/api/app.py b/src/eduid/webapp/common/api/app.py index 5b29ab7b0..6e361ffa3 100644 --- a/src/eduid/webapp/common/api/app.py +++ b/src/eduid/webapp/common/api/app.py @@ -8,7 +8,7 @@ import os from abc import ABCMeta from sys import stderr -from typing import TYPE_CHECKING, Any, Optional, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from cookies_samesite_compat import CookiesSameSiteCompatMiddleware from flask import Flask @@ -106,7 +106,7 @@ def __init__( self.stats = init_app_stats(config) self.session_interface = SessionFactory(config) - self._central_userdb: Optional[AmDB] = None + self._central_userdb: AmDB | None = None if init_central_userdb: self._central_userdb = AmDB(config.mongo_uri) diff --git a/src/eduid/webapp/common/api/checks.py b/src/eduid/webapp/common/api/checks.py index a163bd3f7..14d61a71d 100644 --- a/src/eduid/webapp/common/api/checks.py +++ b/src/eduid/webapp/common/api/checks.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field, replace from datetime import datetime, timedelta from os import environ -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, cast import redis from flask import current_app as flask_current_app @@ -34,24 +34,24 @@ def get_current_app() -> EduIDBaseApp: @dataclass class CheckResult: healthy: bool - status: Optional[str] = None + status: str | None = None hostname: str = field(default_factory=lambda: environ.get("HOSTNAME", "UNKNOWN")) - reason: Optional[str] = None + reason: str | None = None @dataclass class FailCountItem: first_failure: datetime = field(repr=False) - restart_at: Optional[datetime] = None - restart_interval: Optional[int] = None - exit_at: Optional[datetime] = None + restart_at: datetime | None = None + restart_interval: int | None = None + exit_at: datetime | None = None count: int = 0 def __str__(self): return f"(first_failure: {self.first_failure.isoformat()}, fail count: {self.count})" -def log_failure_info(key: str, msg: str, exc: Optional[Exception] = None) -> None: +def log_failure_info(key: str, msg: str, exc: Exception | None = None) -> None: current_app = get_current_app() if key not in current_app.failure_info: @@ -132,7 +132,7 @@ def check_redis() -> bool: def check_am() -> bool: current_app = get_current_app() - am_relay: Optional[AmRelay] = getattr(current_app, "am_relay", None) + am_relay: AmRelay | None = getattr(current_app, "am_relay", None) if not am_relay: return True try: @@ -149,7 +149,7 @@ def check_am() -> bool: def check_msg() -> bool: current_app = get_current_app() - msg_relay: Optional[MsgRelay] = getattr(current_app, "msg_relay", None) + msg_relay: MsgRelay | None = getattr(current_app, "msg_relay", None) if not msg_relay: return True try: @@ -167,7 +167,7 @@ def check_msg() -> bool: def check_mail() -> bool: current_app = get_current_app() - mail_relay: Optional[MailRelay] = getattr(current_app, "mail_relay", None) + mail_relay: MailRelay | None = getattr(current_app, "mail_relay", None) if not mail_relay: return True try: @@ -185,7 +185,7 @@ def check_mail() -> bool: def check_lookup_mobile() -> bool: current_app = get_current_app() - _relay: Optional[LookupMobileRelay] = getattr(current_app, "lookup_mobile_relay", None) + _relay: LookupMobileRelay | None = getattr(current_app, "lookup_mobile_relay", None) if not _relay: return True try: diff --git a/src/eduid/webapp/common/api/decorators.py b/src/eduid/webapp/common/api/decorators.py index d9f4b9dbc..8b601562f 100644 --- a/src/eduid/webapp/common/api/decorators.py +++ b/src/eduid/webapp/common/api/decorators.py @@ -2,7 +2,7 @@ import logging from collections.abc import Awaitable, Callable, Mapping from functools import wraps -from typing import Any, Optional, TypeVar, Union, cast +from typing import Any, TypeVar, cast from flask import abort, jsonify, request from flask.typing import ResponseReturnValue as FlaskResponseReturnValue @@ -29,14 +29,11 @@ EduidViewBackwardsCompat = Mapping[str, Any] # TODO: Make our views stop returning dicts and remove this -EduidViewResult = Union[FluxData, WerkzeugResponse, EduidViewBackwardsCompat] +EduidViewResult = FluxData | WerkzeugResponse | EduidViewBackwardsCompat # The Flask route decorator first in the chain makes us have to accept the full ResponseReturnValue too -EduidViewReturnType = Union[ - EduidViewResult, - FlaskResponseReturnValue, - Awaitable[EduidViewResult], - Awaitable[FlaskResponseReturnValue], -] +EduidViewReturnType = ( + EduidViewResult | FlaskResponseReturnValue | Awaitable[EduidViewResult] | Awaitable[FlaskResponseReturnValue] +) EduidRouteCallable = Callable[..., EduidViewReturnType] @@ -198,7 +195,7 @@ def unmarshal_decorator( flux_logger.debug("") flux_logger.debug(f"--- New request ({request.path})") # silent=True lets get_json return None even if mime-type is not application/json - json_data: Optional[Mapping[str, Any]] = request.get_json(silent=True) + json_data: Mapping[str, Any] | None = request.get_json(silent=True) if json_data is None: json_data = {} _data_str = str(json_data) diff --git a/src/eduid/webapp/common/api/errors.py b/src/eduid/webapp/common/api/errors.py index 3760b218c..7f99e177a 100644 --- a/src/eduid/webapp/common/api/errors.py +++ b/src/eduid/webapp/common/api/errors.py @@ -1,6 +1,5 @@ from datetime import datetime from enum import Enum -from typing import Optional from flask import redirect from werkzeug.wrappers import Response as WerkzeugResponse @@ -18,7 +17,7 @@ class EduidErrorsContext(str, Enum): def goto_errors_response( - errors_url: str, ctx: EduidErrorsContext, rp: str, tid: Optional[str] = None, now: Optional[datetime] = None + errors_url: str, ctx: EduidErrorsContext, rp: str, tid: str | None = None, now: datetime | None = None ) -> WerkzeugResponse: if now is None: now = utc_now() diff --git a/src/eduid/webapp/common/api/exceptions.py b/src/eduid/webapp/common/api/exceptions.py index 5330bfb03..d4b1e3888 100644 --- a/src/eduid/webapp/common/api/exceptions.py +++ b/src/eduid/webapp/common/api/exceptions.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from flask import jsonify @@ -15,8 +15,8 @@ class ApiException(Exception): def __init__( self, message: str = "ApiException", - status_code: Optional[int] = None, - payload: Optional[Mapping[str, Any]] = None, + status_code: int | None = None, + payload: Mapping[str, Any] | None = None, ): """ :param message: Error message diff --git a/src/eduid/webapp/common/api/helpers.py b/src/eduid/webapp/common/api/helpers.py index 671be574d..97f853830 100644 --- a/src/eduid/webapp/common/api/helpers.py +++ b/src/eduid/webapp/common/api/helpers.py @@ -1,6 +1,6 @@ import warnings from dataclasses import dataclass -from typing import Any, Optional, TypeVar, Union, cast, overload +from typing import Any, TypeVar, cast, overload from flask import current_app, render_template, request @@ -33,7 +33,7 @@ __author__ = "lundberg" -def get_marked_given_name(given_name: str, given_name_marking: Optional[str]) -> str: +def get_marked_given_name(given_name: str, given_name_marking: str | None) -> str: """ Given name marking denotes up to two given names, and is used to determine which of the given names are to be primarily used in addressing a person. @@ -55,7 +55,7 @@ def get_marked_given_name(given_name: str, given_name_marking: Optional[str]) -> return given_name # cheating with indexing - _given_names: list[Optional[str]] = [None] + _given_names: list[str | None] = [None] for name in given_name.split(): if "-" in name: # hyphenated names are counted separately @@ -63,7 +63,7 @@ def get_marked_given_name(given_name: str, given_name_marking: Optional[str]) -> else: _given_names.append(name) - _optional_marked_names: list[Optional[str]] = [] + _optional_marked_names: list[str | None] = [] for i in given_name_marking: _optional_marked_names.append(_given_names[int(i)]) # remove None values @@ -194,7 +194,7 @@ def add_nin_to_user(user, proofing_state, user_type=ProofingUser): def verify_nin_for_user( - user: Union[User, ProofingUser], proofing_state: NinProofingState, proofing_log_entry: NinProofingLogElement + user: User | ProofingUser, proofing_state: NinProofingState, proofing_log_entry: NinProofingLogElement ) -> bool: """ Mark a nin on a user as verified, after logging data about the proofing to the proofing log. @@ -288,8 +288,8 @@ def send_mail( text_template: str, html_template: str, app: EduIDBaseApp, - context: Optional[dict[str, Any]] = None, - reference: Optional[str] = None, + context: dict[str, Any] | None = None, + reference: str | None = None, ): """ :param subject: subject text @@ -356,7 +356,7 @@ def check_magic_cookie(config: MagicCookieMixin) -> bool: @dataclass class ProofingNavetData: user_postal_address: FullPostalAddress - deregistration_information: Optional[DeregistrationInformation] = None + deregistration_information: DeregistrationInformation | None = None def get_proofing_log_navet_data(nin: str) -> ProofingNavetData: diff --git a/src/eduid/webapp/common/api/messages.py b/src/eduid/webapp/common/api/messages.py index 7770de663..156792f12 100644 --- a/src/eduid/webapp/common/api/messages.py +++ b/src/eduid/webapp/common/api/messages.py @@ -2,7 +2,7 @@ from copy import copy from dataclasses import asdict, dataclass from enum import Enum, unique -from typing import Any, Optional, Union +from typing import Any from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit from flask import redirect @@ -70,14 +70,14 @@ class AuthnStatusMsg(TranslatableMsg): class FluxData: status: FluxResponseStatus payload: Mapping[str, Any] - meta: Optional[Mapping[str, Any]] = None + meta: Mapping[str, Any] | None = None def to_dict(self) -> dict[str, Any]: return asdict(self) def success_response( - payload: Optional[Mapping[str, Any]] = None, message: Optional[Union[TranslatableMsg, str]] = None + payload: Mapping[str, Any] | None = None, message: TranslatableMsg | str | None = None ) -> FluxData: """ Make a success response, that can be marshalled into a response that eduid-front understands. @@ -96,9 +96,7 @@ def success_response( return FluxData(status=FluxResponseStatus.OK, payload=_make_payload(payload, message, True)) -def error_response( - payload: Optional[Mapping[str, Any]] = None, message: Optional[Union[TranslatableMsg, str]] = None -) -> FluxData: +def error_response(payload: Mapping[str, Any] | None = None, message: TranslatableMsg | str | None = None) -> FluxData: """ Make an error response, that can be marshalled into a response that eduid-front understands. @@ -113,7 +111,7 @@ def error_response( def need_authentication_response( - frontend_action: FrontendAction, authn_status: AuthnActionStatus, payload: Optional[Mapping[str, Any]] = None + frontend_action: FrontendAction, authn_status: AuthnActionStatus, payload: Mapping[str, Any] | None = None ) -> FluxData: meta = { "frontend_action": frontend_action.value, @@ -127,7 +125,7 @@ def need_authentication_response( def _make_payload( - payload: Optional[Mapping[str, Any]], message: Optional[Union[TranslatableMsg, str]], success: bool + payload: Mapping[str, Any] | None, message: TranslatableMsg | str | None, success: bool ) -> Mapping[str, Any]: res: dict[str, Any] = {} if payload is not None: @@ -161,7 +159,7 @@ def make_query_string(msg: TranslatableMsg, error: bool = True): return urlencode({"msg": msg_str}) -def redirect_with_msg(url: str, msg: Union[TranslatableMsg, str], error: bool = True) -> WerkzeugResponse: +def redirect_with_msg(url: str, msg: TranslatableMsg | str, error: bool = True) -> WerkzeugResponse: """ :param url: URL to redirect to :param msg: message to append to query string diff --git a/src/eduid/webapp/common/api/request.py b/src/eduid/webapp/common/api/request.py index 9e064ab51..719e4c55e 100644 --- a/src/eduid/webapp/common/api/request.py +++ b/src/eduid/webapp/common/api/request.py @@ -15,7 +15,7 @@ """ import logging -from typing import Any, AnyStr, Optional +from typing import Any, AnyStr from flask import abort from flask.wrappers import Request as FlaskRequest @@ -35,7 +35,7 @@ class SanitationMixin(Sanitizer): def sanitize_input( self, untrusted_text: AnyStr, - content_type: Optional[str] = None, + content_type: str | None = None, strip_characters: bool = False, ): try: @@ -157,7 +157,7 @@ def __getitem__(self, key): val = super(ImmutableTypeConversionDict, self).__getitem__(key) return self.sanitize_input(str(val)) - def get(self, key, default=None, type=None) -> Optional[Any]: # type: ignore[override] + def get(self, key, default=None, type=None) -> Any | None: # type: ignore[override] """ Sanitized, type conversion get. The value identified by `key` is sanitized, and if `type` diff --git a/src/eduid/webapp/common/api/sanitation.py b/src/eduid/webapp/common/api/sanitation.py index 2bd946272..da9f78e79 100644 --- a/src/eduid/webapp/common/api/sanitation.py +++ b/src/eduid/webapp/common/api/sanitation.py @@ -1,5 +1,5 @@ import logging -from typing import AnyStr, Optional +from typing import AnyStr from urllib.parse import quote, unquote from bleach import clean @@ -19,7 +19,7 @@ class Sanitizer: def sanitize_input( self, untrusted_text: AnyStr, - content_type: Optional[str] = None, + content_type: str | None = None, strip_characters: bool = False, ) -> str: """ @@ -60,7 +60,7 @@ def _sanitize_input( self, untrusted_text: str, strip_characters: bool = False, - content_type: Optional[str] = None, + content_type: str | None = None, percent_encoded: bool = False, ) -> str: """ diff --git a/src/eduid/webapp/common/api/schemas/password.py b/src/eduid/webapp/common/api/schemas/password.py index eb642e7dd..3faf75f49 100644 --- a/src/eduid/webapp/common/api/schemas/password.py +++ b/src/eduid/webapp/common/api/schemas/password.py @@ -1,5 +1,3 @@ -from typing import Optional - from marshmallow import Schema, ValidationError __author__ = "lundberg" @@ -9,9 +7,9 @@ class PasswordSchema(Schema): class Meta: - zxcvbn_terms: Optional[list[str]] = None - min_entropy: Optional[int] = None - min_score: Optional[int] = None + zxcvbn_terms: list[str] | None = None + min_entropy: int | None = None + min_score: int | None = None def __init__(self, *args, **kwargs): self.Meta.zxcvbn_terms = kwargs.pop("zxcvbn_terms", []) diff --git a/src/eduid/webapp/common/api/testing.py b/src/eduid/webapp/common/api/testing.py index 6f663065f..a0fd48e13 100644 --- a/src/eduid/webapp/common/api/testing.py +++ b/src/eduid/webapp/common/api/testing.py @@ -9,7 +9,7 @@ from contextlib import contextmanager from copy import deepcopy from datetime import timedelta -from typing import Any, Generic, Optional, TypeVar, cast +from typing import Any, Generic, TypeVar, cast from flask.testing import FlaskClient from werkzeug.test import TestResponse @@ -99,7 +99,7 @@ class EduidAPITestCase(CommonTestCase, Generic[TTestAppVar]): def setUp( # type: ignore[override] self, *args: list[Any], - users: Optional[list[str]] = None, + users: list[str] | None = None, copy_user_to_private: bool = False, **kwargs: dict[str, Any], ) -> None: @@ -119,7 +119,7 @@ def setUp( # type: ignore[override] super().setUp(am_users=am_users, *args, **kwargs) - self.user: Optional[User] = None + self.user: User | None = None # Load the user from the database so that it can be saved there again in tests _test_user = self.amdb.get_user_by_eppn(users[0]) @@ -195,9 +195,9 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: def session_cookie( self, client: CSRFTestClient, - eppn: Optional[str], + eppn: str | None, logged_in: bool = True, - domain: Optional[str] = None, + domain: str | None = None, **kwargs: Any, ) -> Generator[CSRFTestClient, None, None]: if domain is None: @@ -221,11 +221,11 @@ def session_cookie_anon(self, client: CSRFTestClient, **kwargs: Any) -> Generato def session_cookie_and_magic_cookie( self, client: CSRFTestClient, - eppn: Optional[str], + eppn: str | None, logged_in: bool = True, - domain: Optional[str] = None, - magic_cookie_name: Optional[str] = None, - magic_cookie_value: Optional[str] = None, + domain: str | None = None, + magic_cookie_name: str | None = None, + magic_cookie_value: str | None = None, **kwargs: Any, ) -> Generator[CSRFTestClient, None, None]: if domain is None: @@ -247,8 +247,8 @@ def session_cookie_and_magic_cookie( def session_cookie_and_magic_cookie_anon( self, client: CSRFTestClient, - magic_cookie_name: Optional[str] = None, - magic_cookie_value: Optional[str] = None, + magic_cookie_name: str | None = None, + magic_cookie_value: str | None = None, **kwargs: Any, ) -> Generator[CSRFTestClient, None, None]: with self.session_cookie_and_magic_cookie( @@ -260,7 +260,7 @@ def session_cookie_and_magic_cookie_anon( ) as _client: yield _client - def request_user_sync(self, private_user: User, app_name_override: Optional[str] = None) -> bool: + def request_user_sync(self, private_user: User, app_name_override: str | None = None) -> bool: """ Updates the central db user with data from the private db user. @@ -323,9 +323,9 @@ def set_authn_action( frontend_action: FrontendAction, post_authn_action: AuthnAcsAction = AuthnAcsAction.login, age: timedelta = timedelta(seconds=30), - finish_url: Optional[str] = None, + finish_url: str | None = None, force_mfa: bool = False, - credentials_used: Optional[list[ElementKey]] = None, + credentials_used: list[ElementKey] | None = None, ): if not finish_url: finish_url = "https://example.com/ext-return/{app_name}/{authn_id}" @@ -359,7 +359,7 @@ def _get_full_postal_address(): def _check_must_authenticate_response( self, response: TestResponse, - type_: Optional[str], + type_: str | None, frontend_action: FrontendAction, authn_status: AuthnActionStatus, ): @@ -376,10 +376,10 @@ def _check_must_authenticate_response( def _check_error_response( self, response: TestResponse, - type_: Optional[str], - msg: Optional[TranslatableMsg] = None, - error: Optional[Mapping[str, Any]] = None, - payload: Optional[Mapping[str, Any]] = None, + type_: str | None, + msg: TranslatableMsg | None = None, + error: Mapping[str, Any] | None = None, + payload: Mapping[str, Any] | None = None, ): """Check that a call to the API failed in the data validation stage.""" return self._check_api_response(response, 200, type_=type_, message=msg, error=error, payload=payload) @@ -387,9 +387,9 @@ def _check_error_response( def _check_success_response( self, response: TestResponse, - type_: Optional[str], - msg: Optional[TranslatableMsg] = None, - payload: Optional[Mapping[str, Any]] = None, + type_: str | None, + msg: TranslatableMsg | None = None, + payload: Mapping[str, Any] | None = None, ): """ Check the message returned from an eduID webapp endpoint. @@ -404,11 +404,11 @@ def get_response_payload(response: TestResponse) -> dict[str, Any]: Perform some checks to make sure the response is a Flux Standard Action (FSA) response, and return the payload. """ assert response.is_json, "Response is not JSON" - _json: Optional[dict[str, Any]] = response.json + _json: dict[str, Any] | None = response.json assert isinstance(_json, dict), "Response has invalid JSON" - _type: Optional[str] = _json.get("type") + _type: str | None = _json.get("type") assert _type is not None, "Response has no type (is not an FSA response)" - _payload: Optional[dict[str, Any]] = _json.get("payload", {}) + _payload: dict[str, Any] | None = _json.get("payload", {}) assert isinstance(_payload, dict), "Response has invalid payload" return _payload @@ -416,12 +416,12 @@ def get_response_payload(response: TestResponse) -> dict[str, Any]: def _check_api_response( response: TestResponse, status: int, - type_: Optional[str], - message: Optional[TranslatableMsg] = None, - error: Optional[Mapping[str, Any]] = None, - payload: Optional[Mapping[str, Any]] = None, - assure_not_in_payload: Optional[Iterable[str]] = None, - meta: Optional[Mapping[str, Any]] = None, + type_: str | None, + message: TranslatableMsg | None = None, + error: Mapping[str, Any] | None = None, + payload: Mapping[str, Any] | None = None, + assure_not_in_payload: Iterable[str] | None = None, + meta: Mapping[str, Any] | None = None, ): """ Check data returned from an eduID webapp endpoint. @@ -508,8 +508,8 @@ def _check_nin_verified_ok( self, user: User, proofing_state: NinProofingState, - number: Optional[str] = None, - created_by: Optional[str] = None, + number: str | None = None, + created_by: str | None = None, ): if number is None and (self.test_user is not None and self.test_user.identities.nin): number = self.test_user.identities.nin.number @@ -528,7 +528,7 @@ def _check_nin_verified_ok( assert isinstance(_log, ProofingLog) assert _log.db_count() == 1 - def _check_nin_not_verified(self, user: User, number: Optional[str] = None, created_by: Optional[str] = None): + def _check_nin_not_verified(self, user: User, number: str | None = None, created_by: str | None = None): if number is None and (self.test_user is not None and self.test_user.identities.nin): number = self.test_user.identities.nin.number diff --git a/src/eduid/webapp/common/api/tests/test_backdoor.py b/src/eduid/webapp/common/api/tests/test_backdoor.py index 20aa8c011..45e6b20cf 100644 --- a/src/eduid/webapp/common/api/tests/test_backdoor.py +++ b/src/eduid/webapp/common/api/tests/test_backdoor.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from flask import Blueprint, abort, current_app, request @@ -44,7 +44,7 @@ class BackdoorTests(EduidAPITestCase[BackdoorTestApp]): def setUp( # type: ignore[override] self, *args: list[Any], - users: Optional[list[str]] = None, + users: list[str] | None = None, copy_user_to_private: bool = False, **kwargs: dict[str, Any], ) -> None: diff --git a/src/eduid/webapp/common/api/tests/test_nin_helpers.py b/src/eduid/webapp/common/api/tests/test_nin_helpers.py index bc819f0a1..66c542cf5 100644 --- a/src/eduid/webapp/common/api/tests/test_nin_helpers.py +++ b/src/eduid/webapp/common/api/tests/test_nin_helpers.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch import pytest @@ -109,7 +109,7 @@ def insert_no_nins_user(self) -> User: return self.app.central_userdb.get_user_by_eppn(user.eppn) def _get_nin_navet_proofing_log_entry( - self, user: User, nin: str, created_by: str, navet_data: Optional[FullPostalAddress] = None + self, user: User, nin: str, created_by: str, navet_data: FullPostalAddress | None = None ) -> NinNavetProofingLogElement: if navet_data is None: navet_data = self.navet_response() diff --git a/src/eduid/webapp/common/api/translation.py b/src/eduid/webapp/common/api/translation.py index 1b1b0386e..3ba40c6f2 100644 --- a/src/eduid/webapp/common/api/translation.py +++ b/src/eduid/webapp/common/api/translation.py @@ -1,5 +1,3 @@ -from typing import Optional - import pkg_resources from flask import current_app, request from flask_babel import Babel @@ -11,10 +9,10 @@ __author__ = "lundberg" -def get_user_locale() -> Optional[str]: +def get_user_locale() -> str | None: app = current_app assert isinstance(app, EduIDBaseApp) - lang: Optional[str] # mypy 0.910 needs this + lang: str | None # mypy 0.910 needs this # if a user is logged in, use the locale from the user settings if session.common.preferred_language is not None: lang = session.common.preferred_language diff --git a/src/eduid/webapp/common/api/utils.py b/src/eduid/webapp/common/api/utils.py index 8e8bb3a69..a836935af 100644 --- a/src/eduid/webapp/common/api/utils.py +++ b/src/eduid/webapp/common/api/utils.py @@ -2,7 +2,7 @@ import os import re from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast from unittest.mock import MagicMock from urllib.parse import urlparse from uuid import uuid4 @@ -117,7 +117,7 @@ def get_user() -> User: def save_and_sync_user( - user: User, private_userdb: Optional[UserDB[User]] = None, app_name_override: Optional[str] = None + user: User, private_userdb: UserDB[User] | None = None, app_name_override: str | None = None ) -> bool: """ Save (new) user object to the private userdb and propagate the changes to the central user db. @@ -194,7 +194,7 @@ def get_flux_type(req: Request, suffix: str) -> str: return flux_type -def sanitise_redirect_url(redirect_url: Optional[str], safe_default: str = "/") -> str: +def sanitise_redirect_url(redirect_url: str | None, safe_default: str = "/") -> str: """ Make sure the URL provided in relay_state is safe and does not provide an open redirect. @@ -242,7 +242,7 @@ def hash_password(password: str) -> str: return ret -def check_password_hash(password: str, hashed: Optional[str]) -> bool: +def check_password_hash(password: str, hashed: str | None) -> bool: """ Check that the provided password corresponds to the provided hash """ diff --git a/src/eduid/webapp/common/api/views/status.py b/src/eduid/webapp/common/api/views/status.py index d82567bf9..78cecaa08 100644 --- a/src/eduid/webapp/common/api/views/status.py +++ b/src/eduid/webapp/common/api/views/status.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from dataclasses import asdict, dataclass from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Any, Literal, Optional, cast, overload +from typing import TYPE_CHECKING, Any, Literal, cast, overload from flask import Blueprint, jsonify from flask import current_app as flask_current_app @@ -38,10 +38,10 @@ def cached_json_response(key: str, data: dict[str, Any]) -> Response: ... @overload -def cached_json_response(key: str, data: Literal[None]) -> Optional[Response]: ... +def cached_json_response(key: str, data: Literal[None]) -> Response | None: ... -def cached_json_response(key: str, data: Optional[dict[str, Any]] = None) -> Optional[Response]: +def cached_json_response(key: str, data: dict[str, Any] | None = None) -> Response | None: cache_for_seconds = get_from_current_app("conf", EduIDBaseAppConfig).status_cache_seconds now = datetime.utcnow() if SIMPLE_CACHE.get(key) is not None: diff --git a/src/eduid/webapp/common/authn/acs_registry.py b/src/eduid/webapp/common/authn/acs_registry.py index f321c8524..15e69d697 100644 --- a/src/eduid/webapp/common/authn/acs_registry.py +++ b/src/eduid/webapp/common/authn/acs_registry.py @@ -13,7 +13,6 @@ from collections.abc import Callable from dataclasses import dataclass from enum import Enum -from typing import Optional, Union from flask import current_app from werkzeug.wrappers import Response as WerkzeugResponse @@ -28,17 +27,17 @@ @dataclass class ACSArgs: session_info: SessionInfo - authn_req: Union[SP_AuthnRequest, RP_AuthnRequest] - proofing_method: Optional[ProofingMethod] = None + authn_req: SP_AuthnRequest | RP_AuthnRequest + proofing_method: ProofingMethod | None = None backdoor: bool = False - user: Optional[User] = None + user: User | None = None @dataclass class ACSResult: - response: Optional[WerkzeugResponse] = None + response: WerkzeugResponse | None = None success: bool = False - message: Optional[TranslatableMsg] = None + message: TranslatableMsg | None = None # This is the list of ACS actions loaded. It is populated by decorating functions with the @acs_action. @@ -69,9 +68,7 @@ def inner(*args, **kwargs) -> ACSResult: return outer -def get_action( - default_action: Optional[Enum], authndata: Union[SP_AuthnRequest, RP_AuthnRequest] -) -> Callable[..., ACSResult]: +def get_action(default_action: Enum | None, authndata: SP_AuthnRequest | RP_AuthnRequest) -> Callable[..., ACSResult]: """ Retrieve an action from the registry based on the AcsAction stored in the session. diff --git a/src/eduid/webapp/common/authn/eduid_saml2.py b/src/eduid/webapp/common/authn/eduid_saml2.py index d664de138..415d8a80f 100644 --- a/src/eduid/webapp/common/authn/eduid_saml2.py +++ b/src/eduid/webapp/common/authn/eduid_saml2.py @@ -2,7 +2,7 @@ import pprint from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Optional, Union +from typing import Any from xml.etree.ElementTree import ParseError from dateutil.parser import parse as dt_parse @@ -35,7 +35,7 @@ class BadSAMLResponse(Exception): """Bad SAML response""" -def get_authn_ctx(session_info: SessionInfo) -> Optional[str]: +def get_authn_ctx(session_info: SessionInfo) -> str | None: """ Get the SAML2 AuthnContext of the currently logged in users session. @@ -60,12 +60,12 @@ def get_authn_request( session: EduidSession, relay_state: str, authn_id: AuthnRequestRef, - selected_idp: Optional[str], + selected_idp: str | None, force_authn: bool = False, - req_authn_ctx: Optional[list] = None, - sign_alg: Optional[str] = None, - digest_alg: Optional[str] = None, - subject: Optional[Subject] = None, + req_authn_ctx: list | None = None, + sign_alg: str | None = None, + digest_alg: str | None = None, + subject: Subject | None = None, ) -> SAMLHttpArgs: logger.debug(f"Authn request args: force_authn={force_authn}") @@ -160,7 +160,7 @@ def get_authn_response( return response, authn_reqref -def authenticate(session_info: SessionInfo, strip_suffix: Optional[str], userdb: UserDB) -> Optional[User]: +def authenticate(session_info: SessionInfo, strip_suffix: str | None, userdb: UserDB) -> User | None: """ Locate a user using the identity found in the SAML assertion. @@ -243,7 +243,7 @@ def saml_logout(sp_config: SPConfig, user: User, location: str) -> WerkzeugRespo @dataclass class AssertionData: session_info: SessionInfo - user: Optional[User] + user: User | None authndata: SP_AuthnRequest authn_req_ref: AuthnRequestRef @@ -257,9 +257,9 @@ def __str__(self) -> str: def process_assertion( form: Mapping[str, Any], sp_data: SPAuthnData, - strip_suffix: Optional[str] = None, + strip_suffix: str | None = None, authenticate_user: bool = True, -) -> Union[AssertionData, WerkzeugResponse]: +) -> AssertionData | WerkzeugResponse: """ Common code for our various SAML SPs (currently authn and eidas) to process a received SAML assertion. diff --git a/src/eduid/webapp/common/authn/middleware.py b/src/eduid/webapp/common/authn/middleware.py index 40ddcefe5..446e4e2ea 100644 --- a/src/eduid/webapp/common/authn/middleware.py +++ b/src/eduid/webapp/common/authn/middleware.py @@ -3,7 +3,7 @@ import re from abc import ABCMeta from collections.abc import Iterable, Mapping -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from flask import Request, current_app @@ -101,7 +101,7 @@ def __call__(self, environ: "WSGIEnvironment", start_response: "StartResponse") return [] def _add_cors_headers( - self, environ: "WSGIEnvironment", headers: Optional[list[tuple[str, str]]] = None + self, environ: "WSGIEnvironment", headers: list[tuple[str, str]] | None = None ) -> list[tuple[str, str]]: if headers is None: headers = [] diff --git a/src/eduid/webapp/common/authn/tests/responses.py b/src/eduid/webapp/common/authn/tests/responses.py index 908627166..1cabf12be 100644 --- a/src/eduid/webapp/common/authn/tests/responses.py +++ b/src/eduid/webapp/common/authn/tests/responses.py @@ -1,11 +1,10 @@ from datetime import timedelta -from typing import Optional from eduid.common.misc.timeutil import utc_now from eduid.common.models.saml2 import EduidAuthnContextClass -def auth_response(session_id: str, eppn: str, accr: Optional[EduidAuthnContextClass] = None) -> str: +def auth_response(session_id: str, eppn: str, accr: EduidAuthnContextClass | None = None) -> str: """Generates a fresh signed authentication response""" timestamp = utc_now() - timedelta(seconds=10) tomorrow = utc_now() + timedelta(days=1) @@ -114,7 +113,7 @@ def logout_response(session_id: str) -> str: return saml_logout_response -def logout_request(session_id: str, idp: Optional[str] = None) -> str: +def logout_request(session_id: str, idp: str | None = None) -> str: """ Create a SAML logout request from a template. diff --git a/src/eduid/webapp/common/authn/utils.py b/src/eduid/webapp/common/authn/utils.py index 01b50f569..6d74f6fbb 100644 --- a/src/eduid/webapp/common/authn/utils.py +++ b/src/eduid/webapp/common/authn/utils.py @@ -3,7 +3,6 @@ import os.path import sys from collections.abc import Sequence -from typing import Optional from saml2 import server from saml2.config import SPConfig @@ -47,7 +46,7 @@ def get_location(http_info: SAMLHttpArgs) -> str: return header_value -def get_saml_attribute(session_info: SessionInfo, attr_name: str) -> Optional[list[str]]: +def get_saml_attribute(session_info: SessionInfo, attr_name: str) -> list[str] | None: """ Get value from a SAML attribute received from the SAML IdP. @@ -113,7 +112,7 @@ def init_pysaml2(cfgfile: str) -> server.Server: def get_authn_for_action( config: FrontendActionMixin, frontend_action: FrontendAction -) -> tuple[Optional[SP_AuthnRequest], AuthnParameters]: +) -> tuple[SP_AuthnRequest | None, AuthnParameters]: authn_params = config.frontend_action_authn_parameters.get(frontend_action) if authn_params is None: raise BadConfiguration(f"No authn parameters for frontend action {frontend_action}") @@ -131,7 +130,7 @@ def validate_authn_for_action( config: FrontendActionMixin, frontend_action: FrontendAction, user: User, - credential_used: Optional[Credential] = None, + credential_used: Credential | None = None, ) -> AuthnActionStatus: """ Validate the authentication for the given frontend action. @@ -183,7 +182,7 @@ def validate_authn_for_action( return AuthnActionStatus.OK -def credential_recently_used(credential: Credential, action: Optional[SP_AuthnRequest], max_age: int) -> bool: +def credential_recently_used(credential: Credential, action: SP_AuthnRequest | None, max_age: int) -> bool: logger.debug(f"Checking if credential {credential} has been used in the last {max_age} seconds") if action and credential.key in action.credentials_used: if action.authn_instant is not None: diff --git a/src/eduid/webapp/common/authn/vccs.py b/src/eduid/webapp/common/authn/vccs.py index 96e673008..9afd89446 100644 --- a/src/eduid/webapp/common/authn/vccs.py +++ b/src/eduid/webapp/common/authn/vccs.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, cast +from typing import cast from bson import ObjectId @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -def get_vccs_client(vccs_url: Optional[str]) -> VCCSClient: +def get_vccs_client(vccs_url: str | None) -> VCCSClient: """ Instantiate a VCCS client. @@ -23,8 +23,8 @@ def get_vccs_client(vccs_url: Optional[str]) -> VCCSClient: def check_password( - password: str, user: User, vccs_url: Optional[str] = None, vccs: Optional[VCCSClient] = None -) -> Optional[Password]: + password: str, user: User, vccs_url: str | None = None, vccs: VCCSClient | None = None +) -> Password | None: """ Try to validate a user provided password. @@ -53,8 +53,8 @@ def add_password( new_password: str, application: str, is_generated: bool = False, - vccs_url: Optional[str] = None, - vccs: Optional[VCCSClient] = None, + vccs_url: str | None = None, + vccs: VCCSClient | None = None, ) -> bool: """ :param user: User object @@ -91,8 +91,8 @@ def reset_password( new_password: str, application: str, is_generated: bool = False, - vccs_url: Optional[str] = None, - vccs: Optional[VCCSClient] = None, + vccs_url: str | None = None, + vccs: VCCSClient | None = None, ) -> bool: """ :param user: User object @@ -133,11 +133,11 @@ def change_password( user: User, new_password: str, application: str, - old_password: Optional[str] = None, - old_password_id: Optional[str] = None, + old_password: str | None = None, + old_password_id: str | None = None, is_generated: bool = False, - vccs_url: Optional[str] = None, - vccs: Optional[VCCSClient] = None, + vccs_url: str | None = None, + vccs: VCCSClient | None = None, ) -> bool: """ :param user: User object @@ -198,12 +198,12 @@ def change_password( @deprecated def add_credentials( - old_password: Optional[str], + old_password: str | None, new_password: str, user: User, source: str, - vccs_url: Optional[str] = None, - vccs: Optional[VCCSClient] = None, + vccs_url: str | None = None, + vccs: VCCSClient | None = None, ) -> bool: """ Add a new password to a user. Revokes the old one, if one is given. @@ -271,10 +271,10 @@ def revoke_password( user: User, reason: str, reference: str, - old_password: Optional[Password] = None, - old_password_id: Optional[str] = None, - vccs_url: Optional[str] = None, - vccs: Optional[VCCSClient] = None, + old_password: Password | None = None, + old_password_id: str | None = None, + vccs_url: str | None = None, + vccs: VCCSClient | None = None, ) -> bool: if vccs is None: vccs = get_vccs_client(vccs_url) @@ -304,7 +304,7 @@ def revoke_password( def revoke_passwords( - user: User, reason: str, application: str, vccs_url: Optional[str] = None, vccs: Optional[VCCSClient] = None + user: User, reason: str, application: str, vccs_url: str | None = None, vccs: VCCSClient | None = None ) -> bool: """ :param user: User object @@ -340,7 +340,7 @@ def revoke_passwords( @deprecated def revoke_all_credentials( - user, source="dashboard", vccs_url: Optional[str] = None, vccs: Optional[VCCSClient] = None + user, source="dashboard", vccs_url: str | None = None, vccs: VCCSClient | None = None ) -> None: if vccs is None: vccs = get_vccs_client(vccs_url) diff --git a/src/eduid/webapp/common/proofing/base.py b/src/eduid/webapp/common/proofing/base.py index 576dc7aef..94c1b754b 100644 --- a/src/eduid/webapp/common/proofing/base.py +++ b/src/eduid/webapp/common/proofing/base.py @@ -1,6 +1,6 @@ from abc import ABC from dataclasses import dataclass -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar from eduid.common.config.base import ProofingConfigMixin from eduid.common.rpc.exceptions import AmTaskFailed @@ -20,26 +20,26 @@ @dataclass class MatchResult: - error: Optional[TranslatableMsg] = None + error: TranslatableMsg | None = None matched: bool = False - credential_used: Optional[ElementKey] = None + credential_used: ElementKey | None = None @dataclass class VerifyUserResult: - user: Optional[User] = None - error: Optional[TranslatableMsg] = None + user: User | None = None + error: TranslatableMsg | None = None @dataclass class VerifyCredentialResult(VerifyUserResult): - credential: Optional[Credential] = None + credential: Credential | None = None @dataclass class ProofingElementResult: - data: Optional[ProofingLogElement] = None - error: Optional[TranslatableMsg] = None + data: ProofingLogElement | None = None + error: TranslatableMsg | None = None @dataclass() @@ -49,13 +49,13 @@ class ProofingFunctions(ABC, Generic[SessionInfoVar]): config: ProofingConfigMixin backdoor: bool - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: raise NotImplementedError("Subclass must implement get_identity") def verify_identity(self, user: User) -> VerifyUserResult: raise NotImplementedError("Subclass must implement verify_identity") - def verify_credential(self, user: User, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def verify_credential(self, user: User, credential: Credential, loa: str | None) -> VerifyCredentialResult: proofing_user = ProofingUser.from_user(user, current_app.private_userdb) mark_result = self.mark_credential_as_verified(credential, loa) @@ -103,5 +103,5 @@ def match_identity(self, user: User, proofing_method: ProofingMethod) -> MatchRe def credential_proofing_element(self, user: User, credential: Credential) -> ProofingElementResult: raise NotImplementedError("Subclass must implement credential_proofing_element") - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: raise NotImplementedError("Subclass must implement mark_credential_as_verified") diff --git a/src/eduid/webapp/common/proofing/methods.py b/src/eduid/webapp/common/proofing/methods.py index 4ffc6ea3e..f2de1e752 100644 --- a/src/eduid/webapp/common/proofing/methods.py +++ b/src/eduid/webapp/common/proofing/methods.py @@ -1,7 +1,6 @@ import logging from abc import ABC from dataclasses import dataclass -from typing import Optional, Union from flask import request from pydantic import ValidationError @@ -22,10 +21,15 @@ @dataclass class SessionInfoParseResult: - error: Optional[TranslatableMsg] = None - info: Optional[ - Union[NinSessionInfo, ForeignEidSessionInfo, SvipeDocumentUserInfo, BankIDSessionInfo, FrejaEIDDocumentUserInfo] - ] = None + error: TranslatableMsg | None = None + info: ( + NinSessionInfo + | ForeignEidSessionInfo + | SvipeDocumentUserInfo + | BankIDSessionInfo + | FrejaEIDDocumentUserInfo + | None + ) = None @dataclass(frozen=True) @@ -37,7 +41,7 @@ class ProofingMethod(ABC): def parse_session_info(self, session_info: SessionInfo, backdoor: bool) -> SessionInfoParseResult: raise NotImplementedError("Subclass must implement parse_session_info") - def formatted_finish_url(self, app_name: str, authn_id: str) -> Optional[str]: + def formatted_finish_url(self, app_name: str, authn_id: str) -> str | None: if not self.finish_url: return None return self.finish_url.format(app_name=app_name, authn_id=authn_id) @@ -143,12 +147,17 @@ def parse_session_info(self, session_info: SessionInfo, backdoor: bool) -> Sessi def get_proofing_method( - method: Optional[str], + method: str | None, frontend_action: FrontendAction, config: ProofingConfigMixin, -) -> Optional[ - Union[ProofingMethodFreja, ProofingMethodEidas, ProofingMethodSvipe, ProofingMethodBankID, ProofingMethodFrejaEID] -]: +) -> ( + ProofingMethodFreja + | ProofingMethodEidas + | ProofingMethodSvipe + | ProofingMethodBankID + | ProofingMethodFrejaEID + | None +): authn_params = config.frontend_action_authn_parameters.get(frontend_action) assert authn_params is not None # please mypy diff --git a/src/eduid/webapp/common/proofing/saml_helpers.py b/src/eduid/webapp/common/proofing/saml_helpers.py index 7a7d4aa12..a62067102 100644 --- a/src/eduid/webapp/common/proofing/saml_helpers.py +++ b/src/eduid/webapp/common/proofing/saml_helpers.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from saml2.metadata import entity_descriptor @@ -32,7 +31,7 @@ def is_required_loa( return False -def authn_ctx_to_loa(session_info: SessionInfo, authentication_context_map: dict[str, str]) -> Optional[str]: +def authn_ctx_to_loa(session_info: SessionInfo, authentication_context_map: dict[str, str]) -> str | None: """Lookup short name (such as 'loa3') for an authentication context class we've received.""" parsed = BaseSessionInfo(**session_info) for k, v in authentication_context_map.items(): @@ -41,9 +40,7 @@ def authn_ctx_to_loa(session_info: SessionInfo, authentication_context_map: dict return None -def authn_context_class_to_loa( - session_info: BaseSessionInfo, authentication_context_map: dict[str, str] -) -> Optional[str]: +def authn_context_class_to_loa(session_info: BaseSessionInfo, authentication_context_map: dict[str, str]) -> str | None: for key, value in authentication_context_map.items(): if value == session_info.authn_context: return key diff --git a/src/eduid/webapp/common/proofing/testing.py b/src/eduid/webapp/common/proofing/testing.py index de3897b08..43cb4f222 100644 --- a/src/eduid/webapp/common/proofing/testing.py +++ b/src/eduid/webapp/common/proofing/testing.py @@ -1,4 +1,4 @@ -from typing import Generic, Optional +from typing import Generic from eduid.common.config.base import EduIDBaseAppConfig, FrontendAction from eduid.userdb.credentials import FidoCredential @@ -18,11 +18,11 @@ def _verify_status( self, finish_url: str, frontend_action: FrontendAction, - frontend_state: Optional[str], + frontend_state: str | None, method: str, - browser: Optional[CSRFTestClient] = None, + browser: CSRFTestClient | None = None, expect_error: bool = False, - expect_msg: Optional[TranslatableMsg] = None, + expect_msg: TranslatableMsg | None = None, ) -> None: if browser is None: assert isinstance(self.browser, CSRFTestClient) @@ -57,15 +57,15 @@ def _verify_status( def _verify_user_parameters( self, eppn: str, - identity: Optional[IdentityElement] = None, + identity: IdentityElement | None = None, identity_present: bool = True, identity_verified: bool = False, - locked_identity: Optional[IdentityElement] = None, + locked_identity: IdentityElement | None = None, num_mfa_tokens: int = 1, num_proofings: int = 0, token_verified: bool = False, - proofing_method: Optional[IdentityProofingMethod] = None, - proofing_version: Optional[str] = None, + proofing_method: IdentityProofingMethod | None = None, + proofing_version: str | None = None, ): """This function is used to verify a user's parameters at the start of a test case, and then again at the end to ensure the right set of changes occurred to the user in the database. diff --git a/src/eduid/webapp/common/session/eduid_session.py b/src/eduid/webapp/common/session/eduid_session.py index 373845bfe..6f635325f 100644 --- a/src/eduid/webapp/common/session/eduid_session.py +++ b/src/eduid/webapp/common/session/eduid_session.py @@ -6,7 +6,7 @@ import pprint from collections.abc import MutableMapping from datetime import datetime -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from flask import Request as FlaskRequest from flask import Response as FlaskResponse @@ -50,18 +50,18 @@ class EduidNamespaces(BaseModel): - common: Optional[Common] = None - mfa_action: Optional[MfaAction] = None - signup: Optional[Signup] = None - phone: Optional[Phone] = None - reset_password: Optional[ResetPasswordNS] = None - security: Optional[SecurityNS] = None - idp: Optional[IdP_Namespace] = None - eidas: Optional[EidasNamespace] = None - authn: Optional[AuthnNamespace] = None - svipe_id: Optional[SvipeIDNamespace] = None - bankid: Optional[BankIDNamespace] = None - freja_eid: Optional[FrejaEIDNamespace] = None + common: Common | None = None + mfa_action: MfaAction | None = None + signup: Signup | None = None + phone: Phone | None = None + reset_password: ResetPasswordNS | None = None + security: SecurityNS | None = None + idp: IdP_Namespace | None = None + eidas: EidasNamespace | None = None + authn: AuthnNamespace | None = None + svipe_id: SvipeIDNamespace | None = None + bankid: BankIDNamespace | None = None + freja_eid: FrejaEIDNamespace | None = None class EduidSession(SessionMixin, MutableMapping[str, Any]): @@ -321,7 +321,7 @@ def set_cookie(self, response: WerkzeugResponse): def new_csrf_token(self) -> str: # only produce one csrf token by request - _csrf: Optional[str] = self.get("_csrft_") + _csrf: str | None = self.get("_csrft_") if not isinstance(_csrf, str): _csrf = os.urandom(20).hex() self["_csrft_"] = _csrf diff --git a/src/eduid/webapp/common/session/logindata.py b/src/eduid/webapp/common/session/logindata.py index 59664c080..88e229df0 100644 --- a/src/eduid/webapp/common/session/logindata.py +++ b/src/eduid/webapp/common/session/logindata.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Optional from pydantic import BaseModel @@ -14,4 +13,4 @@ class ExternalMfaData(BaseModel): issuer: str authn_context: str timestamp: datetime - credential_id: Optional[ElementKey] = None + credential_id: ElementKey | None = None diff --git a/src/eduid/webapp/common/session/namespaces.py b/src/eduid/webapp/common/session/namespaces.py index dae114ca8..99770f1ec 100644 --- a/src/eduid/webapp/common/session/namespaces.py +++ b/src/eduid/webapp/common/session/namespaces.py @@ -6,7 +6,7 @@ from copy import deepcopy from datetime import datetime from enum import Enum, unique -from typing import Any, NewType, Optional, TypeVar, Union, cast +from typing import Any, NewType, TypeVar, cast from uuid import uuid4 from fido2.webauthn import AuthenticatorAttachment @@ -71,10 +71,10 @@ class LoginApplication(str, Enum): class Common(SessionNSBase): - eppn: Optional[str] = None + eppn: str | None = None is_logged_in: bool = False - login_source: Optional[LoginApplication] = None - preferred_language: Optional[str] = None + login_source: LoginApplication | None = None + preferred_language: str | None = None WebauthnState = NewType("WebauthnState", dict[str, Any]) @@ -82,17 +82,17 @@ class Common(SessionNSBase): class MfaAction(SessionNSBase): success: bool = False - login_ref: Optional[str] = None - authn_req_ref: Optional[AuthnRequestRef] = None - credential_used: Optional[ElementKey] = None + login_ref: str | None = None + authn_req_ref: AuthnRequestRef | None = None + credential_used: ElementKey | None = None # Third-party MFA parameters - framework: Optional[TrustFramework] = None + framework: TrustFramework | None = None required_loa: list[str] = Field(default_factory=list) - issuer: Optional[str] = None - authn_instant: Optional[str] = None - authn_context: Optional[str] = None + issuer: str | None = None + authn_instant: str | None = None + authn_context: str | None = None # Webauthn MFA parameters - webauthn_state: Optional[WebauthnState] = None + webauthn_state: WebauthnState | None = None class TimestampedNS(SessionNSBase): @@ -105,14 +105,14 @@ class TimestampedNS(SessionNSBase): class ResetPasswordNS(SessionNSBase): - generated_password_hash: Optional[str] = None + generated_password_hash: str | None = None # XXX the keys below are not in use yet. They are set in eduid.webapp.common, # in a way that the security app understands. Once the (reset|change) # password views are removed from the security app, we will be able to # start using them. The session key reauthn-for-chpass is in the same # situation. - extrasec_u2f_challenge: Optional[str] = None - extrasec_webauthn_state: Optional[str] = None + extrasec_u2f_challenge: str | None = None + extrasec_webauthn_state: str | None = None class WebauthnRegistration(SessionNSBase): @@ -123,48 +123,48 @@ class WebauthnRegistration(SessionNSBase): class SecurityNS(SessionNSBase): # used for new change_password - generated_password_hash: Optional[str] = None + generated_password_hash: str | None = None # used for update user data from official source - user_requested_update: Optional[datetime] = None - webauthn_registration: Optional[WebauthnRegistration] = None + user_requested_update: datetime | None = None + webauthn_registration: WebauthnRegistration | None = None class Name(SessionNSBase): - given_name: Optional[str] = None - surname: Optional[str] = None + given_name: str | None = None + surname: str | None = None class EmailVerification(SessionNSBase): completed: bool = False - address: Optional[str] = None - verification_code: Optional[str] = None + address: str | None = None + verification_code: str | None = None bad_attempts: int = 0 - sent_at: Optional[datetime] = None - reference: Optional[str] = None + sent_at: datetime | None = None + reference: str | None = None class Invite(SessionNSBase): completed: bool = False initiated_signup: bool = False - invite_code: Optional[str] = None - finish_url: Optional[str] = None + invite_code: str | None = None + finish_url: str | None = None class Tou(SessionNSBase): completed: bool = False - version: Optional[str] = None + version: str | None = None class Captcha(SessionNSBase): completed: bool = False - internal_answer: Optional[str] = None + internal_answer: str | None = None bad_attempts: int = 0 class Credentials(SessionNSBase): completed: bool = False - generated_password: Optional[str] = None - webauthn: Optional[Any] = None # TODO: implement webauthn signup + generated_password: str | None = None + webauthn: Any | None = None # TODO: implement webauthn signup class Signup(TimestampedNS): @@ -203,11 +203,9 @@ def key(self) -> ElementKey: class IdP_PendingRequest(BaseModel, ABC): - aborted: Optional[bool] = False - used: Optional[bool] = False # set to True after the request has been completed (to handle 'back' button presses) - template_show_msg: Optional[str] = ( - None # set when the template version of the idp should show a message to the user - ) + aborted: bool | None = False + used: bool | None = False # set to True after the request has been completed (to handle 'back' button presses) + template_show_msg: str | None = None # set when the template version of the idp should show a message to the user # Credentials used while authenticating _this SAML request_. Not ones inherited from SSO. credentials_used: dict[ElementKey, datetime] = Field(default_factory=dict) onetime_credentials: dict[ElementKey, OnetimeCredential] = Field(default_factory=dict) @@ -216,26 +214,26 @@ class IdP_PendingRequest(BaseModel, ABC): class IdP_SAMLPendingRequest(IdP_PendingRequest): request: str binding: str - relay_state: Optional[str] = None + relay_state: str | None = None # a pointer to an ongoing request to login using another device - other_device_state_id: Optional[OtherDeviceId] = None + other_device_state_id: OtherDeviceId | None = None class IdP_OtherDevicePendingRequest(IdP_PendingRequest): - state_id: Optional[OtherDeviceId] = None # can be None on aborted/expired requests + state_id: OtherDeviceId | None = None # can be None on aborted/expired requests -IdP_PendingRequestSubclass = Union[IdP_SAMLPendingRequest, IdP_OtherDevicePendingRequest] +IdP_PendingRequestSubclass = IdP_SAMLPendingRequest | IdP_OtherDevicePendingRequest class IdP_Namespace(TimestampedNS): # The SSO cookie value last set by the IdP. Used to debug issues with browsers not # honoring Set-Cookie in redirects, or something. - sso_cookie_val: Optional[str] = None + sso_cookie_val: str | None = None pending_requests: dict[RequestRef, IdP_PendingRequestSubclass] = Field(default={}) def log_credential_used( - self, request_ref: RequestRef, credential: Union[Credential, OnetimeCredential], timestamp: datetime + self, request_ref: RequestRef, credential: Credential | OnetimeCredential, timestamp: datetime ) -> None: """Log the credential used in the session, under this particular SAML request""" if isinstance(credential, OnetimeCredential): @@ -245,15 +243,13 @@ def log_credential_used( class BaseAuthnRequest(BaseModel, ABC): frontend_action: FrontendAction # what action frontend is performing - frontend_state: Optional[str] = None # opaque data from frontend, returned in /status - method: Optional[str] = None # proofing method that frontend is invoking - post_authn_action: Optional[ - Union[AuthnAcsAction, EidasAcsAction, SvipeIDAction, BankIDAcsAction, FrejaEIDAction] - ] = None + frontend_state: str | None = None # opaque data from frontend, returned in /status + method: str | None = None # proofing method that frontend is invoking + post_authn_action: AuthnAcsAction | EidasAcsAction | SvipeIDAction | BankIDAcsAction | FrejaEIDAction | None = None created_ts: datetime = Field(default_factory=utc_now) - authn_instant: Optional[datetime] = None - status: Optional[str] = None # populated by the SAML2 ACS/OIDC callback action - error: Optional[bool] = None + authn_instant: datetime | None = None + status: str | None = None # populated by the SAML2 ACS/OIDC callback action + error: bool | None = None finish_url: str # the URL to redirect to after authentication is complete consumed: bool = False # an operation that requires a new authentication has used this one already @@ -262,11 +258,11 @@ class SP_AuthnRequest(BaseAuthnRequest): authn_id: AuthnRequestRef = Field(default_factory=lambda: AuthnRequestRef(uuid4_str())) credentials_used: list[ElementKey] = Field(default_factory=list) # proofing_credential_id is the credential being person-proofed, when doing that - proofing_credential_id: Optional[ElementKey] = None + proofing_credential_id: ElementKey | None = None req_authn_ctx: list[str] = Field( default_factory=list ) # the authentication contexts requested for this authentication - asserted_authn_ctx: Optional[str] = None # the authentication contexts asserted for this authentication + asserted_authn_ctx: str | None = None # the authentication contexts asserted for this authentication def formatted_finish_url(self, app_name: str) -> str: return self.finish_url.format(app_name=app_name, authn_id=self.authn_id) @@ -294,14 +290,14 @@ def authns_cleanup( return v def _remove_get_authn_for_action( - self, action: Union[AuthnAcsAction, EidasAcsAction, BankIDAcsAction] - ) -> Optional[SP_AuthnRequest]: + self, action: AuthnAcsAction | EidasAcsAction | BankIDAcsAction + ) -> SP_AuthnRequest | None: for authn in self.authns.values(): if authn.post_authn_action == action: return authn return None - def get_authn_for_frontend_action(self, action: FrontendAction) -> Optional[SP_AuthnRequest]: + def get_authn_for_frontend_action(self, action: FrontendAction) -> SP_AuthnRequest | None: for authn in self.authns.values(): if authn.frontend_action == action: return authn @@ -314,8 +310,8 @@ class EidasNamespace(SessionNSBase): class AuthnNamespace(SessionNSBase): sp: SPAuthnData = Field(default=SPAuthnData()) - name_id: Optional[str] = None # SAML NameID, used in logout - next: Optional[str] = None + name_id: str | None = None # SAML NameID, used in logout + next: str | None = None class RP_AuthnRequest(BaseAuthnRequest): diff --git a/src/eduid/webapp/common/session/redis_session.py b/src/eduid/webapp/common/session/redis_session.py index ee02403d0..5cf2ec2b5 100644 --- a/src/eduid/webapp/common/session/redis_session.py +++ b/src/eduid/webapp/common/session/redis_session.py @@ -49,7 +49,7 @@ import logging import typing from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any import nacl.encoding import nacl.secret @@ -75,7 +75,7 @@ def __init__( redis_config: RedisConfig, app_secret: str, ttl: int = 600, - whitelist: Optional[list[str]] = None, + whitelist: list[str] | None = None, raise_on_unknown: bool = False, ): """ @@ -133,7 +133,7 @@ def get_session(self, meta: SessionMeta, new: bool) -> RedisEncryptedSession: return res -def get_redis_pool(cfg: RedisConfig) -> Union[sentinel.SentinelConnectionPool, redis.ConnectionPool]: +def get_redis_pool(cfg: RedisConfig) -> sentinel.SentinelConnectionPool | redis.ConnectionPool: logger.debug(f"Redis configuration: {cfg}") if cfg.sentinel_hosts and cfg.sentinel_service_name: host_port = [(x, cfg.port) for x in cfg.sentinel_hosts] @@ -166,7 +166,7 @@ def __init__( db_key: str, encryption_key: bytes, ttl: int, - whitelist: Optional[list[str]] = None, + whitelist: list[str] | None = None, raise_on_unknown: bool = False, ): """ @@ -196,7 +196,7 @@ def __init__( self.whitelist = whitelist self.raise_on_unknown = raise_on_unknown # encrypted data loaded from redis, used to avoid clobbering concurrent updates to the session - self._raw_data: Optional[bytes] = None + self._raw_data: bytes | None = None self.secret_box = nacl.secret.SecretBox(encryption_key) @@ -295,7 +295,7 @@ def encrypt_data(self, data_dict: Mapping[str, Any]) -> bytes: } return bytes(json.dumps(versioned), "ascii") - def decrypt_data(self, data_str: Union[bytes, str]) -> dict[str, Any]: + def decrypt_data(self, data_str: bytes | str) -> dict[str, Any]: """ Decrypt and verify session data read from Redis. diff --git a/src/eduid/webapp/eidas/acs_actions.py b/src/eduid/webapp/eidas/acs_actions.py index d825f5639..f008181e6 100644 --- a/src/eduid/webapp/eidas/acs_actions.py +++ b/src/eduid/webapp/eidas/acs_actions.py @@ -1,5 +1,3 @@ -from typing import Optional - from eduid.userdb import User from eduid.userdb.credentials.fido import FidoCredential from eduid.webapp.common.api.decorators import require_user @@ -19,7 +17,7 @@ __author__ = "lundberg" -def common_saml_checks(args: ACSArgs) -> Optional[ACSResult]: +def common_saml_checks(args: ACSArgs) -> ACSResult | None: """ Check that the user is authenticated and that the session info is valid. """ diff --git a/src/eduid/webapp/eidas/app.py b/src/eduid/webapp/eidas/app.py index 331766c5a..6bbe23fc0 100644 --- a/src/eduid/webapp/eidas/app.py +++ b/src/eduid/webapp/eidas/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -33,7 +33,7 @@ def __init__(self, config: EidasConfig, **kwargs: Any): current_eidas_app: EidasApp = cast(EidasApp, current_app) -def init_eidas_app(name: str = "eidas", test_config: Optional[Mapping[str, Any]] = None) -> EidasApp: +def init_eidas_app(name: str = "eidas", test_config: Mapping[str, Any] | None = None) -> EidasApp: """ Create an instance of an eidas app. diff --git a/src/eduid/webapp/eidas/helpers.py b/src/eduid/webapp/eidas/helpers.py index c74bb62c4..91d9af7f9 100644 --- a/src/eduid/webapp/eidas/helpers.py +++ b/src/eduid/webapp/eidas/helpers.py @@ -1,7 +1,7 @@ import logging from dataclasses import dataclass from enum import unique -from typing import Any, Optional +from typing import Any from saml2 import BINDING_HTTP_REDIRECT from saml2.client import Saml2Client @@ -118,13 +118,13 @@ def attribute_remap(session_info: SessionInfo) -> SessionInfo: @dataclass() class CredentialVerifyResult: verified_ok: bool - message: Optional[EidasMsg] = None - credential_description: Optional[str] = None + message: EidasMsg | None = None + credential_description: str | None = None def check_reauthn( - frontend_action: FrontendAction, user: User, credential_used: Optional[Credential] = None -) -> Optional[AuthnActionStatus]: + frontend_action: FrontendAction, user: User, credential_used: Credential | None = None +) -> AuthnActionStatus | None: """Check if a re-authentication has been performed recently enough for this action""" authn_status = validate_authn_for_action( diff --git a/src/eduid/webapp/eidas/proofing.py b/src/eduid/webapp/eidas/proofing.py index cd9d47d96..f1dbcfd23 100644 --- a/src/eduid/webapp/eidas/proofing.py +++ b/src/eduid/webapp/eidas/proofing.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar from eduid.common.config.base import ProofingConfigMixin from eduid.common.rpc.exceptions import AmTaskFailed @@ -46,7 +46,7 @@ @dataclass class SwedenConnectProofingFunctions(ProofingFunctions[BaseSessionInfoVar], Generic[BaseSessionInfoVar]): - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: raise NotImplementedError("Subclass must implement get_identity") def verify_identity(self, user: User) -> VerifyUserResult: @@ -58,7 +58,7 @@ def match_identity(self, user: User, proofing_method: ProofingMethod) -> MatchRe def credential_proofing_element(self, user: User, credential: Credential) -> ProofingElementResult: raise NotImplementedError("Subclass must implement credential_proofing_element") - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: raise NotImplementedError("Subclass must implement mark_credential_as_verified") def _match_identity_for_mfa( @@ -123,7 +123,7 @@ def _match_identity_for_mfa( @dataclass class FrejaProofingFunctions(SwedenConnectProofingFunctions[NinSessionInfo]): - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: return user.identities.nin def verify_identity(self, user: User) -> VerifyUserResult: @@ -161,8 +161,8 @@ def verify_identity(self, user: User) -> VerifyUserResult: return VerifyUserResult(user=ProofingUser.from_user(_user, current_app.private_userdb)) def identity_proofing_element(self, user: User) -> ProofingElementResult: - issuer: Optional[str] - authn_context: Optional[str] + issuer: str | None + authn_context: str | None if self.backdoor: proofing_version = "1999v1" @@ -192,8 +192,8 @@ def identity_proofing_element(self, user: User) -> ProofingElementResult: return ProofingElementResult(data=data) def credential_proofing_element(self, user: User, credential: Credential) -> ProofingElementResult: - issuer: Optional[str] - authn_context: Optional[str] + issuer: str | None + authn_context: str | None if self.backdoor: proofing_version = "1999v1" @@ -230,7 +230,7 @@ def match_identity(self, user: User, proofing_method: ProofingMethod) -> MatchRe proofing_method=proofing_method, ) - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: if loa != "loa3": return VerifyCredentialResult(error=EidasMsg.authn_context_mismatch) @@ -243,7 +243,7 @@ def mark_credential_as_verified(self, credential: Credential, loa: Optional[str] @dataclass() class EidasProofingFunctions(SwedenConnectProofingFunctions[ForeignEidSessionInfo]): - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: return user.identities.eidas def verify_identity(self, user: User) -> VerifyUserResult: @@ -379,7 +379,7 @@ def _can_replace_identity(self, proofing_user: ProofingUser) -> bool: return True return False - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: if loa not in ["eidas-nf-low", "eidas-nf-sub", "eidas-nf-high"]: return VerifyCredentialResult(error=EidasMsg.authn_context_mismatch) @@ -390,9 +390,7 @@ def mark_credential_as_verified(self, credential: Credential, loa: Optional[str] return VerifyCredentialResult(credential=credential) -def _find_or_add_credential( - user: User, framework: Optional[TrustFramework], required_loa: list[str] -) -> Optional[ElementKey]: +def _find_or_add_credential(user: User, framework: TrustFramework | None, required_loa: list[str]) -> ElementKey | None: if not required_loa: # mainly keep mypy calm current_app.logger.debug("Not recording credential used without required_loa") diff --git a/src/eduid/webapp/eidas/saml_session_info.py b/src/eduid/webapp/eidas/saml_session_info.py index 496e3145e..36c9fc731 100644 --- a/src/eduid/webapp/eidas/saml_session_info.py +++ b/src/eduid/webapp/eidas/saml_session_info.py @@ -1,6 +1,5 @@ import logging from datetime import date -from typing import Optional from pydantic import Field @@ -17,7 +16,7 @@ class NinAttributes(SAMLAttributes): nin: str = Field(alias="personalIdentityNumber") given_name: str = Field(alias="givenName") surname: str = Field(alias="sn") - display_name: Optional[str] = Field(default=None, alias="displayName") + display_name: str | None = Field(default=None, alias="displayName") date_of_birth: date = Field(alias="dateOfBirth") @@ -42,8 +41,8 @@ class ForeignEidAttributes(SAMLAttributes): transaction_identifier: str = Field(alias="transactionIdentifier") # there are plans for a service in the future that provides the attributes below for # swedish citizens living abroad with eID from the country of residence - mapped_personal_identity_number: Optional[str] = Field(alias="mappedPersonalIdentityNumber", default=None) - personal_identity_number_binding: Optional[str] = Field(alias="personalIdentityNumberBinding", default=None) + mapped_personal_identity_number: str | None = Field(alias="mappedPersonalIdentityNumber", default=None) + personal_identity_number_binding: str | None = Field(alias="personalIdentityNumberBinding", default=None) class NinSessionInfo(BaseSessionInfo): diff --git a/src/eduid/webapp/eidas/settings/common.py b/src/eduid/webapp/eidas/settings/common.py index 84d558bbe..3c6e20919 100644 --- a/src/eduid/webapp/eidas/settings/common.py +++ b/src/eduid/webapp/eidas/settings/common.py @@ -3,7 +3,6 @@ """ from collections.abc import Mapping -from typing import Optional from pydantic import Field @@ -57,5 +56,5 @@ class EidasConfig( } ) # magic cookie IdP is used for integration tests when magic cookie is set - magic_cookie_idp: Optional[str] = None - magic_cookie_foreign_id_idp: Optional[str] = None + magic_cookie_idp: str | None = None + magic_cookie_foreign_id_idp: str | None = None diff --git a/src/eduid/webapp/eidas/tests/test_app.py b/src/eduid/webapp/eidas/tests/test_app.py index e707718d4..ec9f23836 100644 --- a/src/eduid/webapp/eidas/tests/test_app.py +++ b/src/eduid/webapp/eidas/tests/test_app.py @@ -3,7 +3,7 @@ import logging import os from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any from unittest import TestCase from unittest.mock import MagicMock, patch @@ -229,9 +229,9 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: ) return config - def add_security_key_to_user(self, eppn: str, credential_id: str, token_type: str) -> Union[U2F, Webauthn]: + def add_security_key_to_user(self, eppn: str, credential_id: str, token_type: str) -> U2F | Webauthn: user = self.app.central_userdb.get_user_by_eppn(eppn) - mfa_token: Union[U2F, Webauthn] + mfa_token: U2F | Webauthn if token_type == "u2f": mfa_token = U2F( version="test", @@ -275,9 +275,9 @@ def generate_auth_response( request_id: str, saml_response_tpl: str, asserted_identity: str, - date_of_birth: Optional[datetime.datetime] = None, + date_of_birth: datetime.datetime | None = None, age: int = 10, - credentials_used: Optional[list[ElementKey]] = None, + credentials_used: list[ElementKey] | None = None, ) -> bytes: """ Generates a fresh signed authentication response @@ -366,14 +366,14 @@ def reauthn( expect_msg: TranslatableMsg, frontend_action: FrontendAction, age: int = 10, - browser: Optional[CSRFTestClient] = None, - eppn: Optional[str] = None, + browser: CSRFTestClient | None = None, + eppn: str | None = None, expect_error: bool = False, - identity: Optional[Union[NinIdentity, EIDASIdentity]] = None, + identity: NinIdentity | EIDASIdentity | None = None, logged_in: bool = True, method: str = "freja", - next_url: Optional[str] = None, - response_template: Optional[str] = None, + next_url: str | None = None, + response_template: str | None = None, ) -> None: return self._call_endpoint_and_saml_acs( age=age, @@ -396,15 +396,15 @@ def verify_token( expect_msg: TranslatableMsg, frontend_action: FrontendAction, age: int = 10, - browser: Optional[CSRFTestClient] = None, - credentials_used: Optional[list[ElementKey]] = None, - eppn: Optional[str] = None, + browser: CSRFTestClient | None = None, + credentials_used: list[ElementKey] | None = None, + eppn: str | None = None, expect_error: bool = False, expect_saml_error: bool = False, - identity: Optional[Union[NinIdentity, EIDASIdentity]] = None, + identity: NinIdentity | EIDASIdentity | None = None, method: str = "freja", - response_template: Optional[str] = None, - verify_credential: Optional[ElementKey] = None, + response_template: str | None = None, + verify_credential: ElementKey | None = None, ) -> None: return self._call_endpoint_and_saml_acs( age=age, @@ -429,8 +429,8 @@ def _get_authn_redirect_url( method: str, frontend_action: FrontendAction, expect_success: bool = True, - verify_credential: Optional[ElementKey] = None, - frontend_state: Optional[str] = None, + verify_credential: ElementKey | None = None, + frontend_state: str | None = None, ) -> str: with browser.session_transaction() as sess: csrf_token = sess.get_csrf_token() @@ -462,20 +462,20 @@ def _call_endpoint_and_saml_acs( self, endpoint: str, method: str, - eppn: Optional[str], + eppn: str | None, expect_msg: TranslatableMsg, frontend_action: FrontendAction, age: int = 10, - browser: Optional[CSRFTestClient] = None, - credentials_used: Optional[list[ElementKey]] = None, + browser: CSRFTestClient | None = None, + credentials_used: list[ElementKey] | None = None, expect_error: bool = False, expect_saml_error: bool = False, - identity: Optional[Union[NinIdentity, EIDASIdentity]] = None, + identity: NinIdentity | EIDASIdentity | None = None, logged_in: bool = True, - next_url: Optional[str] = None, - response_template: Optional[str] = None, - verify_credential: Optional[ElementKey] = None, - frontend_state: Optional[str] = "This is a unit test", + next_url: str | None = None, + response_template: str | None = None, + verify_credential: ElementKey | None = None, + frontend_state: str | None = "This is a unit test", ) -> None: if eppn is None: eppn = self.test_user_eppn diff --git a/src/eduid/webapp/eidas/views.py b/src/eduid/webapp/eidas/views.py index dadecf6fb..c0b9d7dcf 100644 --- a/src/eduid/webapp/eidas/views.py +++ b/src/eduid/webapp/eidas/views.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from flask import Blueprint, make_response, redirect, request from saml2.request import AuthnRequest @@ -79,7 +78,7 @@ def get_status(authn_id: AuthnRequestRef) -> FluxData: @MarshalWith(EidasCommonResponseSchema) @require_user def verify_credential( - user: User, method: str, credential_id: ElementKey, frontend_action: str, frontend_state: Optional[str] = None + user: User, method: str, credential_id: ElementKey, frontend_action: str, frontend_state: str | None = None ) -> FluxData: current_app.logger.debug(f"verify-credential called with credential_id: {credential_id}") @@ -122,7 +121,7 @@ def verify_credential( @UnmarshalWith(EidasCommonRequestSchema) @MarshalWith(EidasCommonResponseSchema) @require_user -def verify_identity(user: User, method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def verify_identity(user: User, method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: current_app.logger.debug(f"verify-identity called for method {method}") result = _authn( @@ -141,7 +140,7 @@ def verify_identity(user: User, method: str, frontend_action: str, frontend_stat @eidas_views.route("/mfa-authenticate", methods=["POST"]) @UnmarshalWith(EidasCommonRequestSchema) @MarshalWith(EidasCommonResponseSchema) -def mfa_authentication(method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def mfa_authentication(method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: current_app.logger.debug("mfa-authenticate called") result = _authn( @@ -159,18 +158,18 @@ def mfa_authentication(method: str, frontend_action: str, frontend_state: Option @dataclass class AuthnResult: - authn_req: Optional[AuthnRequest] = None - authn_id: Optional[AuthnRequestRef] = None - error: Optional[TranslatableMsg] = None - url: Optional[str] = None + authn_req: AuthnRequest | None = None + authn_id: AuthnRequestRef | None = None + error: TranslatableMsg | None = None + url: str | None = None def _authn( action: EidasAcsAction, method: str, frontend_action: str, - frontend_state: Optional[str] = None, - proofing_credential_id: Optional[ElementKey] = None, + frontend_state: str | None = None, + proofing_credential_id: ElementKey | None = None, ) -> AuthnResult: current_app.logger.debug(f"Requested method: {method}, frontend action: {frontend_action}") try: diff --git a/src/eduid/webapp/email/app.py b/src/eduid/webapp/email/app.py index b8e6faaf6..8fe00ee26 100644 --- a/src/eduid/webapp/email/app.py +++ b/src/eduid/webapp/email/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -30,7 +30,7 @@ def __init__(self, config: EmailConfig, **kwargs: Any): current_email_app: EmailApp = cast(EmailApp, current_app) -def email_init_app(name: str = "email", test_config: Optional[Mapping[str, Any]] = None) -> EmailApp: +def email_init_app(name: str = "email", test_config: Mapping[str, Any] | None = None) -> EmailApp: """ Create an instance of an eduid email app. diff --git a/src/eduid/webapp/email/tests/test_app.py b/src/eduid/webapp/email/tests/test_app.py index 8d07956ad..123f911b8 100644 --- a/src/eduid/webapp/email/tests/test_app.py +++ b/src/eduid/webapp/email/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import patch from eduid.common.config.base import EduidEnvironment @@ -76,7 +76,7 @@ def _post_email( self, mock_code_verification: Any, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, send_data: bool = True, ): """ @@ -110,7 +110,7 @@ def _post_email( return client.post("/new") @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _post_primary(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = None): + def _post_primary(self, mock_request_user_sync: Any, data1: dict[str, Any] | None = None): """ Choose an email of the test user as primary @@ -135,7 +135,7 @@ def _post_primary(self, mock_request_user_sync: Any, data1: Optional[dict[str, A return client.post("/primary", data=json.dumps(data), content_type=self.content_type_json) @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _remove(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = None): + def _remove(self, mock_request_user_sync: Any, data1: dict[str, Any] | None = None): """ POST to remove an email address form the test user @@ -158,7 +158,7 @@ def _remove(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = return client.post("/remove", data=json.dumps(data), content_type=self.content_type_json) @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _resend_code(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = None): + def _resend_code(self, mock_request_user_sync: Any, data1: dict[str, Any] | None = None): """ Trigger resending a new verification code to the email being verified @@ -183,8 +183,8 @@ def _verify( self, mock_code_verification: Any, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, ): """ POST a new email address for the test user, and then verify it. @@ -231,10 +231,10 @@ def _get_code_backdoor( self, mock_code_verification: Any, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, email: str = "johnsmith3@example.com", code: str = "123456", - magic_cookie_name: Optional[str] = None, + magic_cookie_name: str | None = None, ): """ POST email data to generate a verification state, diff --git a/src/eduid/webapp/freja_eid/app.py b/src/eduid/webapp/freja_eid/app.py index cfacadb96..3514f735c 100644 --- a/src/eduid/webapp/freja_eid/app.py +++ b/src/eduid/webapp/freja_eid/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from authlib.integrations.flask_client import OAuth from flask import current_app @@ -49,7 +49,7 @@ def __init__(self, config: FrejaEIDConfig, **kwargs): current_freja_eid_app = cast(FrejaEIDApp, current_app) -def freja_eid_init_app(name: str = "freja_eid", test_config: Optional[Mapping[str, Any]] = None) -> FrejaEIDApp: +def freja_eid_init_app(name: str = "freja_eid", test_config: Mapping[str, Any] | None = None) -> FrejaEIDApp: """ :param name: The name of the instance, it will affect the configuration loaded. :param test_config: Override config. Used in test cases. diff --git a/src/eduid/webapp/freja_eid/helpers.py b/src/eduid/webapp/freja_eid/helpers.py index be2738b6c..adede6d61 100644 --- a/src/eduid/webapp/freja_eid/helpers.py +++ b/src/eduid/webapp/freja_eid/helpers.py @@ -1,7 +1,7 @@ import logging from datetime import date from enum import Enum, unique -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict, Field @@ -42,7 +42,7 @@ def get(key: str) -> Any: return session.freja_eid.rp.authlib_cache.get(key) @staticmethod - def set(key: str, value: Any, expires: Optional[int] = None) -> None: + def set(key: str, value: Any, expires: int | None = None) -> None: session.freja_eid.rp.authlib_cache[key] = value logger.debug(f"Set {key}={value} (expires={expires}) in session.freja_eid.oauth_cache") @@ -84,7 +84,7 @@ class FrejaEIDDocumentUserInfo(UserInfoBase): family_name: str given_name: str name: str - personal_identity_number: Optional[str] = Field( + personal_identity_number: str | None = Field( alias="https:/frejaeid.com/oidc/claims/personalIdentityNumber", default=None ) date_of_birth: date = Field(alias="birthdate") diff --git a/src/eduid/webapp/freja_eid/proofing.py b/src/eduid/webapp/freja_eid/proofing.py index 2ee9dce72..9402ad679 100644 --- a/src/eduid/webapp/freja_eid/proofing.py +++ b/src/eduid/webapp/freja_eid/proofing.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from datetime import datetime -from typing import Optional from iso3166 import countries from pymongo.errors import PyMongoError @@ -50,7 +49,7 @@ def is_swedish_document(self) -> bool: return True return False - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: if self.is_swedish_document(): return user.identities.nin return user.identities.freja @@ -230,7 +229,7 @@ def match_identity(self, user: User, proofing_method: ProofingMethod) -> MatchRe def credential_proofing_element(self, user: User, credential: Credential) -> ProofingElementResult: raise NotImplementedError("No support for credential proofing") - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: raise NotImplementedError("No support for credential proofing") diff --git a/src/eduid/webapp/freja_eid/settings/common.py b/src/eduid/webapp/freja_eid/settings/common.py index 77af9ad8e..bcbc9c2de 100644 --- a/src/eduid/webapp/freja_eid/settings/common.py +++ b/src/eduid/webapp/freja_eid/settings/common.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field from eduid.common.clients.oidc_client.base import AuthlibClientConfig @@ -29,7 +27,7 @@ class FrejaEIDClientConfig(AuthlibClientConfig): "https://frejaeid.com/oidc/scopes/birthdate", ] ) - claims_request: dict[str, Union[None, dict[str, bool]]] = Field(default={}) + claims_request: dict[str, None | dict[str, bool]] = Field(default={}) class FrejaEIDConfig( diff --git a/src/eduid/webapp/freja_eid/tests/test_app.py b/src/eduid/webapp/freja_eid/tests/test_app.py index 7043cc8d7..39a7f820e 100644 --- a/src/eduid/webapp/freja_eid/tests/test_app.py +++ b/src/eduid/webapp/freja_eid/tests/test_app.py @@ -1,6 +1,6 @@ import json from datetime import date, datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from urllib.parse import parse_qs, urlparse @@ -137,7 +137,7 @@ def _user_setup(self): @staticmethod def get_mock_userinfo( issuing_country: Country, - personal_identity_number: Optional[str] = "123456789", + personal_identity_number: str | None = "123456789", registration_level: FrejaRegistrationLevel = FrejaRegistrationLevel.EXTENDED, birthdate: date = date(year=1901, month=2, day=3), freja_user_id: str = "unique_freja_eid", @@ -145,8 +145,8 @@ def get_mock_userinfo( given_name: str = "Test", family_name: str = "Testsson", now: datetime = utc_now(), - userinfo_expires: Optional[datetime] = None, - document_expires: Optional[datetime] = None, + userinfo_expires: datetime | None = None, + document_expires: datetime | None = None, ) -> FrejaEIDDocumentUserInfo: if userinfo_expires is None: userinfo_expires = now + timedelta(minutes=5) diff --git a/src/eduid/webapp/freja_eid/views.py b/src/eduid/webapp/freja_eid/views.py index 07fa7c0a0..de9f23122 100644 --- a/src/eduid/webapp/freja_eid/views.py +++ b/src/eduid/webapp/freja_eid/views.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from urllib.parse import parse_qs, urlparse from authlib.integrations.base_client import OAuthError @@ -61,7 +60,7 @@ def get_status(authn_id: OIDCState) -> FluxData: @UnmarshalWith(FrejaEIDCommonRequestSchema) @MarshalWith(FrejaEIDCommonResponseSchema) @require_user -def verify_identity(user: User, method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def verify_identity(user: User, method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: res = _authn(FrejaEIDAction.verify_identity, method, frontend_action, frontend_state) if res.error: current_app.logger.error(f"Failed to start verify identity: {res.error}") @@ -71,17 +70,17 @@ def verify_identity(user: User, method: str, frontend_action: str, frontend_stat @dataclass class AuthnResult: - authn_req: Optional[RP_AuthnRequest] = None - authn_id: Optional[OIDCState] = None - error: Optional[TranslatableMsg] = None - url: Optional[str] = None + authn_req: RP_AuthnRequest | None = None + authn_id: OIDCState | None = None + error: TranslatableMsg | None = None + url: str | None = None def _authn( action: FrejaEIDAction, method: str, frontend_action: str, - frontend_state: Optional[str] = None, + frontend_state: str | None = None, ) -> AuthnResult: current_app.logger.debug(f"Requested method: {method}, frontend action: {frontend_action}") @@ -137,7 +136,7 @@ def authn_callback(user) -> WerkzeugResponse: current_app.logger.debug("authn_callback called") current_app.logger.debug(f"request.args: {request.args}") authn_req = None - oidc_state: Optional[OIDCState] = None + oidc_state: OIDCState | None = None if "state" in request.args: oidc_state = OIDCState(request.args["state"]) if oidc_state is not None: diff --git a/src/eduid/webapp/group_management/app.py b/src/eduid/webapp/group_management/app.py index 79c7cab61..8ef1ccdde 100644 --- a/src/eduid/webapp/group_management/app.py +++ b/src/eduid/webapp/group_management/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -45,7 +45,7 @@ def __init__(self, config: GroupManagementConfig, **kwargs): def init_group_management_app( - name: str = "group_management", test_config: Optional[Mapping[str, Any]] = None + name: str = "group_management", test_config: Mapping[str, Any] | None = None ) -> GroupManagementApp: """ :param name: The name of the instance, it will affect the configuration loaded. diff --git a/src/eduid/webapp/group_management/helpers.py b/src/eduid/webapp/group_management/helpers.py index 761115ce5..b3ae9c61b 100644 --- a/src/eduid/webapp/group_management/helpers.py +++ b/src/eduid/webapp/group_management/helpers.py @@ -1,6 +1,6 @@ from dataclasses import asdict, dataclass from enum import unique -from typing import Any, Optional, Union +from typing import Any from uuid import UUID from flask_babel import gettext as _ @@ -44,8 +44,8 @@ class UserGroup: display_name: str is_owner: bool is_member: bool - owners: set[Union[GraphUser, GraphGroup]] - members: set[Union[GraphUser, GraphGroup]] + owners: set[GraphUser | GraphGroup] + members: set[GraphUser | GraphGroup] @classmethod def from_scimapigroup(cls, group: ScimApiGroup, is_owner: bool = False, is_member: bool = False): @@ -59,7 +59,7 @@ def from_scimapigroup(cls, group: ScimApiGroup, is_owner: bool = False, is_membe ) -def get_scim_user_by_eppn(eppn: str) -> Optional[ScimApiUser]: +def get_scim_user_by_eppn(eppn: str) -> ScimApiUser | None: external_id = f"{eppn}@{current_app.conf.scim_external_id_scope}" scim_user = current_app.scimapi_userdb.get_user_by_external_id(external_id=external_id) return scim_user diff --git a/src/eduid/webapp/group_management/settings/common.py b/src/eduid/webapp/group_management/settings/common.py index 0c5859dbf..0082ed254 100644 --- a/src/eduid/webapp/group_management/settings/common.py +++ b/src/eduid/webapp/group_management/settings/common.py @@ -2,7 +2,7 @@ Configuration (file) handling for the eduID group_management app. """ -from typing import Any, Optional +from typing import Any from eduid.common.config.base import EduIDBaseAppConfig, MailConfigMixin @@ -20,7 +20,7 @@ class GroupManagementConfig(EduIDBaseAppConfig, MailConfigMixin): group_delete_invite_template_txt: str = "group_delete_invite_email.txt.jinja2" group_invite_url: str = "https://dashboard.eduid.se" mail_default_from: str = "no-reply@eduid.se" - neo4j_config: Optional[dict[str, Any]] = None + neo4j_config: dict[str, Any] | None = None neo4j_uri: str = "" scim_data_owner: str = "eduid.se" scim_external_id_scope: str = "eduid.se" diff --git a/src/eduid/webapp/group_management/tests/test_app.py b/src/eduid/webapp/group_management/tests/test_app.py index 0149cd46f..20d01fddf 100644 --- a/src/eduid/webapp/group_management/tests/test_app.py +++ b/src/eduid/webapp/group_management/tests/test_app.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from unittest.mock import patch from uuid import UUID @@ -96,7 +96,7 @@ def _add_scim_user(self, scim_id: UUID, eppn: str) -> ScimApiUser: return scim_api_user def _add_scim_group( - self, scim_id: UUID, display_name: str, extensions: Optional[GroupExtensions] = None + self, scim_id: UUID, display_name: str, extensions: GroupExtensions | None = None ) -> ScimApiGroup: if extensions is None: extensions = GroupExtensions() diff --git a/src/eduid/webapp/idp/app.py b/src/eduid/webapp/idp/app.py index 6a973f6a2..b81e59214 100644 --- a/src/eduid/webapp/idp/app.py +++ b/src/eduid/webapp/idp/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -63,7 +63,7 @@ def __init__(self, config: IdPConfig, **kwargs: Any) -> None: self.logger.info("eduid-IdP application started") # OLD way, call sso_session.get_sso_session() directly instead, or use the @uses_sso_session decorator - def _lookup_sso_session(self) -> Optional[SSOSession]: + def _lookup_sso_session(self) -> SSOSession | None: """ Locate any existing SSO session for this request. @@ -75,7 +75,7 @@ def _lookup_sso_session(self) -> Optional[SSOSession]: current_idp_app = cast(IdPApp, current_app) -def init_idp_app(name: str = "idp", test_config: Optional[Mapping[str, Any]] = None) -> IdPApp: +def init_idp_app(name: str = "idp", test_config: Mapping[str, Any] | None = None) -> IdPApp: """ :param name: The name of the instance, it will affect the configuration loaded. :param test_config: Override configuration - used in tests. diff --git a/src/eduid/webapp/idp/exceptions.py b/src/eduid/webapp/idp/exceptions.py index d91c55c37..c26a68e45 100644 --- a/src/eduid/webapp/idp/exceptions.py +++ b/src/eduid/webapp/idp/exceptions.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from flask import render_template, request from werkzeug.exceptions import HTTPException @@ -53,7 +53,7 @@ def _handle_flask_http_exception(error: HTTPException) -> WerkzeugResponse: return app -def _get_error_template(status_code: Optional[int], message: Optional[str]) -> str: +def _get_error_template(status_code: int | None, message: str | None) -> str: pages = { 400: "bad_request.jinja2", 401: "unauthorized.jinja2", diff --git a/src/eduid/webapp/idp/helpers.py b/src/eduid/webapp/idp/helpers.py index ff6df1410..3b132a74a 100644 --- a/src/eduid/webapp/idp/helpers.py +++ b/src/eduid/webapp/idp/helpers.py @@ -1,5 +1,4 @@ from enum import Enum, unique -from typing import Optional from saml2 import BINDING_HTTP_POST @@ -55,7 +54,7 @@ class IdPAction(str, Enum): FINISHED = "FINISHED" -def lookup_user(username: str, managed_account_allowed: bool = False) -> Optional[IdPUser]: +def lookup_user(username: str, managed_account_allowed: bool = False) -> IdPUser | None: """ Lookup a user by username in both central userdb and in managed account db """ diff --git a/src/eduid/webapp/idp/idp_authn.py b/src/eduid/webapp/idp/idp_authn.py index 4b5e4f0d1..ec18f840a 100644 --- a/src/eduid/webapp/idp/idp_authn.py +++ b/src/eduid/webapp/idp/idp_authn.py @@ -9,7 +9,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any from bson import ObjectId from pydantic import BaseModel, ConfigDict, Field @@ -45,7 +45,7 @@ class AuthnData(BaseModel): cred_id: ElementKey timestamp: datetime = Field(default_factory=utc_now, alias="authn_ts") # authn_ts was the old name in the db - external: Optional[ExternalAuthnData] = None + external: ExternalAuthnData | None = None model_config = ConfigDict(populate_by_name=True) def to_dict(self) -> dict[str, Any]: @@ -65,7 +65,7 @@ class PasswordAuthnResponse: timestamp: datetime = field(default_factory=utc_now) @property - def authndata(self) -> Optional[AuthnData]: + def authndata(self) -> AuthnData | None: if not self.credential: return None return AuthnData(cred_id=self.credential.key, timestamp=self.timestamp) @@ -90,7 +90,7 @@ def __init__( assert config.mongo_uri is not None self.authn_store = AuthnInfoStore(uri=config.mongo_uri) - def password_authn(self, username: str, password: str) -> Optional[PasswordAuthnResponse]: + def password_authn(self, username: str, password: str) -> PasswordAuthnResponse | None: """ Authenticate someone using a username and password. @@ -111,7 +111,7 @@ def password_authn(self, username: str, password: str) -> Optional[PasswordAuthn return PasswordAuthnResponse(user=user, credential=cred) - def _verify_username_and_password2(self, user: IdPUser, password: str) -> Optional[Password]: + def _verify_username_and_password2(self, user: IdPUser, password: str) -> Password | None: """ Attempt to verify that a password is valid for a specific user. @@ -148,7 +148,7 @@ def _verify_username_and_password2(self, user: IdPUser, password: str) -> Option return self._authn_passwords(user, password, pw_credentials) - def _authn_passwords(self, user: IdPUser, password: str, pw_credentials: Sequence[Password]) -> Optional[Password]: + def _authn_passwords(self, user: IdPUser, password: str, pw_credentials: Sequence[Password]) -> Password | None: """ Perform the final actual authentication of a user based on a list of (password) credentials. @@ -258,7 +258,7 @@ def __init__(self, uri: str, db_name: str = "eduid_idp_authninfo", collection_na self._db = MongoDB(db_uri=uri, db_name=db_name) self.collection = self._db.get_collection(collection_name) - def credential_success(self, cred_ids: Sequence[str], ts: Optional[datetime] = None) -> None: + def credential_success(self, cred_ids: Sequence[str], ts: datetime | None = None) -> None: """ Kantara AL2_CM_CSM#050 requires that any credential that is not used for a period of 18 months is disabled (taken to mean revoked). @@ -282,7 +282,7 @@ def credential_success(self, cred_ids: Sequence[str], ts: Optional[datetime] = N return None def update_user( - self, user_id: ObjectId, success: Sequence[str], failure: Sequence[str], ts: Optional[datetime] = None + self, user_id: ObjectId, success: Sequence[str], failure: Sequence[str], ts: datetime | None = None ) -> None: """ Log authentication result data for this user. @@ -318,7 +318,7 @@ def update_user( ) return None - def unlock_user(self, user_id: ObjectId, fail_count: int = 0, ts: Optional[datetime] = None) -> None: + def unlock_user(self, user_id: ObjectId, fail_count: int = 0, ts: datetime | None = None) -> None: """ Set the fail count for a specific user and month. @@ -346,7 +346,7 @@ def get_user_authn_info(self, user: IdPUser) -> UserAuthnInfo: return UserAuthnInfo(failures_this_month=0, last_used_credentials=[]) return UserAuthnInfo.from_dict(docs[0]) - def get_credential_last_used(self, cred_id: str) -> Optional[datetime]: + def get_credential_last_used(self, cred_id: str) -> datetime | None: """Get the timestamp for when a specific credential was last used successfully. :return: Time of last successful use, or None @@ -371,7 +371,7 @@ class UserAuthnInfo: last_used_credentials: list[str] @classmethod - def from_dict(cls: type[UserAuthnInfo], data: dict[str, Any], ts: Optional[datetime] = None) -> UserAuthnInfo: + def from_dict(cls: type[UserAuthnInfo], data: dict[str, Any], ts: datetime | None = None) -> UserAuthnInfo: """Construct element from a data dict in database format.""" data = dict(data) # to not modify callers data diff --git a/src/eduid/webapp/idp/idp_saml.py b/src/eduid/webapp/idp/idp_saml.py index 05f07455c..194adfcb9 100644 --- a/src/eduid/webapp/idp/idp_saml.py +++ b/src/eduid/webapp/idp/idp_saml.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field from hashlib import sha1 -from typing import Any, NewType, Optional, Union +from typing import Any, NewType import saml2.server from pydantic import BaseModel @@ -42,13 +42,13 @@ class SAMLValidationError(Exception): @dataclass class SAMLResponseParams: url: str - post_params: Mapping[str, Optional[Union[str, bool]]] + post_params: Mapping[str, str | bool | None] binding: str http_args: HttpArgs missing_attributes: list[dict[str, str]] = field(default_factory=list) -def gen_key(something: Union[str, bytes]) -> ReqSHA1: +def gen_key(something: str | bytes) -> ReqSHA1: """ Generate a unique (not strictly guaranteed) key based on `something'. @@ -85,7 +85,7 @@ def __init__( self._binding = binding self._idp = idp self._debug = debug - self._service_info: Optional[dict[str, Any]] = None + self._service_info: dict[str, Any] | None = None try: self._req_info = idp.parse_authn_request(request, binding) @@ -136,7 +136,7 @@ def request(self) -> str: return self._request @property - def raw_requested_authn_context(self) -> Optional[RequestedAuthnContext]: + def raw_requested_authn_context(self) -> RequestedAuthnContext | None: return self._req_info.message.requested_authn_context def get_requested_authn_contexts(self) -> list[str]: @@ -195,7 +195,7 @@ def request_id(self) -> str: return _res @property - def login_subject(self) -> Optional[str]: + def login_subject(self) -> str | None: """Get information about who the SP thinks should log in. This is used by the IdPProxy when doing MFA Step-up authentication, to signal @@ -231,7 +231,7 @@ def sp_entity_attributes(self) -> Mapping[str, Any]: return res @property - def service_info(self) -> Optional[dict[str, Any]]: + def service_info(self) -> dict[str, Any] | None: """Information about the service where the user is logging in""" if self._service_info is None: res: dict[str, Any] = {} diff --git a/src/eduid/webapp/idp/known_device.py b/src/eduid/webapp/idp/known_device.py index 953e3e6dd..a7326b347 100644 --- a/src/eduid/webapp/idp/known_device.py +++ b/src/eduid/webapp/idp/known_device.py @@ -4,7 +4,7 @@ import logging from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, NewType, Optional +from typing import Any, NewType from uuid import uuid4 import nacl @@ -57,11 +57,11 @@ def new(cls: type[BrowserDeviceInfo], app_secret_box: SecretBox) -> BrowserDevic class KnownDeviceData(BaseModel): - eppn: Optional[str] = None - last_login: Optional[datetime] = None - ip_address: Optional[str] = None - user_agent: Optional[str] = None - login_counter: Optional[int] = 0 + eppn: str | None = None + last_login: datetime | None = None + ip_address: str | None = None + user_agent: str | None = None + login_counter: int | None = 0 def to_json(self): return self.json(exclude_none=True) @@ -111,7 +111,7 @@ def __init__( } self.setup_indexes(indexes) - def save(self, state: KnownDevice, from_browser: BrowserDeviceInfo, ttl: Optional[timedelta] = None) -> bool: + def save(self, state: KnownDevice, from_browser: BrowserDeviceInfo, ttl: timedelta | None = None) -> bool: """ Add a new KnownDevice to the database, or update an existing one. """ @@ -125,7 +125,7 @@ def save(self, state: KnownDevice, from_browser: BrowserDeviceInfo, ttl: Optiona ) return result.acknowledged - def get_state_by_browser_info(self, from_browser: BrowserDeviceInfo) -> Optional[KnownDevice]: + def get_state_by_browser_info(self, from_browser: BrowserDeviceInfo) -> KnownDevice | None: state = self._get_document_by_attr("state_id", from_browser.state_id) if not state: logger.debug(f"Known-device state with state_id {from_browser.state_id} not found in the database") diff --git a/src/eduid/webapp/idp/login.py b/src/eduid/webapp/idp/login.py index 2a3cf9399..e3b09a754 100644 --- a/src/eduid/webapp/idp/login.py +++ b/src/eduid/webapp/idp/login.py @@ -64,7 +64,7 @@ import time from base64 import b64encode from hashlib import sha256 -from typing import Any, Optional +from typing import Any from uuid import uuid4 from defusedxml import ElementTree as DefusedElementTree @@ -121,9 +121,9 @@ class MustAuthenticate(Exception): class NextResult(BaseModel): message: IdPMsg error: bool = False - authn_info: Optional[AuthnInfo] = None - authn_state: Optional[AuthnState] = None - user: Optional[User] = None + authn_info: AuthnInfo | None = None + authn_state: AuthnState | None = None + user: User | None = None model_config = ConfigDict(arbitrary_types_allowed=True) def __str__(self): @@ -133,7 +133,7 @@ def __str__(self): ) -def login_next_step(ticket: LoginContext, sso_session: Optional[SSOSession]) -> NextResult: +def login_next_step(ticket: LoginContext, sso_session: SSOSession | None) -> NextResult: """The main state machine for the login flow(s).""" if ticket.pending_request.aborted: current_app.logger.debug("Login request is aborted") @@ -243,7 +243,7 @@ class SSO(Service): Single Sign On service. """ - def __init__(self, sso_session: Optional[SSOSession]): + def __init__(self, sso_session: SSOSession | None): super().__init__(sso_session) def redirect(self) -> WerkzeugResponse: @@ -524,7 +524,7 @@ def _get_eptid(self, relying_party: str, user_eppn: str) -> list[dict[str, str]] } ] - def _get_pairwise_id(self, relying_party: str, user_eppn: str) -> Optional[str]: + def _get_pairwise_id(self, relying_party: str, user_eppn: str) -> str | None: """ Given a particular relying party, a value (the unique ID and scope together) MUST be bound to only one subject, but the same unique ID given a different scope may refer to the same or (far more likely) a different subject. @@ -595,7 +595,7 @@ def _add_saml_request_to_session(info: SAMLQueryParams, binding: str) -> Request return request_ref -def get_ticket(info: SAMLQueryParams, binding: Optional[str]) -> Optional[LoginContext]: +def get_ticket(info: SAMLQueryParams, binding: str | None) -> LoginContext | None: """ Get the SSOLoginData from the eduid.webapp.common session, or from query parameters. """ diff --git a/src/eduid/webapp/idp/login_context.py b/src/eduid/webapp/idp/login_context.py index 822532b87..f1c817fe2 100644 --- a/src/eduid/webapp/idp/login_context.py +++ b/src/eduid/webapp/idp/login_context.py @@ -33,10 +33,10 @@ class LoginContext(ABC, BaseModel): """ request_ref: RequestRef - known_device_info: Optional[BrowserDeviceInfo] = None - remember_me: Optional[bool] = None # if the user wants to be remembered or not (on this device) - _known_device: Optional[KnownDevice] = None - _pending_request: Optional[IdP_PendingRequest] = None + known_device_info: BrowserDeviceInfo | None = None + remember_me: bool | None = None # if the user wants to be remembered or not (on this device) + _known_device: KnownDevice | None = None + _pending_request: IdP_PendingRequest | None = None model_config = ConfigDict() def __str__(self) -> str: @@ -55,7 +55,7 @@ def pending_request(self) -> IdP_PendingRequest: return self._pending_request @property - def request_id(self) -> Optional[str]: + def request_id(self) -> str | None: raise NotImplementedError("Subclass must implement request_id") @property @@ -67,17 +67,17 @@ def reauthn_required(self) -> bool: raise NotImplementedError("Subclass must implement reauthn_required") @property - def service_requested_eppn(self) -> Optional[str]: + def service_requested_eppn(self) -> str | None: """The eppn of the user the service (e.g. SAML SP) requests logs in""" raise NotImplementedError("Subclass must implement service_requested_eppn") @property - def service_info(self) -> Optional[ServiceInfo]: + def service_info(self) -> ServiceInfo | None: """Information about the service where the user is logging in""" raise NotImplementedError("Subclass must implement service_requested_eppn") @property - def other_device_state_id(self) -> Optional[OtherDeviceId]: + def other_device_state_id(self) -> OtherDeviceId | None: """Get the state_id for the OtherDevice state, if the user wants to log in using another device.""" raise NotImplementedError("Subclass must implement other_device_state_id") @@ -91,7 +91,7 @@ def is_other_device_2(self) -> bool: """Check if this is a request to log in on another device (specifically device #2).""" raise NotImplementedError("Subclass must implement is_other_device_2") - def set_other_device_state(self, state_id: Optional[OtherDeviceId]) -> None: + def set_other_device_state(self, state_id: OtherDeviceId | None) -> None: if isinstance(self.pending_request, IdP_SAMLPendingRequest): self.pending_request.other_device_state_id = state_id elif isinstance(self.pending_request, IdP_OtherDevicePendingRequest): @@ -99,11 +99,11 @@ def set_other_device_state(self, state_id: Optional[OtherDeviceId]) -> None: else: raise TypeError(f"Can't set other_device on pending request of type {type(self.pending_request)}") - def get_requested_authn_context(self) -> Optional[EduidAuthnContextClass]: + def get_requested_authn_context(self) -> EduidAuthnContextClass | None: raise NotImplementedError("Subclass must implement get_requested_authn_context") @property - def known_device(self) -> Optional[KnownDevice]: + def known_device(self) -> KnownDevice | None: if not self._known_device: if self.known_device_info: from eduid.webapp.idp.app import current_idp_app as current_app @@ -166,7 +166,7 @@ def saml_req(self) -> IdP_SAMLRequest: return self._saml_req @property - def request_id(self) -> Optional[str]: + def request_id(self) -> str | None: return self.saml_req.request_id @property @@ -178,7 +178,7 @@ def reauthn_required(self) -> bool: return self.saml_req.force_authn @property - def service_requested_eppn(self) -> Optional[str]: + def service_requested_eppn(self) -> str | None: res = None _login_subject = self.saml_req.login_subject if _login_subject is not None: @@ -200,7 +200,7 @@ def service_requested_eppn(self) -> Optional[str]: return res @property - def service_info(self) -> Optional[ServiceInfo]: + def service_info(self) -> ServiceInfo | None: """Information about the service where the user is logging in""" _info = self.saml_req.service_info if not _info: @@ -208,7 +208,7 @@ def service_info(self) -> Optional[ServiceInfo]: return ServiceInfo(display_name=_info.get("display_name", {})) @property - def other_device_state_id(self) -> Optional[OtherDeviceId]: + def other_device_state_id(self) -> OtherDeviceId | None: # On device #1, the pending_request has a pointer to the other-device-state # Use temporary variable to avoid pycharm warning # Unresolved attribute reference 'other_device_state_id' for class 'IdP_PendingRequest' @@ -232,7 +232,7 @@ def is_other_device_2(self) -> bool: """Check if this is a request to log in on another device (specifically device #2).""" return False - def get_requested_authn_context(self) -> Optional[EduidAuthnContextClass]: + def get_requested_authn_context(self) -> EduidAuthnContextClass | None: """ Return the authn context (if any) that was originally requested. @@ -245,7 +245,7 @@ class LoginContextOtherDevice(LoginContext): other_device_req: OtherDevice @property - def request_id(self) -> Optional[str]: + def request_id(self) -> str | None: return self.other_device_req.device1.request_id @property @@ -259,7 +259,7 @@ def reauthn_required(self) -> bool: return self.other_device_req.device1.reauthn_required @property - def other_device_state_id(self) -> Optional[OtherDeviceId]: + def other_device_state_id(self) -> OtherDeviceId | None: # On device #2, the pending request is the other-device-state _pending = self.pending_request if isinstance(_pending, IdP_OtherDevicePendingRequest): @@ -280,7 +280,7 @@ def is_other_device_2(self) -> bool: has used a camera to scan the QR code shown on the OTHER device (first, initiating).""" return self.other_device_state_id is not None - def get_requested_authn_context(self) -> Optional[EduidAuthnContextClass]: + def get_requested_authn_context(self) -> EduidAuthnContextClass | None: """ Return the authn context (if any) that was originally requested on the first device. @@ -289,16 +289,16 @@ def get_requested_authn_context(self) -> Optional[EduidAuthnContextClass]: return _pick_authn_context(self.authn_contexts, self.request_ref) @property - def service_requested_eppn(self) -> Optional[str]: + def service_requested_eppn(self) -> str | None: return self.other_device_req.eppn @property - def service_info(self) -> Optional[ServiceInfo]: + def service_info(self) -> ServiceInfo | None: """Information about the service where the user is logging in""" return None -def _pick_authn_context(accrs: Sequence[str], log_tag: str) -> Optional[EduidAuthnContextClass]: +def _pick_authn_context(accrs: Sequence[str], log_tag: str) -> EduidAuthnContextClass | None: if len(accrs) > 1: logger.warning(f"{log_tag}: More than one authnContextClassRef, using the first recognised: {accrs}") # first, select the ones recognised by this IdP diff --git a/src/eduid/webapp/idp/mfa_action.py b/src/eduid/webapp/idp/mfa_action.py index a806209d4..ee490083a 100644 --- a/src/eduid/webapp/idp/mfa_action.py +++ b/src/eduid/webapp/idp/mfa_action.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from eduid.userdb.credentials import Credential, FidoCredential from eduid.userdb.credentials.external import BankIDCredential, SwedenConnectCredential @@ -24,7 +23,7 @@ def need_security_key(user: IdPUser, ticket: LoginContext) -> bool: return False for cred_key in ticket.pending_request.credentials_used: - credential: Optional[Credential] + credential: Credential | None if cred_key in ticket.pending_request.onetime_credentials: credential = ticket.pending_request.onetime_credentials[cred_key] else: diff --git a/src/eduid/webapp/idp/mischttp.py b/src/eduid/webapp/idp/mischttp.py index 92d5db970..761cd9519 100644 --- a/src/eduid/webapp/idp/mischttp.py +++ b/src/eduid/webapp/idp/mischttp.py @@ -65,7 +65,7 @@ import pprint from collections.abc import Mapping, Sequence from dataclasses import dataclass -from typing import Any, Optional +from typing import Any import user_agents from bleach import clean @@ -90,7 +90,7 @@ class HttpArgs: method: str url: str headers: Sequence[tuple[str, str]] - body: Optional[str] + body: str | None @classmethod def from_pysaml2_dict(cls: type[Self], http_args: dict[str, Any]) -> Self: @@ -113,7 +113,7 @@ def from_pysaml2_dict(cls: type[Self], http_args: dict[str, Any]) -> Self: return cls(method=method, url=url, headers=headers, body=message) @property - def redirect_url(self) -> Optional[str]: + def redirect_url(self) -> str | None: """ Get the destination URL for a redirect. @@ -191,7 +191,7 @@ def _sanitise_items(data: Mapping[str, Any]) -> dict[str, str]: # ---------------------------------------------------------------------------- # Cookie handling # ---------------------------------------------------------------------------- -def read_cookie(name: str) -> Optional[str]: +def read_cookie(name: str) -> str | None: """ Read a browser cookie. @@ -272,7 +272,7 @@ class IdPUserAgent: safe_str: str -def get_user_agent() -> Optional[IdPUserAgent]: +def get_user_agent() -> IdPUserAgent | None: """Get the request User-Agent and parse it in a safe and controlled way""" user_agent = request.headers.get("user-agent") if not user_agent: diff --git a/src/eduid/webapp/idp/other_device/db.py b/src/eduid/webapp/idp/other_device/db.py index bc49c35cf..6e0a465e1 100644 --- a/src/eduid/webapp/idp/other_device/db.py +++ b/src/eduid/webapp/idp/other_device/db.py @@ -6,7 +6,7 @@ import uuid from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from bson import ObjectId from flask import request @@ -31,20 +31,20 @@ class Device1Data(BaseModel): ref: str # the login 'ref' on device 1 (where login using another device was initiated) - authn_context: Optional[EduidAuthnContextClass] = None # the level of authentication required on device 1 - request_id: Optional[str] = None # the request ID on device 1 (SAML authnRequest request id for example) + authn_context: EduidAuthnContextClass | None = None # the level of authentication required on device 1 + request_id: str | None = None # the request ID on device 1 (SAML authnRequest request id for example) reauthn_required: bool # if reauthn is required for the login on device 1 ip_address: str # the IP address of device 1, to be used by the user on device 2 to assess the request - user_agent: Optional[str] = ( + user_agent: str | None = ( None # the user agent of device 1, to be used by the user on device 2 to assess the request ) - service_info: Optional[ServiceInfo] = None # information about the service (SP) where the user is logging in + service_info: ServiceInfo | None = None # information about the service (SP) where the user is logging in is_known_device: bool # device 1 is a device that has previously logged in as state.eppn class Device2Data(BaseModel): - ref: Optional[str] = None # the pending_request 'ref' on device 2 - response_code: Optional[str] = None # code from login event (using device 2) that has to be entered on device 1 + ref: str | None = None # the pending_request 'ref' on device 2 + response_code: str | None = None # code from login event (using device 2) that has to be entered on device 1 # TODO: doesn't work with onetime_credentials credentials_used: list[UsedCredential] = Field(default=[]) @@ -54,7 +54,7 @@ class OtherDevice(BaseModel): created_at: datetime device1: Device1Data device2: Device2Data - eppn: Optional[str] = ( + eppn: str | None = ( None # the eppn of the user on device 1, either from the SSO session or derived from e-mail address ) expires_at: datetime @@ -68,10 +68,10 @@ class OtherDevice(BaseModel): def from_parameters( cls: type[OtherDevice], ticket: LoginContext, - eppn: Optional[str], - authn_context: Optional[EduidAuthnContextClass], + eppn: str | None, + authn_context: EduidAuthnContextClass | None, ip_address: str, - user_agent: Optional[str], + user_agent: str | None, ttl: timedelta, ) -> OtherDevice: _uuid = uuid.uuid4() @@ -139,14 +139,14 @@ def save(self, state: OtherDevice) -> bool: ) return result.acknowledged - def get_state_by_id(self, state_id: OtherDeviceId) -> Optional[OtherDevice]: + def get_state_by_id(self, state_id: OtherDeviceId) -> OtherDevice | None: state = self._get_document_by_attr("state_id", str(state_id)) if not state: logger.debug(f"Other-device state with state_id {state_id} not found in the database") return None return OtherDevice.from_dict(state) - def add_new_state(self, ticket: LoginContext, user: Optional[User], ttl: timedelta) -> OtherDevice: + def add_new_state(self, ticket: LoginContext, user: User | None, ttl: timedelta) -> OtherDevice: user_agent = None ua = get_user_agent() if ua: @@ -170,7 +170,7 @@ def add_new_state(self, ticket: LoginContext, user: Optional[User], ttl: timedel logger.debug(f" Full other-device state: {state.to_json()}") return state - def abort(self, state: OtherDevice) -> Optional[OtherDevice]: + def abort(self, state: OtherDevice) -> OtherDevice | None: """ Abort a state. @@ -191,7 +191,7 @@ def abort(self, state: OtherDevice) -> Optional[OtherDevice]: return None return state - def grab(self, state: OtherDevice, device2_ref: str) -> Optional[OtherDevice]: + def grab(self, state: OtherDevice, device2_ref: str) -> OtherDevice | None: """ Grab a state, on device 2. This has to be an atomic operation to ensure two devices (one attacker and one victim) can't have pending_requests pointing at this very same OtherDevice state. Otherwise, the attacker @@ -216,7 +216,7 @@ def grab(self, state: OtherDevice, device2_ref: str) -> Optional[OtherDevice]: return None return state - def logged_in(self, state: OtherDevice, eppn: str, credentials_used: list[UsedCredential]) -> Optional[OtherDevice]: + def logged_in(self, state: OtherDevice, eppn: str, credentials_used: list[UsedCredential]) -> OtherDevice | None: """ Finish a state, on device 2. """ diff --git a/src/eduid/webapp/idp/other_device/device1.py b/src/eduid/webapp/idp/other_device/device1.py index 5bd841517..643c03315 100644 --- a/src/eduid/webapp/idp/other_device/device1.py +++ b/src/eduid/webapp/idp/other_device/device1.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from datetime import datetime from io import BytesIO -from typing import Any, Optional, Union +from typing import Any import nacl import nacl.encoding @@ -27,8 +27,8 @@ def device1_check_response_code( - response_code: Optional[str], sso_session: Optional[SSOSession], state: OtherDevice, ticket: LoginContext -) -> Union[Optional[SSOSession], FluxData]: + response_code: str | None, sso_session: SSOSession | None, state: OtherDevice, ticket: LoginContext +) -> SSOSession | None | FluxData: """ Validate the response code supplied by the user on device 1. @@ -78,7 +78,7 @@ def device1_check_response_code( def device1_login_user_from_device2( - state: OtherDevice, ticket: LoginContext, sso_session: Optional[SSOSession] + state: OtherDevice, ticket: LoginContext, sso_session: SSOSession | None ) -> SSOSession: """ Copy the credentials used for authentication on device 2 into the SSO session diff --git a/src/eduid/webapp/idp/other_device/helpers.py b/src/eduid/webapp/idp/other_device/helpers.py index c744d49d1..6e77db061 100644 --- a/src/eduid/webapp/idp/other_device/helpers.py +++ b/src/eduid/webapp/idp/other_device/helpers.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import Optional from eduid.webapp.common.api.messages import FluxData, error_response from eduid.webapp.common.session.namespaces import IdP_OtherDevicePendingRequest, RequestRef @@ -13,9 +12,9 @@ @dataclass class OtherDeviceRefResult: - response: Optional[FluxData] = None - ticket: Optional[LoginContext] = None - state: Optional[OtherDevice] = None + response: FluxData | None = None + ticket: LoginContext | None = None + state: OtherDevice | None = None def _get_other_device_state_using_ref(ref: RequestRef, device: int) -> OtherDeviceRefResult: diff --git a/src/eduid/webapp/idp/schemas.py b/src/eduid/webapp/idp/schemas.py index f85fd3f57..c4a1a61e6 100644 --- a/src/eduid/webapp/idp/schemas.py +++ b/src/eduid/webapp/idp/schemas.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from marshmallow import Schema, ValidationError, fields @@ -77,7 +77,7 @@ class MfaAuthResponsePayload(EduidSchema, CSRFResponseMixin): class ToUVersions(fields.Field): """Handle list of ToU versions available in the frontend both as comma-separated string (bug) and as list""" - def _deserialize(self, value: Any, attr: Optional[str], data: Any, **kwargs) -> Optional[list[str]]: + def _deserialize(self, value: Any, attr: str | None, data: Any, **kwargs) -> list[str] | None: if value is None: return None if isinstance(value, str): diff --git a/src/eduid/webapp/idp/service.py b/src/eduid/webapp/idp/service.py index c5c33014f..1e908717b 100644 --- a/src/eduid/webapp/idp/service.py +++ b/src/eduid/webapp/idp/service.py @@ -60,7 +60,7 @@ """ from abc import ABC -from typing import Any, Optional +from typing import Any from flask import request from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -73,9 +73,9 @@ class SAMLQueryParams(BaseModel): - SAMLRequest: Optional[str] = None - RelayState: Optional[str] = None - request_ref: Optional[RequestRef] = Field(default=None, alias="ref") + SAMLRequest: str | None = None + RelayState: str | None = None + request_ref: RequestRef | None = Field(default=None, alias="ref") model_config = ConfigDict(populate_by_name=True) @field_validator("SAMLRequest", "RelayState") @@ -104,7 +104,7 @@ class Service(ABC): :param session: SSO session """ - def __init__(self, sso_session: Optional[SSOSession]): + def __init__(self, sso_session: SSOSession | None): self.sso_session = sso_session def unpack_redirect(self) -> SAMLQueryParams: diff --git a/src/eduid/webapp/idp/settings/common.py b/src/eduid/webapp/idp/settings/common.py index a2e71ef2e..63bfcb305 100644 --- a/src/eduid/webapp/idp/settings/common.py +++ b/src/eduid/webapp/idp/settings/common.py @@ -3,7 +3,6 @@ """ from datetime import timedelta -from typing import Optional from pydantic import Field, HttpUrl, field_validator from pydantic_core.core_schema import ValidationInfo @@ -30,7 +29,7 @@ class IdPConfig(EduIDBaseAppConfig, TouConfigMixin, WebauthnConfigMixin2, AmConf pysaml2_config: str = "eduid.webapp.common.authn.idp_conf" # SAML F-TICKS user anonymization key. If this is set, the IdP will log F-TICKS data # on every login. - fticks_secret_key: Optional[str] = None + fticks_secret_key: str | None = None # Get SAML F-TICKS format string. fticks_format_string: str = "F-TICKS/SWAMID/2.0#TS={ts}#RP={rp}#AP={ap}#PN={pn}#AM={am}#" # URL to static resources that can be used in templates @@ -77,7 +76,7 @@ class IdPConfig(EduIDBaseAppConfig, TouConfigMixin, WebauthnConfigMixin2, AmConf tou_reaccept_interval: timedelta = Field(default=timedelta(days=3 * 365)) # Legacy parameters for the SSO cookie. Keep in sync with sso_cookie above until removed! sso_cookie_name: str = "idpauthn" - sso_cookie_domain: Optional[str] = None + sso_cookie_domain: str | None = None # Cookie for IdP-specific session allowing users to SSO. # Must be specified after sso_cookie_name and sso_cookie_domain while those are present. sso_cookie: CookieConfig = Field(default_factory=lambda: CookieConfig(key="idpauthn")) @@ -88,8 +87,8 @@ class IdPConfig(EduIDBaseAppConfig, TouConfigMixin, WebauthnConfigMixin2, AmConf eduperson_targeted_id_secret_key: str = "" pairwise_id_secret_key: str = "" eduid_site_url: str - login_bundle_url: Optional[HttpUrlStr] = None - other_device_url: Optional[HttpUrlStr] = None + login_bundle_url: HttpUrlStr | None = None + other_device_url: HttpUrlStr | None = None esi_ladok_prefix: str = Field(default="urn:schac:personalUniqueCode:int:esi:ladok.se:externtstudentuid-") allow_other_device_logins: bool = False other_device_logins_ttl: timedelta = Field(default=timedelta(minutes=2)) @@ -102,9 +101,9 @@ class IdPConfig(EduIDBaseAppConfig, TouConfigMixin, WebauthnConfigMixin2, AmConf known_devices_ttl: timedelta = Field(default=timedelta(days=90)) known_devices_feature_enabled: bool = False # secret key for encrypting personal information for geo-location service - geo_statistics_secret_key: Optional[str] = None + geo_statistics_secret_key: str | None = None geo_statistics_feature_enabled: bool = False - geo_statistics_url: Optional[HttpUrlStr] = None + geo_statistics_url: HttpUrlStr | None = None swamid_assurance_profile_1: list[SwamidAssurance] = Field( default=[ SwamidAssurance.SWAMID_AL1, diff --git a/src/eduid/webapp/idp/sso_cache.py b/src/eduid/webapp/idp/sso_cache.py index b39bdc758..1f0da233c 100644 --- a/src/eduid/webapp/idp/sso_cache.py +++ b/src/eduid/webapp/idp/sso_cache.py @@ -61,7 +61,7 @@ from collections import deque from collections.abc import Mapping from threading import Lock -from typing import Any, Optional, cast +from typing import Any, cast from eduid.userdb.db import BaseDB from eduid.userdb.exceptions import EduIDDBError @@ -109,7 +109,7 @@ class ExpiringCacheMem: :param lock: threading.Lock compatible locking instance """ - def __init__(self, name: str, logger: Optional[logging.Logger], ttl: int, lock: Optional[Lock] = None): + def __init__(self, name: str, logger: logging.Logger | None, ttl: int, lock: Lock | None = None): self.logger = logger self.ttl = ttl self.name = name @@ -122,7 +122,7 @@ def __init__(self, name: str, logger: Optional[logging.Logger], ttl: int, lock: if self.logger is not None: warnings.warn("Object logger deprecated, using module_logger", DeprecationWarning) - def add(self, key: SSOSessionId, info: Any, now: Optional[int] = None) -> None: + def add(self, key: SSOSessionId, info: Any, now: int | None = None) -> None: """ Add entry to the cache. @@ -169,7 +169,7 @@ def _purge_expired(self, timestamp: int) -> None: finally: self.lock.release() - def get(self, key: SSOSessionId) -> Optional[Mapping[str, Any]]: + def get(self, key: SSOSessionId) -> Mapping[str, Any] | None: """ Fetch data from cache based on `key'. @@ -248,7 +248,7 @@ def save(self, session: SSOSession) -> None: ) return None - def get_session(self, sid: SSOSessionId) -> Optional[SSOSession]: + def get_session(self, sid: SSOSessionId) -> SSOSession | None: """ Lookup an SSO session using the session id (same `sid' previously used with add_session). diff --git a/src/eduid/webapp/idp/sso_session.py b/src/eduid/webapp/idp/sso_session.py index 91cd6aeb8..2c1dee673 100644 --- a/src/eduid/webapp/idp/sso_session.py +++ b/src/eduid/webapp/idp/sso_session.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, NewType, Optional +from typing import Any, NewType from bson import ObjectId from pydantic import BaseModel, ConfigDict, Field @@ -67,7 +67,7 @@ class SSOSession(BaseModel): expires_at: datetime = Field(default_factory=lambda: utc_now() + timedelta(minutes=5)) # TODO: should be obsolete now, everything in here should also be available in authn_credentials # (AuthnData.external), stored per credential instead of once per session. - external_mfa: Optional[ExternalMfaData] = None + external_mfa: ExternalMfaData | None = None obj_id: ObjectId = Field(default_factory=ObjectId, alias="_id") session_id: SSOSessionId = Field(default_factory=create_session_id) model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True) @@ -125,7 +125,7 @@ def add_authn_credential(self, authn: AuthnData) -> None: def record_authentication( ticket: LoginContext, eppn: str, - sso_session: Optional[SSOSession], + sso_session: SSOSession | None, credentials: list[AuthnData], sso_session_lifetime: timedelta, ) -> SSOSession: @@ -145,7 +145,7 @@ def record_authentication( return sso_session -def get_sso_session() -> Optional[SSOSession]: +def get_sso_session() -> SSOSession | None: """ Locate any existing SSO session for this request. @@ -168,7 +168,7 @@ def get_sso_session() -> Optional[SSOSession]: return session -def _lookup_sso_session(sso_sessions: SSOSessionCache) -> Optional[SSOSession]: +def _lookup_sso_session(sso_sessions: SSOSessionCache) -> SSOSession | None: """ See if a SSO session exists for this request, and return the data about the currently logged in user from the session store. @@ -205,7 +205,7 @@ def _lookup_sso_session(sso_sessions: SSOSessionCache) -> Optional[SSOSession]: return _sso -def get_sso_session_id() -> Optional[SSOSessionId]: +def get_sso_session_id() -> SSOSessionId | None: """ Get the SSO session id from the IdP SSO cookie. diff --git a/src/eduid/webapp/idp/tests/test_SSO.py b/src/eduid/webapp/idp/tests/test_SSO.py index 5d8f5a967..206564c70 100644 --- a/src/eduid/webapp/idp/tests/test_SSO.py +++ b/src/eduid/webapp/idp/tests/test_SSO.py @@ -2,7 +2,6 @@ import logging from collections.abc import Mapping, Sequence -from typing import Optional, Union from uuid import uuid4 import saml2.server @@ -44,7 +43,7 @@ logger = logging.getLogger(__name__) -def make_SAML_request(class_ref: Optional[Union[EduidAuthnContextClass, str]] = None): +def make_SAML_request(class_ref: EduidAuthnContextClass | str | None = None): if isinstance(class_ref, EduidAuthnContextClass): class_ref = class_ref.value if class_ref is not None: @@ -81,8 +80,8 @@ def _transport_encode(data): class SSOIdPTests(IdPAPITests): def _make_login_ticket( self, - req_class_ref: Optional[Union[EduidAuthnContextClass, str]] = None, - request_ref: Optional[RequestRef] = None, + req_class_ref: EduidAuthnContextClass | str | None = None, + request_ref: RequestRef | None = None, ) -> LoginContext: xmlstr = make_SAML_request(class_ref=req_class_ref) binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" @@ -152,7 +151,7 @@ def get_user_set_nins( self, eppn: str, nins: Sequence[str], - proofing_method: Optional[IdentityProofingMethod] = None, + proofing_method: IdentityProofingMethod | None = None, nin_verified_by: str = "unittest", ) -> IdPUser: """ @@ -184,9 +183,9 @@ def get_user_set_nins( def _get_login_response_authn( self, - req_class_ref: Optional[Union[EduidAuthnContextClass, str]], - credentials: list[Union[str, Credential, AuthnData, ExternalMfaData]], - user: Optional[IdPUser] = None, + req_class_ref: EduidAuthnContextClass | str | None, + credentials: list[str | Credential | AuthnData | ExternalMfaData], + user: IdPUser | None = None, add_tou: bool = True, add_credentials_to_this_request: bool = True, ) -> NextResult: @@ -255,9 +254,9 @@ def _check_login_response_authn( authn_result: NextResult, message: IdPMsg, expect_success: bool = True, - accr: Optional[EduidAuthnContextClass] = None, - assurance_profile: Optional[list[SwamidAssurance]] = None, - expect_error: Optional[bool] = False, + accr: EduidAuthnContextClass | None = None, + assurance_profile: list[SwamidAssurance] | None = None, + expect_error: bool | None = False, ): assert authn_result.message == message, f"Message: {authn_result.message}, Expected: {message}" if expect_success: diff --git a/src/eduid/webapp/idp/tests/test_api.py b/src/eduid/webapp/idp/tests/test_api.py index 82958e80e..85ee636b9 100644 --- a/src/eduid/webapp/idp/tests/test_api.py +++ b/src/eduid/webapp/idp/tests/test_api.py @@ -4,7 +4,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import PurePath -from typing import Any, Optional, Union +from typing import Any from unittest.mock import MagicMock, patch from bson import ObjectId @@ -45,12 +45,12 @@ class GenericResult: @dataclass class NextResult(GenericResult): - error: Optional[dict[str, Any]] = None + error: dict[str, Any] | None = None @dataclass class PwAuthResult(GenericResult): - sso_cookie_val: Optional[str] = None + sso_cookie_val: str | None = None cookies: dict[str, Any] = field(default_factory=dict) @@ -71,22 +71,22 @@ class FinishedResultAPI(GenericResult): @dataclass class TestUser: - eppn: Optional[str] - password: Optional[str] + eppn: str | None + password: str | None @dataclass class LoginResultAPI: response: TestResponse - ref: Optional[str] = None - sso_cookie_val: Optional[str] = None + ref: str | None = None + sso_cookie_val: str | None = None visit_count: dict[str, int] = field(default_factory=dict) visit_order: list[IdPAction] = field(default_factory=list) - pwauth_result: Optional[PwAuthResult] = None - tou_result: Optional[TouResult] = None - mfa_result: Optional[MfaResult] = None - finished_result: Optional[FinishedResultAPI] = None - error: Optional[dict[str, Any]] = None + pwauth_result: PwAuthResult | None = None + tou_result: TouResult | None = None + mfa_result: MfaResult | None = None + finished_result: FinishedResultAPI | None = None + error: dict[str, Any] | None = None class IdPAPITests(EduidAPITestCase[IdPApp]): @@ -111,7 +111,7 @@ def setUp( self.saml2_client = Saml2Client(config=self.sp_config, identity_cache=self.pysaml2_identity) self.default_user = TestUser(eppn=self.test_user.eppn, password="bar") - def load_app(self, config: Optional[Mapping[str, Any]]) -> IdPApp: + def load_app(self, config: Mapping[str, Any] | None) -> IdPApp: """ Called from the parent class, so we can provide the appropriate flask app for this test case. @@ -142,13 +142,13 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: def _try_login( self, - saml2_client: Optional[Saml2Client] = None, - authn_context: Optional[Mapping[str, Any]] = None, + saml2_client: Saml2Client | None = None, + authn_context: Mapping[str, Any] | None = None, force_authn: bool = False, - assertion_consumer_service_url: Optional[str] = None, - test_user: Optional[TestUser] = None, - sso_cookie_val: Optional[str] = None, - mfa_credential: Optional[Credential] = None, + assertion_consumer_service_url: str | None = None, + test_user: TestUser | None = None, + sso_cookie_val: str | None = None, + mfa_credential: Credential | None = None, ) -> LoginResultAPI: """ Try logging in to the IdP. @@ -290,7 +290,7 @@ def _call_pwauth(self, target: str, ref: str, username: str, password: str) -> P return result - def _call_tou(self, target: str, ref: str, user_accepts: Optional[str]) -> TouResult: + def _call_tou(self, target: str, ref: str, user_accepts: str | None) -> TouResult: with self.session_cookie_anon(self.browser) as client: with self.app.test_request_context(): with client.session_transaction() as sess: @@ -366,7 +366,7 @@ def _extract_path_from_url(self, url): return path def parse_saml_authn_response( - self, result: FinishedResultAPI, saml2_client: Optional[Saml2Client] = None + self, result: FinishedResultAPI, saml2_client: Saml2Client | None = None ) -> AuthnResponse: _saml2_client = saml2_client if saml2_client is not None else self.saml2_client @@ -374,12 +374,12 @@ def parse_saml_authn_response( outstanding_queries = self.pysaml2_oq.outstanding_queries() return _saml2_client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries) - def get_sso_session(self, sso_cookie_val: str) -> Optional[SSOSession]: + def get_sso_session(self, sso_cookie_val: str) -> SSOSession | None: if sso_cookie_val is None: return None return self.app.sso_sessions.get_session(SSOSessionId(sso_cookie_val)) - def add_test_user_tou(self, eppn: Optional[str] = None, version: Optional[str] = None) -> tuple[IdPUser, ToUEvent]: + def add_test_user_tou(self, eppn: str | None = None, version: str | None = None) -> tuple[IdPUser, ToUEvent]: """Utility function to add a valid ToU to the default test user""" if version is None: version = self.app.conf.tou_version @@ -412,11 +412,11 @@ def add_test_user_mail_address(self, mail_address: MailAddress) -> None: def add_test_user_security_key( self, - user: Optional[User] = None, - credential_id: Optional[str] = "webauthn_keyhandle", + user: User | None = None, + credential_id: str | None = "webauthn_keyhandle", is_verified: bool = False, mfa_approved: bool = False, - credential: Optional[FidoCredential] = None, + credential: FidoCredential | None = None, always_use_security_key_user_preference: bool = True, ): if user is None: @@ -442,8 +442,8 @@ def add_test_user_security_key( def add_test_user_external_mfa_cred( self, - user: Optional[User] = None, - trust_framework: Optional[TrustFramework] = None, + user: User | None = None, + trust_framework: TrustFramework | None = None, ): if user is None: user = self.test_user @@ -461,7 +461,7 @@ def add_test_user_external_mfa_cred( user.credentials.add(cred) self.request_user_sync(user) - def get_attributes(self, result, saml2_client: Optional[Saml2Client] = None): + def get_attributes(self, result, saml2_client: Saml2Client | None = None): assert result.finished_result is not None authn_response = self.parse_saml_authn_response(result.finished_result, saml2_client=saml2_client) session_info = authn_response.session_info() @@ -480,10 +480,10 @@ def _check_login_result( self, result: LoginResultAPI, visit_order: list[IdPAction], - sso_cookie_val: Optional[Union[str, bool]] = True, - finish_result: Optional[FinishedResultAPI] = None, - pwauth_result: Optional[PwAuthResult] = None, - error: Optional[dict[str, Any]] = None, + sso_cookie_val: str | bool | None = True, + finish_result: FinishedResultAPI | None = None, + pwauth_result: PwAuthResult | None = None, + error: dict[str, Any] | None = None, ): assert result.visit_order == visit_order, f"visit_order: {result.visit_order}, expected: {visit_order}" diff --git a/src/eduid/webapp/idp/util.py b/src/eduid/webapp/idp/util.py index a8d414fa5..c822b1cc9 100644 --- a/src/eduid/webapp/idp/util.py +++ b/src/eduid/webapp/idp/util.py @@ -4,14 +4,13 @@ import ipaddress import logging from enum import Enum -from typing import Union from eduid.userdb.idp import IdPUser logger = logging.getLogger(__name__) -def b64encode(source: Union[str, bytes]) -> str: +def b64encode(source: str | bytes) -> str: if isinstance(source, str): _source = bytes(source, "utf-8") else: @@ -19,7 +18,7 @@ def b64encode(source: Union[str, bytes]) -> str: return base64.b64encode(_source).decode("utf-8") -def maybe_xml_to_string(message: Union[str, bytes]) -> str: +def maybe_xml_to_string(message: str | bytes) -> str: """ Try to parse message as an XML string, and then return it pretty-printed. diff --git a/src/eduid/webapp/idp/views/mfa_auth.py b/src/eduid/webapp/idp/views/mfa_auth.py index 645886848..b71399847 100644 --- a/src/eduid/webapp/idp/views/mfa_auth.py +++ b/src/eduid/webapp/idp/views/mfa_auth.py @@ -1,7 +1,7 @@ from collections.abc import Mapping from copy import deepcopy from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from flask import Blueprint @@ -31,7 +31,7 @@ @require_ticket @uses_sso_session def mfa_auth( - ticket: LoginContext, sso_session: Optional[SSOSession], webauthn_response: Optional[Mapping[str, str]] = None + ticket: LoginContext, sso_session: SSOSession | None, webauthn_response: Mapping[str, str] | None = None ) -> FluxData: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- MFA authentication ({ticket.request_ref}) ---") @@ -102,14 +102,14 @@ def mfa_auth( @dataclass class CheckResult: - response: Optional[FluxData] = None - credential: Optional[Credential] = None - authn_data: Optional[AuthnData] = None + response: FluxData | None = None + credential: Credential | None = None + authn_data: AuthnData | None = None def _check_external_mfa( mfa_action: MfaAction, session: EduidSession, user: User, ref: RequestRef, sso_session: SSOSession -) -> Optional[CheckResult]: +) -> CheckResult | None: # Third party service MFA if mfa_action.success is True: # Explicit check that success is the boolean True if mfa_action.login_ref: @@ -165,8 +165,8 @@ def _check_external_mfa( def _check_webauthn( - webauthn_response: Optional[Mapping[str, str]], mfa_action: MfaAction, user: User -) -> Optional[CheckResult]: + webauthn_response: Mapping[str, str] | None, mfa_action: MfaAction, user: User +) -> CheckResult | None: if webauthn_response is None: return None diff --git a/src/eduid/webapp/idp/views/misc.py b/src/eduid/webapp/idp/views/misc.py index 3bf2608c6..ac8c397ee 100644 --- a/src/eduid/webapp/idp/views/misc.py +++ b/src/eduid/webapp/idp/views/misc.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from flask import Blueprint, jsonify, redirect, request from werkzeug.wrappers import Response as WerkzeugResponse @@ -44,7 +44,7 @@ def abort(ticket: LoginContext) -> FluxData: @UnmarshalWith(LogoutRequestSchema) @MarshalWith(LogoutResponseSchema) @uses_sso_session -def logout(ref: Optional[str], sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def logout(ref: str | None, sso_session: SSOSession | None) -> WerkzeugResponse: """Logout from the current SSO session""" current_app.logger.debug("\n\n") _session_id = sso_session.session_id if sso_session else None @@ -57,7 +57,7 @@ def logout(ref: Optional[str], sso_session: Optional[SSOSession]) -> WerkzeugRes location = None ticket = None - old_saml_req: Optional[IdP_SAMLPendingRequest] = None + old_saml_req: IdP_SAMLPendingRequest | None = None _ref = None if ref: _info = SAMLQueryParams(request_ref=ref) diff --git a/src/eduid/webapp/idp/views/next.py b/src/eduid/webapp/idp/views/next.py index 236cc85f1..32dc03f40 100644 --- a/src/eduid/webapp/idp/views/next.py +++ b/src/eduid/webapp/idp/views/next.py @@ -1,7 +1,7 @@ import re from base64 import urlsafe_b64decode from dataclasses import asdict, dataclass -from typing import Any, Optional +from typing import Any import requests from cryptography.hazmat.primitives import hashes, hmac @@ -33,7 +33,7 @@ @MarshalWith(NextResponseSchema) @require_ticket @uses_sso_session -def next_view(ticket: LoginContext, sso_session: Optional[SSOSession]) -> FluxData: +def next_view(ticket: LoginContext, sso_session: SSOSession | None) -> FluxData: """Main state machine for frontend""" current_app.logger.debug("\n\n") current_app.logger.debug(f"--- Next ({ticket.request_ref}) ---") @@ -213,9 +213,9 @@ class AuthnOptions: # Is this a re-authentication request? Meaning the very same user _has_ to log in, can't switch to another user. is_reauthn: bool # What to use in the greeting of the user at the login page - display_name: Optional[str] = None + display_name: str | None = None # Is this login locked to being performed by a particular user? (Identified by the email/phone/...) - forced_username: Optional[str] = None + forced_username: str | None = None # Can an unknown user log in using just a swedish eID? Yes, if there is an eduID user with the users (verified) NIN. freja_eidplus: bool = True # TODO: remove freja_eidplus replaced by swedish_eid swedish_eid: bool = True @@ -241,7 +241,7 @@ def valid_options(self) -> list[str]: return [x for x in _data.keys() if _data[x]] -def _get_authn_options(ticket: LoginContext, sso_session: Optional[SSOSession], eppn: Optional[str]) -> dict[str, Any]: +def _get_authn_options(ticket: LoginContext, sso_session: SSOSession | None, eppn: str | None) -> dict[str, Any]: res = AuthnOptions(is_reauthn=ticket.reauthn_required) # Availability of "login using another device" is controlled by configuration for now. @@ -265,11 +265,11 @@ def _get_authn_options(ticket: LoginContext, sso_session: Optional[SSOSession], @dataclass class RequiredUserResult: - response: Optional[FluxData] = None - eppn: Optional[str] = None + response: FluxData | None = None + eppn: str | None = None -def get_required_user(ticket: LoginContext, sso_session: Optional[SSOSession]) -> RequiredUserResult: +def get_required_user(ticket: LoginContext, sso_session: SSOSession | None) -> RequiredUserResult: """ Figure out if something dictates a user that _must_ be used to log in at this time. @@ -355,7 +355,7 @@ def _set_user_options(res: AuthnOptions, eppn: str) -> None: return None -def _geo_statistics(ticket: LoginContext, sso_session: Optional[SSOSession]) -> None: +def _geo_statistics(ticket: LoginContext, sso_session: SSOSession | None) -> None: """Log user statistics from login event""" if not sso_session: diff --git a/src/eduid/webapp/idp/views/pw_auth.py b/src/eduid/webapp/idp/views/pw_auth.py index 8cc179e31..7da07b750 100644 --- a/src/eduid/webapp/idp/views/pw_auth.py +++ b/src/eduid/webapp/idp/views/pw_auth.py @@ -1,5 +1,3 @@ -from typing import Union - from flask import Blueprint, jsonify, request from werkzeug.wrappers import Response as WerkzeugResponse @@ -24,7 +22,7 @@ @UnmarshalWith(PwAuthRequestSchema) @MarshalWith(PwAuthResponseSchema) @require_ticket -def pw_auth(ticket: LoginContext, username: str, password: str) -> Union[FluxData, WerkzeugResponse]: +def pw_auth(ticket: LoginContext, username: str, password: str) -> FluxData | WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- Password authentication ({ticket.request_ref}) ---") diff --git a/src/eduid/webapp/idp/views/saml.py b/src/eduid/webapp/idp/views/saml.py index c05068b51..efa2a334d 100644 --- a/src/eduid/webapp/idp/views/saml.py +++ b/src/eduid/webapp/idp/views/saml.py @@ -1,5 +1,3 @@ -from typing import Optional - from flask import Blueprint, request from werkzeug.wrappers import Response as WerkzeugResponse @@ -14,7 +12,7 @@ @saml_views.route("/sso/post", methods=["POST"]) @uses_sso_session -def sso_post(sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def sso_post(sso_session: SSOSession | None) -> WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- SingleSignOn POST: {request.path} ---") return SSO(sso_session).post() @@ -22,7 +20,7 @@ def sso_post(sso_session: Optional[SSOSession]) -> WerkzeugResponse: @saml_views.route("/sso/redirect", methods=["GET"]) @uses_sso_session -def sso_redirect(sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def sso_redirect(sso_session: SSOSession | None) -> WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- SingleSignOn REDIRECT: {request.path} ---") return SSO(sso_session).redirect() @@ -30,7 +28,7 @@ def sso_redirect(sso_session: Optional[SSOSession]) -> WerkzeugResponse: @saml_views.route("/slo/post", methods=["POST"]) @uses_sso_session -def slo_post(sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def slo_post(sso_session: SSOSession | None) -> WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- SingleLogOut POST: {request.path} ---") return SLO(sso_session).post() @@ -38,7 +36,7 @@ def slo_post(sso_session: Optional[SSOSession]) -> WerkzeugResponse: @saml_views.route("/slo/soap", methods=["POST"]) @uses_sso_session -def slo_soap(sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def slo_soap(sso_session: SSOSession | None) -> WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- SingleLogOut SOAP: {request.path} ---") return SLO(sso_session).soap() @@ -46,7 +44,7 @@ def slo_soap(sso_session: Optional[SSOSession]) -> WerkzeugResponse: @saml_views.route("/slo/redirect", methods=["GET"]) @uses_sso_session -def slo_redirect(sso_session: Optional[SSOSession]) -> WerkzeugResponse: +def slo_redirect(sso_session: SSOSession | None) -> WerkzeugResponse: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- SingleLogOut REDIRECT: {request.path} ---") return SLO(sso_session).redirect() diff --git a/src/eduid/webapp/idp/views/tou.py b/src/eduid/webapp/idp/views/tou.py index 5f7d62a89..6c503848a 100644 --- a/src/eduid/webapp/idp/views/tou.py +++ b/src/eduid/webapp/idp/views/tou.py @@ -1,5 +1,4 @@ from collections.abc import Sequence -from typing import Optional from bson import ObjectId from flask import Blueprint @@ -28,9 +27,9 @@ @uses_sso_session def tou( ticket: LoginContext, - sso_session: Optional[SSOSession], - versions: Optional[Sequence[str]] = None, - user_accepts: Optional[str] = None, + sso_session: SSOSession | None, + versions: Sequence[str] | None = None, + user_accepts: str | None = None, ) -> FluxData: current_app.logger.debug("\n\n") current_app.logger.debug(f"--- Terms of Use ({ticket.request_ref}) ---") diff --git a/src/eduid/webapp/idp/views/use_other.py b/src/eduid/webapp/idp/views/use_other.py index 545dfa8c3..ca6a2effb 100644 --- a/src/eduid/webapp/idp/views/use_other.py +++ b/src/eduid/webapp/idp/views/use_other.py @@ -1,4 +1,3 @@ -from typing import Optional, Union from uuid import uuid4 import nacl @@ -44,11 +43,11 @@ @uses_sso_session def use_other_1( ticket: LoginContext, - sso_session: Optional[SSOSession], - username: Optional[str] = None, - action: Optional[str] = None, - response_code: Optional[str] = None, -) -> Union[FluxData, WerkzeugResponse]: + sso_session: SSOSession | None, + username: str | None = None, + action: str | None = None, + response_code: str | None = None, +) -> FluxData | WerkzeugResponse: """ The user requests to start a "Login using another device" flow. @@ -159,10 +158,10 @@ def use_other_1( @MarshalWith(UseOther2ResponseSchema) @uses_sso_session def use_other_2( - ref: Optional[RequestRef], - state_id: Optional[OtherDeviceId], - sso_session: Optional[SSOSession], - action: Optional[str] = None, + ref: RequestRef | None, + state_id: OtherDeviceId | None, + sso_session: SSOSession | None, + action: str | None = None, ) -> FluxData: """ "Login using another device" flow. diff --git a/src/eduid/webapp/jsconfig/app.py b/src/eduid/webapp/jsconfig/app.py index ec2f91ca8..6eada7295 100644 --- a/src/eduid/webapp/jsconfig/app.py +++ b/src/eduid/webapp/jsconfig/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -21,7 +21,7 @@ def __init__(self, config: JSConfigConfig, **kwargs): current_jsconfig_app: JSConfigApp = cast(JSConfigApp, current_app) -def jsconfig_init_app(name: str = "jsconfig", test_config: Optional[Mapping[str, Any]] = None) -> JSConfigApp: +def jsconfig_init_app(name: str = "jsconfig", test_config: Mapping[str, Any] | None = None) -> JSConfigApp: """ Create an instance of an eduid jsconfig data app. diff --git a/src/eduid/webapp/jsconfig/settings/jsapps.py b/src/eduid/webapp/jsconfig/settings/jsapps.py index a8d7d8ff0..85215a94e 100644 --- a/src/eduid/webapp/jsconfig/settings/jsapps.py +++ b/src/eduid/webapp/jsconfig/settings/jsapps.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from eduid.common.config.base import EduidEnvironment, PasswordConfigMixin @@ -14,7 +12,7 @@ class JsAppsConfig(PasswordConfigMixin): """ available_languages: dict[str, str] = Field(default={"en": "English", "sv": "Svenska"}) - csrf_token: Optional[str] = None + csrf_token: str | None = None dashboard_link: HttpUrlStr debug: bool = False eduid_site_link: HttpUrlStr = Field(default=HttpUrlStr("https://eduid.se")) @@ -23,7 +21,7 @@ class JsAppsConfig(PasswordConfigMixin): faq_link: HttpUrlStr # reset_password_link is used for directing a user to the reset password app reset_password_link: HttpUrlStr - sentry_dsn: Optional[str] = None + sentry_dsn: str | None = None signup_link: HttpUrlStr # backend endpoint urls authn_service_url: HttpUrlStr @@ -31,15 +29,15 @@ class JsAppsConfig(PasswordConfigMixin): eidas_service_url: HttpUrlStr emails_service_url: HttpUrlStr # error_info_url needs to be a full URL since the backend is on the idp, not on https://eduid.se - error_info_url: Optional[HttpUrlStr] = None - freja_eid_service_url: Optional[HttpUrlStr] = None + error_info_url: HttpUrlStr | None = None + freja_eid_service_url: HttpUrlStr | None = None group_mgmt_service_url: HttpUrlStr ladok_service_url: HttpUrlStr letter_proofing_service_url: HttpUrlStr # login_next_url needs to be a full URL since the backend is on the idp, not on https://eduid.se login_next_url: HttpUrlStr # login_request_other_url needs to be a full URL since the backend is on the idp, not on https://eduid.se - login_request_other_url: Optional[HttpUrlStr] = None + login_request_other_url: HttpUrlStr | None = None login_service_url: HttpUrlStr lookup_mobile_proofing_service_url: HttpUrlStr orcid_service_url: HttpUrlStr @@ -48,9 +46,9 @@ class JsAppsConfig(PasswordConfigMixin): reset_password_service_url: HttpUrlStr security_service_url: HttpUrlStr signup_service_url: HttpUrlStr - svipe_service_url: Optional[HttpUrlStr] = None # if not set the frontend component will not show + svipe_service_url: HttpUrlStr | None = None # if not set the frontend component will not show # Dashboard config default_country_code: int = 46 token_verify_idp: HttpUrlStr # Signup config - tous: Optional[dict[str, str]] = None + tous: dict[str, str] | None = None diff --git a/src/eduid/webapp/ladok/app.py b/src/eduid/webapp/ladok/app.py index ede19178f..d43076471 100644 --- a/src/eduid/webapp/ladok/app.py +++ b/src/eduid/webapp/ladok/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -34,7 +34,7 @@ def __init__(self, config: LadokConfig, **kwargs): current_ladok_app: LadokApp = cast(LadokApp, current_app) -def init_ladok_app(name: str = "ladok", test_config: Optional[Mapping[str, Any]] = None) -> LadokApp: +def init_ladok_app(name: str = "ladok", test_config: Mapping[str, Any] | None = None) -> LadokApp: """ Create an instance of an ladok app. diff --git a/src/eduid/webapp/ladok/client.py b/src/eduid/webapp/ladok/client.py index b84c54c62..9e786c055 100644 --- a/src/eduid/webapp/ladok/client.py +++ b/src/eduid/webapp/ladok/client.py @@ -1,7 +1,6 @@ import logging from collections.abc import Mapping from datetime import datetime -from typing import Optional import requests from pydantic import BaseModel, ConfigDict, Field, ValidationError @@ -20,8 +19,8 @@ class LadokClientException(Exception): class Error(BaseModel): - id: Optional[str] = None - detail: Optional[str] = Field(default=None, alias="details") + id: str | None = None + detail: str | None = Field(default=None, alias="details") class LadokBaseModel(BaseModel): @@ -29,8 +28,8 @@ class LadokBaseModel(BaseModel): class UniversityName(LadokBaseModel): - sv: Optional[str] = Field(default=None, alias="long_name_sv") - en: Optional[str] = Field(default=None, alias="long_name_en") + sv: str | None = Field(default=None, alias="long_name_sv") + en: str | None = Field(default=None, alias="long_name_en") class UniversitiesData(LadokBaseModel): @@ -38,26 +37,26 @@ class UniversitiesData(LadokBaseModel): class UniversitiesInfoResponse(LadokBaseModel): - data: Optional[UniversitiesData] = None - error: Optional[Error] = None + data: UniversitiesData | None = None + error: Error | None = None class LadokUserInfo(LadokBaseModel): external_id: str = Field(alias="ladok_externt_uid") - esi: Optional[str] = None - is_student: Optional[bool] = None - student_until: Optional[datetime] = Field(default=None, alias="expire_student") + esi: str | None = None + is_student: bool | None = None + student_until: datetime | None = Field(default=None, alias="expire_student") class LadokUserInfoResponse(LadokBaseModel): - data: Optional[LadokUserInfo] = None - error: Optional[Error] = None + data: LadokUserInfo | None = None + error: Error | None = None class LadokClientConfig(LadokBaseModel): url: HttpUrlStr version: str = "v1" - dev_universities: Optional[dict[str, UniversityName]] = None # used for local development + dev_universities: dict[str, UniversityName] | None = None # used for local development class University(BaseModel): @@ -117,7 +116,7 @@ def get_universities(self) -> UniversitiesData: assert universities_response.data is not None # please mypy return universities_response.data - def get_user_info(self, ladok_name: str, nin: str) -> Optional[LadokUserInfo]: + def get_user_info(self, ladok_name: str, nin: str) -> LadokUserInfo | None: """ path: /api/v1/kf/ladokinfo Request body: diff --git a/src/eduid/webapp/letter_proofing/app.py b/src/eduid/webapp/letter_proofing/app.py index 3245200b5..3da626352 100644 --- a/src/eduid/webapp/letter_proofing/app.py +++ b/src/eduid/webapp/letter_proofing/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -42,7 +42,7 @@ def __init__(self, config: LetterProofingConfig, **kwargs: Any): def init_letter_proofing_app( - name: str = "letter_proofing", test_config: Optional[Mapping[str, Any]] = None + name: str = "letter_proofing", test_config: Mapping[str, Any] | None = None ) -> LetterProofingApp: """ :param name: The name of the instance, it will affect the configuration loaded. diff --git a/src/eduid/webapp/letter_proofing/settings/common.py b/src/eduid/webapp/letter_proofing/settings/common.py index a3cae99a6..565fcb55f 100644 --- a/src/eduid/webapp/letter_proofing/settings/common.py +++ b/src/eduid/webapp/letter_proofing/settings/common.py @@ -1,5 +1,3 @@ -from typing import Optional - from eduid.common.config.base import AmConfigMixin, EduIDBaseAppConfig, MagicCookieMixin, MsgConfigMixin @@ -25,7 +23,7 @@ class LetterProofingConfig(EduIDBaseAppConfig, MagicCookieMixin, AmConfigMixin, ekopost_api_plex: str = "simplex" # Setting ekopost_debug_pdf to a path means that the other ekopost settings will be ignored and that the pdf # only will be written to to the supplied path, not sent to the letter service. - ekopost_debug_pdf_path: Optional[str] = None + ekopost_debug_pdf_path: str | None = None # Remove expired states on GET /proofing if this is set to True backwards_compat_remove_expired_state: bool = False diff --git a/src/eduid/webapp/letter_proofing/tests/test_app.py b/src/eduid/webapp/letter_proofing/tests/test_app.py index d61857ac3..5e53535c1 100644 --- a/src/eduid/webapp/letter_proofing/tests/test_app.py +++ b/src/eduid/webapp/letter_proofing/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, AnyStr, Optional +from typing import Any, AnyStr from unittest.mock import MagicMock, Mock, patch from werkzeug.test import TestResponse @@ -28,9 +28,9 @@ def setUp(self): @staticmethod def mock_response( status_code: int = 200, - content: Optional[AnyStr] = None, - json_data: Optional[Mapping[str, Any]] = None, - headers: Optional[Mapping[str, Any]] = None, + content: AnyStr | None = None, + json_data: Mapping[str, Any] | None = None, + headers: Mapping[str, Any] | None = None, raise_for_status: Any = None, ) -> Mock: """ @@ -82,7 +82,7 @@ def get_state(self): self.assertEqual(response.status_code, 200) return json.loads(response.data) - def send_letter(self, nin: str, csrf_token: Optional[str] = None, validate_response: bool = True) -> TestResponse: + def send_letter(self, nin: str, csrf_token: str | None = None, validate_response: bool = True) -> TestResponse: """ Invoke the POST /proofing endpoint, check that the HTTP response code is 200 and return the response. @@ -101,7 +101,7 @@ def send_letter(self, nin: str, csrf_token: Optional[str] = None, validate_respo def _send_letter2( self, nin: str, - csrf_token: Optional[str], + csrf_token: str | None, mock_get_postal_address: MagicMock, mock_request_user_sync: MagicMock, mock_hammock: MagicMock, @@ -119,7 +119,7 @@ def _send_letter2( response = client.post("/proofing", data=json.dumps(data), content_type=self.content_type_json) return response - def verify_code(self, code: str, csrf_token: Optional[str] = None, validate_response: bool = True) -> TestResponse: + def verify_code(self, code: str, csrf_token: str | None = None, validate_response: bool = True) -> TestResponse: """ Invoke the POST /verify-code endpoint, check that the HTTP response code is 200 and return the response. @@ -135,7 +135,7 @@ def verify_code(self, code: str, csrf_token: Optional[str] = None, validate_resp @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") @patch("eduid.common.rpc.msg_relay.MsgRelay.get_postal_address") def _verify_code2( - self, code: str, csrf_token: Optional[str], mock_get_postal_address, mock_request_user_sync: MagicMock + self, code: str, csrf_token: str | None, mock_get_postal_address, mock_request_user_sync: MagicMock ): if csrf_token is None: _state = self.get_state() @@ -156,8 +156,8 @@ def get_code_backdoor( mock_get_postal_address: Any, mock_request_user_sync: Any, mock_hammock: Any, - cookie_name: Optional[str] = None, - cookie_value: Optional[str] = None, + cookie_name: str | None = None, + cookie_value: str | None = None, add_cookie: bool = True, ): ekopost_response = self.mock_response(json_data={"id": "test"}) diff --git a/src/eduid/webapp/lookup_mobile_proofing/app.py b/src/eduid/webapp/lookup_mobile_proofing/app.py index 26d364d8a..0e55d9ff3 100644 --- a/src/eduid/webapp/lookup_mobile_proofing/app.py +++ b/src/eduid/webapp/lookup_mobile_proofing/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -35,7 +35,7 @@ def __init__(self, config: MobileProofingConfig, **kwargs: Any): def init_lookup_mobile_proofing_app( - name: str = "lookup_mobile_proofing", test_config: Optional[Mapping[str, Any]] = None + name: str = "lookup_mobile_proofing", test_config: Mapping[str, Any] | None = None ) -> MobileProofingApp: """ :param name: The name of the instance, it will affect the configuration loaded. diff --git a/src/eduid/webapp/lookup_mobile_proofing/helpers.py b/src/eduid/webapp/lookup_mobile_proofing/helpers.py index e77cc979f..2f5e69ac4 100644 --- a/src/eduid/webapp/lookup_mobile_proofing/helpers.py +++ b/src/eduid/webapp/lookup_mobile_proofing/helpers.py @@ -1,6 +1,5 @@ from datetime import datetime from enum import unique -from typing import Optional from eduid.common.rpc.exceptions import LookupMobileTaskFailed from eduid.common.rpc.msg_relay import FullPostalAddress @@ -34,7 +33,7 @@ class MobileMsg(TranslatableMsg): no_match = "nins.no-mobile-match" -def nin_to_age(nin: str, now: Optional[datetime] = None) -> int: +def nin_to_age(nin: str, now: datetime | None = None) -> int: """ :param nin: National Identity Number, YYYYMMDDXXXX :return: Age in years @@ -60,7 +59,7 @@ def create_proofing_state(user: User, nin: str) -> NinProofingState: def match_mobile_to_user( user: User, self_asserted_nin: str, verified_mobile_numbers: list[str] -) -> Optional[TeleAdressProofing]: +) -> TeleAdressProofing | None: """ Lookup the user's phone number in the TeleAdress external database. If the phone number comes back registered to the self asserted NIN of the user, create a proofing log entry and return it. diff --git a/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py b/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py index 19cfbe5af..c93e56b32 100644 --- a/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py +++ b/src/eduid/webapp/lookup_mobile_proofing/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from eduid.common.config.base import EduidEnvironment @@ -42,7 +42,7 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: ) return config - def _check_nin_verified_ok_no_proofing_state(self, user: User, number: Optional[str] = None): + def _check_nin_verified_ok_no_proofing_state(self, user: User, number: str | None = None): nin_number = number or self.test_user_nin assert user.identities.nin is not None assert user.identities.nin.number == nin_number @@ -51,7 +51,7 @@ def _check_nin_verified_ok_no_proofing_state(self, user: User, number: Optional[ assert user.identities.nin.is_verified is True assert self.app.proofing_log.db_count() == 1 - def _check_nin_not_verified_no_proofing_state(self, user: User, number: Optional[str] = None): + def _check_nin_not_verified_no_proofing_state(self, user: User, number: str | None = None): nin_number = number or self.test_user_nin assert user.identities.nin is not None assert user.identities.nin.number == nin_number diff --git a/src/eduid/webapp/oidc_proofing/app.py b/src/eduid/webapp/oidc_proofing/app.py index 0ee37e236..b2f943dc8 100644 --- a/src/eduid/webapp/oidc_proofing/app.py +++ b/src/eduid/webapp/oidc_proofing/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -44,7 +44,7 @@ def __init__(self, config: OIDCProofingConfig, **kwargs): def init_oidc_proofing_app( - name: str = "oidc_proofing", test_config: Optional[Mapping[str, Any]] = None + name: str = "oidc_proofing", test_config: Mapping[str, Any] | None = None ) -> OIDCProofingApp: """ Create an instance of an oidc proofing app. diff --git a/src/eduid/webapp/oidc_proofing/views.py b/src/eduid/webapp/oidc_proofing/views.py index 152d01dc3..93c2faacb 100644 --- a/src/eduid/webapp/oidc_proofing/views.py +++ b/src/eduid/webapp/oidc_proofing/views.py @@ -2,7 +2,7 @@ import binascii from collections.abc import Mapping from io import BytesIO -from typing import Any, Union +from typing import Any import qrcode import qrcode.image.svg @@ -159,7 +159,7 @@ def get_seleg_state(user: User) -> dict[str, Any]: @MarshalWith(schemas.NonceResponseSchema) @can_verify_nin @require_user -def seleg_proofing(user: User, nin: str) -> Union[FluxData, WerkzeugResponse]: +def seleg_proofing(user: User, nin: str) -> FluxData | WerkzeugResponse: proofing_state = current_app.proofing_statedb.get_state_by_eppn(user.eppn) if not proofing_state: current_app.logger.debug(f"No proofing state found for user {user!s}. Initializing new proofing flow.") @@ -227,7 +227,7 @@ def get_freja_state(user: User) -> Mapping[str, Any]: @MarshalWith(schemas.FrejaResponseSchema) @can_verify_nin @require_user -def freja_proofing(user: User, nin: str) -> Union[FluxData, WerkzeugResponse]: +def freja_proofing(user: User, nin: str) -> FluxData | WerkzeugResponse: proofing_state = current_app.proofing_statedb.get_state_by_eppn(user.eppn) if not proofing_state: current_app.logger.debug(f"No proofing state found for user {user!s}. Initializing new proofing flow.") diff --git a/src/eduid/webapp/orcid/app.py b/src/eduid/webapp/orcid/app.py index d141905d6..8c71fef8a 100644 --- a/src/eduid/webapp/orcid/app.py +++ b/src/eduid/webapp/orcid/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -35,7 +35,7 @@ def __init__(self, config: OrcidConfig, **kwargs): current_orcid_app: OrcidApp = cast(OrcidApp, current_app) -def init_orcid_app(name: str = "orcid", test_config: Optional[Mapping[str, Any]] = None) -> OrcidApp: +def init_orcid_app(name: str = "orcid", test_config: Mapping[str, Any] | None = None) -> OrcidApp: """ :param name: The name of the instance, it will affect the configuration loaded. :param test_config: Override config, used in test cases. diff --git a/src/eduid/webapp/orcid/helpers.py b/src/eduid/webapp/orcid/helpers.py index 1f9884e89..24b899bcc 100644 --- a/src/eduid/webapp/orcid/helpers.py +++ b/src/eduid/webapp/orcid/helpers.py @@ -1,5 +1,4 @@ from enum import unique -from typing import Optional from pydantic import BaseModel, Field @@ -31,6 +30,6 @@ class OrcidMsg(TranslatableMsg): class OrcidUserinfo(BaseModel): orcid: HttpUrlStr = Field(alias="id") sub: str - name: Optional[str] = None + name: str | None = None family_name: str given_name: str diff --git a/src/eduid/webapp/personal_data/app.py b/src/eduid/webapp/personal_data/app.py index 405340d52..313c8a1ed 100644 --- a/src/eduid/webapp/personal_data/app.py +++ b/src/eduid/webapp/personal_data/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -25,7 +25,7 @@ def __init__(self, config: PersonalDataConfig, **kwargs): current_pdata_app: PersonalDataApp = cast(PersonalDataApp, current_app) -def pd_init_app(name: str = "personal_data", test_config: Optional[Mapping[str, Any]] = None) -> PersonalDataApp: +def pd_init_app(name: str = "personal_data", test_config: Mapping[str, Any] | None = None) -> PersonalDataApp: """ Create an instance of an eduid personal data app. diff --git a/src/eduid/webapp/personal_data/helpers.py b/src/eduid/webapp/personal_data/helpers.py index b684b1caf..b9bc996b6 100644 --- a/src/eduid/webapp/personal_data/helpers.py +++ b/src/eduid/webapp/personal_data/helpers.py @@ -1,7 +1,6 @@ import logging import re from enum import unique -from typing import Optional from eduid.common.config.base import FrontendAction from eduid.userdb import User @@ -40,7 +39,7 @@ def unique_name_parts(name: str) -> list[str]: return list(set(name_parts)) -def is_valid_chosen_given_name(given_name: Optional[str] = None, chosen_given_name: Optional[str] = None) -> bool: +def is_valid_chosen_given_name(given_name: str | None = None, chosen_given_name: str | None = None) -> bool: """ Validate the chosen given name is made up of a combination of given_name. """ @@ -72,7 +71,7 @@ def is_valid_chosen_given_name(given_name: Optional[str] = None, chosen_given_na return False -def check_reauthn(frontend_action: FrontendAction, user: User) -> Optional[FluxData]: +def check_reauthn(frontend_action: FrontendAction, user: User) -> FluxData | None: """Check if a re-authentication has been performed recently enough for this action""" authn_status = validate_authn_for_action(config=current_app.conf, frontend_action=frontend_action, user=user) diff --git a/src/eduid/webapp/personal_data/tests/test_app.py b/src/eduid/webapp/personal_data/tests/test_app.py index 6cf333c32..25aefca65 100644 --- a/src/eduid/webapp/personal_data/tests/test_app.py +++ b/src/eduid/webapp/personal_data/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import patch from werkzeug.test import TestResponse @@ -36,7 +36,7 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: # parameterized test methods - def _get_user(self, eppn: Optional[str] = None): + def _get_user(self, eppn: str | None = None): """ Send a GET request to get the personal data of a user @@ -65,7 +65,7 @@ def _get_user_all_data(self, eppn: str) -> TestResponse: @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") def _post_user( - self, mock_request_user_sync: Any, mod_data: Optional[dict[str, Any]] = None, verified_user: bool = True + self, mock_request_user_sync: Any, mod_data: dict[str, Any] | None = None, verified_user: bool = True ): """ POST personal data for the test user @@ -93,7 +93,7 @@ def _post_user( data.update(mod_data) return client.post("/user", data=json.dumps(data), content_type=self.content_type_json) - def _get_preferences(self, eppn: Optional[str] = None): + def _get_preferences(self, eppn: str | None = None): """ Send a GET request to get the personal data of a user @@ -109,7 +109,7 @@ def _get_preferences(self, eppn: Optional[str] = None): return response2 @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _post_preferences(self, mock_request_user_sync: Any, mod_data: Optional[dict[str, Any]] = None): + def _post_preferences(self, mock_request_user_sync: Any, mod_data: dict[str, Any] | None = None): """ POST preferences for the test user """ @@ -127,7 +127,7 @@ def _post_preferences(self, mock_request_user_sync: Any, mod_data: Optional[dict data["csrf_token"] = sess.get_csrf_token() return client.post("/preferences", json=data) - def _get_user_identities(self, eppn: Optional[str] = None): + def _get_user_identities(self, eppn: str | None = None): """ GET a list of all the identities of a user diff --git a/src/eduid/webapp/personal_data/views.py b/src/eduid/webapp/personal_data/views.py index 20eba64af..e45d5f961 100644 --- a/src/eduid/webapp/personal_data/views.py +++ b/src/eduid/webapp/personal_data/views.py @@ -1,5 +1,3 @@ -from typing import Optional - from flask import Blueprint from eduid.common.config.base import FrontendAction @@ -51,7 +49,7 @@ def get_user(user: User) -> FluxData: @MarshalWith(PersonalDataResponseSchema) @require_user def update_personal_data( - user: User, given_name: str, surname: str, language: str, chosen_given_name: Optional[str] = None + user: User, given_name: str, surname: str, language: str, chosen_given_name: str | None = None ) -> FluxData: personal_data_user = PersonalDataUser.from_user(user, current_app.private_userdb) current_app.logger.debug(f"Trying to save user {user}") diff --git a/src/eduid/webapp/phone/app.py b/src/eduid/webapp/phone/app.py index d5bf60905..b844d0256 100644 --- a/src/eduid/webapp/phone/app.py +++ b/src/eduid/webapp/phone/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -35,7 +35,7 @@ def __init__(self, config: PhoneConfig, **kwargs): current_phone_app: PhoneApp = cast(PhoneApp, current_app) -def phone_init_app(name: str = "phone", test_config: Optional[Mapping[str, Any]] = None) -> PhoneApp: +def phone_init_app(name: str = "phone", test_config: Mapping[str, Any] | None = None) -> PhoneApp: """ Create an instance of an eduid phone app. diff --git a/src/eduid/webapp/phone/tests/test_app.py b/src/eduid/webapp/phone/tests/test_app.py index 4dae8bb07..18dc4a029 100644 --- a/src/eduid/webapp/phone/tests/test_app.py +++ b/src/eduid/webapp/phone/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from urllib.parse import quote_plus @@ -37,7 +37,7 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: # parameterized test methods - def _get_all_phone(self, eppn: Optional[str] = None): + def _get_all_phone(self, eppn: str | None = None): """ GET all phone data for some user @@ -60,7 +60,7 @@ def _post_phone( mock_phone_validator: Any, mock_code_verification: Any, mock_request_user_sync: Any, - mod_data: Optional[dict[str, Any]] = None, + mod_data: dict[str, Any] | None = None, send_data: bool = True, ): """ @@ -93,7 +93,7 @@ def _post_phone( return client.post("/new") @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _post_primary(self, mock_request_user_sync: Any, mod_data: Optional[dict[str, Any]] = None): + def _post_primary(self, mock_request_user_sync: Any, mod_data: dict[str, Any] | None = None): """ Set phone number as the primary number for the test user @@ -116,7 +116,7 @@ def _post_primary(self, mock_request_user_sync: Any, mod_data: Optional[dict[str return client.post("/primary", data=json.dumps(data), content_type=self.content_type_json) @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _remove(self, mock_request_user_sync: Any, mod_data: Optional[dict[str, Any]] = None): + def _remove(self, mock_request_user_sync: Any, mod_data: dict[str, Any] | None = None): """ Remove phone number from the test user @@ -146,7 +146,7 @@ def _send_code( mock_phone_validator: Any, mock_request_user_sync: Any, mock_verification_code: Any, - mod_data: Optional[dict[str, Any]] = None, + mod_data: dict[str, Any] | None = None, captcha_completed: bool = True, ): """ @@ -179,10 +179,10 @@ def _get_code_backdoor( mock_phone_validator: Any, mock_code_verification: Any, mock_request_user_sync: Any, - mod_data: Optional[dict[str, Any]] = None, + mod_data: dict[str, Any] | None = None, phone: str = "+34670123456", code: str = "5250f9a4", - magic_cookie_name: Optional[str] = None, + magic_cookie_name: str | None = None, ): """ POST phone data to generate a verification state, diff --git a/src/eduid/webapp/phone/views.py b/src/eduid/webapp/phone/views.py index 307b9c574..ad65591ff 100644 --- a/src/eduid/webapp/phone/views.py +++ b/src/eduid/webapp/phone/views.py @@ -1,5 +1,3 @@ -from typing import Optional - from flask import Blueprint, abort, request from eduid.common.rpc.exceptions import MsgTaskFailed @@ -266,7 +264,7 @@ def captcha_request() -> FluxData: @phone_views.route("/captcha", methods=["POST"]) @UnmarshalWith(CaptchaCompleteRequest) @MarshalWith(PhoneResponseSchema) -def captcha_response(internal_response: Optional[str] = None) -> FluxData: +def captcha_response(internal_response: str | None = None) -> FluxData: """ Check for humanness. """ diff --git a/src/eduid/webapp/reset_password/app.py b/src/eduid/webapp/reset_password/app.py index 1ddcc97f6..e401596a2 100644 --- a/src/eduid/webapp/reset_password/app.py +++ b/src/eduid/webapp/reset_password/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -39,7 +39,7 @@ def __init__(self, config: ResetPasswordConfig, **kwargs): def init_reset_password_app( - name: str = "reset_password", test_config: Optional[Mapping[str, Any]] = None + name: str = "reset_password", test_config: Mapping[str, Any] | None = None ) -> ResetPasswordApp: """ :param name: The name of the instance, it will affect the configuration loaded. diff --git a/src/eduid/webapp/reset_password/helpers.py b/src/eduid/webapp/reset_password/helpers.py index c4777aec3..35405ef57 100644 --- a/src/eduid/webapp/reset_password/helpers.py +++ b/src/eduid/webapp/reset_password/helpers.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import timedelta from enum import unique -from typing import Any, Optional, Union +from typing import Any from flask import render_template @@ -86,13 +86,13 @@ class ResetPwMsg(TranslatableMsg): class StateException(Exception): - def __init__(self, msg: Optional[TranslatableMsg] = None): + def __init__(self, msg: TranslatableMsg | None = None): self.msg = msg @dataclass class ResetPasswordContext: - state: Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState] + state: ResetPasswordEmailState | ResetPasswordEmailAndPhoneState user: User @@ -114,7 +114,7 @@ def get_context(email_code: str) -> ResetPasswordContext: return ResetPasswordContext(state=state, user=user) -def get_pwreset_state(email_code: str) -> Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState]: +def get_pwreset_state(email_code: str) -> ResetPasswordEmailState | ResetPasswordEmailAndPhoneState: """ get the password reset state for the provided code @@ -231,7 +231,7 @@ def generate_suggested_password(password_length: int) -> str: def extra_security_used( - state: Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState], mfa_used: bool = False + state: ResetPasswordEmailState | ResetPasswordEmailAndPhoneState, mfa_used: bool = False ) -> bool: """ Check if any extra security method was used @@ -277,7 +277,7 @@ def unverify_user(user: ResetPasswordUser) -> None: def reset_user_password( user: User, - state: Union[ResetPasswordEmailState, ResetPasswordEmailAndPhoneState], + state: ResetPasswordEmailState | ResetPasswordEmailAndPhoneState, password: str, mfa_used: bool = False, ) -> FluxData: @@ -424,7 +424,7 @@ def send_verify_phone_code(state: ResetPasswordEmailState, phone_number: str): current_app.logger.debug(f"Phone number: {phone_state.phone_number}") -def send_sms(phone_number: str, text_template: str, reference: str, context: Optional[Mapping[str, Any]] = None): +def send_sms(phone_number: str, text_template: str, reference: str, context: Mapping[str, Any] | None = None): """ :param phone_number: the recipient of the sms :param text_template: message as a jinja template diff --git a/src/eduid/webapp/reset_password/tests/test_app.py b/src/eduid/webapp/reset_password/tests/test_app.py index 3b072f754..46440f609 100644 --- a/src/eduid/webapp/reset_password/tests/test_app.py +++ b/src/eduid/webapp/reset_password/tests/test_app.py @@ -1,7 +1,7 @@ import datetime import json from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from unittest.mock import Mock, patch from urllib.parse import quote_plus @@ -43,7 +43,7 @@ def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) self.other_test_user = UserFixtures().mocked_user_standard_2 - def load_app(self, config: Optional[Mapping[str, Any]]) -> ResetPasswordApp: + def load_app(self, config: Mapping[str, Any] | None) -> ResetPasswordApp: """ Called from the parent class, so we can provide the appropriate flask app for this test case. @@ -68,7 +68,7 @@ def update_config(self, config: dict[str, Any]): def _post_email_address( self, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, ): """ POST an email address to start the reset password process for the corresponding account. @@ -93,8 +93,8 @@ def _post_email_address( return response def _post_reset_code( - self, data1: Optional[dict[str, Any]] = None, data2: Optional[dict[str, Any]] = None - ) -> Optional[TestResponse]: + self, data1: dict[str, Any] | None = None, data2: dict[str, Any] | None = None + ) -> TestResponse | None: """ Create a password rest state for the test user, grab the created verification code from the db, and use it to get configuration for the reset form. @@ -125,8 +125,8 @@ def _post_reset_password( self, mock_request_user_sync: Any, mock_get_vccs_client: Any, - data1: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, ): """ Test sending data from the reset password form, without extra security. @@ -180,9 +180,9 @@ def _post_choose_extra_sec( mock_request_user_sync: Any, mock_get_vccs_client: Any, sendsms_side_effect: Any = None, - data1: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, - data3: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, + data3: dict[str, Any] | None = None, repeat: bool = False, ): """ @@ -243,8 +243,8 @@ def _post_reset_password_secure_phone( mock_sendsms: Any, mock_request_user_sync: Any, mock_get_vccs_client: Any, - data1: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, ): """ Test fully resetting the password with extra security via a verification code sent by SMS. @@ -301,11 +301,11 @@ def _post_reset_password_secure_token( mock_verify: Any, mock_request_user_sync: Any, mock_get_vccs_client: Any, - data1: Optional[dict[str, Any]] = None, - credential_data: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, - fido2state: Optional[WebauthnState] = None, - custom_password: Optional[str] = None, + data1: dict[str, Any] | None = None, + credential_data: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, + fido2state: WebauthnState | None = None, + custom_password: str | None = None, ): """ Test resetting the password with extra security via a fido token. @@ -370,10 +370,10 @@ def _post_reset_password_secure_external_mfa( self, mock_request_user_sync: Any, mock_get_vccs_client: Any, - data1: Optional[dict[str, Any]] = None, - data2: Optional[dict[str, Any]] = None, - external_mfa_state: Optional[dict[str, Any]] = None, - custom_password: Optional[str] = None, + data1: dict[str, Any] | None = None, + data2: dict[str, Any] | None = None, + external_mfa_state: dict[str, Any] | None = None, + custom_password: str | None = None, ): """ Test resetting the password with extra security via a external MFA. @@ -426,7 +426,7 @@ def _post_reset_password_secure_external_mfa( return c.post(url, data=json.dumps(data), content_type=self.content_type_json) - def _get_email_code_backdoor(self, data1: Optional[dict[str, Any]] = None, magic_cookie_name: Optional[str] = None): + def _get_email_code_backdoor(self, data1: dict[str, Any] | None = None, magic_cookie_name: str | None = None): """ Create a password rest state for the test user, grab the created verification code from the db, and use it to get configuration for the reset form. @@ -450,7 +450,7 @@ def _get_phone_code_backdoor( mock_request_user_sync: Any, mock_get_vccs_client: Any, sendsms_side_effect: Any = None, - magic_cookie_name: Optional[str] = None, + magic_cookie_name: str | None = None, ): """ Test choosing extra security via a confirmed phone number to reset the password, diff --git a/src/eduid/webapp/reset_password/views/reset_password.py b/src/eduid/webapp/reset_password/views/reset_password.py index 6a79ef051..26291b9d2 100644 --- a/src/eduid/webapp/reset_password/views/reset_password.py +++ b/src/eduid/webapp/reset_password/views/reset_password.py @@ -43,8 +43,6 @@ her data. """ -from typing import Optional - from flask import Blueprint, abort, request from eduid.common.rpc.exceptions import MailTaskFailed, MsgTaskFailed @@ -376,11 +374,11 @@ def set_new_pw_extra_security_phone(email_code: str, password: str, phone_code: def set_new_pw_extra_security_token( email_code: str, password: str, - token_response: Optional[str] = None, - authenticator_data: Optional[str] = None, - client_data_json: Optional[str] = None, - credential_id: Optional[str] = None, - signature: Optional[str] = None, + token_response: str | None = None, + authenticator_data: str | None = None, + client_data_json: str | None = None, + credential_id: str | None = None, + signature: str | None = None, ) -> FluxData: """ View that receives an emailed reset password code, hw token data, diff --git a/src/eduid/webapp/security/app.py b/src/eduid/webapp/security/app.py index 1d67c81d9..7379d8daf 100644 --- a/src/eduid/webapp/security/app.py +++ b/src/eduid/webapp/security/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from fido_mds import FidoMetadataStore from flask import current_app @@ -38,7 +38,7 @@ def __init__(self, config: SecurityConfig, **kwargs): current_security_app: SecurityApp = cast(SecurityApp, current_app) -def security_init_app(name: str = "security", test_config: Optional[Mapping[str, Any]] = None) -> SecurityApp: +def security_init_app(name: str = "security", test_config: Mapping[str, Any] | None = None) -> SecurityApp: """ Create an instance of an eduid security (passwords) app. diff --git a/src/eduid/webapp/security/helpers.py b/src/eduid/webapp/security/helpers.py index 4f66595fe..eeaa7a88f 100644 --- a/src/eduid/webapp/security/helpers.py +++ b/src/eduid/webapp/security/helpers.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from enum import unique from functools import cache -from typing import Any, Optional +from typing import Any from fido_mds.models.webauthn import AttestationFormat @@ -84,9 +84,9 @@ class CredentialInfo: key: str credential_type: str created_ts: datetime - success_ts: Optional[datetime] + success_ts: datetime | None verified: bool = False - description: Optional[str] = None + description: str | None = None def compile_credential_list(user: User) -> list[CredentialInfo]: @@ -98,7 +98,7 @@ def compile_credential_list(user: User) -> list[CredentialInfo]: for cred_key, authn in authn_info.items(): cred = user.credentials.find(cred_key) # pick up attributes not present on all types of credentials - _description: Optional[str] = None + _description: str | None = None _is_verified = False _d = getattr(cred, "description", None) if isinstance(_d, str): @@ -191,7 +191,7 @@ def send_termination_mail(user): current_app.logger.info(f"Sent termination mail to user {user} to address {email}.") -def check_reauthn(frontend_action: FrontendAction, user: User) -> Optional[FluxData]: +def check_reauthn(frontend_action: FrontendAction, user: User) -> FluxData | None: """Check if a re-authentication has been performed recently enough for this action""" authn_status = validate_authn_for_action(config=current_app.conf, frontend_action=frontend_action, user=user) diff --git a/src/eduid/webapp/security/settings/common.py b/src/eduid/webapp/security/settings/common.py index 26bf9982a..79f054c9d 100644 --- a/src/eduid/webapp/security/settings/common.py +++ b/src/eduid/webapp/security/settings/common.py @@ -1,5 +1,4 @@ from datetime import timedelta -from typing import Optional from fido2.webauthn import AttestationConveyancePreference from fido_mds.models.fido_mds import AuthenticatorStatus @@ -45,7 +44,7 @@ class SecurityConfig( webauthn_proofing_method: str = Field(default="webauthn metadata") webauthn_proofing_version: str = Field(default="2022v1") webauthn_max_allowed_tokens: int = 10 - webauthn_attestation: Optional[AttestationConveyancePreference] = None + webauthn_attestation: AttestationConveyancePreference | None = None webauthn_allowed_user_verification_methods: list[str] = Field( default=[ "faceprint_internal", diff --git a/src/eduid/webapp/security/tests/test_app.py b/src/eduid/webapp/security/tests/test_app.py index 4cd1b9c59..470216247 100644 --- a/src/eduid/webapp/security/tests/test_app.py +++ b/src/eduid/webapp/security/tests/test_app.py @@ -1,7 +1,7 @@ import json from collections.abc import Mapping from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from eduid.common.config.base import FrontendAction @@ -54,7 +54,7 @@ def _delete_account( self, mock_revoke: Any, mock_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, ): """ Send a GET request to the endpoint to actually terminate the account, @@ -75,7 +75,7 @@ def _delete_account( return client.post("/terminate-account", json=data) @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _remove_nin(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = None, unverify: bool = False): + def _remove_nin(self, mock_request_user_sync: Any, data1: dict[str, Any] | None = None, unverify: bool = False): """ Send a POST request to remove a NIN from the test user, possibly unverifying his verified NIN. @@ -104,7 +104,7 @@ def _remove_nin(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any return client.post("/remove-nin", data=json.dumps(data), content_type=self.content_type_json) @patch("eduid.common.rpc.am_relay.AmRelay.request_user_sync") - def _remove_identity(self, mock_request_user_sync: Any, data1: Optional[dict[str, Any]] = None): + def _remove_identity(self, mock_request_user_sync: Any, data1: dict[str, Any] | None = None): """ Send a POST request to remove all identities from the test user @@ -129,7 +129,7 @@ def _remove_identity(self, mock_request_user_sync: Any, data1: Optional[dict[str def _add_nin( self, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, remove: bool = True, unverify: bool = False, ): @@ -171,7 +171,7 @@ def _refresh_user_data( mock_request_user_sync: Any, mock_get_all_navet_data: Any, user: User, - navet_return_value: Optional[Any] = None, + navet_return_value: Any | None = None, ): mock_request_user_sync.side_effect = self.request_user_sync if navet_return_value is None: diff --git a/src/eduid/webapp/security/tests/test_change_password.py b/src/eduid/webapp/security/tests/test_change_password.py index 6b5384088..5a1059e3a 100644 --- a/src/eduid/webapp/security/tests/test_change_password.py +++ b/src/eduid/webapp/security/tests/test_change_password.py @@ -1,6 +1,6 @@ import json from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from unittest.mock import patch from eduid.common.config.base import FrontendAction @@ -46,7 +46,7 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: # parameterized test methods - def _get_suggested(self, reauthn: Optional[int] = 60): + def _get_suggested(self, reauthn: int | None = 60): """ GET a suggested password. """ @@ -58,7 +58,7 @@ def _get_suggested(self, reauthn: Optional[int] = 60): def _change_password( self, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, ): """ To change the password of the test user, POST old and new passwords, @@ -83,7 +83,7 @@ def _change_password( def _get_suggested_and_change( self, mock_request_user_sync: Any, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, correct_old_password: bool = True, ): """ diff --git a/src/eduid/webapp/security/tests/test_webauthn.py b/src/eduid/webapp/security/tests/test_webauthn.py index da951e56d..4b5a95ec5 100644 --- a/src/eduid/webapp/security/tests/test_webauthn.py +++ b/src/eduid/webapp/security/tests/test_webauthn.py @@ -1,7 +1,7 @@ import base64 import json from collections.abc import Mapping -from typing import Any, Optional +from typing import Any from unittest.mock import patch from fido2.webauthn import AttestationObject, AuthenticatorAttachment, CollectedClientData @@ -194,10 +194,10 @@ def _check_removal(self, data, user_token): # parameterized test methods def _begin_register_key( self, - other: Optional[str] = None, + other: str | None = None, authenticator: str = "cross-platform", existing_legacy_token: bool = False, - csrf: Optional[str] = None, + csrf: str | None = None, check_session: bool = True, ): """ @@ -255,7 +255,7 @@ def _finish_register_key( state: dict, cred_id: bytes, existing_legacy_token: bool = False, - csrf: Optional[str] = None, + csrf: str | None = None, ): """ Finish registering a webauthn token. @@ -318,7 +318,7 @@ def _remove( attestation_2: bytes, state_2: dict, existing_legacy_token: bool = False, - csrf: Optional[str] = None, + csrf: str | None = None, ): """ Send a POST request to remove a webauthn credential from the test user. diff --git a/src/eduid/webapp/security/views/change_password.py b/src/eduid/webapp/security/views/change_password.py index 955da805f..76ad33b7d 100644 --- a/src/eduid/webapp/security/views/change_password.py +++ b/src/eduid/webapp/security/views/change_password.py @@ -1,5 +1,3 @@ -from typing import Optional - from flask import Blueprint from eduid.common.config.base import FrontendAction @@ -52,7 +50,7 @@ def get_suggested(user: User) -> FluxData: @UnmarshalWith(ChangePasswordRequestSchema) @MarshalWith(SecurityResponseSchema) @require_user -def change_password_view(user: User, new_password: str, old_password: Optional[str] = None) -> FluxData: +def change_password_view(user: User, new_password: str, old_password: str | None = None) -> FluxData: """ View to change the password """ diff --git a/src/eduid/webapp/security/views/webauthn.py b/src/eduid/webapp/security/views/webauthn.py index 833dce68f..484b7a212 100644 --- a/src/eduid/webapp/security/views/webauthn.py +++ b/src/eduid/webapp/security/views/webauthn.py @@ -1,6 +1,5 @@ import base64 from collections.abc import Sequence -from typing import Optional from fido2 import cbor from fido2.server import Fido2Server, PublicKeyCredentialRpEntity @@ -54,7 +53,7 @@ def get_webauthn_server( - rp_id: str, rp_name: str, attestation: Optional[AttestationConveyancePreference] = None + rp_id: str, rp_name: str, attestation: AttestationConveyancePreference | None = None ) -> Fido2Server: rp = PublicKeyCredentialRpEntity(id=rp_id, name=rp_name) return Fido2Server(rp, attestation=attestation) diff --git a/src/eduid/webapp/security/webauthn_proofing.py b/src/eduid/webapp/security/webauthn_proofing.py index e34787bf4..c195f49b8 100644 --- a/src/eduid/webapp/security/webauthn_proofing.py +++ b/src/eduid/webapp/security/webauthn_proofing.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field from datetime import date, datetime, time from enum import Enum -from typing import Optional, Union from uuid import UUID from fido2.utils import websafe_decode @@ -24,14 +23,14 @@ class OtherAuthenticatorStatus(str, Enum): @dataclass class AuthenticatorInformation: - authenticator_id: Union[UUID, str] + authenticator_id: UUID | str attestation_format: AttestationFormat user_present: bool user_verified: bool - status: Optional[Union[AuthenticatorStatus, OtherAuthenticatorStatus]] = field(default=None) - last_status_change: Optional[datetime] = field(default=None) - icon: Optional[str] = field(default=None) - description: Optional[str] = field(default=None) + status: AuthenticatorStatus | OtherAuthenticatorStatus | None = field(default=None) + last_status_change: datetime | None = field(default=None) + icon: str | None = field(default=None) + description: str | None = field(default=None) key_protection: list[str] = field(default_factory=list) user_verification_methods: list[str] = field(default_factory=list) diff --git a/src/eduid/webapp/signup/app.py b/src/eduid/webapp/signup/app.py index 13201503f..24160280b 100644 --- a/src/eduid/webapp/signup/app.py +++ b/src/eduid/webapp/signup/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app @@ -48,7 +48,7 @@ def get_scim_client_for(self, data_owner: str) -> SCIMClient: current_signup_app: SignupApp = cast(SignupApp, current_app) -def signup_init_app(name: str = "signup", test_config: Optional[Mapping[str, Any]] = None) -> SignupApp: +def signup_init_app(name: str = "signup", test_config: Mapping[str, Any] | None = None) -> SignupApp: """ Create an instance of an eduid signup app. diff --git a/src/eduid/webapp/signup/helpers.py b/src/eduid/webapp/signup/helpers.py index 25a77394d..61746c5e0 100644 --- a/src/eduid/webapp/signup/helpers.py +++ b/src/eduid/webapp/signup/helpers.py @@ -3,7 +3,6 @@ from dataclasses import replace from datetime import datetime from enum import Enum, unique -from typing import Optional import proquint from flask import abort @@ -202,8 +201,8 @@ def create_and_sync_user( surname: str, email: str, tou_version: str, - generated_password: Optional[str] = None, - custom_password: Optional[str] = None, + generated_password: str | None = None, + custom_password: str | None = None, ) -> SignupUser: """ * Create a new user in the central userdb @@ -420,13 +419,13 @@ def update_or_create_scim_user(invite: Invite, signup_user: SignupUser) -> UserR return client.update_user(user=update_user, version=scim_user.meta.version) -def is_email_verification_expired(sent_ts: Optional[datetime]) -> bool: +def is_email_verification_expired(sent_ts: datetime | None) -> bool: if sent_ts is None: return True return utc_now() - sent_ts > current_app.conf.email_verification_timeout -def is_valid_custom_password(custom_password: Optional[str]) -> bool: +def is_valid_custom_password(custom_password: str | None) -> bool: if custom_password is None: return False diff --git a/src/eduid/webapp/signup/settings/common.py b/src/eduid/webapp/signup/settings/common.py index 51468dde5..6640c24eb 100644 --- a/src/eduid/webapp/signup/settings/common.py +++ b/src/eduid/webapp/signup/settings/common.py @@ -1,6 +1,5 @@ # from datetime import timedelta -from typing import Optional from pydantic import Field @@ -45,7 +44,7 @@ class SignupConfig( default_finish_url: str = "https://www.eduid.se/" eduid_site_url: str = "https://www.eduid.se" # TODO: Backwards compatibility, remove when no longer used eduid_site_name: str = "eduID" - scim_api_url: Optional[AnyUrlStr] = None - gnap_auth_data: Optional[GNAPClientAuthData] = None + scim_api_url: AnyUrlStr | None = None + gnap_auth_data: GNAPClientAuthData | None = None eduid_scope: str = "eduid.se" - private_userdb_auto_expire: Optional[timedelta] = Field(default=timedelta(days=7)) + private_userdb_auto_expire: timedelta | None = Field(default=timedelta(days=7)) diff --git a/src/eduid/webapp/signup/tests/test_app.py b/src/eduid/webapp/signup/tests/test_app.py index ac855df9d..7f2f672ca 100644 --- a/src/eduid/webapp/signup/tests/test_app.py +++ b/src/eduid/webapp/signup/tests/test_app.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -88,7 +88,7 @@ def update_config(self, config: dict[str, Any]) -> dict[str, Any]: def _get_captcha( self, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, + expected_message: TranslatableMsg | None = None, logged_in: bool = False, ): eppn = None @@ -139,12 +139,12 @@ def _get_state(self, logged_in: bool = False) -> SignupResult: # parameterized test methods def _captcha( self, - captcha_data: Optional[Mapping[str, Any]] = None, + captcha_data: Mapping[str, Any] | None = None, add_magic_cookie: bool = False, - magic_cookie_name: Optional[str] = None, + magic_cookie_name: str | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -215,13 +215,13 @@ def _captcha( def _register_email( self, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, given_name: str = "Test", surname: str = "Testdotter", email: str = "dummy@example.com", expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -289,10 +289,10 @@ def _register_email( def _verify_email( self, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -357,12 +357,12 @@ def _verify_email( def _accept_tou( self, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, accept_tou: bool = True, - tou_version: Optional[str] = None, + tou_version: str | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -427,10 +427,10 @@ def _accept_tou( def _generate_password( self, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -494,7 +494,7 @@ def _prepare_for_create_user( tou_accepted: bool = True, captcha_completed: bool = True, email_verified: bool = True, - generated_password: Optional[str] = "test_password", + generated_password: str | None = "test_password", logged_in: bool = False, ): eppn = None @@ -519,11 +519,11 @@ def _create_user( self, mock_add_credentials: Any, mock_request_user_sync: Any, - data: Optional[dict[str, Any]] = None, - custom_password: Optional[str] = None, + data: dict[str, Any] | None = None, + custom_password: str | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -631,11 +631,11 @@ def _get_invite_data( self, email: str, invite_code: str, - eppn: Optional[str] = None, - data1: Optional[dict[str, Any]] = None, + eppn: str | None = None, + data1: dict[str, Any] | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): """ @@ -708,10 +708,10 @@ def _accept_invite( email: str, invite_code: str, email_verified: bool = True, - data1: Optional[dict[str, Any]] = None, + data1: dict[str, Any] | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, logged_in: bool = False, ): eppn = None @@ -773,11 +773,11 @@ def _accept_invite( def _complete_invite( self, mock_request_user_sync: MagicMock, - eppn: Optional[str] = None, - data1: Optional[dict[str, Any]] = None, + eppn: str | None = None, + data1: dict[str, Any] | None = None, expect_success: bool = True, - expected_message: Optional[TranslatableMsg] = None, - expected_payload: Optional[Mapping[str, Any]] = None, + expected_message: TranslatableMsg | None = None, + expected_payload: Mapping[str, Any] | None = None, ): mock_request_user_sync.side_effect = self.request_user_sync logged_in = False @@ -832,7 +832,7 @@ def _complete_invite( return SignupResult(url=endpoint, reached_state=SignupState.S7_COMPLETE_INVITE, response=response) - def _get_code_backdoor(self, email: str, magic_cookie_name: Optional[str] = None): + def _get_code_backdoor(self, email: str, magic_cookie_name: str | None = None): """ Test getting the generated verification code through the backdoor """ diff --git a/src/eduid/webapp/signup/views.py b/src/eduid/webapp/signup/views.py index 91e952ca2..824359caa 100644 --- a/src/eduid/webapp/signup/views.py +++ b/src/eduid/webapp/signup/views.py @@ -1,4 +1,3 @@ -from typing import Optional from uuid import uuid4 from flask import Blueprint, abort, request @@ -204,7 +203,7 @@ def captcha_request() -> FluxData: @UnmarshalWith(CaptchaCompleteRequest) @MarshalWith(SignupStatusResponse) @require_not_logged_in -def captcha_response(internal_response: Optional[str] = None) -> FluxData: +def captcha_response(internal_response: str | None = None) -> FluxData: """ Check for humanness even at level AL1. """ @@ -263,7 +262,7 @@ def get_password() -> FluxData: @UnmarshalWith(CreateUserRequest) @MarshalWith(SignupStatusResponse) @require_not_logged_in -def create_user(use_suggested_password: bool, use_webauthn: bool, custom_password: Optional[str] = None) -> FluxData: +def create_user(use_suggested_password: bool, use_webauthn: bool, custom_password: str | None = None) -> FluxData: current_app.logger.info("Creating user") use_password = False diff --git a/src/eduid/webapp/support/app.py b/src/eduid/webapp/support/app.py index ae38a32f0..0c9ed3b8a 100644 --- a/src/eduid/webapp/support/app.py +++ b/src/eduid/webapp/support/app.py @@ -1,6 +1,6 @@ import operator from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from flask import current_app from jinja2.exceptions import UndefinedError @@ -62,7 +62,7 @@ def sort_multi(items, *operators, **kwargs): return items @app.template_global() - def static_url_for(f: str, version: Optional[str] = None) -> str: + def static_url_for(f: str, version: str | None = None) -> str: """ Get the static url for a file and optionally have a version argument appended for cache busting. """ @@ -74,7 +74,7 @@ def static_url_for(f: str, version: Optional[str] = None) -> str: return None -def support_init_app(name: str = "support", test_config: Optional[Mapping[str, Any]] = None) -> SupportApp: +def support_init_app(name: str = "support", test_config: Mapping[str, Any] | None = None) -> SupportApp: """ Create an instance of an eduid support app. diff --git a/src/eduid/webapp/svipe_id/app.py b/src/eduid/webapp/svipe_id/app.py index 5ff5ad6b3..63403013e 100644 --- a/src/eduid/webapp/svipe_id/app.py +++ b/src/eduid/webapp/svipe_id/app.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any, Optional, cast +from typing import Any, cast from authlib.integrations.flask_client import OAuth from flask import current_app @@ -49,7 +49,7 @@ def __init__(self, config: SvipeIdConfig, **kwargs): current_svipe_id_app = cast(SvipeIdApp, current_app) -def svipe_id_init_app(name: str = "svipe_id", test_config: Optional[Mapping[str, Any]] = None) -> SvipeIdApp: +def svipe_id_init_app(name: str = "svipe_id", test_config: Mapping[str, Any] | None = None) -> SvipeIdApp: """ :param name: The name of the instance, it will affect the configuration loaded. :param test_config: Override config. Used in test cases. diff --git a/src/eduid/webapp/svipe_id/helpers.py b/src/eduid/webapp/svipe_id/helpers.py index ad34e5215..daa2a1a05 100644 --- a/src/eduid/webapp/svipe_id/helpers.py +++ b/src/eduid/webapp/svipe_id/helpers.py @@ -1,7 +1,7 @@ import logging from datetime import date from enum import unique -from typing import Any, Optional +from typing import Any from iso3166 import countries from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -40,7 +40,7 @@ def get(key: str) -> Any: return session.svipe_id.rp.authlib_cache.get(key) @staticmethod - def set(key: str, value: Any, expires: Optional[int] = None) -> None: + def set(key: str, value: Any, expires: int | None = None) -> None: session.svipe_id.rp.authlib_cache[key] = value logger.debug(f"Set {key}={value} (expires={expires}) in session.svipe_id.oauth_cache") @@ -66,9 +66,7 @@ class UserInfoBase(BaseModel): class SvipeDocumentUserInfo(UserInfoBase): birthdate: date - document_administrative_number: Optional[str] = Field( - alias="com.svipe:document_administrative_number", default=None - ) + document_administrative_number: str | None = Field(alias="com.svipe:document_administrative_number", default=None) document_expiry_date: date = Field(alias="com.svipe:document_expiry_date") # Issuing Country: SWE document_issuing_country: str = Field(alias="com.svipe:document_issuing_country") @@ -79,7 +77,7 @@ class SvipeDocumentUserInfo(UserInfoBase): document_type_sdn_en: str = Field(alias="com.svipe:document_type_sdn_en") family_name: str given_name: str - name: Optional[str] = None + name: str | None = None svipe_id: str = Field(alias="com.svipe:svipeid") transaction_id: str = Field(alias="com.svipe:meta_transaction_id") diff --git a/src/eduid/webapp/svipe_id/proofing.py b/src/eduid/webapp/svipe_id/proofing.py index 99fb572ce..24f102ba7 100644 --- a/src/eduid/webapp/svipe_id/proofing.py +++ b/src/eduid/webapp/svipe_id/proofing.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from datetime import datetime -from typing import Optional from iso3166 import countries from pymongo.errors import PyMongoError @@ -44,7 +43,7 @@ def is_swedish_document(self) -> bool: return True return False - def get_identity(self, user: User) -> Optional[IdentityElement]: + def get_identity(self, user: User) -> IdentityElement | None: if self.is_swedish_document(): return user.identities.nin return user.identities.svipe @@ -219,7 +218,7 @@ def match_identity(self, user: User, proofing_method: ProofingMethod) -> MatchRe def credential_proofing_element(self, user: User, credential: Credential) -> ProofingElementResult: raise NotImplementedError("No support for credential proofing") - def mark_credential_as_verified(self, credential: Credential, loa: Optional[str]) -> VerifyCredentialResult: + def mark_credential_as_verified(self, credential: Credential, loa: str | None) -> VerifyCredentialResult: raise NotImplementedError("No support for credential proofing") diff --git a/src/eduid/webapp/svipe_id/settings/common.py b/src/eduid/webapp/svipe_id/settings/common.py index 7b2979552..98cac6f5e 100644 --- a/src/eduid/webapp/svipe_id/settings/common.py +++ b/src/eduid/webapp/svipe_id/settings/common.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field from eduid.common.clients.oidc_client.base import AuthlibClientConfig @@ -18,7 +16,7 @@ class SvipeClientConfig(AuthlibClientConfig): acr_values: list[str] = Field(default=["face_present"]) scopes: list[str] = Field(default=["openid"]) - claims_request: dict[str, Union[None, dict[str, bool]]] = Field( + claims_request: dict[str, None | dict[str, bool]] = Field( default={ "birthdate": {"essential": True}, "com.svipe:document_administrative_number": {"essential": True}, diff --git a/src/eduid/webapp/svipe_id/tests/test_app.py b/src/eduid/webapp/svipe_id/tests/test_app.py index 56f1b8619..e80da2ea4 100644 --- a/src/eduid/webapp/svipe_id/tests/test_app.py +++ b/src/eduid/webapp/svipe_id/tests/test_app.py @@ -1,6 +1,6 @@ import json from datetime import date, datetime, timedelta -from typing import Any, Optional +from typing import Any from unittest.mock import MagicMock, patch from urllib.parse import parse_qs, urlparse @@ -146,15 +146,15 @@ def _user_setup(self): def get_mock_userinfo( issuing_country: Country, nationality: Country, - administrative_number: Optional[str] = "123456789", + administrative_number: str | None = "123456789", birthdate: date = date(year=1901, month=2, day=3), svipe_id: str = "unique_svipe_id", transaction_id: str = "unique_transaction_id", given_name: str = "Test", family_name: str = "Testsson", now: datetime = utc_now(), - userinfo_expires: Optional[datetime] = None, - document_expires: Optional[datetime] = None, + userinfo_expires: datetime | None = None, + document_expires: datetime | None = None, ) -> SvipeDocumentUserInfo: if userinfo_expires is None: userinfo_expires = now + timedelta(minutes=5) diff --git a/src/eduid/webapp/svipe_id/views.py b/src/eduid/webapp/svipe_id/views.py index 3b8ffbc65..4b249e93f 100644 --- a/src/eduid/webapp/svipe_id/views.py +++ b/src/eduid/webapp/svipe_id/views.py @@ -1,6 +1,5 @@ import json from dataclasses import dataclass -from typing import Optional from urllib.parse import parse_qs, urlparse from authlib.integrations.base_client import OAuthError @@ -62,7 +61,7 @@ def get_status(authn_id: OIDCState) -> FluxData: @UnmarshalWith(SvipeIDCommonRequestSchema) @MarshalWith(SvipeIDCommonResponseSchema) @require_user -def verify_identity(user: User, method: str, frontend_action: str, frontend_state: Optional[str] = None) -> FluxData: +def verify_identity(user: User, method: str, frontend_action: str, frontend_state: str | None = None) -> FluxData: res = _authn(SvipeIDAction.verify_identity, method, frontend_action, frontend_state) if res.error: current_app.logger.error(f"Failed to start verify identity: {res.error}") @@ -72,17 +71,17 @@ def verify_identity(user: User, method: str, frontend_action: str, frontend_stat @dataclass class AuthnResult: - authn_req: Optional[RP_AuthnRequest] = None - authn_id: Optional[OIDCState] = None - error: Optional[TranslatableMsg] = None - url: Optional[str] = None + authn_req: RP_AuthnRequest | None = None + authn_id: OIDCState | None = None + error: TranslatableMsg | None = None + url: str | None = None def _authn( action: SvipeIDAction, method: str, frontend_action: str, - frontend_state: Optional[str] = None, + frontend_state: str | None = None, ) -> AuthnResult: current_app.logger.debug(f"Requested method: {method}, frontend action: {frontend_action}") @@ -140,7 +139,7 @@ def authn_callback(user) -> WerkzeugResponse: current_app.logger.debug("authn_callback called") current_app.logger.debug(f"request.args: {request.args}") authn_req = None - oidc_state: Optional[OIDCState] = None + oidc_state: OIDCState | None = None if "state" in request.args: oidc_state = OIDCState(request.args["state"]) if oidc_state is not None: diff --git a/src/eduid/workers/am/ams/common.py b/src/eduid/workers/am/ams/common.py index ba87d5c37..e396ae9a0 100644 --- a/src/eduid/workers/am/ams/common.py +++ b/src/eduid/workers/am/ams/common.py @@ -1,7 +1,7 @@ __author__ = "eperez" from abc import ABC, abstractmethod -from typing import Any, Optional +from typing import Any import bson from celery.utils.log import get_task_logger @@ -23,7 +23,7 @@ def __init__(self, worker_config: AmConfig): if not isinstance(worker_config, AmConfig): raise TypeError("AttributeFetcher config should be AmConfig") self.conf = worker_config - self.private_db: Optional[UserDB[User]] = None + self.private_db: UserDB[User] | None = None if worker_config.mongo_uri: self.private_db = self.get_user_db(worker_config.mongo_uri) diff --git a/src/eduid/workers/am/consistency_checks.py b/src/eduid/workers/am/consistency_checks.py index 12a8a6d1c..a2e9f860f 100644 --- a/src/eduid/workers/am/consistency_checks.py +++ b/src/eduid/workers/am/consistency_checks.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from bson import ObjectId from celery.utils.log import get_task_logger @@ -40,7 +40,7 @@ def unverify_duplicates(userdb: AmDB, user_id: ObjectId, attributes: dict) -> di return {"mail_count": mail_count, "phone_count": phone_count, "nin_count": nin_count} -def unverify_mail_aliases(userdb: AmDB, user_id: ObjectId, mail_aliases: Optional[list[dict[str, Any]]]) -> int: +def unverify_mail_aliases(userdb: AmDB, user_id: ObjectId, mail_aliases: list[dict[str, Any]] | None) -> int: """ :param userdb: Central userdb :param user_id: User document _id diff --git a/src/eduid/workers/am/tasks.py b/src/eduid/workers/am/tasks.py index 9c32f757c..beef6a0d8 100644 --- a/src/eduid/workers/am/tasks.py +++ b/src/eduid/workers/am/tasks.py @@ -1,5 +1,3 @@ -from typing import Optional - import bson from celery import Task from celery.utils.log import get_task_logger @@ -20,10 +18,10 @@ class AttributeManager(Task): abstract = True # This means Celery won't register this as another task def __init__(self): - self._userdb: Optional[AmDB] = None + self._userdb: AmDB | None = None @property - def userdb(self) -> Optional[AmDB]: + def userdb(self) -> AmDB | None: if self._userdb: return self._userdb if AmCelerySingleton.worker_config.mongo_uri: diff --git a/src/eduid/workers/am/testing.py b/src/eduid/workers/am/testing.py index 69836e9f0..07f4343fd 100644 --- a/src/eduid/workers/am/testing.py +++ b/src/eduid/workers/am/testing.py @@ -7,7 +7,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone -from typing import Any, Optional +from typing import Any from uuid import UUID import bson @@ -107,7 +107,7 @@ class WorkerTestCase(CommonTestCase): """ def setUp( # type: ignore[override] - self, *args: Any, am_settings: Optional[dict[str, Any]] = None, want_mongo_uri: bool = True, **kwargs: Any + self, *args: Any, am_settings: dict[str, Any] | None = None, want_mongo_uri: bool = True, **kwargs: Any ): """ set up tests @@ -153,8 +153,8 @@ def tearDown(self): class ProofingTestCase(AMTestCase): - fetcher_name: Optional[str] = None - fetcher: Optional[AttributeFetcher] = None + fetcher_name: str | None = None + fetcher: AttributeFetcher | None = None def setUp(self, *args: Any, **kwargs: Any): super().setUp(*args, **kwargs) diff --git a/src/eduid/workers/amapi/app.py b/src/eduid/workers/amapi/app.py index 4dd610f9c..97867d0a4 100644 --- a/src/eduid/workers/amapi/app.py +++ b/src/eduid/workers/amapi/app.py @@ -1,5 +1,3 @@ -from typing import Optional - from fastapi import FastAPI from eduid.common.config.parsers import load_config @@ -19,14 +17,14 @@ class AMAPI(FastAPI): - def __init__(self, name: str = "am_api", test_config: Optional[dict] = None): + def __init__(self, name: str = "am_api", test_config: dict | None = None): self.config = load_config(typ=AMApiConfig, app_name=name, ns="api", test_config=test_config) super().__init__() self.context = Context(config=self.config) self.context.logger.info(f"Starting {name} app") -def init_api(name: str = "am_api", test_config: Optional[dict] = None) -> AMAPI: +def init_api(name: str = "am_api", test_config: dict | None = None) -> AMAPI: app = AMAPI(name=name, test_config=test_config) app.router.route_class = ContextRequestRoute diff --git a/src/eduid/workers/amapi/config.py b/src/eduid/workers/amapi/config.py index 50b5cd169..4cbcc9db1 100644 --- a/src/eduid/workers/amapi/config.py +++ b/src/eduid/workers/amapi/config.py @@ -1,7 +1,7 @@ import logging from enum import Enum from pathlib import Path -from typing import NewType, Optional +from typing import NewType from pydantic import BaseModel, Field, field_validator @@ -48,5 +48,5 @@ class AMApiConfig(RootConfig, LoggingConfigMixin): keystore_path: Path no_authn_urls: list[str] = Field(default=["/status/healthy", "/openapi.json"]) status_cache_seconds: int = 10 - requested_access_type: Optional[str] = "am_api" + requested_access_type: str | None = "am_api" user_restriction: dict[ServiceName, list[EndpointRestriction]] diff --git a/src/eduid/workers/amapi/context_request.py b/src/eduid/workers/amapi/context_request.py index e1b36d87a..2a054036a 100644 --- a/src/eduid/workers/amapi/context_request.py +++ b/src/eduid/workers/amapi/context_request.py @@ -2,7 +2,6 @@ from collections.abc import Callable from dataclasses import asdict, dataclass -from typing import Union from fastapi import Request, Response from fastapi.routing import APIRoute @@ -34,7 +33,7 @@ def context(self, context: Context): class ContextRequestMixin: @staticmethod - def make_context_request(request: Union[Request, ContextRequest]) -> ContextRequest: + def make_context_request(request: Request | ContextRequest) -> ContextRequest: if not isinstance(request, ContextRequest): request = ContextRequest(request.scope, request.receive) return request @@ -48,7 +47,7 @@ class ContextRequestRoute(APIRoute, ContextRequestMixin): def get_route_handler(self) -> Callable: original_route_handler = super().get_route_handler() - async def context_route_handler(request: Union[Request, ContextRequest]) -> Response: + async def context_route_handler(request: Request | ContextRequest) -> Response: request = self.make_context_request(request) return await original_route_handler(request) diff --git a/src/eduid/workers/amapi/routers/utils/status.py b/src/eduid/workers/amapi/routers/utils/status.py index d2dad6f2c..e0033dd82 100644 --- a/src/eduid/workers/amapi/routers/utils/status.py +++ b/src/eduid/workers/amapi/routers/utils/status.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from dataclasses import dataclass, field, replace from datetime import datetime, timedelta -from typing import Any, Optional +from typing import Any from fastapi import Response @@ -20,9 +20,9 @@ class SimpleCacheItem: @dataclass class FailCountItem: first_failure: datetime = field(repr=False) - restart_at: Optional[datetime] = None - restart_interval: Optional[int] = None - exit_at: Optional[datetime] = None + restart_at: datetime | None = None + restart_interval: int | None = None + exit_at: datetime | None = None count: int = 0 def __str__(self): @@ -33,7 +33,7 @@ def __str__(self): FAILURE_INFO: dict[str, FailCountItem] = dict() -def log_failure_info(ctx: ContextRequest, key: str, msg: str, exc: Optional[Exception] = None) -> None: +def log_failure_info(ctx: ContextRequest, key: str, msg: str, exc: Exception | None = None) -> None: if key not in FAILURE_INFO: FAILURE_INFO[key] = FailCountItem(first_failure=datetime.utcnow()) FAILURE_INFO[key].count += 1 @@ -67,7 +67,7 @@ def check_restart(key, restart: int, terminate: int) -> bool: return res -def get_cached_response(ctx: ContextRequest, resp: Response, key: str) -> Optional[Mapping[str, Any]]: +def get_cached_response(ctx: ContextRequest, resp: Response, key: str) -> Mapping[str, Any] | None: cache_for_seconds = ctx.app.config.status_cache_seconds resp.headers["Cache-Control"] = f"public,max-age={cache_for_seconds}" diff --git a/src/eduid/workers/amapi/routers/utils/users.py b/src/eduid/workers/amapi/routers/utils/users.py index 6f009aef2..3f94d0c03 100644 --- a/src/eduid/workers/amapi/routers/utils/users.py +++ b/src/eduid/workers/amapi/routers/utils/users.py @@ -1,5 +1,3 @@ -from typing import Union - from deepdiff import DeepDiff from eduid.common.misc.timeutil import utc_now @@ -21,14 +19,12 @@ def update_user( req: ContextRequest, eppn: str, - data: Union[ - UserUpdateEmailRequest, - UserUpdateNameRequest, - UserUpdateLanguageRequest, - UserUpdatePhoneRequest, - UserUpdateMetaCleanedRequest, - UserUpdateTerminateRequest, - ], + data: UserUpdateEmailRequest + | UserUpdateNameRequest + | UserUpdateLanguageRequest + | UserUpdatePhoneRequest + | UserUpdateMetaCleanedRequest + | UserUpdateTerminateRequest, ) -> UserUpdateResponse: """General function for updating a user object""" user_obj = req.app.context.db.get_user_by_eppn(eppn=eppn) diff --git a/src/eduid/workers/amapi/tests/test_user.py b/src/eduid/workers/amapi/tests/test_user.py index db06f0777..6324b8fce 100644 --- a/src/eduid/workers/amapi/tests/test_user.py +++ b/src/eduid/workers/amapi/tests/test_user.py @@ -1,7 +1,7 @@ __author__ = "masv" import datetime -from typing import Any, Optional +from typing import Any from bson import ObjectId from fastapi import status @@ -20,7 +20,7 @@ class TestUsers(TestAMBase, GNAPBearerTokenMixin): def setUp(self, *args, **kwargs): super().setUp(am_users=[UserFixtures().new_user_example]) - def _make_url(self, endpoint: Optional[str] = None) -> str: + def _make_url(self, endpoint: str | None = None) -> str: if endpoint is None: return f"/users/{self.eppn}" return f"/users/{self.eppn}/{endpoint}" @@ -33,7 +33,7 @@ def _check_audit_log(self, diff: dict[str, Any]) -> None: assert audit_logs[0].source == self.source assert audit_logs[0].diff == self.as_json(diff) - def make_put_call(self, json_data: dict, oauth_header: Headers, endpoint: Optional[str] = None) -> Response: + def make_put_call(self, json_data: dict, oauth_header: Headers, endpoint: str | None = None) -> Response: response = self.client.put( url=self._make_url(endpoint), json=json_data, diff --git a/src/eduid/workers/job_runner/app.py b/src/eduid/workers/job_runner/app.py index cfc51fc53..70de4dad8 100644 --- a/src/eduid/workers/job_runner/app.py +++ b/src/eduid/workers/job_runner/app.py @@ -1,6 +1,5 @@ from collections.abc import Callable from contextlib import asynccontextmanager -from typing import Optional from fastapi import FastAPI @@ -14,9 +13,7 @@ class JobRunner(FastAPI): scheduler: JobScheduler = JobScheduler(timezone="UTC") - def __init__( - self, name: str = "job_runner", test_config: Optional[dict] = None, lifespan: Optional[Callable] = None - ): + def __init__(self, name: str = "job_runner", test_config: dict | None = None, lifespan: Callable | None = None): self.config = load_config(typ=JobRunnerConfig, app_name=name, ns="worker", test_config=test_config) super().__init__(root_path=self.config.application_root, lifespan=lifespan) @@ -33,7 +30,7 @@ async def lifespan(app: JobRunner): app.scheduler.shutdown() -def init_app(name: str = "job_runner", test_config: Optional[dict] = None) -> JobRunner: +def init_app(name: str = "job_runner", test_config: dict | None = None) -> JobRunner: app = JobRunner(name, test_config, lifespan=lifespan) app.context.logger.info(app.config) diff --git a/src/eduid/workers/job_runner/config.py b/src/eduid/workers/job_runner/config.py index b48164cd3..074ae4c88 100644 --- a/src/eduid/workers/job_runner/config.py +++ b/src/eduid/workers/job_runner/config.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, tzinfo -from typing import Any, NewType, Optional, Union +from typing import Any, NewType from pydantic import BaseModel, ConfigDict, field_validator, model_validator @@ -19,18 +19,18 @@ class JobCronConfig(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") - year: Optional[Union[int, str]] = None - month: Optional[Union[int, str]] = None - day: Optional[Union[int, str]] = None - week: Optional[Union[int, str]] = None - day_of_week: Optional[Union[int, str]] = None - hour: Optional[Union[int, str]] = None - minute: Optional[Union[int, str]] = None - second: Optional[Union[int, str]] = None - start_date: Optional[Union[datetime, str]] = None - end_date: Optional[Union[datetime, str]] = None - timezone: Optional[Union[tzinfo, str]] = None - jitter: Optional[int] = None + year: int | str | None = None + month: int | str | None = None + day: int | str | None = None + week: int | str | None = None + day_of_week: int | str | None = None + hour: int | str | None = None + minute: int | str | None = None + second: int | str | None = None + start_date: datetime | str | None = None + end_date: datetime | str | None = None + timezone: tzinfo | str | None = None + jitter: int | None = None @model_validator(mode="before") @classmethod @@ -55,7 +55,7 @@ class JobRunnerConfig(RootConfig, LoggingConfigMixin, StatsConfigMixin, MsgConfi log_format: str = "{asctime} | {levelname:7} | {hostname} | {name:35} | {module:10} | {message}" mongo_uri: str = "" status_cache_seconds: int = 10 - jobs: Optional[JobsConfig] = None + jobs: JobsConfig | None = None gnap_auth_data: GNAPClientAuthData @field_validator("application_root") diff --git a/src/eduid/workers/lookup_mobile/client/mobile_lookup_client.py b/src/eduid/workers/lookup_mobile/client/mobile_lookup_client.py index 0acdd97f6..8c839b2be 100644 --- a/src/eduid/workers/lookup_mobile/client/mobile_lookup_client.py +++ b/src/eduid/workers/lookup_mobile/client/mobile_lookup_client.py @@ -1,5 +1,3 @@ -from typing import Optional - from suds.client import Client from eduid.common.config.base import EduidEnvironment @@ -17,7 +15,7 @@ def __init__(self, logger, config: MobConfig) -> None: # enable transaction logging if configured self.transaction_audit = self.conf.transaction_audit and self.conf.mongo_uri - self._client: Optional[Client] = None + self._client: Client | None = None self.logger = logger @property @@ -49,7 +47,7 @@ def find_mobiles_by_NIN(self, national_identity_number: str, number_region=None) return format_mobile_number(mobiles, number_region) @TransactionAudit() - def find_NIN_by_mobile(self, mobile_number) -> Optional[str]: + def find_NIN_by_mobile(self, mobile_number) -> str | None: nin = self._search_by_mobile(mobile_number) if not nin: self.logger.debug(f"Did not get search result from mobile number: {mobile_number}") @@ -103,7 +101,7 @@ def _search_by_SSNo(self, national_identity_number: str) -> list[str]: return mobile_numbers - def _search_by_mobile(self, mobile_number: str) -> Optional[str]: + def _search_by_mobile(self, mobile_number: str) -> str | None: person_search = self._get_find_person() # Set the eduid user id and password diff --git a/src/eduid/workers/lookup_mobile/tasks.py b/src/eduid/workers/lookup_mobile/tasks.py index 1b1fcadd2..fa201b292 100644 --- a/src/eduid/workers/lookup_mobile/tasks.py +++ b/src/eduid/workers/lookup_mobile/tasks.py @@ -1,5 +1,3 @@ -from typing import Optional - from celery import Task from celery.utils.log import get_task_logger @@ -18,7 +16,7 @@ class MobWorker(Task): abstract = True # This means Celery won't register this as another task def __init__(self): - self._lookup_client: Optional[MobileLookupClient] = None + self._lookup_client: MobileLookupClient | None = None @property def lookup_client(self) -> MobileLookupClient: @@ -39,7 +37,7 @@ def find_mobiles_by_NIN(self: MobWorker, national_identity_number: str, number_r @app.task(bind=True, base=MobWorker, name="eduid_lookup_mobile.tasks.find_NIN_by_mobile") -def find_NIN_by_mobile(self: MobWorker, mobile_number: str) -> Optional[str]: +def find_NIN_by_mobile(self: MobWorker, mobile_number: str) -> str | None: """ Searches nin with the registered mobile number :return: the nin with the registered mobile number diff --git a/src/eduid/workers/lookup_mobile/utilities.py b/src/eduid/workers/lookup_mobile/utilities.py index a7b7c02ec..5030caea7 100644 --- a/src/eduid/workers/lookup_mobile/utilities.py +++ b/src/eduid/workers/lookup_mobile/utilities.py @@ -2,12 +2,11 @@ import re from collections.abc import Sequence -from typing import Optional import phonenumbers -def format_NIN(nin: Optional[str]) -> Optional[str]: +def format_NIN(nin: str | None) -> str | None: if nin is None: return None @@ -16,7 +15,7 @@ def format_NIN(nin: Optional[str]) -> Optional[str]: return nin -def format_mobile_number(numbers: Sequence[str], region: Optional[str]) -> list[str]: +def format_mobile_number(numbers: Sequence[str], region: str | None) -> list[str]: """ Format a list of numbers to E.164 standard :param numbers: a list of phone numbers @@ -26,6 +25,6 @@ def format_mobile_number(numbers: Sequence[str], region: Optional[str]) -> list[ return [_format_number(x, region) for x in numbers] -def _format_number(number: str, region: Optional[str]): +def _format_number(number: str, region: str | None): """Parse a number and reconstruct it to the canonical E.164 format""" return phonenumbers.format_number(phonenumbers.parse(number, region), phonenumbers.PhoneNumberFormat.E164) diff --git a/src/eduid/workers/msg/cache.py b/src/eduid/workers/msg/cache.py index d6d08eca9..c8dd69a74 100644 --- a/src/eduid/workers/msg/cache.py +++ b/src/eduid/workers/msg/cache.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from eduid.userdb.db import BaseDB, TUserDbDocument from eduid.userdb.util import utc_now @@ -23,7 +23,7 @@ def add_cache_item(self, identifier: str, data: dict[str, Any]): self._coll.insert_one(TUserDbDocument(doc)) return True - def get_cache_item(self, identifier: str) -> Optional[dict[str, Any]]: + def get_cache_item(self, identifier: str) -> dict[str, Any] | None: query = {"identifier": identifier} result = self._coll.find_one(query) if result is not None: diff --git a/src/eduid/workers/msg/decorators.py b/src/eduid/workers/msg/decorators.py index 5f75032b1..835ba5a24 100644 --- a/src/eduid/workers/msg/decorators.py +++ b/src/eduid/workers/msg/decorators.py @@ -1,14 +1,14 @@ from collections.abc import Callable from datetime import datetime from inspect import isclass -from typing import Any, Optional +from typing import Any from eduid.userdb.db import MongoDB class TransactionAudit: enabled = False - db_uri: Optional[str] = None + db_uri: str | None = None db_name: str = "eduid_msg" collection_name: str = "transaction_audit" @@ -40,7 +40,7 @@ def audit(*args, **kwargs): return audit @classmethod - def enable(cls, db_uri: str, db_name: Optional[str] = None): + def enable(cls, db_uri: str, db_name: str | None = None): # if not isinstance(db_uri, str) or not db_uri: # raise ValueError('Invalid db_uri passed to TransactionAudit') if isinstance(db_uri, str): diff --git a/src/eduid/workers/msg/tasks.py b/src/eduid/workers/msg/tasks.py index c73eb122f..53687e6b6 100644 --- a/src/eduid/workers/msg/tasks.py +++ b/src/eduid/workers/msg/tasks.py @@ -2,7 +2,7 @@ import logging import smtplib from collections import OrderedDict -from typing import Any, Optional +from typing import Any from celery import Task from celery.utils.log import get_task_logger @@ -41,8 +41,8 @@ class MessageSender(Task): abstract = True - _sms: Optional[SMSClient] = None - _navet_api: Optional[Hammock] = None + _sms: SMSClient | None = None + _navet_api: Hammock | None = None @property def sms(self) -> SMSClient: @@ -103,8 +103,8 @@ def send_message( recipient: str, template: str, language: str, - subject: Optional[str] = None, - ) -> Optional[str]: + subject: str | None = None, + ) -> str | None: """ :param message_type: Message notification type (sms or mm) :param reference: Unique reference id @@ -146,7 +146,7 @@ def send_message( logger.debug(f"send_message result: {status}") return status - def get_postal_address(self, identity_number: str) -> Optional[OrderedDict[str, Any]]: + def get_postal_address(self, identity_number: str) -> OrderedDict[str, Any] | None: """ Fetch name and postal address from NAVET @@ -180,7 +180,7 @@ def get_devel_postal_address() -> OrderedDict[str, Any]: ) return result - def get_relations(self, identity_number: str) -> Optional[OrderedDict[str, Any]]: + def get_relations(self, identity_number: str) -> OrderedDict[str, Any] | None: """ Fetch information about someones relatives from NAVET @@ -228,7 +228,7 @@ def get_devel_relations() -> OrderedDict[str, Any]: ) return result - def get_all_navet_data(self, identity_number: str) -> Optional[OrderedDict[str, Any]]: + def get_all_navet_data(self, identity_number: str) -> OrderedDict[str, Any] | None: # Only log the message if devel_mode is enabled if MsgCelerySingleton.worker_config.devel_mode is True: return self.get_devel_all_navet_data(identity_number) @@ -282,7 +282,7 @@ def get_devel_all_navet_data(identity_number: str = "190102031234") -> OrderedDi return result @TransactionAudit() - def _get_navet_data(self, identity_number: str) -> Optional[dict[str, Any]]: + def _get_navet_data(self, identity_number: str) -> dict[str, Any] | None: """ Fetch all data about a NIN from Navet. @@ -379,7 +379,7 @@ def sendsms(self, recipient: str, message: str, reference: str) -> str: return self.sms.send(message, MsgCelerySingleton.worker_config.sms_sender, recipient, prio=2) - def pong(self, app_name: Optional[str]) -> str: + def pong(self, app_name: str | None) -> str: # Leverage cache to test mongo db health if self.cache("pong", 0).is_healthy(): if app_name: @@ -431,7 +431,7 @@ def sendsms(self: MessageSender, recipient: str, message: str, reference: str) - @app.task(bind=True, base=MessageSender, name="eduid_msg.tasks.get_all_navet_data") -def get_all_navet_data(self: MessageSender, identity_number: str) -> Optional[OrderedDict[str, Any]]: +def get_all_navet_data(self: MessageSender, identity_number: str) -> OrderedDict[str, Any] | None: """ Retrieve all data about the person from the Swedish population register using a Swedish national identity number. @@ -450,7 +450,7 @@ def get_all_navet_data(self: MessageSender, identity_number: str) -> Optional[Or @app.task(bind=True, base=MessageSender, name="eduid_msg.tasks.get_postal_address") -def get_postal_address(self: MessageSender, identity_number: str) -> Optional[OrderedDict[str, Any]]: +def get_postal_address(self: MessageSender, identity_number: str) -> OrderedDict[str, Any] | None: """ Retrieve name and postal address from the Swedish population register using a Swedish national identity number. @@ -532,7 +532,7 @@ def set_audit_log_postal_address(self: MessageSender, audit_reference: str) -> b @app.task(bind=True, base=MessageSender, name="eduid_msg.tasks.pong") -def pong(self: MessageSender, app_name: Optional[str] = None) -> str: +def pong(self: MessageSender, app_name: str | None = None) -> str: """ eduID webapps periodically ping workers as a part of their health assessment. diff --git a/src/eduid/workers/msg/utils.py b/src/eduid/workers/msg/utils.py index e4a704ae5..d474fa534 100644 --- a/src/eduid/workers/msg/utils.py +++ b/src/eduid/workers/msg/utils.py @@ -5,10 +5,10 @@ import os from collections import OrderedDict from collections.abc import Mapping -from typing import Any, Optional +from typing import Any -def is_deregistered(person: Optional[dict[str, Any]]) -> bool: +def is_deregistered(person: dict[str, Any] | None) -> bool: if person is None: return False deregistration_information = person["DeregistrationInformation"] @@ -36,7 +36,7 @@ def load_template(template_dir: str, filename: str, message_dict: Mapping[str, s raise RuntimeError("template not found") -def navet_get_name_and_official_address(navet_data: Optional[dict[str, Any]]) -> Optional[OrderedDict[str, Any]]: +def navet_get_name_and_official_address(navet_data: dict[str, Any] | None) -> OrderedDict[str, Any] | None: """ :param navet_data: Loaded JSON response from eduid-navet_service :return: Name and official address data objects @@ -55,7 +55,7 @@ def navet_get_name_and_official_address(navet_data: Optional[dict[str, Any]]) -> return None -def navet_get_relations(navet_data: Optional[dict[str, Any]]) -> Optional[OrderedDict[str, Any]]: +def navet_get_relations(navet_data: dict[str, Any] | None) -> OrderedDict[str, Any] | None: """ :param navet_data: Loaded JSON response from eduid-navet_service :return: Relations data object @@ -72,7 +72,7 @@ def navet_get_relations(navet_data: Optional[dict[str, Any]]) -> Optional[Ordere return None -def navet_get_person(navet_data: Optional[dict[str, Any]]) -> Optional[OrderedDict[str, Any]]: +def navet_get_person(navet_data: dict[str, Any] | None) -> OrderedDict[str, Any] | None: """ :param navet_data: Loaded JSON response from eduid-navet_service :return: Personpost @@ -112,7 +112,7 @@ def navet_get_person(navet_data: Optional[dict[str, Any]]) -> Optional[OrderedDi return None -def navet_get_all_data(navet_data: Optional[dict[str, Any]]) -> Optional[OrderedDict[str, Any]]: +def navet_get_all_data(navet_data: dict[str, Any] | None) -> OrderedDict[str, Any] | None: """ :param navet_data: Loaded JSON response from eduid-navet_service :return: all available data from Navet From 3e1e38e8c30710ff7ed3f79a24e897818d39393e Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 14:41:46 +0000 Subject: [PATCH 13/23] use updated union operator update isinstance calls to use updated | operator for union types PEP604 --- src/eduid/common/misc/encoders.py | 2 +- src/eduid/common/testing_base.py | 2 +- src/eduid/webapp/common/api/decorators.py | 2 +- src/eduid/webapp/idp/settings/common.py | 11 +---------- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/eduid/common/misc/encoders.py b/src/eduid/common/misc/encoders.py index 96f659469..3a366eba6 100644 --- a/src/eduid/common/misc/encoders.py +++ b/src/eduid/common/misc/encoders.py @@ -16,7 +16,7 @@ def default(self, o: Any) -> str | Any: return o.isoformat() if isinstance(o, timedelta): return TypeAdapter(timedelta).dump_python(o, mode="json") - if isinstance(o, (ObjectId, NameID)): + if isinstance(o, ObjectId | NameID): return str(o) if isinstance(o, Enum): return o.value diff --git a/src/eduid/common/testing_base.py b/src/eduid/common/testing_base.py index 92f2504ea..9566edaed 100644 --- a/src/eduid/common/testing_base.py +++ b/src/eduid/common/testing_base.py @@ -53,7 +53,7 @@ def default(self, o: Any) -> str | Any: o = o.replace(microsecond=0) return o.isoformat() - if isinstance(o, (ObjectId, uuid.UUID, Enum, Exception)): + if isinstance(o, ObjectId | uuid.UUID | Enum | Exception): return str(o) # catch all for wierd stuff in pydantic 2 errors diff --git a/src/eduid/webapp/common/api/decorators.py b/src/eduid/webapp/common/api/decorators.py index 8b601562f..f3840a631 100644 --- a/src/eduid/webapp/common/api/decorators.py +++ b/src/eduid/webapp/common/api/decorators.py @@ -145,7 +145,7 @@ def marshal_decorator(*args: Any, **kwargs: Any) -> WerkzeugResponse: # or in special cases an WerkzeugResponse (e.g. when a redirect is performed). ret = f(*args, **kwargs) - if isinstance(ret, (WerkzeugResponse, FlaskResponse)): + if isinstance(ret, WerkzeugResponse | FlaskResponse): # No need to Marshal again, someone else already did that return ret diff --git a/src/eduid/webapp/idp/settings/common.py b/src/eduid/webapp/idp/settings/common.py index 63bfcb305..00cf60c9d 100644 --- a/src/eduid/webapp/idp/settings/common.py +++ b/src/eduid/webapp/idp/settings/common.py @@ -174,15 +174,6 @@ def validate_sso_session_lifetime(cls, v): if isinstance(v, int): # legacy format for this was number of minutes v = v * 60 - if not ( - isinstance( - v, - ( - int, - str, - timedelta, - ), - ) - ): + if not isinstance(v, int | str | timedelta): raise ValueError("Invalid sso_session_lifetime (must be int, str or timedelta)") return v From d4bbd0eab9c0f6e4b27c9298b8a67a94fc743d17 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 14:49:18 +0000 Subject: [PATCH 14/23] remove unnecessary utf-8 encoding declaration as it is the default --- src/eduid/common/clients/oidc_client/__init__.py | 1 - src/eduid/common/clients/oidc_client/base.py | 2 -- src/eduid/common/models/generic.py | 2 -- src/eduid/common/models/saml2.py | 2 -- src/eduid/common/models/saml_models.py | 1 - src/eduid/queue/client.py | 2 -- src/eduid/satosa/entity_category/__init__.py | 1 - src/eduid/userdb/db/__init__.py | 2 -- src/eduid/userdb/db/async_db.py | 1 - src/eduid/userdb/db/base.py | 1 - src/eduid/webapp/common/api/captcha.py | 1 - src/eduid/webapp/common/api/schemas/authn_status.py | 1 - 12 files changed, 17 deletions(-) diff --git a/src/eduid/common/clients/oidc_client/__init__.py b/src/eduid/common/clients/oidc_client/__init__.py index 73c8b5f0c..97fffd58e 100644 --- a/src/eduid/common/clients/oidc_client/__init__.py +++ b/src/eduid/common/clients/oidc_client/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __author__ = "lundberg" diff --git a/src/eduid/common/clients/oidc_client/base.py b/src/eduid/common/clients/oidc_client/base.py index 6f913acf7..03068f9f4 100644 --- a/src/eduid/common/clients/oidc_client/base.py +++ b/src/eduid/common/clients/oidc_client/base.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from pydantic import AnyUrl, BaseModel, Field __author__ = "lundberg" diff --git a/src/eduid/common/models/generic.py b/src/eduid/common/models/generic.py index 578509810..431e79803 100644 --- a/src/eduid/common/models/generic.py +++ b/src/eduid/common/models/generic.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from typing import Annotated, Any from bson import ObjectId diff --git a/src/eduid/common/models/saml2.py b/src/eduid/common/models/saml2.py index f69065dd5..50e3647b1 100644 --- a/src/eduid/common/models/saml2.py +++ b/src/eduid/common/models/saml2.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from enum import Enum, unique __author__ = "lundberg" diff --git a/src/eduid/common/models/saml_models.py b/src/eduid/common/models/saml_models.py index b413532c0..70e86853b 100644 --- a/src/eduid/common/models/saml_models.py +++ b/src/eduid/common/models/saml_models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging from datetime import datetime diff --git a/src/eduid/queue/client.py b/src/eduid/queue/client.py index 7ddf476c4..da236b976 100644 --- a/src/eduid/queue/client.py +++ b/src/eduid/queue/client.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from datetime import timedelta diff --git a/src/eduid/satosa/entity_category/__init__.py b/src/eduid/satosa/entity_category/__init__.py index 73c8b5f0c..97fffd58e 100644 --- a/src/eduid/satosa/entity_category/__init__.py +++ b/src/eduid/satosa/entity_category/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- __author__ = "lundberg" diff --git a/src/eduid/userdb/db/__init__.py b/src/eduid/userdb/db/__init__.py index c8cc0a0d6..51dace9c8 100644 --- a/src/eduid/userdb/db/__init__.py +++ b/src/eduid/userdb/db/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - # Keep existing imports working from eduid.userdb.db.sync_db import BaseDB, MongoDB, SaveResult, TUserDbDocument diff --git a/src/eduid/userdb/db/async_db.py b/src/eduid/userdb/db/async_db.py index 8d565bec2..1412a69c4 100644 --- a/src/eduid/userdb/db/async_db.py +++ b/src/eduid/userdb/db/async_db.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging from collections.abc import Mapping from typing import Any diff --git a/src/eduid/userdb/db/base.py b/src/eduid/userdb/db/base.py index ccff66eb1..36884d55e 100644 --- a/src/eduid/userdb/db/base.py +++ b/src/eduid/userdb/db/base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import copy import logging from collections.abc import Mapping diff --git a/src/eduid/webapp/common/api/captcha.py b/src/eduid/webapp/common/api/captcha.py index f084f5605..09765614f 100644 --- a/src/eduid/webapp/common/api/captcha.py +++ b/src/eduid/webapp/common/api/captcha.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from base64 import b64encode from io import BytesIO diff --git a/src/eduid/webapp/common/api/schemas/authn_status.py b/src/eduid/webapp/common/api/schemas/authn_status.py index d0cdb206c..13b9f26f2 100644 --- a/src/eduid/webapp/common/api/schemas/authn_status.py +++ b/src/eduid/webapp/common/api/schemas/authn_status.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from enum import Enum, unique from marshmallow import fields From e9d077d9f8d1443bf450c891d0a818717ef5ef7c Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 15:02:44 +0000 Subject: [PATCH 15/23] use Path.open like in similar cases in the code --- src/eduid/maccapi/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/maccapi/util.py b/src/eduid/maccapi/util.py index 4c427370d..c52edd4af 100644 --- a/src/eduid/maccapi/util.py +++ b/src/eduid/maccapi/util.py @@ -16,7 +16,7 @@ def make_presentable_password(password: str) -> str: def load_jwks(config: MAccApiConfig) -> jwk.JWKSet: if not config.keystore_path.exists(): raise BadConfiguration(f"JWKS path {config.keystore_path} does not exist") - with open(config.keystore_path, "r") as f: + with config.keystore_path.open("r") as f: jwks = jwk.JWKSet.from_json(f.read()) logger.info(f"jwks loaded from {config.keystore_path}") return jwks From ef446ffaf90af6cb175c601b9e8e0b59c51c3d87 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 15:04:18 +0000 Subject: [PATCH 16/23] remove unnecessary encode to utf-8 --- src/eduid/satosa/scimapi/serve_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/satosa/scimapi/serve_static.py b/src/eduid/satosa/scimapi/serve_static.py index 05e1447bb..9d8efd3cf 100644 --- a/src/eduid/satosa/scimapi/serve_static.py +++ b/src/eduid/satosa/scimapi/serve_static.py @@ -56,7 +56,7 @@ def _handle(self, context): mimetype = mimetypes.guess_type(file)[0] logger.debug(f"mimetype {mimetype}") except IOError: - response = "File not found".encode() + response = "File not found" mimetype = "text/html" status = "404 Not Found" From 1f80f10779bcb53a27732914c2f6ae4c590a20d4 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 15:09:57 +0000 Subject: [PATCH 17/23] replace OSError alias since python 3.3 IOError and a few others have been merged into OSError and is only an alias --- src/eduid/satosa/scimapi/serve_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/satosa/scimapi/serve_static.py b/src/eduid/satosa/scimapi/serve_static.py index 9d8efd3cf..41d00fffe 100644 --- a/src/eduid/satosa/scimapi/serve_static.py +++ b/src/eduid/satosa/scimapi/serve_static.py @@ -55,7 +55,7 @@ def _handle(self, context): response = f.read() mimetype = mimetypes.guess_type(file)[0] logger.debug(f"mimetype {mimetype}") - except IOError: + except OSError: response = "File not found" mimetype = "text/html" status = "404 Not Found" From 38fa5e82da8d5e7dd74284fcba2fa47fa756ab61 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 15:32:16 +0000 Subject: [PATCH 18/23] update to f-strings --- src/eduid/queue/workers/base.py | 2 +- src/eduid/userdb/admin/__init__.py | 21 +++++++------------ src/eduid/userdb/db/base.py | 9 ++------ src/eduid/userdb/exceptions.py | 6 +----- src/eduid/vccs/client/__init__.py | 7 +------ .../webapp/authn/tests/saml2_settings.py | 6 +++--- .../webapp/bankid/tests/saml2_settings.py | 6 +++--- src/eduid/webapp/common/api/exceptions.py | 4 +--- src/eduid/webapp/common/authn/idp_conf.py | 16 +++++++------- src/eduid/webapp/common/authn/utils.py | 2 +- .../webapp/eidas/tests/saml2_settings.py | 6 +++--- src/eduid/webapp/email/verifications.py | 4 +--- src/eduid/webapp/email/views.py | 10 ++++----- src/eduid/webapp/idp/idp_authn.py | 4 +--- src/eduid/webapp/idp/sso_cache.py | 4 +--- 15 files changed, 38 insertions(+), 69 deletions(-) diff --git a/src/eduid/queue/workers/base.py b/src/eduid/queue/workers/base.py index 9ebab7f08..5b50883f7 100644 --- a/src/eduid/queue/workers/base.py +++ b/src/eduid/queue/workers/base.py @@ -22,7 +22,7 @@ def cancel_task(signame, task): - logger.info("got signal %s: exit" % signame) + logger.info(f"got signal {signame}: exit") task.cancel() diff --git a/src/eduid/userdb/admin/__init__.py b/src/eduid/userdb/admin/__init__.py index 48a649cdd..39d0a913e 100644 --- a/src/eduid/userdb/admin/__init__.py +++ b/src/eduid/userdb/admin/__init__.py @@ -67,8 +67,8 @@ def find(self, db: str, collection: str, search_filter: Any) -> Generator[RawDat yield RawData(doc, db, collection) except PyMongoError as exc: sys.stderr.write( - "{}\n\nFailed reading from mongodb ({}.{}) - " - "try sourcing the file /root/.mongo_credentials first?\n".format(exc, db, collection) + f"{exc}\n\nFailed reading from mongodb ({db}.{collection}) - " + "try sourcing the file /root/.mongo_credentials first?\n" ) sys.exit(1) @@ -84,9 +84,8 @@ def save_with_backup(self, raw: RawData, dry_run: bool = True) -> Any: if not os.path.isdir(self._backupbase): sys.stderr.write( - "\n\nBackup basedir {} not found, " "running in a container without the volume mounted?\n".format( - self._backupbase - ) + f"\n\nBackup basedir {self._backupbase} not found, " + "running in a container without the volume mounted?\n" ) sys.exit(1) @@ -156,12 +155,7 @@ def safe_encode(k2: Any, v2: Any) -> str: if k not in raw.before: continue if raw.doc[k] != raw.before[k]: - fd.write( - "MOD: BEFORE={} AFTER={}\n".format( - safe_encode(k, raw.before[k]), - safe_encode(k, raw.doc[k]), - ) - ) + fd.write(f"MOD: BEFORE={safe_encode(k, raw.before[k])} AFTER={safe_encode(k, raw.doc[k])}\n") fd.write(f"DB_RESULT: {res}\n") return res @@ -209,9 +203,8 @@ def _make_backupdir(self, db_coll: str, _id: str) -> str: if not os.path.isdir(self._backupbase): sys.stderr.write( - "\n\nBackup basedir {} not found, running in a container " "without the volume mounted?\n".format( - self._backupbase - ) + f"\n\nBackup basedir {self._backupbase} not found, running in a container " + "without the volume mounted?\n" ) sys.exit(1) diff --git a/src/eduid/userdb/db/base.py b/src/eduid/userdb/db/base.py index 36884d55e..b6b0009ec 100644 --- a/src/eduid/userdb/db/base.py +++ b/src/eduid/userdb/db/base.py @@ -119,11 +119,6 @@ def _format_mongodb_uri(parsed_uri: Mapping[str, Any]) -> str: db_name = parsed_uri.get("database") or "" - res = "mongodb://{user_pass!s}{nodelist!s}/{db_name!s}{options!s}".format( - user_pass=user_pass, - nodelist=nodelist, - db_name=db_name, - # collection is ignored - options=options, - ) + # collection is ignored + res = f"mongodb://{user_pass!s}{nodelist!s}/{db_name!s}{options!s}" return res diff --git a/src/eduid/userdb/exceptions.py b/src/eduid/userdb/exceptions.py index df5c0689f..c411f80f8 100644 --- a/src/eduid/userdb/exceptions.py +++ b/src/eduid/userdb/exceptions.py @@ -18,11 +18,7 @@ def __init__(self, reason: Any): self.reason = reason def __str__(self): - return "<{cl} instance at {addr}: {reason!r}>".format( - cl=self.__class__.__name__, - addr=hex(id(self)), - reason=self.reason, - ) + return f"<{self.__class__.__name__} instance at {hex(id(self))}: {self.reason!r}>" class ConnectionError(EduIDDBError): diff --git a/src/eduid/vccs/client/__init__.py b/src/eduid/vccs/client/__init__.py index 60a8123ab..e61aa767a 100644 --- a/src/eduid/vccs/client/__init__.py +++ b/src/eduid/vccs/client/__init__.py @@ -74,12 +74,7 @@ def __init__(self, reason: str, http_code: int): self.http_code = http_code def __str__(self): - return "<{cl} instance at {addr}: {code!r} {reason!r}>".format( - cl=self.__class__.__name__, - addr=hex(id(self)), - code=self.http_code, - reason=self.reason, - ) + return f"<{self.__class__.__name__} instance at {hex(id(self))}: {self.http_code!r} {self.reason!r}>" class VCCSFactor: diff --git a/src/eduid/webapp/authn/tests/saml2_settings.py b/src/eduid/webapp/authn/tests/saml2_settings.py index 098557b02..337446124 100644 --- a/src/eduid/webapp/authn/tests/saml2_settings.py +++ b/src/eduid/webapp/authn/tests/saml2_settings.py @@ -12,7 +12,7 @@ # full path to the xmlsec1 binary programm "xmlsec_binary": "/usr/bin/xmlsec1", # your entity id, usually your subdomain plus the url to the metadata view - "entityid": "%ssaml2-metadata" % BASE_URL, + "entityid": f"{BASE_URL}saml2-metadata", # directory with attribute mapping "attribute_map_dir": DEFAULT_ATTRIBUTEMAPS, # this block states what services we provide @@ -24,12 +24,12 @@ # url and binding to the assetion consumer service view # do not change the binding or service name "assertion_consumer_service": [ - ("%ssaml2-acs" % BASE_URL, saml2.BINDING_HTTP_POST), + (f"{BASE_URL}saml2-acs", saml2.BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name "single_logout_service": [ - ("%ssaml2-ls" % BASE_URL, saml2.BINDING_HTTP_REDIRECT), + (f"{BASE_URL}saml2-ls", saml2.BINDING_HTTP_REDIRECT), ], }, # Do not check for signature during tests diff --git a/src/eduid/webapp/bankid/tests/saml2_settings.py b/src/eduid/webapp/bankid/tests/saml2_settings.py index 277874385..fd5d6ac23 100644 --- a/src/eduid/webapp/bankid/tests/saml2_settings.py +++ b/src/eduid/webapp/bankid/tests/saml2_settings.py @@ -12,7 +12,7 @@ # full path to the xmlsec1 binary programm "xmlsec_binary": "/usr/bin/xmlsec1", # your entity id, usually your subdomain plus the url to the metadata view - "entityid": "%ssaml2-metadata" % BASE_URL, + "entityid": f"{BASE_URL}saml2-metadata", # directory with attribute mapping "attribute_map_dir": DEFAULT_ATTRIBUTEMAPS, "allow_unknown_attributes": True, # Allow eduidIdPCredentialsUsed @@ -25,12 +25,12 @@ # url and binding to the assetion consumer service view # do not change the binding or service name "assertion_consumer_service": [ - ("%ssaml2-acs" % BASE_URL, saml2.BINDING_HTTP_POST), + (f"{BASE_URL}saml2-acs", saml2.BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name "single_logout_service": [ - ("%ssaml2-ls" % BASE_URL, saml2.BINDING_HTTP_REDIRECT), + (f"{BASE_URL}saml2-ls", saml2.BINDING_HTTP_REDIRECT), ], }, # Do not check for signature during tests diff --git a/src/eduid/webapp/common/api/exceptions.py b/src/eduid/webapp/common/api/exceptions.py index d4b1e3888..5e0d11b2a 100644 --- a/src/eduid/webapp/common/api/exceptions.py +++ b/src/eduid/webapp/common/api/exceptions.py @@ -30,9 +30,7 @@ def __init__( self.payload = payload def __repr__(self): - return "ApiException (message={!s}, status_code={!s}, payload={!r})".format( - self.message, self.status_code, self.payload - ) + return f"ApiException (message={self.message!s}, status_code={self.status_code!s}, payload={self.payload!r})" def __unicode__(self): return self.__str__() diff --git a/src/eduid/webapp/common/authn/idp_conf.py b/src/eduid/webapp/common/authn/idp_conf.py index b0e3c2114..999c8e87a 100644 --- a/src/eduid/webapp/common/authn/idp_conf.py +++ b/src/eduid/webapp/common/authn/idp_conf.py @@ -22,27 +22,27 @@ BASE = "http://localhost:8088" CONFIG = { - "entityid": "%s/idp.xml" % BASE, + "entityid": f"{BASE}/idp.xml", "description": "My IDP", "service": { "aa": { - "endpoints": {"attribute_service": [("%s/attr" % BASE, BINDING_SOAP)]}, + "endpoints": {"attribute_service": [(f"{BASE}/attr", BINDING_SOAP)]}, "name_id_format": [NAMEID_FORMAT_TRANSIENT, NAMEID_FORMAT_PERSISTENT], }, "aq": { - "endpoints": {"authn_query_service": [("%s/aqs" % BASE, BINDING_SOAP)]}, + "endpoints": {"authn_query_service": [(f"{BASE}/aqs", BINDING_SOAP)]}, }, "idp": { "name": "Rolands IdP", "endpoints": { "single_sign_on_service": [ - ("%s/sso/redirect" % BASE, BINDING_HTTP_REDIRECT), - ("%s/sso/post" % BASE, BINDING_HTTP_POST), + (f"{BASE}/sso/redirect", BINDING_HTTP_REDIRECT), + (f"{BASE}/sso/post", BINDING_HTTP_POST), ], "single_logout_service": [ - ("%s/slo/soap" % BASE, BINDING_SOAP), - ("%s/slo/post" % BASE, BINDING_HTTP_POST), - ("%s/slo/redirect" % BASE, BINDING_HTTP_REDIRECT), + (f"{BASE}/slo/soap", BINDING_SOAP), + (f"{BASE}/slo/post", BINDING_HTTP_POST), + (f"{BASE}/slo/redirect", BINDING_HTTP_REDIRECT), ], }, "policy": { diff --git a/src/eduid/webapp/common/authn/utils.py b/src/eduid/webapp/common/authn/utils.py index 6d74f6fbb..38f28bc93 100644 --- a/src/eduid/webapp/common/authn/utils.py +++ b/src/eduid/webapp/common/authn/utils.py @@ -68,7 +68,7 @@ def get_saml_attribute(session_info: SessionInfo, attr_name: str) -> list[str] | attributes = session_info["ava"] - logger.debug("SAML attributes received: %s" % attributes) + logger.debug(f"SAML attributes received: {attributes}") # Look for the canonicalized attribute in the SAML assertion attributes for saml_attr, _ in attributes.items(): diff --git a/src/eduid/webapp/eidas/tests/saml2_settings.py b/src/eduid/webapp/eidas/tests/saml2_settings.py index 277874385..fd5d6ac23 100644 --- a/src/eduid/webapp/eidas/tests/saml2_settings.py +++ b/src/eduid/webapp/eidas/tests/saml2_settings.py @@ -12,7 +12,7 @@ # full path to the xmlsec1 binary programm "xmlsec_binary": "/usr/bin/xmlsec1", # your entity id, usually your subdomain plus the url to the metadata view - "entityid": "%ssaml2-metadata" % BASE_URL, + "entityid": f"{BASE_URL}saml2-metadata", # directory with attribute mapping "attribute_map_dir": DEFAULT_ATTRIBUTEMAPS, "allow_unknown_attributes": True, # Allow eduidIdPCredentialsUsed @@ -25,12 +25,12 @@ # url and binding to the assetion consumer service view # do not change the binding or service name "assertion_consumer_service": [ - ("%ssaml2-acs" % BASE_URL, saml2.BINDING_HTTP_POST), + (f"{BASE_URL}saml2-acs", saml2.BINDING_HTTP_POST), ], # url and binding to the single logout service view # do not change the binding or service name "single_logout_service": [ - ("%ssaml2-ls" % BASE_URL, saml2.BINDING_HTTP_REDIRECT), + (f"{BASE_URL}saml2-ls", saml2.BINDING_HTTP_REDIRECT), ], }, # Do not check for signature during tests diff --git a/src/eduid/webapp/email/verifications.py b/src/eduid/webapp/email/verifications.py index ad9255f94..e3ed736ce 100644 --- a/src/eduid/webapp/email/verifications.py +++ b/src/eduid/webapp/email/verifications.py @@ -59,9 +59,7 @@ def send_verification_code(email: str, user: User) -> bool: # Debug-log the code and message in development environment current_app.logger.debug(f"code: {state.verification.verification_code}") current_app.logger.debug(f"Generating verification e-mail with context:\n{payload}") - current_app.logger.info( - "Sent email address verification mail to user {}" " about address {!s}.".format(user, email) - ) + current_app.logger.info(f"Sent email address verification mail to user {user}" f" about address {email!s}.") return True diff --git a/src/eduid/webapp/email/views.py b/src/eduid/webapp/email/views.py index dda73f5e8..92152cd58 100644 --- a/src/eduid/webapp/email/views.py +++ b/src/eduid/webapp/email/views.py @@ -150,7 +150,7 @@ def verify(user: User, code: str, email: str) -> FluxData: @require_user def post_remove(user, email): proofing_user = ProofingUser.from_user(user, current_app.private_userdb) - current_app.logger.debug("Trying to remove email address {!r} " "from user {}".format(email, proofing_user)) + current_app.logger.debug(f"Trying to remove email address {email!r} " f"from user {proofing_user}") # Do not let the user remove all mail addresses if proofing_user.mail_addresses.count == 1: @@ -184,19 +184,17 @@ def post_remove(user, email): @MarshalWith(EmailResponseSchema) @require_user def resend_code(user: User, email: str) -> FluxData: - current_app.logger.debug( - "Trying to send new verification code for email " "address {} for user {}".format(email, user) - ) + current_app.logger.debug("Trying to send new verification code for email " f"address {email} for user {user}") if not user.mail_addresses.find(email): - current_app.logger.debug("Unknown email {!r} in resend_code_action," " user {}".format(email, user)) + current_app.logger.debug(f"Unknown email {email!r} in resend_code_action," f" user {user}") return error_response(message=CommonMsg.out_of_sync) sent = send_verification_code(email, user) if not sent: return error_response(message=EmailMsg.still_valid_code) - current_app.logger.debug("New verification code sent to " "address {} for user {}".format(email, user)) + current_app.logger.debug("New verification code sent to " f"address {email} for user {user}") current_app.stats.count(name="email_resend_code", value=1) emails = {"emails": user.mail_addresses.to_list_of_dicts()} diff --git a/src/eduid/webapp/idp/idp_authn.py b/src/eduid/webapp/idp/idp_authn.py index ec18f840a..32c624d6a 100644 --- a/src/eduid/webapp/idp/idp_authn.py +++ b/src/eduid/webapp/idp/idp_authn.py @@ -129,9 +129,7 @@ def _verify_username_and_password2(self, user: IdPUser, password: str) -> Passwo authn_info = self.authn_store.get_user_authn_info(user) if authn_info.failures_this_month > self.config.max_authn_failures_per_month: logger.info( - "User {!r} AuthN failures this month {!r} > {!r}".format( - user, authn_info.failures_this_month, self.config.max_authn_failures_per_month - ) + f"User {user!r} AuthN failures this month {authn_info.failures_this_month!r} > {self.config.max_authn_failures_per_month!r}" ) raise exceptions.EduidTooManyRequests("Too Many Requests") diff --git a/src/eduid/webapp/idp/sso_cache.py b/src/eduid/webapp/idp/sso_cache.py index 1f0da233c..4ac22e03f 100644 --- a/src/eduid/webapp/idp/sso_cache.py +++ b/src/eduid/webapp/idp/sso_cache.py @@ -161,9 +161,7 @@ def _purge_expired(self, timestamp: int) -> None: self._ages.appendleft((_exp_ts, _exp_key)) break logger.debug( - "Purged {!s} cache entry {!s} seconds over limit : {!s}".format( - self.name, timestamp - _exp_ts, _exp_key - ) + f"Purged {self.name!s} cache entry {timestamp - _exp_ts!s} seconds over limit : {_exp_key!s}" ) self.delete(_exp_key) finally: From 4c13fcdca32e299839f0ec03578d4abce1bbfc42 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Mon, 16 Sep 2024 15:33:20 +0000 Subject: [PATCH 19/23] add pyupgrade ruleset UP to ruff --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 97e910dd1..e43d6b495 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,4 +3,4 @@ line-length = 120 target-version = "py310" [lint] -select = ["E4", "E7", "E9", "F", "W", "I", "ASYNC"] +select = ["E4", "E7", "E9", "F", "W", "I", "ASYNC", "UP"] From eeaea695c3b1ff32895c26697ee54339ebafbaba Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Tue, 17 Sep 2024 07:26:54 +0000 Subject: [PATCH 20/23] make sure response is a bytes-string --- src/eduid/satosa/scimapi/serve_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/satosa/scimapi/serve_static.py b/src/eduid/satosa/scimapi/serve_static.py index 41d00fffe..6a5e2bbec 100644 --- a/src/eduid/satosa/scimapi/serve_static.py +++ b/src/eduid/satosa/scimapi/serve_static.py @@ -56,7 +56,7 @@ def _handle(self, context): mimetype = mimetypes.guess_type(file)[0] logger.debug(f"mimetype {mimetype}") except OSError: - response = "File not found" + response = b"File not found" mimetype = "text/html" status = "404 Not Found" From a31fee9bc8085f1351853839010b75543e7a2ad2 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Tue, 17 Sep 2024 07:27:49 +0000 Subject: [PATCH 21/23] add fix for Lock not being a class until python 3.13 --- src/eduid/webapp/idp/sso_cache.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/eduid/webapp/idp/sso_cache.py b/src/eduid/webapp/idp/sso_cache.py index 4ac22e03f..ce7b8d09e 100644 --- a/src/eduid/webapp/idp/sso_cache.py +++ b/src/eduid/webapp/idp/sso_cache.py @@ -109,7 +109,11 @@ class ExpiringCacheMem: :param lock: threading.Lock compatible locking instance """ - def __init__(self, name: str, logger: logging.Logger | None, ttl: int, lock: Lock | None = None): + # TODO: fix when python 3.13 is our target environment + # use quote annotations on lock for now until python 3.13 is released + # threading.Lock is not a class but a factory function, see: + # https://github.com/python/cpython/pull/114479 + def __init__(self, name: str, logger: logging.Logger | None, ttl: int, lock: "Lock | None" = None): self.logger = logger self.ttl = ttl self.name = name From da0538b6a09df85ed350ca1855dd8e5654a3324b Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Wed, 18 Sep 2024 08:29:24 +0000 Subject: [PATCH 22/23] remove duplicated code --- src/eduid/userdb/deprecation.py | 68 --------------------------------- src/eduid/userdb/security/db.py | 2 +- 2 files changed, 1 insertion(+), 69 deletions(-) delete mode 100644 src/eduid/userdb/deprecation.py diff --git a/src/eduid/userdb/deprecation.py b/src/eduid/userdb/deprecation.py deleted file mode 100644 index 2c0e62e66..000000000 --- a/src/eduid/userdb/deprecation.py +++ /dev/null @@ -1,68 +0,0 @@ -import inspect -import warnings -from functools import wraps - - -# https://stackoverflow.com/questions/2536307/how-do-i-deprecate-python-functions/40301488#40301488 -def deprecated(reason): - """ - This is a decorator which can be used to mark functions - as deprecated. It will result in a warning being emitted - when the function is used. - """ - - if isinstance(reason, str): - # The @deprecated is used with a 'reason'. - # - # .. code-block:: python - # - # @deprecated("please, use another function") - # def old_function(x, y): - # pass - - def decorator(func1): - if inspect.isclass(func1): - fmt1 = "Call to deprecated class {name} ({reason})." - else: - fmt1 = "Call to deprecated function {name} ({reason})." - - @wraps(func1) - def new_func1(*args, **kwargs): - warnings.simplefilter("always", DeprecationWarning) - warnings.warn( - fmt1.format(name=func1.__name__, reason=reason), category=DeprecationWarning, stacklevel=2 - ) - warnings.simplefilter("default", DeprecationWarning) - return func1(*args, **kwargs) - - return new_func1 - - return decorator - - elif inspect.isclass(reason) or inspect.isfunction(reason): - # The @deprecated is used without any 'reason'. - # - # .. code-block:: python - # - # @deprecated - # def old_function(x, y): - # pass - - func2 = reason - - if inspect.isclass(func2): - fmt2 = "Call to deprecated class {name}." - else: - fmt2 = "Call to deprecated function {name}." - - @wraps(func2) - def new_func2(*args, **kwargs): - warnings.simplefilter("always", DeprecationWarning) - warnings.warn(fmt2.format(name=func2.__name__), category=DeprecationWarning, stacklevel=2) - warnings.simplefilter("default", DeprecationWarning) - return func2(*args, **kwargs) - - return new_func2 - - else: - raise TypeError(repr(type(reason))) diff --git a/src/eduid/userdb/security/db.py b/src/eduid/userdb/security/db.py index d1017a893..9b5dee43d 100644 --- a/src/eduid/userdb/security/db.py +++ b/src/eduid/userdb/security/db.py @@ -4,7 +4,7 @@ from typing import Any from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument -from eduid.userdb.deprecation import deprecated +from eduid.common.decorators import deprecated from eduid.userdb.exceptions import MultipleDocumentsReturned from eduid.userdb.security.state import PasswordResetEmailAndPhoneState, PasswordResetEmailState, PasswordResetState from eduid.userdb.security.user import SecurityUser From 62a6e7cf8d6896148fa7947e83f2a1a04ad5b1f0 Mon Sep 17 00:00:00 2001 From: Lasse Yledahl Date: Wed, 18 Sep 2024 08:35:15 +0000 Subject: [PATCH 23/23] make reformat --- src/eduid/userdb/security/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eduid/userdb/security/db.py b/src/eduid/userdb/security/db.py index 9b5dee43d..e05488a93 100644 --- a/src/eduid/userdb/security/db.py +++ b/src/eduid/userdb/security/db.py @@ -3,8 +3,8 @@ from collections.abc import Mapping from typing import Any -from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.common.decorators import deprecated +from eduid.userdb.db import BaseDB, SaveResult, TUserDbDocument from eduid.userdb.exceptions import MultipleDocumentsReturned from eduid.userdb.security.state import PasswordResetEmailAndPhoneState, PasswordResetEmailState, PasswordResetState from eduid.userdb.security.user import SecurityUser