~homeworkprod/byceps

1c559ffd2a46d29d1a518bea7dab01ab48dead2b — Jochen Kupperschmidt 5 months ago 1b947be
Use PEP 585 type hinting generics
114 files changed, 522 insertions(+), 440 deletions(-)

M byceps/announce/helpers.py
M byceps/application.py
M byceps/blueprints/admin/orga_presence/views.py
M byceps/blueprints/admin/party/views.py
M byceps/blueprints/admin/shop/order/service.py
M byceps/blueprints/admin/ticketing/service.py
M byceps/blueprints/admin/user/service.py
M byceps/blueprints/admin/webhook/forms.py
M byceps/blueprints/api/v1/tourney/match/comments/views.py
M byceps/blueprints/site/board/models.py
M byceps/blueprints/site/board/service.py
M byceps/blueprints/site/dashboard/views.py
M byceps/blueprints/site/seating/service.py
M byceps/blueprints/site/snippet/templating.py
M byceps/blueprints/site/user/creation/forms.py
M byceps/blueprints/site/user/creation/views.py
M byceps/database.py
M byceps/email.py
M byceps/services/attendance/service.py
M byceps/services/attendance/transfer/models.py
M byceps/services/authentication/session/models/current_user.py
M byceps/services/authentication/session/service.py
M byceps/services/authorization/impex_service.py
M byceps/services/authorization/service.py
M byceps/services/board/posting_command_service.py
M byceps/services/board/posting_query_service.py
M byceps/services/board/topic_command_service.py
M byceps/services/board/topic_query_service.py
M byceps/services/brand/service.py
M byceps/services/brand/settings_service.py
M byceps/services/consent/consent_service.py
M byceps/services/consent/subject_service.py
M byceps/services/country/service.py
M byceps/services/email/service.py
M byceps/services/email/transfer/models.py
M byceps/services/global_setting/service.py
M byceps/services/image/service.py
M byceps/services/metrics/models.py
M byceps/services/metrics/service.py
M byceps/services/news/channel_service.py
M byceps/services/news/html_service.py
M byceps/services/news/service.py
M byceps/services/news/transfer/models.py
M byceps/services/newsletter/service.py
M byceps/services/orga/birthday_service.py
M byceps/services/orga/service.py
M byceps/services/orga_presence/service.py
M byceps/services/orga_team/service.py
M byceps/services/party/service.py
M byceps/services/party/settings_service.py
M byceps/services/seating/seat_service.py
M byceps/services/shop/article/models/compilation.py
M byceps/services/shop/article/sequence_service.py
M byceps/services/shop/article/service.py
M byceps/services/shop/cart/models.py
M byceps/services/shop/catalog/service.py
M byceps/services/shop/catalog/transfer/models.py
M byceps/services/shop/order/action_service.py
M byceps/services/shop/order/dbmodels/order.py
M byceps/services/shop/order/email/service.py
M byceps/services/shop/order/event_service.py
M byceps/services/shop/order/export/service.py
M byceps/services/shop/order/ordered_articles_service.py
M byceps/services/shop/order/sequence_service.py
M byceps/services/shop/order/service.py
M byceps/services/shop/order/transfer/models.py
M byceps/services/shop/shipping/service.py
M byceps/services/shop/shop/service.py
M byceps/services/shop/shop/transfer/models.py
M byceps/services/shop/storefront/service.py
M byceps/services/site/service.py
M byceps/services/site/settings_service.py
M byceps/services/snippet/mountpoint_service.py
M byceps/services/snippet/service.py
M byceps/services/terms/consent_service.py
M byceps/services/ticketing/attendance_service.py
M byceps/services/ticketing/category_service.py
M byceps/services/ticketing/event_service.py
M byceps/services/ticketing/ticket_code_service.py
M byceps/services/ticketing/ticket_revocation_service.py
M byceps/services/ticketing/ticket_service.py
M byceps/services/tourney/avatar/service.py
M byceps/services/tourney/category_service.py
M byceps/services/tourney/match_comment_service.py
M byceps/services/tourney/participant_service.py
M byceps/services/tourney/tourney_service.py
M byceps/services/user/creation_service.py
M byceps/services/user/event_service.py
M byceps/services/user/service.py
M byceps/services/user/transfer/models.py
M byceps/services/user_avatar/service.py
M byceps/services/user_badge/awarding_service.py
M byceps/services/user_badge/badge_service.py
M byceps/services/user_group/service.py
M byceps/services/webhooks/dbmodels.py
M byceps/services/webhooks/service.py
M byceps/services/webhooks/transfer/models.py
M byceps/util/authorization.py
M byceps/util/export.py
M byceps/util/iterables.py
M byceps/util/navigation.py
M byceps/util/templating.py
M byceps/util/user_session.py
M scripts/clean_up_after_deleted_users.py
M scripts/find_logins_for_ipaddress.py
M scripts/generate_sql_to_delete_user.py
M scripts/set_current_terms_version.py
M sites/cozylan/extension.py
M tests/conftest.py
M tests/helpers.py
M tests/integration/announce/irc/helpers.py
M tests/integration/api/helpers.py
M tests/integration/util/test_authorization.py
M tests/unit/services/orga/test_birthday_service.py
M byceps/announce/helpers.py => byceps/announce/helpers.py +4 -3
@@ 6,8 6,9 @@ byceps.announce.helpers
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from http import HTTPStatus
from typing import Any, Dict, List
from typing import Any

from flask import current_app
import requests


@@ 23,7 24,7 @@ class WebhookError(Exception):
    pass


def get_webhooks(event: _BaseEvent) -> List[OutgoingWebhook]:
def get_webhooks(event: _BaseEvent) -> list[OutgoingWebhook]:
    event_name = get_name_for_event(event)
    webhooks = webhook_service.get_enabled_outgoing_webhooks(event_name)



@@ 72,7 73,7 @@ def call_webhook(webhook: OutgoingWebhook, text: str) -> None:

def _assemble_request_data(
    webhook: OutgoingWebhook, text: str
) -> Dict[str, Any]:
) -> dict[str, Any]:
    if webhook.format == 'discord':
        return {'content': text}


M byceps/application.py => byceps/application.py +6 -5
@@ 6,11 6,12 @@ byceps.application
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from http import HTTPStatus
from importlib import import_module
import os
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Union
from typing import Any, Callable, Optional, Union

from flask import current_app, Flask, g, redirect
from flask_babel import Babel


@@ 30,7 31,7 @@ from .util.views import redirect_to
def create_app(
    *,
    config_filename: Optional[Union[Path, str]] = None,
    config_overrides: Optional[Dict[str, Any]] = None,
    config_overrides: Optional[dict[str, Any]] = None,
) -> Flask:
    """Create the actual Flask application."""
    app = Flask('byceps')


@@ 143,7 144,7 @@ def _set_url_root_path(app: Flask) -> None:
    app.add_url_rule('/', endpoint='root', view_func=_redirect)


def _get_site_template_context() -> Dict[str, Any]:
def _get_site_template_context() -> dict[str, Any]:
    """Return the site-specific additions to the template context."""
    site_context_processor = _find_site_template_context_processor_cached(
        g.site_id


@@ 157,7 158,7 @@ def _get_site_template_context() -> Dict[str, Any]:

def _find_site_template_context_processor_cached(
    site_id: str,
) -> Optional[Callable[[], Dict[str, Any]]]:
) -> Optional[Callable[[], dict[str, Any]]]:
    """Return the template context processor for the site.

    A processor will be cached after it has been obtained for the first


@@ 176,7 177,7 @@ def _find_site_template_context_processor_cached(

def _find_site_template_context_processor(
    site_id: str,
) -> Optional[Callable[[], Dict[str, Any]]]:
) -> Optional[Callable[[], dict[str, Any]]]:
    """Import a template context processor from the site's package.

    If a site package contains a module named `extension` and that

M byceps/blueprints/admin/orga_presence/views.py => byceps/blueprints/admin/orga_presence/views.py +3 -2
@@ 6,9 6,10 @@ byceps.blueprints.admin.orga_presence.views
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import defaultdict
from datetime import datetime
from typing import Dict, Iterable
from typing import Iterable

from flask import abort



@@ 70,7 71,7 @@ def view(party_id):

def _group_presences_by_orga(
    presences: Iterable[PresenceTimeSlot],
) -> Dict[User, PresenceTimeSlot]:
) -> dict[User, PresenceTimeSlot]:
    d = defaultdict(set)

    for presence in presences:

M byceps/blueprints/admin/party/views.py => byceps/blueprints/admin/party/views.py +3 -3
@@ 6,9 6,9 @@ byceps.blueprints.admin.party.views
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import dataclasses
from datetime import date, datetime
from typing import Dict, List

from flask import abort, request
from flask_babel import gettext


@@ 93,13 93,13 @@ def index_for_brand(brand_id, page):
    }


def _get_days_by_party_id(parties) -> Dict[PartyID, List[date]]:
def _get_days_by_party_id(parties) -> dict[PartyID, list[date]]:
    return {party.id: party_service.get_party_days(party) for party in parties}


def _get_ticket_sale_stats_by_party_id(
    parties,
) -> Dict[PartyID, TicketSaleStats]:
) -> dict[PartyID, TicketSaleStats]:
    return {
        party.id: ticket_service.get_ticket_sale_stats(party.id)
        for party in parties

M byceps/blueprints/admin/shop/order/service.py => byceps/blueprints/admin/shop/order/service.py +7 -6
@@ 6,9 6,10 @@ byceps.blueprints.admin.shop.order.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
import dataclasses
from typing import Dict, Iterator, Sequence
from typing import Iterator, Sequence

from .....services.shop.article import service as article_service
from .....services.shop.article.transfer.models import Article, ArticleNumber


@@ 45,7 46,7 @@ def extend_order_tuples_with_orderer(
        yield OrderWithOrderer(*values)


def get_articles_by_item_number(order: Order) -> Dict[ArticleNumber, Article]:
def get_articles_by_item_number(order: Order) -> dict[ArticleNumber, Article]:
    numbers = {item.article_number for item in order.items}

    articles = article_service.get_articles_by_numbers(numbers)


@@ 91,7 92,7 @@ def _fake_order_placement_event(order_id: OrderID) -> OrderEvent:


def _get_additional_data(
    event: OrderEvent, users_by_id: Dict[UserID, User]
    event: OrderEvent, users_by_id: dict[UserID, User]
) -> OrderEventData:
    if event.event_type == 'badge-awarded':
        return _get_additional_data_for_badge_awarded(event)


@@ 110,7 111,7 @@ def _get_additional_data(


def _get_additional_data_for_standard_event(
    event: OrderEvent, users_by_id: Dict[UserID, User]
    event: OrderEvent, users_by_id: dict[UserID, User]
) -> OrderEventData:
    initiator_id = event.data['initiator_id']



@@ 151,7 152,7 @@ def _get_additional_data_for_ticket_bundle_created(


def _get_additional_data_for_ticket_bundle_revoked(
    event: OrderEvent, users_by_id: Dict[UserID, User]
    event: OrderEvent, users_by_id: dict[UserID, User]
) -> OrderEventData:
    bundle_id = event.data['ticket_bundle_id']



@@ 181,7 182,7 @@ def _get_additional_data_for_ticket_created(


def _get_additional_data_for_ticket_revoked(
    event: OrderEvent, users_by_id: Dict[UserID, User]
    event: OrderEvent, users_by_id: dict[UserID, User]
) -> OrderEventData:
    ticket_id = event.data['ticket_id']
    ticket_code = event.data['ticket_code']

M byceps/blueprints/admin/ticketing/service.py => byceps/blueprints/admin/ticketing/service.py +15 -14
@@ 6,7 6,8 @@ byceps.blueprints.admin.ticketing.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Any, Dict, Iterator, Optional, Sequence, Set, Tuple
from __future__ import annotations
from typing import Any, Iterator, Optional, Sequence

from ....services.seating import seat_service
from ....services.ticketing import event_service


@@ 48,7 49,7 @@ def _fake_ticket_creation_event(ticket_id: TicketID) -> TicketEvent:
    return TicketEvent(ticket.created_at, 'ticket-created', ticket.id, data)


def _get_users_by_id(events: Sequence[TicketEvent]) -> Dict[str, User]:
def _get_users_by_id(events: Sequence[TicketEvent]) -> dict[str, User]:
    user_ids = set(
        _find_values_for_keys(
            events,


@@ 67,7 68,7 @@ def _get_users_by_id(events: Sequence[TicketEvent]) -> Dict[str, User]:


def _find_values_for_keys(
    events: Sequence[TicketEvent], keys: Set[str]
    events: Sequence[TicketEvent], keys: set[str]
) -> Iterator[Any]:
    for event in events:
        for key in keys:


@@ 77,8 78,8 @@ def _find_values_for_keys(


def _get_additional_data(
    event: TicketEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    event: TicketEvent, users_by_id: dict[str, User]
) -> Iterator[tuple[str, Any]]:
    yield from _get_initiators(event, users_by_id)

    if event.event_type == 'seat-manager-appointed':


@@ 118,8 119,8 @@ def _get_additional_data(


def _get_initiators(
    event: TicketEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    event: TicketEvent, users_by_id: dict[str, User]
) -> Iterator[tuple[str, Any]]:
    if event.event_type in {
        'seat-manager-appointed',
        'seat-manager-withdrawn',


@@ 140,8 141,8 @@ def _get_initiators(


def _get_additional_data_for_user_initiated_event(
    event: TicketEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    event: TicketEvent, users_by_id: dict[str, User]
) -> Iterator[tuple[str, Any]]:
    initiator_id = event.data.get('initiator_id')
    if initiator_id is not None:
        yield 'initiator', users_by_id[initiator_id]


@@ 149,7 150,7 @@ def _get_additional_data_for_user_initiated_event(

def _get_additional_data_for_seat_occupied_event(
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
) -> Iterator[tuple[str, Any]]:
    seat_id = event.data['seat_id']
    seat = seat_service.find_seat(seat_id)
    yield 'seat_label', seat.label


@@ 162,7 163,7 @@ def _get_additional_data_for_seat_occupied_event(

def _get_additional_data_for_seat_released_event(
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
) -> Iterator[tuple[str, Any]]:
    seat_id = event.data.get('seat_id')
    if seat_id:
        seat = seat_service.find_seat(seat_id)


@@ 171,7 172,7 @@ def _get_additional_data_for_seat_released_event(

def _get_additional_data_for_ticket_revoked_event(
    event: TicketEvent,
) -> Iterator[Tuple[str, Any]]:
) -> Iterator[tuple[str, Any]]:
    reason = event.data.get('reason')
    if reason:
        yield 'reason', reason


@@ 179,10 180,10 @@ def _get_additional_data_for_ticket_revoked_event(

def _look_up_user_for_id(
    event: TicketEvent,
    users_by_id: Dict[str, User],
    users_by_id: dict[str, User],
    user_id_key: str,
    user_key: str,
) -> Tuple[str, Optional[User]]:
) -> tuple[str, Optional[User]]:
    user_id = event.data[user_id_key]
    user = users_by_id.get(user_id)
    return user_key, user

M byceps/blueprints/admin/user/service.py => byceps/blueprints/admin/user/service.py +14 -13
@@ 6,11 6,12 @@ byceps.blueprints.admin.user.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import defaultdict
from datetime import datetime, timedelta
from itertools import chain
from operator import attrgetter
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple
from typing import Any, Iterator, Optional, Sequence

from ....database import db, paginate, Pagination
from ....services.consent import consent_service


@@ 95,7 96,7 @@ def _filter_by_search_term(query, search_term):
        .filter(db.or_(*clauses))


def _generate_search_clauses_for_term(search_term: str) -> List:
def _generate_search_clauses_for_term(search_term: str) -> list:
    ilike_pattern = f'%{search_term}%'

    return [


@@ 108,7 109,7 @@ def _generate_search_clauses_for_term(search_term: str) -> List:

def get_users_created_since(
    delta: timedelta, limit: Optional[int] = None
) -> List[User]:
) -> list[User]:
    """Return the user accounts created since `delta` ago."""
    filter_starts_at = datetime.utcnow() - delta



@@ 150,7 151,7 @@ def _db_entity_to_user_with_creation_details(

def get_parties_and_tickets(
    user_id: UserID,
) -> List[Tuple[Party, List[DbTicket]]]:
) -> list[tuple[Party, list[DbTicket]]]:
    """Return tickets the user uses or manages, and the related parties."""
    tickets = ticket_service.find_tickets_related_to_user(user_id)



@@ 171,8 172,8 @@ def get_parties_and_tickets(

def _group_tickets_by_party_id(
    tickets: Sequence[DbTicket],
) -> Dict[PartyID, List[DbTicket]]:
    tickets_by_party_id: Dict[PartyID, List[DbTicket]] = defaultdict(list)
) -> dict[PartyID, list[DbTicket]]:
    tickets_by_party_id: dict[PartyID, list[DbTicket]] = defaultdict(list)

    for ticket in tickets:
        tickets_by_party_id[ticket.category.party_id].append(ticket)


@@ 180,12 181,12 @@ def _group_tickets_by_party_id(
    return tickets_by_party_id


def _get_parties_by_id(party_ids: Set[PartyID]) -> Dict[PartyID, Party]:
def _get_parties_by_id(party_ids: set[PartyID]) -> dict[PartyID, Party]:
    parties = party_service.get_parties(party_ids)
    return {p.id: p for p in parties}


def get_attended_parties(user_id: UserID) -> List[Party]:
def get_attended_parties(user_id: UserID) -> list[Party]:
    """Return the parties attended by the user, in order."""
    attended_parties = attendance_service.get_attended_parties(user_id)
    attended_parties.sort(key=attrgetter('starts_at'), reverse=True)


@@ 194,7 195,7 @@ def get_attended_parties(user_id: UserID) -> List[Party]:

def get_newsletter_subscriptions(
    user_id: UserID,
) -> Iterator[Tuple[NewsletterList, bool]]:
) -> Iterator[tuple[NewsletterList, bool]]:
    lists = newsletter_service.get_all_lists()
    for list_ in lists:
        is_subscribed = newsletter_service.is_subscribed(user_id, list_.id)


@@ 295,8 296,8 @@ def _fake_order_events(user_id: UserID) -> Iterator[DbUserEvent]:


def _get_additional_data(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    event: DbUserEvent, users_by_id: dict[str, User]
) -> Iterator[tuple[str, Any]]:
    if event.event_type in {
        'user-created',
        'user-deleted',


@@ 345,8 346,8 @@ def _get_additional_data(


def _get_additional_data_for_user_initiated_event(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    event: DbUserEvent, users_by_id: dict[str, User]
) -> Iterator[tuple[str, Any]]:
    initiator_id = event.data.get('initiator_id')
    if initiator_id is not None:
        yield 'initiator', users_by_id[initiator_id]

M byceps/blueprints/admin/webhook/forms.py => byceps/blueprints/admin/webhook/forms.py +6 -6
@@ 6,7 6,7 @@ byceps.blueprints.admin.webhook.forms
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Set, Type
from __future__ import annotations

from flask_babel import lazy_gettext
from wtforms import BooleanField, StringField


@@ 30,18 30,18 @@ class UpdateForm(_BaseForm):
    enabled = BooleanField(lazy_gettext('Enabled'))


def assemble_create_form(event_names: Set[str]) -> Type[LocalizedForm]:
def assemble_create_form(event_names: set[str]) -> type[LocalizedForm]:
    return _extend_form_for_event_types(CreateForm, event_names)


def assemble_update_form(event_names: Set[str]) -> Type[LocalizedForm]:
def assemble_update_form(event_names: set[str]) -> type[LocalizedForm]:
    return _extend_form_for_event_types(UpdateForm, event_names)


def _extend_form_for_event_types(
    form_class,
    event_names: Set[str],
) -> Type[LocalizedForm]:
    event_names: set[str],
) -> type[LocalizedForm]:
    """Dynamically add a checkbox per event type to the form."""

    class FormWithEventTypes(form_class):


@@ 61,7 61,7 @@ def _add_event_field_getter_to_form(form_class) -> None:
    form_class.get_field_for_event_name = get_field_for_event_name


def _add_event_fields_to_form(form_class, event_names: Set[str]) -> None:
def _add_event_fields_to_form(form_class, event_names: set[str]) -> None:
    for event_name in event_names:
        field_name = _create_event_field_name(event_name)
        field = BooleanField()

M byceps/blueprints/api/v1/tourney/match/comments/views.py => byceps/blueprints/api/v1/tourney/match/comments/views.py +6 -5
@@ 6,8 6,9 @@ byceps.blueprints.api.v1.tourney.match.views
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, Optional
from typing import Any, Optional

from flask import abort, jsonify, request, url_for
from marshmallow import ValidationError


@@ 73,7 74,7 @@ def get_comments_for_match(match_id):
    )


def _comment_to_json(comment: MatchComment) -> Dict[str, Any]:
def _comment_to_json(comment: MatchComment) -> dict[str, Any]:
    creator = comment.created_by
    last_editor = comment.last_edited_by
    moderator = comment.hidden_by


@@ 97,11 98,11 @@ def _potential_datetime_to_json(dt: Optional[datetime]) -> Optional[str]:
    return dt.isoformat() if (dt is not None) else None


def _potential_user_to_json(user: Optional[User]) -> Optional[Dict[str, Any]]:
def _potential_user_to_json(user: Optional[User]) -> Optional[dict[str, Any]]:
    return _user_to_json(user) if (user is not None) else None


def _user_to_json(user: User) -> Dict[str, Any]:
def _user_to_json(user: User) -> dict[str, Any]:
    return {
        'user_id': str(user.id),
        'screen_name': user.screen_name,


@@ 216,7 217,7 @@ def _get_comment_or_404(comment_id: MatchCommentID) -> MatchComment:
    return comment


def _parse_request(schema_class: SchemaMeta) -> Dict[str, Any]:
def _parse_request(schema_class: SchemaMeta) -> dict[str, Any]:
    schema = schema_class()
    request_data = request.get_json()


M byceps/blueprints/site/board/models.py => byceps/blueprints/site/board/models.py +3 -3
@@ 8,7 8,7 @@ byceps.blueprints.site.board.models

from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Set
from typing import Optional

from ....services.board.transfer.models import CategoryWithLastUpdate
from ....services.user.transfer.models import User


@@ 46,12 46,12 @@ class Ticket:

@dataclass(frozen=True)
class Creator(User):
    badges: Set[Badge]
    badges: set[Badge]
    ticket: Ticket

    @classmethod
    def from_(
        cls, user: User, badges: Set[Badge], ticket: Optional[Ticket]
        cls, user: User, badges: set[Badge], ticket: Optional[Ticket]
    ) -> Creator:
        return cls(
            user.id,

M byceps/blueprints/site/board/service.py => byceps/blueprints/site/board/service.py +4 -3
@@ 6,8 6,9 @@ byceps.blueprints.site.board.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Dict, Optional, Sequence, Set
from typing import Optional, Sequence

from flask import g



@@ 144,8 145,8 @@ def enrich_creators(


def _get_badges_for_users(
    user_ids: Set[UserID], brand_id: BrandID
) -> Dict[UserID, Set[Badge]]:
    user_ids: set[UserID], brand_id: BrandID
) -> dict[UserID, set[Badge]]:
    """Fetch users' badges that are either global or belong to the brand."""
    badges_by_user_id = badge_awarding_service.get_badges_awarded_to_users(
        user_ids, featured_only=True

M byceps/blueprints/site/dashboard/views.py => byceps/blueprints/site/dashboard/views.py +5 -5
@@ 8,7 8,7 @@ Current user's dashboard
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List
from __future__ import annotations

from flask import abort, g



@@ 63,7 63,7 @@ def index():
    }


def _get_open_orders(site: Site, user_id: UserID) -> List[Order]:
def _get_open_orders(site: Site, user_id: UserID) -> list[Order]:
    storefront_id = site.storefront_id
    if storefront_id is None:
        return []


@@ 79,14 79,14 @@ def _get_open_orders(site: Site, user_id: UserID) -> List[Order]:
    return orders


def _get_tickets(user_id: UserID) -> List[DbTicket]:
def _get_tickets(user_id: UserID) -> list[DbTicket]:
    if g.party_id is None:
        return []

    return ticket_service.find_tickets_used_by_user(user_id, g.party_id)


def _get_news_headlines(site: Site) -> List[NewsHeadline]:
def _get_news_headlines(site: Site) -> list[NewsHeadline]:
    channel_id = site.news_channel_id
    if channel_id is None:
        return []


@@ 94,7 94,7 @@ def _get_news_headlines(site: Site) -> List[NewsHeadline]:
    return news_item_service.get_recent_headlines(channel_id, limit=4)


def _get_board_topics(site: Site, current_user: CurrentUser) -> List[DbTopic]:
def _get_board_topics(site: Site, current_user: CurrentUser) -> list[DbTopic]:
    board_id = site.board_id
    if board_id is None:
        return []

M byceps/blueprints/site/seating/service.py => byceps/blueprints/site/seating/service.py +7 -6
@@ 6,9 6,10 @@ byceps.blueprints.site.seating.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from itertools import chain
from typing import Dict, Iterator, Optional, Sequence
from typing import Iterator, Optional, Sequence

from ....services.seating.dbmodels.seat import Seat as DbSeat
from ....services.seating.transfer.models import SeatID


@@ 45,7 46,7 @@ class Seat:

def get_users(
    seats: Sequence[DbSeat], managed_tickets: Sequence[DbTicket]
) -> Dict[UserID, User]:
) -> dict[UserID, User]:
    seat_tickets = _get_seat_tickets(seats)
    tickets = chain(seat_tickets, managed_tickets)



@@ 58,7 59,7 @@ def _get_seat_tickets(seats: Sequence[DbSeat]) -> Iterator[DbTicket]:
            yield seat.occupied_by_ticket


def _get_ticket_users_by_id(tickets: Sequence[DbTicket]) -> Dict[UserID, User]:
def _get_ticket_users_by_id(tickets: Sequence[DbTicket]) -> dict[UserID, User]:
    user_ids = set(_get_ticket_user_ids(tickets))
    users = user_service.find_users(user_ids, include_avatars=True)
    return user_service.index_users_by_id(users)


@@ 72,7 73,7 @@ def _get_ticket_user_ids(tickets: Sequence[DbTicket]) -> Iterator[UserID]:


def get_seats(
    seats: Sequence[DbSeat], users_by_id: Dict[UserID, User]
    seats: Sequence[DbSeat], users_by_id: dict[UserID, User]
) -> Iterator[Seat]:
    for seat in seats:
        if seat.is_occupied:


@@ 97,7 98,7 @@ def get_seats(


def get_managed_tickets(
    managed_tickets: Sequence[DbTicket], users_by_id: Dict[UserID, User]
    managed_tickets: Sequence[DbTicket], users_by_id: dict[UserID, User]
) -> Iterator[ManagedTicket]:
    for ticket in managed_tickets:
        user = _find_ticket_user(ticket, users_by_id)


@@ 113,7 114,7 @@ def get_managed_tickets(


def _find_ticket_user(
    ticket: DbTicket, users_by_id: Dict[UserID, User]
    ticket: DbTicket, users_by_id: dict[UserID, User]
) -> Optional[User]:
    if ticket.used_by_id is None:
        return None

M byceps/blueprints/site/snippet/templating.py => byceps/blueprints/site/snippet/templating.py +2 -2
@@ 6,9 6,9 @@ byceps.blueprints.site.snippet.templating
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import sys
import traceback
from typing import Set

from flask import abort, g, render_template, url_for
from jinja2 import TemplateNotFound


@@ 112,7 112,7 @@ def url_for_snippet(endpoint_suffix: str, **kwargs) -> str:
    return url_for(f'snippet.view', url_path=mountpoint.url_path, **kwargs)


def _get_mountpoints() -> Set[Mountpoint]:
def _get_mountpoints() -> set[Mountpoint]:
    """Return site-specific mountpoints.

    Preferrably from request-local cache, if available. From the

M byceps/blueprints/site/user/creation/forms.py => byceps/blueprints/site/user/creation/forms.py +2 -2
@@ 6,8 6,8 @@ byceps.blueprints.site.user.creation.forms
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import re
from typing import Set

from flask_babel import lazy_gettext
from wtforms import BooleanField, PasswordField, StringField


@@ 73,7 73,7 @@ class UserCreateForm(LocalizedForm):

def assemble_user_create_form(
    real_name_required: bool,
    required_consent_subjects: Set[Subject],
    required_consent_subjects: set[Subject],
    newsletter_offered: bool,
):
    extra_fields = {}

M byceps/blueprints/site/user/creation/views.py => byceps/blueprints/site/user/creation/views.py +3 -2
@@ 6,8 6,9 @@ byceps.blueprints.site.user.creation.views
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Optional, Set
from typing import Optional

from flask import abort, g, request
from flask_babel import gettext


@@ 160,7 161,7 @@ def _is_real_name_required() -> bool:
    return value != 'false'


def _get_required_consent_subjects() -> Set[Subject]:
def _get_required_consent_subjects() -> set[Subject]:
    """Return the consent subjects required for this brand."""
    return consent_subject_service.get_subjects_required_for_brand(g.brand_id)


M byceps/database.py => byceps/database.py +8 -7
@@ 8,7 8,8 @@ Database utilities.
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Any, Callable, Dict, Iterable, Optional, TypeVar
from __future__ import annotations
from typing import Any, Callable, Iterable, Optional, TypeVar
import uuid

from sqlalchemy.dialects.postgresql import insert


@@ 80,7 81,7 @@ def paginate(
    return Pagination(None, page, per_page, total, items)


def insert_ignore_on_conflict(table: Table, values: Dict[str, Any]) -> None:
def insert_ignore_on_conflict(table: Table, values: dict[str, Any]) -> None:
    """Insert the record identified by the primary key (specified as
    part of the values), or do nothing on conflict.
    """


@@ 93,7 94,7 @@ def insert_ignore_on_conflict(table: Table, values: Dict[str, Any]) -> None:


def upsert(
    table: Table, identifier: Dict[str, Any], replacement: Dict[str, Any]
    table: Table, identifier: dict[str, Any], replacement: dict[str, Any]
) -> None:
    """Insert or update the record identified by `identifier` with value
    `replacement`.


@@ 104,8 105,8 @@ def upsert(

def upsert_many(
    table: Table,
    identifiers: Iterable[Dict[str, Any]],
    replacement: Dict[str, Any],
    identifiers: Iterable[dict[str, Any]],
    replacement: dict[str, Any],
) -> None:
    """Insert or update the record identified by `identifier` with value
    `replacement`.


@@ 117,7 118,7 @@ def upsert_many(


def execute_upsert(
    table: Table, identifier: Dict[str, Any], replacement: Dict[str, Any]
    table: Table, identifier: dict[str, Any], replacement: dict[str, Any]
) -> None:
    """Execute, but do not commit, an UPSERT."""
    query = _build_upsert_query(table, identifier, replacement)


@@ 125,7 126,7 @@ def execute_upsert(


def _build_upsert_query(
    table: Table, identifier: Dict[str, Any], replacement: Dict[str, Any]
    table: Table, identifier: dict[str, Any], replacement: dict[str, Any]
) -> Insert:
    values = identifier.copy()
    values.update(replacement)

M byceps/email.py => byceps/email.py +3 -2
@@ 8,7 8,8 @@ Sending e-mail.
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from flask import current_app
from marrow.mailer import Mailer


@@ 57,7 58,7 @@ def _get_config(app):


def send(
    sender: Optional[str], recipients: List[str], subject: str, body: str
    sender: Optional[str], recipients: list[str], subject: str, body: str
) -> None:
    """Assemble and send an e-mail."""
    mailer = current_app.marrowmailer

M byceps/services/attendance/service.py => byceps/services/attendance/service.py +8 -7
@@ 6,8 6,9 @@ byceps.services.attendance.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import defaultdict
from typing import Dict, Iterable, List, Optional, Set, Tuple
from typing import Iterable, Optional

from ...database import db, paginate, Pagination
from ...typing import PartyID, UserID


@@ 71,8 72,8 @@ def _get_users_paginated(


def _get_tickets_for_users(
    party_id: PartyID, user_ids: Set[UserID]
) -> List[DbTicket]:
    party_id: PartyID, user_ids: set[UserID]
) -> list[DbTicket]:
    return DbTicket.query \
        .options(
            db.joinedload('category'),


@@ 86,7 87,7 @@ def _get_tickets_for_users(

def _index_tickets_by_user_id(
    tickets: Iterable[DbTicket],
) -> Dict[UserID, Set[DbTicket]]:
) -> dict[UserID, set[DbTicket]]:
    tickets_by_user_id = defaultdict(set)
    for ticket in tickets:
        tickets_by_user_id[ticket.used_by_id].add(ticket)


@@ 94,7 95,7 @@ def _index_tickets_by_user_id(


def _generate_attendees(
    users: Iterable[DbUser], tickets_by_user_id: Dict[UserID, Set[DbTicket]]
    users: Iterable[DbUser], tickets_by_user_id: dict[UserID, set[DbTicket]]
) -> Iterable[Attendee]:
    for user in users:
        tickets = tickets_by_user_id[user.id]


@@ 102,7 103,7 @@ def _generate_attendees(
        yield Attendee(user, attendee_tickets)


def _to_attendee_tickets(tickets: Iterable[DbTicket]) -> List[AttendeeTicket]:
def _to_attendee_tickets(tickets: Iterable[DbTicket]) -> list[AttendeeTicket]:
    attendee_tickets = [
        AttendeeTicket(t.occupied_seat, t.user_checked_in) for t in tickets
    ]


@@ 112,7 113,7 @@ def _to_attendee_tickets(tickets: Iterable[DbTicket]) -> List[AttendeeTicket]:

def _get_attendee_ticket_sort_key(
    attendee_ticket: AttendeeTicket,
) -> Tuple[bool, str, bool]:
) -> tuple[bool, str, bool]:
    return (
        # List tickets with occupied seat first.
        attendee_ticket.seat is None,

M byceps/services/attendance/transfer/models.py => byceps/services/attendance/transfer/models.py +3 -2
@@ 6,8 6,9 @@ byceps.services.attendance.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional
from typing import Optional

from ....services.seating.dbmodels.seat import Seat
from ....services.user.dbmodels.user import User


@@ 22,4 23,4 @@ class AttendeeTicket:
@dataclass  # Not yet frozen b/c models are not immutable.
class Attendee:
    user: User
    tickets: List[AttendeeTicket]
    tickets: list[AttendeeTicket]

M byceps/services/authentication/session/models/current_user.py => byceps/services/authentication/session/models/current_user.py +3 -2
@@ 6,9 6,10 @@ byceps.services.authentication.session.models.current_user
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Set
from typing import Optional

from .....services.user.transfer.models import User



@@ 18,7 19,7 @@ class CurrentUser(User):
    """The current user, anonymous or logged in."""

    authenticated: bool
    permissions: Set[Enum]
    permissions: set[Enum]
    locale: Optional[str]

    def __eq__(self, other) -> bool:

M byceps/services/authentication/session/service.py => byceps/services/authentication/session/service.py +4 -3
@@ 6,9 6,10 @@ byceps.services.authentication.session.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Optional, Set, Tuple
from typing import Optional
from uuid import UUID, uuid4

from ....database import db, insert_ignore_on_conflict, upsert


@@ 107,7 108,7 @@ def _is_token_valid_for_user(token: str, user_id: UserID) -> bool:

def log_in_user(
    user_id: UserID, ip_address: str, *, site_id: Optional[SiteID] = None
) -> Tuple[str, UserLoggedIn]:
) -> tuple[str, UserLoggedIn]:
    """Create a session token and record the log in."""
    session_token = get_session_token(user_id)



@@ 183,7 184,7 @@ def get_anonymous_current_user(*, locale: Optional[str] = None) -> CurrentUser:


def get_authenticated_current_user(
    user: User, *, permissions: Set[Enum] = None, locale: Optional[str] = None
    user: User, *, permissions: set[Enum] = None, locale: Optional[str] = None
) -> CurrentUser:
    """Return an authenticated current user object."""
    if permissions is None:

M byceps/services/authorization/impex_service.py => byceps/services/authorization/impex_service.py +7 -6
@@ 8,8 8,9 @@ Import/export
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from pathlib import Path
from typing import Dict, Iterator, List, Tuple, Union
from typing import Iterator, Union

import rtoml



@@ 23,7 24,7 @@ from . import service
# import


def import_from_file(path: Path) -> Tuple[int, int]:
def import_from_file(path: Path) -> tuple[int, int]:
    """Import permissions, roles, and their relations from TOML."""
    data = rtoml.load(path)



@@ 36,12 37,12 @@ def import_from_file(path: Path) -> Tuple[int, int]:
    return len(permissions), len(roles)


def _create_permissions(permissions: List[Dict[str, str]]) -> None:
def _create_permissions(permissions: list[dict[str, str]]) -> None:
    for permission in permissions:
        service.create_permission(permission['id'], permission['title'])


def _create_roles(roles: List[Dict[str, Union[str, List[str]]]]) -> None:
def _create_roles(roles: list[dict[str, Union[str, list[str]]]]) -> None:
    for role in roles:
        role_id = role['id']



@@ 68,7 69,7 @@ def export() -> str:
    return rtoml.dumps(data, pretty=True)


def _collect_permissions() -> Iterator[Dict[str, str]]:
def _collect_permissions() -> Iterator[dict[str, str]]:
    """Collect all permissions, even those not assigned to any role."""
    permissions = DbPermission.query \
        .options(


@@ 84,7 85,7 @@ def _collect_permissions() -> Iterator[Dict[str, str]]:
        }


def _collect_roles() -> Iterator[Dict[str, Union[str, List[str]]]]:
def _collect_roles() -> Iterator[dict[str, Union[str, list[str]]]]:
    """Collect all roles and the permissions assigned to them."""
    roles = DbRole.query \
        .options(

M byceps/services/authorization/service.py => byceps/services/authorization/service.py +10 -9
@@ 6,7 6,8 @@ byceps.services.authorization.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, List, Optional, Sequence, Set
from __future__ import annotations
from typing import Optional, Sequence

from sqlalchemy.exc import IntegrityError



@@ 88,7 89,7 @@ def find_role(role_id: RoleID) -> Optional[Role]:
    return _db_entity_to_role(role)


def find_role_ids_for_user(user_id: UserID) -> Set[RoleID]:
def find_role_ids_for_user(user_id: UserID) -> set[RoleID]:
    """Return the IDs of the roles assigned to the user."""
    roles = DbRole.query \
        .join(DbUserRole) \


@@ 98,7 99,7 @@ def find_role_ids_for_user(user_id: UserID) -> Set[RoleID]:
    return {r.id for r in roles}


def find_user_ids_for_role(role_id: RoleID) -> Set[UserID]:
def find_user_ids_for_role(role_id: RoleID) -> set[UserID]:
    """Return the IDs of the users that have this role assigned."""
    rows = db.session \
        .query(DbUserRole.user_id) \


@@ 196,7 197,7 @@ def _is_role_assigned_to_user(role_id: RoleID, user_id: UserID) -> bool:
    return db.session.query(subquery).scalar()


def get_permission_ids_for_user(user_id: UserID) -> Set[PermissionID]:
def get_permission_ids_for_user(user_id: UserID) -> set[PermissionID]:
    """Return the IDs of all permissions the user has through the roles
    assigned to it.
    """


@@ 229,7 230,7 @@ def get_all_roles_with_titles() -> Sequence[DbRole]:
        .all()


def get_permissions_by_roles_with_titles() -> Dict[Role, Set[Permission]]:
def get_permissions_by_roles_with_titles() -> dict[Role, set[Permission]]:
    """Return all roles with their assigned permissions.

    Titles are undeferred to avoid lots of additional queries.


@@ 252,7 253,7 @@ def get_permissions_by_roles_with_titles() -> Dict[Role, Set[Permission]]:

def get_permissions_by_roles_for_user_with_titles(
    user_id: UserID,
) -> Dict[Role, Set[Permission]]:
) -> dict[Role, set[Permission]]:
    """Return permissions grouped by their respective roles for that user.

    Titles are undeferred to avoid lots of additional queries.


@@ 284,9 285,9 @@ def get_permissions_by_roles_for_user_with_titles(


def _index_permissions_by_role(
    permissions: List[DbPermission], roles: List[DbRole]
) -> Dict[Role, Set[Permission]]:
    permissions_by_role: Dict[DbRole, Set[DbPermission]] = {
    permissions: list[DbPermission], roles: list[DbRole]
) -> dict[Role, set[Permission]]:
    permissions_by_role: dict[DbRole, set[DbPermission]] = {
        role: set() for role in roles
    }


M byceps/services/board/posting_command_service.py => byceps/services/board/posting_command_service.py +2 -2
@@ 6,8 6,8 @@ byceps.services.board.posting_command_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Tuple

from ...database import db
from ...events.board import (


@@ 30,7 30,7 @@ from .transfer.models import PostingID, TopicID

def create_posting(
    topic_id: TopicID, creator_id: UserID, body: str
) -> Tuple[DbPosting, BoardPostingCreated]:
) -> tuple[DbPosting, BoardPostingCreated]:
    """Create a posting in that topic."""
    topic = topic_query_service.get_topic(topic_id)
    creator = _get_user(creator_id)

M byceps/services/board/posting_query_service.py => byceps/services/board/posting_query_service.py +4 -3
@@ 6,7 6,8 @@ byceps.services.board.posting_query_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db, Pagination
from ...typing import PartyID, UserID


@@ 76,8 77,8 @@ def paginate_postings(


def _get_users_by_id(
    user_ids: Set[UserID], party_id: Optional[PartyID]
) -> Dict[UserID, User]:
    user_ids: set[UserID], party_id: Optional[PartyID]
) -> dict[UserID, User]:
    users = user_service.find_users(
        user_ids, include_avatars=True, include_orga_flags_for_party_id=party_id
    )

M byceps/services/board/topic_command_service.py => byceps/services/board/topic_command_service.py +2 -2
@@ 6,8 6,8 @@ byceps.services.board.topic_command_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Tuple

from ...database import db
from ...events.board import (


@@ 37,7 37,7 @@ from .transfer.models import CategoryID, TopicID

def create_topic(
    category_id: CategoryID, creator_id: UserID, title: str, body: str
) -> Tuple[DbTopic, BoardTopicCreated]:
) -> tuple[DbTopic, BoardTopicCreated]:
    """Create a topic with an initial posting in that category."""
    creator = _get_user(creator_id)


M byceps/services/board/topic_query_service.py => byceps/services/board/topic_query_service.py +4 -3
@@ 6,8 6,9 @@ byceps.services.board.topic_query_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List, Optional, Set
from typing import Optional

from ...database import db, Pagination, Query



@@ 61,7 62,7 @@ def find_topic_visible_for_user(

def get_recent_topics(
    board_id: BoardID, include_hidden: bool, limit: int
) -> List[DbTopic]:
) -> list[DbTopic]:
    """Paginate topics in that board."""
    return _query_topics(include_hidden) \
        .join(DbCategory) \


@@ 84,7 85,7 @@ def paginate_topics(
        .paginate(page, topics_per_page)


def get_all_topic_ids_in_category(category_id: CategoryID) -> Set[TopicID]:
def get_all_topic_ids_in_category(category_id: CategoryID) -> set[TopicID]:
    """Return the IDs of all topics in the category."""
    rows = db.session \
        .query(DbTopic.id) \

M byceps/services/brand/service.py => byceps/services/brand/service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.brand.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import BrandID


@@ 78,7 79,7 @@ def get_brand(brand_id: BrandID) -> Brand:
    return brand


def get_all_brands() -> List[Brand]:
def get_all_brands() -> list[Brand]:
    """Return all brands, ordered by title."""
    brands = DbBrand.query \
        .order_by(DbBrand.title) \

M byceps/services/brand/settings_service.py => byceps/services/brand/settings_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.brand.settings_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db, upsert
from ...typing import BrandID


@@ 80,7 81,7 @@ def find_setting_value(brand_id: BrandID, name: str) -> Optional[str]:
    return setting.value


def get_settings(brand_id: BrandID) -> Set[BrandSetting]:
def get_settings(brand_id: BrandID) -> set[BrandSetting]:
    """Return all settings for that brand."""
    settings = DbSetting.query \
        .filter_by(brand_id=brand_id) \

M byceps/services/consent/consent_service.py => byceps/services/consent/consent_service.py +6 -5
@@ 6,8 6,9 @@ byceps.services.consent.consent_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Dict, Iterable, Sequence, Set
from typing import Iterable, Sequence

from ...database import db
from ...typing import UserID


@@ 53,7 54,7 @@ def consent_to_subjects(
    db.session.commit()


def count_consents_by_subject() -> Dict[str, int]:
def count_consents_by_subject() -> dict[str, int]:
    """Return the number of given consents per subject."""
    rows = db.session \
        .query(


@@ 75,8 76,8 @@ def get_consents_by_user(user_id: UserID) -> Sequence[DbConsent]:


def get_unconsented_subject_ids(
    user_id: UserID, required_subject_ids: Set[SubjectID]
) -> Set[SubjectID]:
    user_id: UserID, required_subject_ids: set[SubjectID]
) -> set[SubjectID]:
    """Return the IDs of the subjects the user has not consented to."""
    unconsented_subject_ids = set()



@@ 88,7 89,7 @@ def get_unconsented_subject_ids(


def has_user_consented_to_all_subjects(
    user_id: UserID, subject_ids: Set[SubjectID]
    user_id: UserID, subject_ids: set[SubjectID]
) -> bool:
    """Return `True` if the user has consented to all given subjects."""
    for subject_id in subject_ids:

M byceps/services/consent/subject_service.py => byceps/services/consent/subject_service.py +8 -7
@@ 6,7 6,8 @@ byceps.services.consent.subject_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import BrandID


@@ 40,7 41,7 @@ def create_subject(
    return _db_entity_to_subject(subject)


def get_subjects(subject_ids: Set[SubjectID]) -> Set[Subject]:
def get_subjects(subject_ids: set[SubjectID]) -> set[Subject]:
    """Return the subjects."""
    rows = DbSubject.query \
        .filter(DbSubject.id.in_(subject_ids)) \


@@ 54,7 55,7 @@ def get_subjects(subject_ids: Set[SubjectID]) -> Set[Subject]:


def _check_for_unknown_subject_ids(
    subject_ids: Set[SubjectID], subjects: Set[Subject]
    subject_ids: set[SubjectID], subjects: set[Subject]
) -> None:
    """Raise exception on unknown IDs."""
    found_subject_ids = {subject.id for subject in subjects}


@@ 67,8 68,8 @@ def _check_for_unknown_subject_ids(


def get_subjects_with_consent_counts(
    *, limit_to_subject_ids: Optional[Set[SubjectID]] = None
) -> Dict[Subject, int]:
    *, limit_to_subject_ids: Optional[set[SubjectID]] = None
) -> dict[Subject, int]:
    """Return subjects and their consent counts."""
    query = db.session \
        .query(


@@ 123,7 124,7 @@ def delete_brand_requirement(brand_id: BrandID, subject_id: SubjectID) -> None:
    db.session.commit()


def get_subject_ids_required_for_brand(brand_id: BrandID) -> Set[SubjectID]:
def get_subject_ids_required_for_brand(brand_id: BrandID) -> set[SubjectID]:
    """Return the IDs of the subjects required for the brand."""
    rows = db.session \
        .query(DbSubject.id) \


@@ 134,7 135,7 @@ def get_subject_ids_required_for_brand(brand_id: BrandID) -> Set[SubjectID]:
    return {row[0] for row in rows}


def get_subjects_required_for_brand(brand_id: BrandID) -> Set[Subject]:
def get_subjects_required_for_brand(brand_id: BrandID) -> set[Subject]:
    """Return the subjects required for the brand."""
    rows = DbSubject.query \
        .join(DbBrandRequirement) \

M byceps/services/country/service.py => byceps/services/country/service.py +3 -3
@@ 6,10 6,10 @@ byceps.services.country.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import codecs
from dataclasses import dataclass
import json
from typing import List

from flask import current_app



@@ 21,7 21,7 @@ class Country:
    alpha3: str


def get_countries() -> List[Country]:
def get_countries() -> list[Country]:
    """Load countries from JSON file."""
    reader = codecs.getreader('utf-8')



@@ 32,6 32,6 @@ def get_countries() -> List[Country]:
    return [Country(**record) for record in records]


def get_country_names() -> List[str]:
def get_country_names() -> list[str]:
    """Return country names."""
    return [country.name for country in get_countries()]

M byceps/services/email/service.py => byceps/services/email/service.py +5 -4
@@ 6,7 6,8 @@ byceps.services.email.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from sqlalchemy.exc import IntegrityError



@@ 131,7 132,7 @@ def set_config(
    upsert(table, identifier, replacement)


def get_all_configs() -> List[EmailConfig]:
def get_all_configs() -> list[EmailConfig]:
    """Return all configurations."""
    configs = DbEmailConfig.query.all()



@@ 146,7 147,7 @@ def enqueue_message(message: Message) -> None:


def enqueue_email(
    sender: Optional[Sender], recipients: List[str], subject: str, body: str
    sender: Optional[Sender], recipients: list[str], subject: str, body: str
) -> None:
    """Enqueue e-mail to be sent asynchronously."""
    sender_str = sender.format() if (sender is not None) else None


@@ 155,7 156,7 @@ def enqueue_email(


def send_email(
    sender: Optional[str], recipients: List[str], subject: str, body: str
    sender: Optional[str], recipients: list[str], subject: str, body: str
) -> None:
    """Send e-mail."""
    email.send(sender, recipients, subject, body)

M byceps/services/email/transfer/models.py => byceps/services/email/transfer/models.py +2 -2
@@ 6,9 6,9 @@ byceps.services.email.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from email.utils import formataddr
from typing import List

from ....typing import BrandID



@@ 34,6 34,6 @@ class EmailConfig:
@dataclass(frozen=True)
class Message:
    sender: Sender
    recipients: List[str]
    recipients: list[str]
    subject: str
    body: str

M byceps/services/global_setting/service.py => byceps/services/global_setting/service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.global_setting.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db, upsert



@@ 70,7 71,7 @@ def find_setting_value(name: str) -> Optional[str]:
    return setting.value


def get_settings() -> Set[GlobalSetting]:
def get_settings() -> set[GlobalSetting]:
    """Return all global settings."""
    settings = DbSetting.query.all()


M byceps/services/image/service.py => byceps/services/image/service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.image.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import BinaryIO, FrozenSet, Iterable, Set
from __future__ import annotations
from typing import BinaryIO, FrozenSet, Iterable

from ...util.image import read_dimensions
from ...util.image.models import Dimensions, ImageType


@@ 23,7 24,7 @@ def get_image_type_names(types: Iterable[ImageType]) -> FrozenSet[str]:


def determine_image_type(
    stream: BinaryIO, allowed_types: Set[ImageType]
    stream: BinaryIO, allowed_types: set[ImageType]
) -> ImageType:
    """Extract image type from stream."""
    image_type = guess_type(stream)

M byceps/services/metrics/models.py => byceps/services/metrics/models.py +2 -2
@@ 6,8 6,8 @@ byceps.services.metrics.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass, field
from typing import List


@dataclass(frozen=True)


@@ 38,7 38,7 @@ def _escape_label_value(value: str) -> str:
class Metric:
    name: str
    value: float
    labels: List[Label] = field(default_factory=dict)
    labels: list[Label] = field(default_factory=dict)

    def serialize(self) -> str:
        labels_str = ''

M byceps/services/metrics/service.py => byceps/services/metrics/service.py +7 -6
@@ 6,7 6,8 @@ byceps.metrics.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Iterator, List, Set
from __future__ import annotations
from typing import Iterator

from ...services.brand import service as brand_service
from ...services.board import (


@@ 52,7 53,7 @@ def collect_metrics() -> Iterator[Metric]:
    yield from _collect_user_metrics()


def _collect_board_metrics(brand_ids: List[BrandID]) -> Iterator[Metric]:
def _collect_board_metrics(brand_ids: list[BrandID]) -> Iterator[Metric]:
    for brand_id in brand_ids:
        boards = board_service.get_boards_for_brand(brand_id)
        board_ids = [board.id for board in boards]


@@ 82,7 83,7 @@ def _collect_consent_metrics() -> Iterator[Metric]:


def _collect_shop_ordered_article_metrics(
    shop_ids: Set[ShopID],
    shop_ids: set[ShopID],
) -> Iterator[Metric]:
    """Provide ordered article quantities for shops."""
    stats = shop_article_service.sum_ordered_articles_by_payment_state(shop_ids)


@@ 100,7 101,7 @@ def _collect_shop_ordered_article_metrics(
        )


def _collect_shop_order_metrics(shops: List[Shop]) -> Iterator[Metric]:
def _collect_shop_order_metrics(shops: list[Shop]) -> Iterator[Metric]:
    """Provide order counts grouped by payment state for shops."""
    for shop in shops:
        order_counts_per_payment_state = order_service.count_orders_per_payment_state(


@@ 119,7 120,7 @@ def _collect_shop_order_metrics(shops: List[Shop]) -> Iterator[Metric]:


def _collect_seating_metrics(
    active_party_ids: List[PartyID],
    active_party_ids: list[PartyID],
) -> Iterator[Metric]:
    """Provide seat occupation counts per party and category."""
    for party_id in active_party_ids:


@@ 138,7 139,7 @@ def _collect_seating_metrics(
            )


def _collect_ticket_metrics(active_parties: List[Party]) -> Iterator[Metric]:
def _collect_ticket_metrics(active_parties: list[Party]) -> Iterator[Metric]:
    """Provide ticket counts for active parties."""
    for party in active_parties:
        party_id = party.id

M byceps/services/news/channel_service.py => byceps/services/news/channel_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.news.channel_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional, Sequence
from __future__ import annotations
from typing import Optional, Sequence

from ...database import db
from ...typing import BrandID


@@ 52,7 53,7 @@ def find_channel(channel_id: ChannelID) -> Optional[Channel]:
    return _db_entity_to_channel(channel)


def get_all_channels() -> List[Channel]:
def get_all_channels() -> list[Channel]:
    """Return all channels."""
    channels = DbChannel.query.all()


M byceps/services/news/html_service.py => byceps/services/news/html_service.py +4 -3
@@ 8,8 8,9 @@ Render HTML fragments from news items and images.
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from functools import partial
from typing import List, Optional
from typing import Optional

from flask_babel import gettext
from jinja2 import Markup


@@ 21,7 22,7 @@ from .transfer.models import ChannelID, Image


def render_body(
    raw_body: str, channel_id: ChannelID, images: List[Image]
    raw_body: str, channel_id: ChannelID, images: list[Image]
) -> str:
    """Render item's raw body to HTML."""
    template = load_template(raw_body)


@@ 31,7 32,7 @@ def render_body(

def _render_image(
    channel_id: ChannelID,
    images: List[Image],
    images: list[Image],
    number: int,
    *,
    width: Optional[int] = None,

M byceps/services/news/service.py => byceps/services/news/service.py +4 -3
@@ 6,10 6,11 @@ byceps.services.news.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import dataclasses
from datetime import datetime
from functools import partial
from typing import Dict, List, Optional, Sequence
from typing import Optional, Sequence

from ...database import db, paginate, Pagination, Query
from ...events.news import NewsItemPublished


@@ 232,7 233,7 @@ def get_items_paginated(
        .paginate(page, items_per_page)


def get_recent_headlines(channel_id: ChannelID, limit: int) -> List[Headline]:
def get_recent_headlines(channel_id: ChannelID, limit: int) -> list[Headline]:
    """Return the most recent headlines."""
    items = DbItem.query \
        .for_channel(channel_id) \


@@ 281,7 282,7 @@ def find_item_version(version_id: ItemVersionID) -> DbItemVersion:
    return DbItemVersion.query.get(version_id)


def get_item_count_by_channel_id() -> Dict[ChannelID, int]:
def get_item_count_by_channel_id() -> dict[ChannelID, int]:
    """Return news item count (including 0) per channel, indexed by
    channel ID.
    """

M byceps/services/news/transfer/models.py => byceps/services/news/transfer/models.py +3 -2
@@ 6,9 6,10 @@ byceps.services.news.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import List, NewType, Optional
from typing import NewType, Optional
from uuid import UUID

from ....typing import BrandID, UserID


@@ 58,7 59,7 @@ class Item:
    body: str
    external_url: str
    image_url_path: Optional[str]
    images: List[Image]
    images: list[Image]


@dataclass(frozen=True)

M byceps/services/newsletter/service.py => byceps/services/newsletter/service.py +4 -12
@@ 6,17 6,9 @@ byceps.services.newsletter.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from operator import itemgetter
from typing import (
    Dict,
    Iterable,
    Iterator,
    Optional,
    Sequence,
    Set,
    Tuple,
    Union,
)
from typing import Iterable, Iterator, Optional, Sequence, Union

from ...database import BaseQuery, db
from ...typing import UserID


@@ 105,7 97,7 @@ def _build_query_for_current_subscribers(list_id: ListID) -> BaseQuery:
        .filter(DbSubscriptionUpdate.list_id == list_id)


def _get_subscriber_details(user_ids: Set[UserID]) -> Iterator[Subscriber]:
def _get_subscriber_details(user_ids: set[UserID]) -> Iterator[Subscriber]:
    """Yield screen name and email address of each eligible user."""
    if not user_ids:
        return []


@@ 129,7 121,7 @@ def _get_subscriber_details(user_ids: Set[UserID]) -> Iterator[Subscriber]:

def count_subscriptions_by_state(
    list_id: ListID,
) -> Dict[Union[SubscriptionState, str], int]:
) -> dict[Union[SubscriptionState, str], int]:
    """Return the totals for each state as well as an overall total."""
    rows = _build_query_for_current_state(list_id) \
        .all()

M byceps/services/orga/birthday_service.py => byceps/services/orga/birthday_service.py +8 -7
@@ 6,8 6,9 @@ byceps.services.orga.birthday_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from itertools import islice
from typing import Dict, Iterator, Optional, Sequence, Set, Tuple
from typing import Iterator, Optional, Sequence

from ...database import db



@@ 20,7 21,7 @@ from .dbmodels import OrgaFlag as DbOrgaFlag
from .transfer.models import Birthday


def get_orgas_with_birthday_today() -> Set[User]:
def get_orgas_with_birthday_today() -> set[User]:
    """Return the orgas whose birthday is today."""
    orgas_with_birthdays = _collect_orgas_with_known_birthdays()



@@ 31,7 32,7 @@ def get_orgas_with_birthday_today() -> Set[User]:

def collect_orgas_with_next_birthdays(
    *, limit: Optional[int] = None
) -> Iterator[Tuple[User, Birthday]]:
) -> Iterator[tuple[User, Birthday]]:
    """Yield the next birthdays of organizers, sorted by month and day."""
    orgas_with_birthdays = _collect_orgas_with_known_birthdays()



@@ 43,7 44,7 @@ def collect_orgas_with_next_birthdays(
    return sorted_orgas


def _collect_orgas_with_known_birthdays() -> Iterator[Tuple[User, Birthday]]:
def _collect_orgas_with_known_birthdays() -> Iterator[tuple[User, Birthday]]:
    """Return all organizers whose birthday is known."""
    users = DbUser.query \
        .join(DbOrgaFlag) \


@@ 64,7 65,7 @@ def _collect_orgas_with_known_birthdays() -> Iterator[Tuple[User, Birthday]]:


def _to_user_dto(
    user: DbUser, avatar_urls_by_user_id: Dict[UserID, str]
    user: DbUser, avatar_urls_by_user_id: dict[UserID, str]
) -> User:
    """Create user DTO from database entity."""
    avatar_url = avatar_urls_by_user_id.get(user.id)


@@ 80,8 81,8 @@ def _to_user_dto(


def sort_users_by_next_birthday(
    users_and_birthdays: Sequence[Tuple[User, Birthday]]
) -> Sequence[Tuple[User, Birthday]]:
    users_and_birthdays: Sequence[tuple[User, Birthday]]
) -> Sequence[tuple[User, Birthday]]:
    return sorted(
        users_and_birthdays,
        key=lambda user_and_birthday: (

M byceps/services/orga/service.py => byceps/services/orga/service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.orga.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Sequence
from __future__ import annotations
from typing import Optional, Sequence

from ...database import db
from ...typing import BrandID, UserID


@@ 19,7 20,7 @@ from ..user.dbmodels.user import User as DbUser
from .dbmodels import OrgaFlag as DbOrgaFlag


def get_person_count_by_brand_id() -> Dict[BrandID, int]:
def get_person_count_by_brand_id() -> dict[BrandID, int]:
    """Return organizer count (including 0) per brand, indexed by brand ID."""
    brand_ids_and_orga_flag_counts = db.session \
        .query(

M byceps/services/orga_presence/service.py => byceps/services/orga_presence/service.py +6 -5
@@ 6,9 6,10 @@ byceps.services.orga_presence.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import date, datetime
from itertools import groupby
from typing import Iterator, List, Sequence, Tuple
from typing import Iterator, Sequence

import pendulum
from pendulum import DateTime


@@ 21,7 22,7 @@ from .dbmodels import Presence as DbPresence, Task as DbTask
from .transfer.models import PresenceTimeSlot, TaskTimeSlot, TimeSlot


def get_presences(party_id: PartyID) -> List[PresenceTimeSlot]:
def get_presences(party_id: PartyID) -> list[PresenceTimeSlot]:
    """Return all presences for that party."""
    presences = DbPresence.query \
        .for_party(party_id) \


@@ 31,7 32,7 @@ def get_presences(party_id: PartyID) -> List[PresenceTimeSlot]:
    return [_presence_to_time_slot(presence) for presence in presences]


def get_tasks(party_id: PartyID) -> List[TaskTimeSlot]:
def get_tasks(party_id: PartyID) -> list[TaskTimeSlot]:
    """Return all tasks for that party."""
    tasks = DbTask.query \
        .for_party(party_id) \


@@ 53,7 54,7 @@ def _task_to_time_slot(task: DbTask) -> TaskTimeSlot:
# -------------------------------------------------------------------- #


def get_hour_ranges(time_slots: List[TimeSlot]) -> Iterator[DateTimeRange]:
def get_hour_ranges(time_slots: list[TimeSlot]) -> Iterator[DateTimeRange]:
    """Yield hour ranges based on the time slots."""
    time_slot_ranges = [time_slot.range for time_slot in time_slots]
    hour_starts = _get_hour_starts(time_slot_ranges)


@@ 85,7 86,7 @@ def _to_datetimes_without_tzinfo(dts: Sequence[DateTime]) -> Iterator[datetime]:

def get_days_and_hour_totals(
    hour_ranges: Sequence[DateTimeRange],
) -> Iterator[Tuple[date, int]]:
) -> Iterator[tuple[date, int]]:
    """Yield (day, relevant hours total) pairs."""

    def get_date(dt_range: DateTimeRange) -> date:

M byceps/services/orga_team/service.py => byceps/services/orga_team/service.py +10 -9
@@ 6,8 6,9 @@ byceps.services.orga_team.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import dataclasses
from typing import Dict, Optional, Sequence, Set, Tuple
from typing import Optional, Sequence

from ...database import db
from ...typing import PartyID, UserID


@@ 53,7 54,7 @@ def delete_team(team_id: OrgaTeamID) -> None:
    db.session.commit()


def count_teams_for_parties(party_ids: Set[PartyID]) -> Dict[PartyID, int]:
def count_teams_for_parties(party_ids: set[PartyID]) -> dict[PartyID, int]:
    """Count orga teams for each party."""
    rows = db.session \
        .query(


@@ 74,7 75,7 @@ def count_teams_for_party(party_id: PartyID) -> int:
        .count()


def get_teams_for_party(party_id: PartyID) -> Set[OrgaTeam]:
def get_teams_for_party(party_id: PartyID) -> set[OrgaTeam]:
    """Return orga teams for that party."""
    teams = DbOrgaTeam.query \
        .filter_by(party_id=party_id) \


@@ 100,7 101,7 @@ def _find_db_team(team_id: OrgaTeamID) -> Optional[DbOrgaTeam]:

def get_teams_and_members_for_party(
    party_id: PartyID,
) -> Sequence[Tuple[OrgaTeam, Set[Member]]]:
) -> Sequence[tuple[OrgaTeam, set[Member]]]:
    """Return all orga teams and their corresponding memberships for
    that party.
    """


@@ 188,7 189,7 @@ def count_memberships_for_party(party_id: PartyID) -> int:
        .count()


def get_memberships_for_party(party_id: PartyID) -> Set[Membership]:
def get_memberships_for_party(party_id: PartyID) -> set[Membership]:
    """Return memberships for that party."""
    memberships = DbMembership.query \
        .for_party(party_id) \


@@ 225,7 226,7 @@ def find_orga_team_for_user_and_party(
        .one_or_none()


def get_orga_activities_for_user(user_id: UserID) -> Set[OrgaActivity]:
def get_orga_activities_for_user(user_id: UserID) -> set[OrgaActivity]:
    """Return all orga team activities for that user."""
    memberships = DbMembership.query \
        .options(


@@ 248,7 249,7 @@ def get_orga_activities_for_user(user_id: UserID) -> Set[OrgaActivity]:
    return {to_activity(ms) for ms in memberships}


def get_public_orgas_for_party(party_id: PartyID) -> Set[PublicOrga]:
def get_public_orgas_for_party(party_id: PartyID) -> set[PublicOrga]:
    """Return all public orgas for that party."""
    memberships = DbMembership.query \
        .for_party(party_id) \


@@ 282,7 283,7 @@ def get_public_orgas_for_party(party_id: PartyID) -> Set[PublicOrga]:

def _get_public_orga_users_by_id(
    memberships: DbMembership,
) -> Dict[UserID, User]:
) -> dict[UserID, User]:
    user_ids = {ms.user_id for ms in memberships}

    users = user_service.find_users(user_ids, include_avatars=True)


@@ 343,7 344,7 @@ def copy_teams_and_memberships(
# organizers


def get_unassigned_orgas_for_party(party_id: PartyID) -> Set[User]:
def get_unassigned_orgas_for_party(party_id: PartyID) -> set[User]:
    """Return organizers that are not assigned to a team for the party."""
    party = party_service.get_party(party_id)


M byceps/services/party/service.py => byceps/services/party/service.py +10 -9
@@ 6,9 6,10 @@ byceps.services.party.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import dataclasses
from datetime import date, datetime, timedelta
from typing import Dict, List, Optional, Set, Union
from typing import Optional, Union

from ...database import db, paginate, Pagination
from ...typing import BrandID, PartyID


@@ 126,7 127,7 @@ def get_party(party_id: PartyID) -> Party:
    return party


def get_all_parties() -> List[Party]:
def get_all_parties() -> list[Party]:
    """Return all parties."""
    parties = DbParty.query \
        .all()


@@ 134,7 135,7 @@ def get_all_parties() -> List[Party]:
    return [_db_entity_to_party(party) for party in parties]


def get_all_parties_with_brands() -> List[PartyWithBrand]:
def get_all_parties_with_brands() -> list[PartyWithBrand]:
    """Return all parties."""
    parties = DbParty.query \
        .options(db.joinedload('brand')) \


@@ 145,7 146,7 @@ def get_all_parties_with_brands() -> List[PartyWithBrand]:

def get_active_parties(
    brand_id: Optional[BrandID] = None, *, include_brands: bool = False
) -> List[Union[Party, PartyWithBrand]]:
) -> list[Union[Party, PartyWithBrand]]:
    """Return active (i.e. non-canceled, non-archived) parties."""
    query = DbParty.query



@@ 169,7 170,7 @@ def get_active_parties(
    return [transform(party) for party in parties]


def get_archived_parties_for_brand(brand_id: BrandID) -> List[Party]:
def get_archived_parties_for_brand(brand_id: BrandID) -> list[Party]:
    """Return archived parties for that brand."""
    parties = DbParty.query \
        .filter_by(brand_id=brand_id) \


@@ 180,7 181,7 @@ def get_archived_parties_for_brand(brand_id: BrandID) -> List[Party]:
    return [_db_entity_to_party(party) for party in parties]


def get_parties(party_ids: Set[PartyID]) -> List[Party]:
def get_parties(party_ids: set[PartyID]) -> list[Party]:
    """Return the parties with those IDs."""
    if not party_ids:
        return []


@@ 192,7 193,7 @@ def get_parties(party_ids: Set[PartyID]) -> List[Party]:
    return [_db_entity_to_party(party) for party in parties]


def get_parties_for_brand(brand_id: BrandID) -> List[Party]:
def get_parties_for_brand(brand_id: BrandID) -> list[Party]:
    """Return the parties for that brand."""
    parties = DbParty.query \
        .filter_by(brand_id=brand_id) \


@@ 212,7 213,7 @@ def get_parties_for_brand_paginated(
    return paginate(query, page, per_page, item_mapper=_db_entity_to_party)


def get_party_count_by_brand_id() -> Dict[BrandID, int]:
def get_party_count_by_brand_id() -> dict[BrandID, int]:
    """Return party count (including 0) per brand, indexed by brand ID."""
    brand_ids_and_party_counts = db.session \
        .query(


@@ 251,7 252,7 @@ def _db_entity_to_party_with_brand(party_entity: DbParty) -> PartyWithBrand:
    return PartyWithBrand(*(party_tuple + brand_tuple))


def get_party_days(party: Party) -> List[date]:
def get_party_days(party: Party) -> list[date]:
    """Return the sequence of dates on which the party happens."""
    starts_on = party.starts_at.date()
    ends_on = party.ends_at.date()

M byceps/services/party/settings_service.py => byceps/services/party/settings_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.party.settings_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db, upsert
from ...typing import PartyID


@@ 80,7 81,7 @@ def find_setting_value(party_id: PartyID, name: str) -> Optional[str]:
    return setting.value


def get_settings(party_id: PartyID) -> Set[PartySetting]:
def get_settings(party_id: PartyID) -> set[PartySetting]:
    """Return all settings for that party."""
    settings = DbSetting.query \
        .filter_by(party_id=party_id) \

M byceps/services/seating/seat_service.py => byceps/services/seating/seat_service.py +5 -4
@@ 6,7 6,8 @@ byceps.services.seating.seat_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, List, Optional, Sequence, Set, Tuple
from __future__ import annotations
from typing import Optional, Sequence

from ...database import db
from ...typing import PartyID


@@ 42,7 43,7 @@ def delete_seat(seat_id: SeatID) -> None:

def count_occupied_seats_by_category(
    party_id: PartyID,
) -> List[Tuple[TicketCategory, int]]:
) -> list[tuple[TicketCategory, int]]:
    """Count occupied seats for the party, grouped by ticket category."""
    subquery = db.session \
        .query(


@@ 95,7 96,7 @@ def get_seat_utilization(party_id: PartyID) -> SeatUtilization:
    return SeatUtilization(occupied_seat_count, total_seat_count)


def get_seat_total_per_area(party_id: PartyID) -> Dict[AreaID, int]:
def get_seat_total_per_area(party_id: PartyID) -> dict[AreaID, int]:
    """Return the number of seats per area for that party."""
    area_ids_and_seat_counts = db.session \
        .query(


@@ 115,7 116,7 @@ def find_seat(seat_id: SeatID) -> Optional[DbSeat]:
    return DbSeat.query.get(seat_id)


def find_seats(seat_ids: Set[SeatID]) -> Set[DbSeat]:
def find_seats(seat_ids: set[SeatID]) -> set[DbSeat]:
    """Return the seats with those IDs."""
    if not seat_ids:
        return set()

M byceps/services/shop/article/models/compilation.py => byceps/services/shop/article/models/compilation.py +3 -2
@@ 6,7 6,8 @@ byceps.services.shop.article.models.compilation
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Iterator, List, Optional
from __future__ import annotations
from typing import Iterator, Optional

from ..transfer.models import Article



@@ 30,7 31,7 @@ class ArticleCompilationItem:
class ArticleCompilation:

    def __init__(self) -> None:
        self._items: List[ArticleCompilationItem] = []
        self._items: list[ArticleCompilationItem] = []

    def append(self, item: ArticleCompilationItem) -> None:
        self._items.append(item)

M byceps/services/shop/article/sequence_service.py => byceps/services/shop/article/sequence_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.shop.article.sequence_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from sqlalchemy.exc import IntegrityError



@@ 72,7 73,7 @@ def find_article_number_sequence(

def find_article_number_sequences_for_shop(
    shop_id: ShopID,
) -> List[ArticleNumberSequence]:
) -> list[ArticleNumberSequence]:
    """Return the article number sequences defined for that shop."""
    sequences = DbArticleNumberSequence.query \
        .filter_by(shop_id=shop_id) \

M byceps/services/shop/article/service.py => byceps/services/shop/article/service.py +7 -6
@@ 6,9 6,10 @@ byceps.services.shop.article.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Sequence, Set, Tuple
from typing import Optional, Sequence

from ....database import BaseQuery, db, Pagination



@@ 208,8 209,8 @@ def find_attached_article(


def get_articles_by_numbers(
    article_numbers: Set[ArticleNumber],
) -> Set[Article]:
    article_numbers: set[ArticleNumber],
) -> set[Article]:
    """Return the articles with those numbers."""
    if not article_numbers:
        return []


@@ 306,7 307,7 @@ def _add_attached_articles(
        )


def get_attachable_articles(article_id: ArticleID) -> Set[Article]:
def get_attachable_articles(article_id: ArticleID) -> set[Article]:
    """Return the articles that can be attached to that article."""
    article = _get_db_article(article_id)



@@ 338,8 339,8 @@ def is_article_available_now(article: Article) -> bool:


def sum_ordered_articles_by_payment_state(
    shop_ids: Set[ShopID],
) -> List[Tuple[ShopID, ArticleNumber, str, PaymentState, int]]:
    shop_ids: set[ShopID],
) -> list[tuple[ShopID, ArticleNumber, str, PaymentState, int]]:
    """Sum ordered articles for those shops, grouped by order payment state."""
    subquery = db.session \
        .query(

M byceps/services/shop/cart/models.py => byceps/services/shop/cart/models.py +3 -2
@@ 6,8 6,9 @@ byceps.services.shop.cart.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from decimal import Decimal
from typing import List, Sequence
from typing import Sequence

from ....util.instances import ReprBuilder



@@ 37,7 38,7 @@ class Cart:
    """A shopping cart."""

    def __init__(self) -> None:
        self._items: List[CartItem] = []
        self._items: list[CartItem] = []

    def add_item(self, article: DbArticle, quantity: int) -> None:
        item = CartItem(article, quantity)

M byceps/services/shop/catalog/service.py => byceps/services/shop/catalog/service.py +4 -3
@@ 6,7 6,8 @@ byceps.services.shop.catalog.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from ....database import db



@@ 56,7 57,7 @@ def _find_db_catalog(catalog_id: CatalogID) -> Optional[DbCatalog]:
    return DbCatalog.query.get(catalog_id)


def get_all_catalogs() -> List[Catalog]:
def get_all_catalogs() -> list[Catalog]:
    """Return all catalogs."""
    catalogs = DbCatalog.query.all()



@@ 96,7 97,7 @@ def delete_collection(collection_id: CollectionID) -> None:
    db.session.commit()


def get_collections_for_catalog(catalog_id: CatalogID) -> List[Collection]:
def get_collections_for_catalog(catalog_id: CatalogID) -> list[Collection]:
    """Return the catalog's collections."""
    collections = DbCollection.query \
        .filter_by(catalog_id=catalog_id) \

M byceps/services/shop/catalog/transfer/models.py => byceps/services/shop/catalog/transfer/models.py +3 -2
@@ 6,8 6,9 @@ byceps.services.shop.catalog.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import List, NewType
from typing import NewType
from uuid import UUID

from ...article.transfer.models import ArticleNumber


@@ 34,7 35,7 @@ class Collection:
    catalog_id: CatalogID
    title: str
    position: int
    article_numbers: List[ArticleNumber]
    article_numbers: list[ArticleNumber]


@dataclass(frozen=True)

M byceps/services/shop/order/action_service.py => byceps/services/shop/order/action_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.shop.order.action_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Callable, Sequence, Set
from __future__ import annotations
from typing import Callable, Sequence

from ....database import db
from ....typing import UserID


@@ 99,7 100,7 @@ def execute_actions(


def _get_actions(
    article_numbers: Set[ArticleNumber], payment_state: PaymentState
    article_numbers: set[ArticleNumber], payment_state: PaymentState
) -> Sequence[OrderAction]:
    """Return the order actions for those article numbers."""
    return OrderAction.query \

M byceps/services/shop/order/dbmodels/order.py => byceps/services/shop/order/dbmodels/order.py +1 -1
@@ 7,7 7,7 @@ byceps.services.shop.order.dbmodels.order
"""

from datetime import datetime
from typing import Optional, Set
from typing import Optional

from sqlalchemy.ext.hybrid import hybrid_property


M byceps/services/shop/order/email/service.py => byceps/services/shop/order/email/service.py +5 -4
@@ 8,9 8,10 @@ Notification e-mails about shop orders
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict
from typing import Any

from flask import current_app
from flask_babel import gettext


@@ 153,7 154,7 @@ def _get_order_email_data(order_id: OrderID) -> OrderEmailData:
    )


def _get_template_context(order_email_data: OrderEmailData) -> Dict[str, Any]:
def _get_template_context(order_email_data: OrderEmailData) -> dict[str, Any]:
    """Collect data required for an order e-mail template."""
    footer = _get_footer(order_email_data.order)



@@ 174,7 175,7 @@ def _get_footer(order: Order) -> str:
def _assemble_email_to_orderer(
    subject: str,
    template_name: str,
    template_context: Dict[str, Any],
    template_context: dict[str, Any],
    brand_id: BrandID,
    recipient_address: str,
) -> Message:


@@ 200,7 201,7 @@ def _get_snippet_body(shop_id: ShopID, name: str) -> str:
    return version.body


def _render_template(name: str, **context: Dict[str, Any]) -> str:
def _render_template(name: str, **context: dict[str, Any]) -> str:
    templates_path = (
        Path(current_app.root_path)
        / 'services'

M byceps/services/shop/order/event_service.py => byceps/services/shop/order/event_service.py +3 -2
@@ 6,8 6,9 @@ byceps.services.shop.order.event_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List, Sequence
from typing import Sequence

from ....database import db



@@ 44,7 45,7 @@ def build_event(
    return OrderEvent(now, event_type, order_id, data)


def get_events_for_order(order_id: OrderID) -> List[OrderEvent]:
def get_events_for_order(order_id: OrderID) -> list[OrderEvent]:
    """Return the events for that order."""
    return OrderEvent.query \
        .filter_by(order_id=order_id) \

M byceps/services/shop/order/export/service.py => byceps/services/shop/order/export/service.py +5 -4
@@ 6,9 6,10 @@ byceps.services.shop.order.export.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, Optional
from typing import Any, Optional

from flask import current_app
import pendulum


@@ 21,7 22,7 @@ from .. import service as order_service
from ..transfer.models import Order, OrderID


def export_order_as_xml(order_id: OrderID) -> Optional[Dict[str, str]]:
def export_order_as_xml(order_id: OrderID) -> Optional[dict[str, str]]:
    """Export the order as an XML document."""
    order = order_service.find_order_with_details(order_id)



@@ 37,7 38,7 @@ def export_order_as_xml(order_id: OrderID) -> Optional[Dict[str, str]]:
    }


def _assemble_context(order: Order) -> Dict[str, Any]:
def _assemble_context(order: Order) -> dict[str, Any]:
    """Assemble template context."""
    placed_by = user_service.get_user(order.placed_by_id)
    email_address = user_service.get_email_address(placed_by.id)


@@ 78,7 79,7 @@ def _format_export_datetime(dt: datetime) -> str:
    return date_time + utc_offset


def _render_template(context: Dict[str, Any]) -> str:
def _render_template(context: dict[str, Any]) -> str:
    """Load and render export template."""
    path = 'services/shop/order/export/templates/export.xml'
    with current_app.open_resource(path, 'r') as f:

M byceps/services/shop/order/ordered_articles_service.py => byceps/services/shop/order/ordered_articles_service.py +3 -2
@@ 6,8 6,9 @@ byceps.services.shop.order.ordered_articles_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import Counter
from typing import Dict, Sequence
from typing import Sequence

from ....database import db



@@ 19,7 20,7 @@ from .transfer.models import OrderItem, PaymentState

def count_ordered_articles(
    article_number: ArticleNumber,
) -> Dict[PaymentState, int]:
) -> dict[PaymentState, int]:
    """Count how often the article has been ordered, grouped by the
    order's payment state.
    """

M byceps/services/shop/order/sequence_service.py => byceps/services/shop/order/sequence_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.shop.order.sequence_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from sqlalchemy.exc import IntegrityError



@@ 70,7 71,7 @@ def find_order_number_sequence(

def find_order_number_sequences_for_shop(
    shop_id: ShopID,
) -> List[OrderNumberSequence]:
) -> list[OrderNumberSequence]:
    """Return the order number sequences defined for that shop."""
    sequences = DbOrderNumberSequence.query \
        .filter_by(shop_id=shop_id) \

M byceps/services/shop/order/service.py => byceps/services/shop/order/service.py +6 -5
@@ 6,8 6,9 @@ byceps.services.shop.order.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Dict, Iterator, Mapping, Optional, Sequence, Set, Tuple
from typing import Iterator, Mapping, Optional, Sequence

from flask import current_app
from sqlalchemy.exc import IntegrityError


@@ 50,7 51,7 @@ def place_order(
    cart: Cart,
    *,
    created_at: Optional[datetime] = None,
) -> Tuple[Order, ShopOrderPlaced]:
) -> tuple[Order, ShopOrderPlaced]:
    """Place an order for one or more articles."""
    storefront = storefront_service.get_storefront(storefront_id)
    shop = shop_service.get_shop(storefront.shop_id)


@@ 409,7 410,7 @@ def count_open_orders(shop_id: ShopID) -> int:
        .count()


def count_orders_per_payment_state(shop_id: ShopID) -> Dict[PaymentState, int]:
def count_orders_per_payment_state(shop_id: ShopID) -> dict[PaymentState, int]:
    """Count orders for the shop, grouped by payment state."""
    counts_by_payment_state = dict.fromkeys(PaymentState, 0)



@@ 473,7 474,7 @@ def find_order_by_order_number(order_number: OrderNumber) -> Optional[Order]:


def find_orders_by_order_numbers(
    order_numbers: Set[OrderNumber],
    order_numbers: set[OrderNumber],
) -> Sequence[Order]:
    """Return the orders with those order numbers."""
    if not order_numbers:


@@ 486,7 487,7 @@ def find_orders_by_order_numbers(
    return [order.to_transfer_object() for order in orders]


def get_order_count_by_shop_id() -> Dict[ShopID, int]:
def get_order_count_by_shop_id() -> dict[ShopID, int]:
    """Return order count (including 0) per shop, indexed by shop ID."""
    shop_ids_and_order_counts = db.session \
        .query(

M byceps/services/shop/order/transfer/models.py => byceps/services/shop/order/transfer/models.py +3 -2
@@ 6,11 6,12 @@ byceps.services.shop.order.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import List, NewType, Optional
from typing import NewType, Optional
from uuid import UUID

from .....typing import UserID


@@ 82,7 83,7 @@ class Order:
    last_name: str
    address: Address
    total_amount: Decimal
    items: List[OrderItem]
    items: list[OrderItem]
    payment_method: Optional[PaymentMethod]
    payment_state: PaymentState
    is_open: bool

M byceps/services/shop/shipping/service.py => byceps/services/shop/shipping/service.py +6 -5
@@ 6,9 6,10 @@ byceps.services.shop.shipping.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import Counter, defaultdict
from dataclasses import dataclass
from typing import Dict, Iterator, Sequence, Set
from typing import Iterator, Sequence

from ..article.dbmodels.article import Article as DbArticle



@@ 56,7 57,7 @@ class OrderItemQuantity:


def _find_order_items(
    shop_id: ShopID, payment_states: Set[PaymentState]
    shop_id: ShopID, payment_states: set[PaymentState]
) -> Iterator[OrderItemQuantity]:
    """Return article quantities for the given payment states."""
    payment_state_names = {ps.name for ps in payment_states}


@@ 88,7 89,7 @@ def _find_order_items(

def _aggregate_ordered_article_quantites(
    order_item_quantities: Sequence[OrderItemQuantity],
    article_descriptions: Dict[ArticleNumber, str],
    article_descriptions: dict[ArticleNumber, str],
) -> Iterator[ArticleToShip]:
    """Aggregate article quantities per payment state."""
    d = defaultdict(Counter)


@@ 111,8 112,8 @@ def _aggregate_ordered_article_quantites(


def _get_article_descriptions(
    article_numbers: Set[ArticleNumber],
) -> Dict[ArticleNumber, str]:
    article_numbers: set[ArticleNumber],
) -> dict[ArticleNumber, str]:
    """Look up description texts of the specified articles."""
    if not article_numbers:
        return []

M byceps/services/shop/shop/service.py => byceps/services/shop/shop/service.py +4 -3
@@ 6,7 6,8 @@ byceps.services.shop.shop.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional, Set
from __future__ import annotations
from typing import Optional

from ....database import db
from ....typing import BrandID


@@ 90,7 91,7 @@ def _get_db_shop(shop_id: ShopID) -> DbShop:
    return shop


def find_shops(shop_ids: Set[ShopID]) -> List[Shop]:
def find_shops(shop_ids: set[ShopID]) -> list[Shop]:
    """Return the shops with those IDs."""
    if not shop_ids:
        return []


@@ 102,7 103,7 @@ def find_shops(shop_ids: Set[ShopID]) -> List[Shop]:
    return [_db_entity_to_shop(shop) for shop in shops]


def get_active_shops() -> List[Shop]:
def get_active_shops() -> list[Shop]:
    """Return all shops that are not archived."""
    shops = DbShop.query \
        .filter_by(archived=False) \

M byceps/services/shop/shop/transfer/models.py => byceps/services/shop/shop/transfer/models.py +3 -2
@@ 6,8 6,9 @@ byceps.services.shop.shop.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, NewType
from typing import Any, NewType

from .....typing import BrandID



@@ 21,4 22,4 @@ class Shop:
    brand_id: BrandID
    title: str
    archived: bool
    extra_settings: Dict[str, Any]
    extra_settings: dict[str, Any]

M byceps/services/shop/storefront/service.py => byceps/services/shop/storefront/service.py +5 -4
@@ 6,7 6,8 @@ byceps.services.shop.storefront.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional, Set
from __future__ import annotations
from typing import Optional

from ....database import db



@@ 112,7 113,7 @@ def _get_db_storefront(storefront_id: StorefrontID) -> DbStorefront:
    return storefront


def find_storefronts(storefront_ids: Set[StorefrontID]) -> List[Storefront]:
def find_storefronts(storefront_ids: set[StorefrontID]) -> list[Storefront]:
    """Return the storefronts with those IDs."""
    if not storefront_ids:
        return []


@@ 124,14 125,14 @@ def find_storefronts(storefront_ids: Set[StorefrontID]) -> List[Storefront]:
    return [_db_entity_to_storefront(storefront) for storefront in storefronts]


def get_all_storefronts() -> List[Storefront]:
def get_all_storefronts() -> list[Storefront]:
    """Return all storefronts."""
    storefronts = DbStorefront.query.all()

    return [_db_entity_to_storefront(storefront) for storefront in storefronts]


def get_storefronts_for_shop(shop_id: ShopID) -> Set[Storefront]:
def get_storefronts_for_shop(shop_id: ShopID) -> set[Storefront]:
    """Return all storefronts for that shop."""
    rows = DbStorefront.query \
        .filter_by(shop_id=shop_id) \

M byceps/services/site/service.py => byceps/services/site/service.py +5 -4
@@ 6,8 6,9 @@ byceps.services.site.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import dataclasses
from typing import Optional, Set, Union
from typing import Optional, Union

from ...database import db
from ...typing import BrandID, PartyID


@@ 131,14 132,14 @@ def get_site(site_id: SiteID) -> Site:
    return site


def get_all_sites() -> Set[Site]:
def get_all_sites() -> set[Site]:
    """Return all sites."""
    sites = DbSite.query.all()

    return {_db_entity_to_site(site) for site in sites}


def get_sites_for_brand(brand_id: BrandID) -> Set[Site]:
def get_sites_for_brand(brand_id: BrandID) -> set[Site]:
    """Return the sites for that brand."""
    sites = DbSite.query \
        .filter_by(brand_id=brand_id) \


@@ 149,7 150,7 @@ def get_sites_for_brand(brand_id: BrandID) -> Set[Site]:

def get_current_sites(
    brand_id: Optional[BrandID] = None, *, include_brands: bool = False
) -> Set[Union[Site, SiteWithBrand]]:
) -> set[Union[Site, SiteWithBrand]]:
    """Return all "current" (i.e. enabled and not archived) sites."""
    query = DbSite.query


M byceps/services/site/settings_service.py => byceps/services/site/settings_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.site.settings_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db, upsert
from ...typing import PartyID


@@ 80,7 81,7 @@ def find_setting_value(site_id: SiteID, name: str) -> Optional[str]:
    return setting.value


def get_settings(site_id: SiteID) -> Set[SiteSetting]:
def get_settings(site_id: SiteID) -> set[SiteSetting]:
    """Return all settings for that site."""
    settings = DbSetting.query \
        .filter_by(site_id=site_id) \

M byceps/services/snippet/mountpoint_service.py => byceps/services/snippet/mountpoint_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.snippet.mountpoint_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db



@@ 50,7 51,7 @@ def find_mountpoint(mountpoint_id: MountpointID) -> Optional[Mountpoint]:
    return _db_entity_to_mountpoint(mountpoint)


def get_mountpoints_for_site(site_id: SiteID) -> Set[Mountpoint]:
def get_mountpoints_for_site(site_id: SiteID) -> set[Mountpoint]:
    """Return all mountpoints for that site."""
    mountpoints = DbMountpoint.query \
        .filter_by(site_id=site_id) \

M byceps/services/snippet/service.py => byceps/services/snippet/service.py +11 -10
@@ 6,8 6,9 @@ byceps.services.snippet.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List, Optional, Sequence, Set, Tuple
from typing import Optional, Sequence

from ...database import db
from ...events.snippet import SnippetCreated, SnippetDeleted, SnippetUpdated


@@ 35,7 36,7 @@ def create_document(
    *,
    head: Optional[str] = None,
    image_url_path: Optional[str] = None,
) -> Tuple[DbSnippetVersion, SnippetCreated]:
) -> tuple[DbSnippetVersion, SnippetCreated]:
    """Create a document and its initial version, and return that version."""
    return _create_snippet(
        scope,


@@ 57,7 58,7 @@ def update_document(
    *,
    head: Optional[str] = None,
    image_url_path: Optional[str] = None,
) -> Tuple[DbSnippetVersion, SnippetUpdated]:
) -> tuple[DbSnippetVersion, SnippetUpdated]:
    """Update document with a new version, and return that version."""
    return _update_snippet(
        snippet_id, creator_id, title, head, body, image_url_path


@@ 70,14 71,14 @@ def update_document(

def create_fragment(
    scope: Scope, name: str, creator_id: UserID, body: str
) -> Tuple[DbSnippetVersion, SnippetCreated]:
) -> tuple[DbSnippetVersion, SnippetCreated]:
    """Create a fragment and its initial version, and return that version."""
    return _create_snippet(scope, name, SnippetType.fragment, creator_id, body)


def update_fragment(
    snippet_id: SnippetID, creator_id: UserID, body: str
) -> Tuple[DbSnippetVersion, SnippetUpdated]:
) -> tuple[DbSnippetVersion, SnippetUpdated]:
    """Update fragment with a new version, and return that version."""
    title = None
    head = None


@@ 102,7 103,7 @@ def _create_snippet(
    title: Optional[str] = None,
    head: Optional[str] = None,
    image_url_path: Optional[str] = None,
) -> Tuple[DbSnippetVersion, SnippetCreated]:
) -> tuple[DbSnippetVersion, SnippetCreated]:
    """Create a snippet and its initial version, and return that version."""
    creator = user_service.get_user(creator_id)



@@ 140,7 141,7 @@ def _update_snippet(
    head: Optional[str],
    body: str,
    image_url_path: Optional[str],
) -> Tuple[DbSnippetVersion, SnippetUpdated]:
) -> tuple[DbSnippetVersion, SnippetUpdated]:
    """Update snippet with a new version, and return that version."""
    snippet = find_snippet(snippet_id)
    if snippet is None:


@@ 173,7 174,7 @@ def _update_snippet(

def delete_snippet(
    snippet_id: SnippetID, *, initiator_id: Optional[UserID] = None
) -> Tuple[bool, Optional[SnippetDeleted]]:
) -> tuple[bool, Optional[SnippetDeleted]]:
    """Delete the snippet and its versions.

    It is expected that no database records (mountpoints, consents,


@@ 226,7 227,7 @@ def find_snippet(snippet_id: SnippetID) -> Optional[DbSnippet]:
    return DbSnippet.query.get(snippet_id)


def get_snippets(snippet_ids: Set[SnippetID]) -> Sequence[DbSnippet]:
def get_snippets(snippet_ids: set[SnippetID]) -> Sequence[DbSnippet]:
    """Return these snippets."""
    return DbSnippet.query \
        .filter(DbSnippet.id.in_(snippet_ids)) \


@@ 280,7 281,7 @@ def get_versions(snippet_id: SnippetID) -> Sequence[DbSnippetVersion]:

def search_snippets(
    search_term: str, scope: Optional[Scope]
) -> List[DbSnippetVersion]:
) -> list[DbSnippetVersion]:
    """Search in (the latest versions of) snippets."""
    q = DbSnippetVersion.query \
        .join(DbCurrentVersionAssociation) \

M byceps/services/terms/consent_service.py => byceps/services/terms/consent_service.py +2 -2
@@ 6,7 6,7 @@ byceps.services.terms.consent_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict
from __future__ import annotations

from ...database import db



@@ 19,7 19,7 @@ from .transfer.models import DocumentID, VersionID

def count_consents_for_document_versions(
    document_id: DocumentID,
) -> Dict[VersionID, int]:
) -> dict[VersionID, int]:
    """Return the number of consents for each version of the document."""
    rows = db.session \
        .query(

M byceps/services/ticketing/attendance_service.py => byceps/services/ticketing/attendance_service.py +13 -14
@@ 6,11 6,10 @@ byceps.services.ticketing.attendance_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import Counter, defaultdict
from datetime import datetime
from itertools import chain
import typing
from typing import Dict, List, Set, Tuple

from sqlalchemy.dialects.postgresql import insert



@@ 51,7 50,7 @@ def delete_archived_attendance(user_id: UserID, party_id: PartyID) -> None:
    db.session.commit()


def get_attended_parties(user_id: UserID) -> List[Party]:
def get_attended_parties(user_id: UserID) -> list[Party]:
    """Return the parties the user has attended in the past."""
    ticket_attendance_party_ids = _get_attended_party_ids(user_id)
    archived_attendance_party_ids = _get_archived_attendance_party_ids(user_id)


@@ 63,7 62,7 @@ def get_attended_parties(user_id: UserID) -> List[Party]:
    return party_service.get_parties(party_ids)


def _get_attended_party_ids(user_id: UserID) -> Set[PartyID]:
def _get_attended_party_ids(user_id: UserID) -> set[PartyID]:
    """Return the IDs of the non-legacy parties the user has attended."""
    party_id_rows = db.session \
        .query(DbParty.id) \


@@ 77,7 76,7 @@ def _get_attended_party_ids(user_id: UserID) -> Set[PartyID]:
    return {row[0] for row in party_id_rows}


def _get_archived_attendance_party_ids(user_id: UserID) -> Set[PartyID]:
def _get_archived_attendance_party_ids(user_id: UserID) -> set[PartyID]:
    """Return the IDs of the legacy parties the user has attended."""
    party_id_rows = db.session \
        .query(DbArchivedAttendance.party_id) \


@@ 87,7 86,7 @@ def _get_archived_attendance_party_ids(user_id: UserID) -> Set[PartyID]:
    return {row[0] for row in party_id_rows}


def get_attendees_by_party(party_ids: Set[PartyID]) -> Dict[PartyID, Set[User]]:
def get_attendees_by_party(party_ids: set[PartyID]) -> dict[PartyID, set[User]]:
    """Return the parties' attendees, indexed by party."""
    if not party_ids:
        return {}


@@ 116,8 115,8 @@ def get_attendees_by_party(party_ids: Set[PartyID]) -> Dict[PartyID, Set[User]]:


def get_attendee_ids_for_parties(
    party_ids: Set[PartyID],
) -> Dict[PartyID, Set[UserID]]:
    party_ids: set[PartyID],
) -> dict[PartyID, set[UserID]]:
    """Return the partys' attendee IDs, indexed by party ID."""
    if not party_ids:
        return {}


@@ 140,14 139,14 @@ def get_attendee_ids_for_parties(

    rows = ticket_rows + archived_attendance_rows

    attendee_ids_by_party_id: Dict[PartyID, Set[UserID]] = defaultdict(set)
    attendee_ids_by_party_id: dict[PartyID, set[UserID]] = defaultdict(set)
    for party_id, attendee_id in rows:
        attendee_ids_by_party_id[party_id].add(attendee_id)

    return dict(attendee_ids_by_party_id)


def get_top_attendees_for_brand(brand_id: BrandID) -> List[Tuple[UserID, int]]:
def get_top_attendees_for_brand(brand_id: BrandID) -> list[tuple[UserID, int]]:
    """Return the attendees with the highest number of parties of this
    brand visited.
    """


@@ 179,7 178,7 @@ def get_top_attendees_for_brand(brand_id: BrandID) -> List[Tuple[UserID, int]]:

def _get_top_ticket_attendees_for_parties(
    brand_id: BrandID,
) -> List[Tuple[UserID, int]]:
) -> list[tuple[UserID, int]]:
    user_id_column = db.aliased(DbTicket).used_by_id

    attendance_count = db.session \


@@ 207,7 206,7 @@ def _get_top_ticket_attendees_for_parties(

def _get_top_archived_attendees_for_parties(
    brand_id: BrandID,
) -> List[Tuple[UserID, int]]:
) -> list[tuple[UserID, int]]:
    attendance_count_column = db.func \
        .count(DbArchivedAttendance.user_id) \
        .label('attendance_count')


@@ 225,8 224,8 @@ def _get_top_archived_attendees_for_parties(


def _merge_top_attendance_counts(
    xs: List[List[Tuple[UserID, int]]]
) -> typing.Counter[UserID]:
    xs: list[list[tuple[UserID, int]]]
) -> Counter[UserID]:
    counter = Counter()

    for x in xs:

M byceps/services/ticketing/category_service.py => byceps/services/ticketing/category_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.ticketing.category_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Sequence
from __future__ import annotations
from typing import Optional, Sequence

from ...database import db
from ...typing import PartyID


@@ 76,7 77,7 @@ def get_categories_for_party(party_id: PartyID) -> Sequence[TicketCategory]:

def get_categories_with_ticket_counts_for_party(
    party_id: PartyID,
) -> Dict[TicketCategory, int]:
) -> dict[TicketCategory, int]:
    """Return all categories with ticket counts for that party."""
    category = db.aliased(DbCategory)


M byceps/services/ticketing/event_service.py => byceps/services/ticketing/event_service.py +2 -2
@@ 6,8 6,8 @@ byceps.services.ticketing.event_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List

from ...database import db



@@ 34,7 34,7 @@ def build_event(
    return TicketEvent(now, event_type, ticket_id, data)


def get_events_for_ticket(ticket_id: TicketID) -> List[TicketEvent]:
def get_events_for_ticket(ticket_id: TicketID) -> list[TicketEvent]:
    """Return the events for that ticket."""
    return TicketEvent.query \
        .filter_by(ticket_id=ticket_id) \

M byceps/services/ticketing/ticket_code_service.py => byceps/services/ticketing/ticket_code_service.py +5 -5
@@ 6,16 6,16 @@ byceps.services.ticketing.ticket_code_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from random import sample
from string import ascii_uppercase, digits
from typing import Set

from .transfer.models import TicketCode


def generate_ticket_codes(quantity: int) -> Set[TicketCode]:
def generate_ticket_codes(quantity: int) -> set[TicketCode]:
    """Generate a number of ticket codes."""
    codes: Set[TicketCode] = set()
    codes: set[TicketCode] = set()

    for _ in range(quantity):
        code = _generate_ticket_code_not_in(codes)


@@ 28,7 28,7 @@ def generate_ticket_codes(quantity: int) -> Set[TicketCode]:


def _generate_ticket_code_not_in(
    codes: Set[TicketCode], *, max_attempts: int = 4
    codes: set[TicketCode], *, max_attempts: int = 4
) -> TicketCode:
    """Generate ticket codes and return the first one not in the set."""
    for _ in range(max_attempts):


@@ 54,7 54,7 @@ def _generate_ticket_code() -> TicketCode:


def _verify_total_matches(
    codes: Set[TicketCode], requested_quantity: int
    codes: set[TicketCode], requested_quantity: int
) -> None:
    """Verify if the number of generated codes matches the number of
    requested codes.

M byceps/services/ticketing/ticket_revocation_service.py => byceps/services/ticketing/ticket_revocation_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.ticketing.ticket_revocation_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import UserID


@@ 39,7 40,7 @@ def revoke_ticket(


def revoke_tickets(
    ticket_ids: Set[TicketID],
    ticket_ids: set[TicketID],
    initiator_id: UserID,
    *,
    reason: Optional[str] = None,

M byceps/services/ticketing/ticket_service.py => byceps/services/ticketing/ticket_service.py +7 -6
@@ 6,7 6,8 @@ byceps.services.ticketing.ticket_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Sequence, Set
from __future__ import annotations
from typing import Optional, Sequence

from ...database import db, Pagination
from ...typing import PartyID, UserID


@@ 84,7 85,7 @@ def find_ticket_by_code(
        .one_or_none()


def find_tickets(ticket_ids: Set[TicketID]) -> Sequence[DbTicket]:
def find_tickets(ticket_ids: set[TicketID]) -> Sequence[DbTicket]:
    """Return the tickets with those ids."""
    if not ticket_ids:
        return []


@@ 195,7 196,7 @@ def uses_any_ticket_for_party(user_id: UserID, party_id: PartyID) -> bool:
    return db.session.query(q.exists()).scalar()


def get_ticket_users_for_party(party_id: PartyID) -> Set[UserID]:
def get_ticket_users_for_party(party_id: PartyID) -> set[UserID]:
    """Return the IDs of the users of tickets for that party."""
    rows = db.session \
        .query(DbTicket.used_by_id) \


@@ 208,8 209,8 @@ def get_ticket_users_for_party(party_id: PartyID) -> Set[UserID]:


def select_ticket_users_for_party(
    user_ids: Set[UserID], party_id: PartyID
) -> Set[UserID]:
    user_ids: set[UserID], party_id: PartyID
) -> set[UserID]:
    """Return the IDs of those users that use a ticket for that party."""
    if not user_ids:
        return set()


@@ 262,7 263,7 @@ def get_tickets_with_details_for_party_paginated(
        .paginate(page, per_page)


def get_ticket_count_by_party_id() -> Dict[PartyID, int]:
def get_ticket_count_by_party_id() -> dict[PartyID, int]:
    """Return ticket count (including 0) per party, indexed by party ID."""
    party = db.aliased(DbParty)


M byceps/services/tourney/avatar/service.py => byceps/services/tourney/avatar/service.py +3 -2
@@ 6,8 6,9 @@ byceps.services.tourney.avatar.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from uuid import UUID
from typing import BinaryIO, Set
from typing import BinaryIO

from ....database import db
from ....typing import PartyID, UserID


@@ 29,7 30,7 @@ def create_avatar_image(
    party_id: PartyID,
    creator_id: UserID,
    stream: BinaryIO,
    allowed_types: Set[ImageType],
    allowed_types: set[ImageType],
    *,
    maximum_dimensions: Dimensions = MAXIMUM_DIMENSIONS,
) -> Avatar:

M byceps/services/tourney/category_service.py => byceps/services/tourney/category_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.tourney.category_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import PartyID


@@ 117,7 118,7 @@ def _get_db_category(category_id: TourneyCategoryID) -> DbTourneyCategory:
    return category


def get_categories_for_party(party_id: PartyID) -> List[TourneyCategory]:
def get_categories_for_party(party_id: PartyID) -> list[TourneyCategory]:
    """Return the categories for this party."""
    categories = DbTourneyCategory.query \
        .filter_by(party_id=party_id) \

M byceps/services/tourney/match_comment_service.py => byceps/services/tourney/match_comment_service.py +4 -3
@@ 6,8 6,9 @@ byceps.services.tourney.match_comment_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Dict, Optional, Sequence, Set
from typing import Optional, Sequence

from ...database import db
from ...services.text_markup import service as text_markup_service


@@ 112,8 113,8 @@ def get_comments(


def _get_users_by_id(
    user_ids: Set[UserID], *, party_id: Optional[PartyID] = None
) -> Dict[UserID, User]:
    user_ids: set[UserID], *, party_id: Optional[PartyID] = None
) -> dict[UserID, User]:
    users = user_service.find_users(
        user_ids, include_avatars=True, include_orga_flags_for_party_id=party_id
    )

M byceps/services/tourney/participant_service.py => byceps/services/tourney/participant_service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.tourney.participant_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db



@@ 52,7 53,7 @@ def find_participant(participant_id: ParticipantID) -> Optional[Participant]:
    return _db_entity_to_participant(participant)


def get_participants_for_tourney(tourney_id: TourneyID) -> Set[Participant]:
def get_participants_for_tourney(tourney_id: TourneyID) -> set[Participant]:
    """Return the participants of the tourney."""
    participants = DbParticipant.query \
        .filter_by(tourney_id=tourney_id) \

M byceps/services/tourney/tourney_service.py => byceps/services/tourney/tourney_service.py +3 -2
@@ 6,8 6,9 @@ byceps.services.tourney.tourney_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional

from ...database import db
from ...typing import PartyID, UserID


@@ 130,7 131,7 @@ def _get_db_tourney(tourney_id: int) -> DbTourney:
    return tourney


def get_tourneys_for_party(party_id: PartyID) -> List[TourneyWithCategory]:
def get_tourneys_for_party(party_id: PartyID) -> list[TourneyWithCategory]:
    """Return the tourneys for that party."""
    rows = db.session \
        .query(DbTourney, DbTourneyCategory, db.func.count(DbParticipant.id)) \

M byceps/services/user/creation_service.py => byceps/services/user/creation_service.py +6 -5
@@ 6,8 6,9 @@ byceps.services.user.creation_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import Optional, Set, Tuple
from typing import Optional

from flask import current_app



@@ 40,8 41,8 @@ def create_user(
    last_name: Optional[str],
    site_id: SiteID,
    *,
    consents: Optional[Set[Consent]] = None,
) -> Tuple[User, UserAccountCreated]:
    consents: Optional[set[Consent]] = None,
) -> tuple[User, UserAccountCreated]:
    """Create a user account and related records."""
    # user with details, password, and roles
    user, event = create_basic_user(


@@ 80,7 81,7 @@ def create_basic_user(
    last_name: Optional[str] = None,
    creator_id: Optional[UserID] = None,
    site_id: Optional[SiteID] = None,
) -> Tuple[User, UserAccountCreated]:
) -> tuple[User, UserAccountCreated]:
    # user with details
    user, event = _create_user(
        screen_name,


@@ 105,7 106,7 @@ def _create_user(
    last_name: Optional[str] = None,
    creator_id: Optional[UserID] = None,
    site_id: Optional[SiteID] = None,
) -> Tuple[User, UserAccountCreated]:
) -> tuple[User, UserAccountCreated]:
    if creator_id is not None:
        creator = user_service.get_user(creator_id)
    else:

M byceps/services/user/event_service.py => byceps/services/user/event_service.py +4 -3
@@ 6,8 6,9 @@ byceps.services.user.event_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Optional

from ...database import db
from ...typing import UserID


@@ 45,7 46,7 @@ def build_event(
    return DbUserEvent(occurred_at, event_type, user_id, data)


def get_events_for_user(user_id: UserID) -> List[DbUserEvent]:
def get_events_for_user(user_id: UserID) -> list[DbUserEvent]:
    """Return the events for that user."""
    return DbUserEvent.query \
        .filter_by(user_id=user_id) \


@@ 55,7 56,7 @@ def get_events_for_user(user_id: UserID) -> List[DbUserEvent]:

def get_events_of_type_for_user(
    user_id: UserID, event_type: str
) -> List[DbUserEvent]:
) -> list[DbUserEvent]:
    """Return the events of that type for that user."""
    return DbUserEvent.query \
        .filter_by(user_id=user_id) \

M byceps/services/user/service.py => byceps/services/user/service.py +8 -7
@@ 6,7 6,8 @@ byceps.services.user.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, Optional, Set, Tuple
from __future__ import annotations
from typing import Optional

from ...database import db, Query
from ...typing import PartyID, UserID


@@ 119,11 120,11 @@ def get_user(


def find_users(
    user_ids: Set[UserID],
    user_ids: set[UserID],
    *,
    include_avatars: bool = False,
    include_orga_flags_for_party_id: Optional[PartyID] = None,
) -> Set[User]:
) -> set[User]:
    """Return the users with those IDs.

    Their respective avatars' URLs are included, if requested.


@@ 179,7 180,7 @@ def _get_orga_flag_subquery(party_id: PartyID):


def _user_row_to_dto(
    row: Tuple[UserID, str, bool, bool, Optional[Avatar], bool]
    row: tuple[UserID, str, bool, bool, Optional[Avatar], bool]
) -> User:
    user_id, screen_name, suspended, deleted, avatar, is_orga = row
    avatar_url = avatar.url if avatar else None


@@ 308,7 309,7 @@ def get_email_address(user_id: UserID) -> str:
    return email_address


def get_email_addresses(user_ids: Set[UserID]) -> Set[Tuple[UserID, str]]:
def get_email_addresses(user_ids: set[UserID]) -> set[tuple[UserID, str]]:
    """Return the users' e-mail addresses."""
    return db.session \
        .query(


@@ 319,7 320,7 @@ def get_email_addresses(user_ids: Set[UserID]) -> Set[Tuple[UserID, str]]:
        .all()


def get_sort_key_for_screen_name(user: User) -> Tuple[bool, str]:
def get_sort_key_for_screen_name(user: User) -> tuple[bool, str]:
    """Return a key for sorting by screen name.

    - Orders screen names case-insensitively.


@@ 331,7 332,7 @@ def get_sort_key_for_screen_name(user: User) -> Tuple[bool, str]:
    return not has_screen_name, normalized_screen_name


def index_users_by_id(users: Set[User]) -> Dict[UserID, User]:
def index_users_by_id(users: set[User]) -> dict[UserID, User]:
    """Map the users' IDs to the corresponding user objects."""
    return {user.id: user for user in users}


M byceps/services/user/transfer/models.py => byceps/services/user/transfer/models.py +3 -2
@@ 6,9 6,10 @@ byceps.services.user.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from typing import Any, Dict, Optional
from typing import Any, Optional

from ....typing import UserID



@@ 34,7 35,7 @@ class UserDetail:
    street: Optional[str]
    phone_number: Optional[str]
    internal_comment: Optional[str]
    extras: Dict[str, Any]
    extras: dict[str, Any]


@dataclass(frozen=True)

M byceps/services/user_avatar/service.py => byceps/services/user_avatar/service.py +5 -4
@@ 6,7 6,8 @@ byceps.services.user_avatar.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import BinaryIO, Dict, List, Optional, Set
from __future__ import annotations
from typing import BinaryIO, Optional

from ...database import db
from ...typing import UserID


@@ 29,7 30,7 @@ MAXIMUM_DIMENSIONS = Dimensions(512, 512)
def update_avatar_image(
    user_id: UserID,
    stream: BinaryIO,
    allowed_types: Set[ImageType],
    allowed_types: set[ImageType],
    *,
    maximum_dimensions: Dimensions = MAXIMUM_DIMENSIONS,
) -> AvatarID:


@@ 79,7 80,7 @@ def remove_avatar_image(user_id: UserID) -> None:
    db.session.commit()


def get_avatars_uploaded_by_user(user_id: UserID) -> List[AvatarUpdate]:
def get_avatars_uploaded_by_user(user_id: UserID) -> list[AvatarUpdate]:
    """Return the avatars uploaded by the user."""
    avatars = DbAvatar.query \
        .filter_by(creator_id=user_id) \


@@ 94,7 95,7 @@ def get_avatar_url_for_user(user_id: UserID) -> Optional[str]:
    return avatar_urls_by_user_id.get(user_id)


def get_avatar_urls_for_users(user_ids: Set[UserID]) -> Dict[UserID, str]:
def get_avatar_urls_for_users(user_ids: set[UserID]) -> dict[UserID, str]:
    """Return the URLs of those users' current avatars."""
    if not user_ids:
        return {}

M byceps/services/user_badge/awarding_service.py => byceps/services/user_badge/awarding_service.py +9 -8
@@ 6,9 6,10 @@ byceps.services.user_badge.awarding_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from collections import defaultdict
from datetime import datetime
from typing import Dict, Optional, Set, Tuple
from typing import Optional

from ...database import db
from ...events.user_badge import UserBadgeAwarded


@@ 29,7 30,7 @@ from .transfer.models import (

def award_badge_to_user(
    badge_id: BadgeID, user_id: UserID, *, initiator_id: Optional[UserID] = None
) -> Tuple[BadgeAwarding, UserBadgeAwarded]:
) -> tuple[BadgeAwarding, UserBadgeAwarded]:
    """Award the badge to the user."""
    badge = find_badge(badge_id)
    if badge is None:


@@ 71,7 72,7 @@ def award_badge_to_user(
    return awarding_dto, event


def count_awardings() -> Dict[BadgeID, int]:
def count_awardings() -> dict[BadgeID, int]:
    """Return the number of times each badge has been awarded.

    Because a badge can be awarded multiple times to a user, the number


@@ 89,7 90,7 @@ def count_awardings() -> Dict[BadgeID, int]:
    return {badge_id: count for badge_id, count in rows}


def get_awardings_of_badge(badge_id: BadgeID) -> Set[QuantifiedBadgeAwarding]:
def get_awardings_of_badge(badge_id: BadgeID) -> set[QuantifiedBadgeAwarding]:
    """Return the awardings of this badge."""
    rows = db.session \
        .query(


@@ 110,7 111,7 @@ def get_awardings_of_badge(badge_id: BadgeID) -> Set[QuantifiedBadgeAwarding]:
    }


def get_badges_awarded_to_user(user_id: UserID) -> Dict[Badge, int]:
def get_badges_awarded_to_user(user_id: UserID) -> dict[Badge, int]:
    """Return all badges that have been awarded to the user (and how often)."""
    rows = db.session \
        .query(


@@ 143,8 144,8 @@ def get_badges_awarded_to_user(user_id: UserID) -> Dict[Badge, int]:


def get_badges_awarded_to_users(
    user_ids: Set[UserID], *, featured_only: bool = False
) -> Dict[UserID, Set[Badge]]:
    user_ids: set[UserID], *, featured_only: bool = False
) -> dict[UserID, set[Badge]]:
    """Return all badges that have been awarded to the users, indexed
    by user ID.



@@ 161,7 162,7 @@ def get_badges_awarded_to_users(
    badges = get_badges(badge_ids, featured_only=featured_only)
    badges_by_id = {badge.id: badge for badge in badges}

    badges_by_user_id: Dict[UserID, Set[Badge]] = defaultdict(set)
    badges_by_user_id: dict[UserID, set[Badge]] = defaultdict(set)
    for awarding in awardings:
        badge = badges_by_id.get(awarding.badge_id)
        if badge:

M byceps/services/user_badge/badge_service.py => byceps/services/user_badge/badge_service.py +5 -4
@@ 6,7 6,8 @@ byceps.services.user_badge.badge_service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Optional, Set
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import BrandID


@@ 98,8 99,8 @@ def find_badge_by_slug(slug: str) -> Optional[Badge]:


def get_badges(
    badge_ids: Set[BadgeID], *, featured_only: bool = False
) -> Set[Badge]:
    badge_ids: set[BadgeID], *, featured_only: bool = False
) -> set[Badge]:
    """Return the badges with those IDs.

    If `featured_only` is `True`, only return featured badges.


@@ 118,7 119,7 @@ def get_badges(
    return {_db_entity_to_badge(badge) for badge in badges}


def get_all_badges() -> Set[Badge]:
def get_all_badges() -> set[Badge]:
    """Return all badges."""
    badges = DbBadge.query.all()


M byceps/services/user_group/service.py => byceps/services/user_group/service.py +3 -2
@@ 6,7 6,8 @@ byceps.services.user_group.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import List, Optional
from __future__ import annotations
from typing import Optional

from ...database import db
from ...typing import PartyID, UserID


@@ 29,6 30,6 @@ def create_group(
    return group


def get_all_groups() -> List[UserGroup]:
def get_all_groups() -> list[UserGroup]:
    """Return all groups."""
    return UserGroup.query.all()

M byceps/services/webhooks/dbmodels.py => byceps/services/webhooks/dbmodels.py +3 -2
@@ 6,7 6,8 @@ byceps.services.webhooks.dbmodels
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Any, Dict, Optional
from __future__ import annotations
from typing import Any, Optional

from sqlalchemy.ext.mutable import MutableDict



@@ 37,7 38,7 @@ class OutgoingWebhook(db.Model):
        enabled: bool,
        *,
        text_prefix: Optional[str] = None,
        extra_fields: Optional[Dict[str, Any]] = None,
        extra_fields: Optional[dict[str, Any]] = None,
        description: Optional[str] = None,
    ) -> None:
        self.event_selectors = event_selectors

M byceps/services/webhooks/service.py => byceps/services/webhooks/service.py +6 -5
@@ 6,7 6,8 @@ byceps.services.webhooks.service
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Any, Dict, List, Optional
from __future__ import annotations
from typing import Any, Optional

from ...database import db



@@ 21,7 22,7 @@ def create_outgoing_webhook(
    enabled: bool,
    *,
    text_prefix: Optional[str] = None,
    extra_fields: Optional[Dict[str, Any]] = None,
    extra_fields: Optional[dict[str, Any]] = None,
    description: Optional[str] = None,
) -> OutgoingWebhook:
    """Create an outgoing webhook."""


@@ 49,7 50,7 @@ def update_outgoing_webhook(
    enabled: bool,
    *,
    text_prefix: Optional[str] = None,
    extra_fields: Optional[Dict[str, Any]] = None,
    extra_fields: Optional[dict[str, Any]] = None,
    description: Optional[str] = None,
) -> OutgoingWebhook:
    """Update an outgoing webhook."""


@@ 93,14 94,14 @@ def _find_db_webhook(webhook_id: WebhookID) -> Optional[DbOutgoingWebhook]:
    return db.session.query(DbOutgoingWebhook).get(webhook_id)


def get_all_webhooks() -> List[OutgoingWebhook]:
def get_all_webhooks() -> list[OutgoingWebhook]:
    """Return all webhooks."""
    webhooks = db.session.query(DbOutgoingWebhook).all()

    return [_db_entity_to_outgoing_webhook(webhook) for webhook in webhooks]


def get_enabled_outgoing_webhooks(event_type: str) -> List[OutgoingWebhook]:
def get_enabled_outgoing_webhooks(event_type: str) -> list[OutgoingWebhook]:
    """Return the configurations for enabled outgoing webhooks for that
    event type.
    """

M byceps/services/webhooks/transfer/models.py => byceps/services/webhooks/transfer/models.py +2 -1
@@ 6,6 6,7 @@ byceps.services.webhooks.transfer.models
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, NewType, Optional
from uuid import UUID


@@ 23,7 24,7 @@ class OutgoingWebhook:
    event_selectors: EventSelectors
    format: str
    text_prefix: Optional[str]
    extra_fields: Optional[Dict[str, Any]]
    extra_fields: Optional[dict[str, Any]]
    url: str
    description: str
    enabled: bool

M byceps/util/authorization.py => byceps/util/authorization.py +4 -4
@@ 6,8 6,8 @@ byceps.util.authorization
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from enum import Enum
from typing import List, Set

from flask import current_app, g



@@ 15,7 15,7 @@ from ..services.authorization import service as authorization_service
from ..typing import UserID


def create_permission_enum(key: str, member_names: List[str]) -> Enum:
def create_permission_enum(key: str, member_names: list[str]) -> Enum:
    """Create a permission enum."""
    name = _derive_enum_name(key)



@@ 39,7 39,7 @@ def register_permission_enum(enum: Enum):
    permission_registry.register_enum(enum)


def get_permissions_for_user(user_id: UserID) -> Set[Enum]:
def get_permissions_for_user(user_id: UserID) -> set[Enum]:
    """Return the permissions this user has been granted."""
    permission_ids = authorization_service.get_permission_ids_for_user(user_id)
    return permission_registry.get_enum_members(permission_ids)


@@ 101,6 101,6 @@ def has_current_user_permission(permission: Enum) -> bool:
    return permission in g.user.permissions


def has_current_user_any_permission(*permissions: Set[Enum]) -> bool:
def has_current_user_any_permission(*permissions: set[Enum]) -> bool:
    """Return `True` if the current user has any of these permissions."""
    return any(map(has_current_user_permission, permissions))

M byceps/util/export.py => byceps/util/export.py +3 -2
@@ 8,13 8,14 @@ Data export as CSV.
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
import csv
import io
from typing import Dict, Iterator, Sequence
from typing import Iterator, Sequence


def serialize_to_csv(
    field_names: Sequence[str], rows: Sequence[Dict[str, str]]
    field_names: Sequence[str], rows: Sequence[dict[str, str]]
) -> Iterator[str]:
    """Serialize the rows (must be dictionary objects) to CSV."""
    with io.StringIO(newline='') as f:

M byceps/util/iterables.py => byceps/util/iterables.py +4 -3
@@ 6,8 6,9 @@ byceps.util.iterables
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from itertools import tee
from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar
from typing import Callable, Iterable, Iterator, Optional, TypeVar


T = TypeVar('T')


@@ 41,7 42,7 @@ def index_of(iterable: Iterable[T], predicate: Predicate) -> Optional[int]:
    return None


def pairwise(iterable: Iterable[T]) -> Iterator[Tuple[T, T]]:
def pairwise(iterable: Iterable[T]) -> Iterator[tuple[T, T]]:
    """Return each element paired with its next one.

    Example:


@@ 58,7 59,7 @@ def pairwise(iterable: Iterable[T]) -> Iterator[Tuple[T, T]]:

def partition(
    iterable: Iterable[T], predicate: Predicate
) -> Tuple[List[T], List[T]]:
) -> tuple[list[T], list[T]]:
    """Partition the iterable into two lists according to the predicate.

    The first list contains all elements that satisfy the predicate, the

M byceps/util/navigation.py => byceps/util/navigation.py +4 -3
@@ 6,9 6,10 @@ byceps.util.navigation
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional
from typing import Optional

from flask import g



@@ 32,7 33,7 @@ class Navigation:

    def __init__(self, title: str) -> None:
        self.title = title
        self.items: List[NavigationItem] = []
        self.items: list[NavigationItem] = []

    def add_item(
        self,


@@ 59,7 60,7 @@ class Navigation:
        self.items.append(item)
        return self

    def get_items(self) -> List[NavigationItem]:
    def get_items(self) -> list[NavigationItem]:
        """Return the navigation items the current user is allowed to see."""

        def user_has_permission(item: NavigationItem) -> bool:

M byceps/util/templating.py => byceps/util/templating.py +3 -2
@@ 8,8 8,9 @@ Templating utilities
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, Optional, Set
from typing import Any, Optional

from flask import g
from jinja2 import (


@@ 27,7 28,7 @@ SITES_PATH = Path('sites')


def load_template(
    source: str, *, template_globals: Optional[Dict[str, Any]] = None
    source: str, *, template_globals: Optional[dict[str, Any]] = None
):
    """Load a template from source, using the sandboxed environment."""
    env = create_sandboxed_environment()

M byceps/util/user_session.py => byceps/util/user_session.py +3 -2
@@ 6,8 6,9 @@ byceps.util.user_session
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from enum import Enum
from typing import Optional, Set
from typing import Optional

from flask import session



@@ 45,7 46,7 @@ def end() -> None:


def get_current_user(
    required_permissions: Set[Enum],
    required_permissions: set[Enum],
    locale: Optional[str],
    *,
    party_id: Optional[PartyID] = None,

M scripts/clean_up_after_deleted_users.py => scripts/clean_up_after_deleted_users.py +16 -15
@@ 6,7 6,8 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Callable, Set
from __future__ import annotations
from typing import Callable

import click



@@ 76,7 77,7 @@ def execute(dry_run, user_ids):
        db.session.commit()


def check_for_undeleted_accounts(user_ids: Set[UserID]) -> None:
def check_for_undeleted_accounts(user_ids: set[UserID]) -> None:
    users = user_service.find_users(user_ids)

    non_deleted_users = [u for u in users if not u.deleted]


@@ 88,63 89,63 @@ def check_for_undeleted_accounts(user_ids: Set[UserID]) -> None:


def delete_records(
    label: str, delete_func: Callable, user_ids: Set[UserID]
    label: str, delete_func: Callable, user_ids: set[UserID]
) -> None:
    click.secho(f'Deleting {label} ... ', nl=False)
    affected = delete_func(user_ids)
    click.secho(str(affected), fg='yellow')


def delete_authn_credentials(user_ids: Set[UserID]) -> int:
def delete_authn_credentials(user_ids: set[UserID]) -> int:
    """Delete authentication credentials for the given users."""
    return _execute_delete_for_users_query(Credential, user_ids)


def delete_authn_recent_logins(user_ids: Set[UserID]) -> int:
def delete_authn_recent_logins(user_ids: set[UserID]) -> int:
    """Delete recent logins for the given users."""
    return _execute_delete_for_users_query(RecentLogin, user_ids)


def delete_authn_session_tokens(user_ids: Set[UserID]) -> int:
def delete_authn_session_tokens(user_ids: set[UserID]) -> int:
    """Delete session tokens for the given users."""
    return _execute_delete_for_users_query(SessionToken, user_ids)


def delete_authz_user_roles(user_ids: Set[UserID]) -> int:
def delete_authz_user_roles(user_ids: set[UserID]) -> int:
    """Delete authorization role assignments from the given users."""
    return _execute_delete_for_users_query(UserRole, user_ids)


def delete_board_category_lastviews(user_ids: Set[UserID]) -> int:
def delete_board_category_lastviews(user_ids: set[UserID]) -> int:
    """Delete last board category view marks for the given users."""
    return _execute_delete_for_users_query(BoardLastCategoryView, user_ids)


def delete_board_topic_lastviews(user_ids: Set[UserID]) -> int:
def delete_board_topic_lastviews(user_ids: set[UserID]) -> int:
    """Delete last board topic view marks for the given users."""
    return _execute_delete_for_users_query(BoardLastTopicView, user_ids)


def delete_consents(user_ids: Set[UserID]) -> int:
def delete_consents(user_ids: set[UserID]) -> int:
    """Delete consents from the given users."""
    return _execute_delete_for_users_query(Consent, user_ids)


def delete_newsletter_subscription_updates(user_ids: Set[UserID]) -> int:
def delete_newsletter_subscription_updates(user_ids: set[UserID]) -> int:
    """Delete newsletter subscription updates for the given users."""
    return _execute_delete_for_users_query(
        NewsletterSubscriptionUpdate, user_ids
    )


def delete_user_avatar_selections(user_ids: Set[UserID]) -> int:
def delete_user_avatar_selections(user_ids: set[UserID]) -> int:
    """Delete user avatar selections (but not user avatar records and
    image files at this point) for the given users.
    """
    return _execute_delete_for_users_query(UserAvatarSelection, user_ids)


def delete_user_events(user_ids: Set[UserID]) -> int:
def delete_user_events(user_ids: set[UserID]) -> int:
    """Delete user events (execpt for those that justify the deletion)
    for the given users.
    """


@@ 156,12 157,12 @@ def delete_user_events(user_ids: Set[UserID]) -> int:
    )


def delete_verification_tokens(user_ids: Set[UserID]) -> int:
def delete_verification_tokens(user_ids: set[UserID]) -> int:
    """Delete verification tokens for the given users."""
    return _execute_delete_for_users_query(VerificationToken, user_ids)


def _execute_delete_for_users_query(model, user_ids: Set[UserID]) -> int:
def _execute_delete_for_users_query(model, user_ids: set[UserID]) -> int:
    """Execute (but not commit) deletions, return number of affected rows."""
    return (
        db.session.query(model)

M scripts/find_logins_for_ipaddress.py => scripts/find_logins_for_ipaddress.py +3 -3
@@ 6,7 6,7 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, List
from __future__ import annotations

import click



@@ 29,7 29,7 @@ def execute(ip_address: str):
        click.echo(f'{event.occurred_at}\t{ip_address}\t{user.screen_name}')


def find_events(ip_address: str) -> List[UserEvent]:
def find_events(ip_address: str) -> list[UserEvent]:
    return UserEvent.query \
        .filter_by(event_type='user-logged-in') \
        .filter(UserEvent.data['ip_address'].astext == ip_address) \


@@ 37,7 37,7 @@ def find_events(ip_address: str) -> List[UserEvent]:
        .all()


def get_users_by_id(events: List[UserEvent]) -> Dict[UserID, User]:
def get_users_by_id(events: list[UserEvent]) -> dict[UserID, User]:
    user_ids = {event.user_id for event in events}
    users = user_service.find_users(user_ids)
    return {user.id: user for user in users}

M scripts/generate_sql_to_delete_user.py => scripts/generate_sql_to_delete_user.py +3 -2
@@ 11,7 11,8 @@ Run script `clean_up_after_deleted_users.py` before this one.
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Iterable, Iterator, List
from __future__ import annotations
from typing import Iterable, Iterator

import click



@@ 22,7 23,7 @@ from _util import call_with_app_context
from _validators import validate_user_id_format


def validate_user_ids(ctx, param, user_ids: Iterable[UserID]) -> List[UserID]:
def validate_user_ids(ctx, param, user_ids: Iterable[UserID]) -> list[UserID]:
    def _validate():
        for user_id in user_ids:
            yield validate_user_id_format(ctx, param, user_id)

M scripts/set_current_terms_version.py => scripts/set_current_terms_version.py +5 -4
@@ 6,7 6,8 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from typing import Dict, List, Optional
from __future__ import annotations
from typing import Optional

import click
from pick import pick


@@ 54,7 55,7 @@ def execute(document_id):


def _request_version_id(
    versions_by_id: Dict[VersionID, Version],
    versions_by_id: dict[VersionID, Version],
    current_version_id: Optional[VersionID],
) -> VersionID:
    version_ids = _get_version_ids_latest_first(versions_by_id)


@@ 84,8 85,8 @@ def _request_version_id(


def _get_version_ids_latest_first(
    versions_by_id: Dict[VersionID, Version]
) -> List[VersionID]:
    versions_by_id: dict[VersionID, Version]
) -> list[VersionID]:
    versions = versions_by_id.values()

    versions_latest_first = list(

M sites/cozylan/extension.py => sites/cozylan/extension.py +3 -2
@@ 2,7 2,8 @@
Site-specific code extension
"""

from typing import Any, Dict
from __future__ import annotations
from typing import Any

from flask import g



@@ 10,7 11,7 @@ from byceps.services.seating import seat_service
from byceps.services.ticketing import ticket_service


def template_context_processor() -> Dict[str, Any]:
def template_context_processor() -> dict[str, Any]:
    """Extend template context."""
    sale_stats = ticket_service.get_ticket_sale_stats(g.party_id)
    seat_utilization = seat_service.get_seat_utilization(g.party_id)

M tests/conftest.py => tests/conftest.py +3 -2
@@ 3,10 3,11 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from pathlib import Path
from secrets import token_hex
from tempfile import TemporaryDirectory
from typing import Optional, Set
from typing import Optional

import pytest



@@ 138,7 139,7 @@ def make_admin(make_user):
    created_permission_ids = set()
    created_role_ids = set()

    def _wrapper(screen_name: str, permission_ids: Set[str]):
    def _wrapper(screen_name: str, permission_ids: set[str]):
        admin = make_user(screen_name)
        user_ids.add(admin.id)


M tests/helpers.py => tests/helpers.py +4 -3
@@ 6,11 6,12 @@ tests.helpers
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from contextlib import contextmanager
from datetime import date, datetime
from pathlib import Path
from secrets import token_hex
from typing import Any, Dict, Optional
from typing import Any, Optional

from flask import appcontext_pushed, g



@@ 34,7 35,7 @@ CONFIG_FILENAME_TEST_SITE = _CONFIG_PATH / 'test_site.py'
CONFIG_FILENAME_TEST_ADMIN = _CONFIG_PATH / 'test_admin.py'


def create_admin_app(config_overrides: Optional[Dict[str, Any]] = None):
def create_admin_app(config_overrides: Optional[dict[str, Any]] = None):
    app = create_app(
        config_filename=CONFIG_FILENAME_TEST_ADMIN,
        config_overrides=config_overrides,


@@ 42,7 43,7 @@ def create_admin_app(config_overrides: Optional[Dict[str, Any]] = None):
    return app


def create_site_app(config_overrides: Optional[Dict[str, Any]] = None):
def create_site_app(config_overrides: Optional[dict[str, Any]] = None):
    app = create_app(
        config_filename=CONFIG_FILENAME_TEST_SITE,
        config_overrides=config_overrides,

M tests/integration/announce/irc/helpers.py => tests/integration/announce/irc/helpers.py +2 -2
@@ 3,10 3,10 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from contextlib import contextmanager
from datetime import datetime
from http import HTTPStatus
from typing import List

from requests_mock import Mocker



@@ 45,7 45,7 @@ def assert_request_data(
    assert actual.keys() == {'channel', 'text'}


def get_submitted_json(mock, expected_call_count: int) -> List[str]:
def get_submitted_json(mock, expected_call_count: int) -> list[str]:
    assert mock.called

    history = mock.request_history

M tests/integration/api/helpers.py => tests/integration/api/helpers.py +2 -2
@@ 6,11 6,11 @@ tests.api.helpers
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from base64 import b64encode
from typing import Tuple


def assemble_authorization_header(api_token: str) -> Tuple[str, str]:
def assemble_authorization_header(api_token: str) -> tuple[str, str]:
    """Assemble header to authorize against the API."""
    encoded_token = b64encode(api_token.encode('ascii')).decode('ascii')


M tests/integration/util/test_authorization.py => tests/integration/util/test_authorization.py +2 -2
@@ 3,8 3,8 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from enum import Enum
from typing import Set

from flask import g
import pytest


@@ 22,7 22,7 @@ ChillPermission = create_permission_enum(


class CurrentUserMock:
    def __init__(self, permissions: Set[Enum]) -> None:
    def __init__(self, permissions: set[Enum]) -> None:
        self.permissions = permissions



M tests/unit/services/orga/test_birthday_service.py => tests/unit/services/orga/test_birthday_service.py +2 -2
@@ 3,8 3,8 @@
:License: Revised BSD (see `LICENSE` file for details)
"""

from __future__ import annotations
from datetime import date
from typing import Tuple

from freezegun import freeze_time



@@ 46,7 46,7 @@ def test_sort():
# helpers


def create_user_and_birthday(date_of_birth: date) -> Tuple[User, Birthday]:
def create_user_and_birthday(date_of_birth: date) -> tuple[User, Birthday]:
    user = User(
        '55ecd4f2-37ca-4cab-a771-79cf3dabb7cb',
        f'born-{date_of_birth}',