diff --git a/mealie/core/security/providers/auth_provider.py b/mealie/core/security/providers/auth_provider.py index ce10afd7d..d6dc84128 100644 --- a/mealie/core/security/providers/auth_provider.py +++ b/mealie/core/security/providers/auth_provider.py @@ -1,6 +1,5 @@ import abc from datetime import UTC, datetime, timedelta -from typing import Generic, TypeVar import jwt from sqlalchemy.orm.session import Session @@ -13,10 +12,8 @@ ALGORITHM = "HS256" ISS = "mealie" remember_me_duration = timedelta(days=14) -T = TypeVar("T") - -class AuthProvider(Generic[T], metaclass=abc.ABCMeta): +class AuthProvider[T](metaclass=abc.ABCMeta): """Base Authentication Provider interface""" def __init__(self, session: Session, data: T) -> None: diff --git a/mealie/repos/repository_generic.py b/mealie/repos/repository_generic.py index 35c3616a6..f8ab3f1b4 100644 --- a/mealie/repos/repository_generic.py +++ b/mealie/repos/repository_generic.py @@ -4,7 +4,7 @@ import random from collections.abc import Iterable from datetime import UTC, datetime from math import ceil -from typing import Any, Generic, TypeVar +from typing import Any from fastapi import HTTPException from pydantic import UUID4, BaseModel @@ -28,18 +28,13 @@ from mealie.schema.response.query_search import SearchFilter from ._utils import NOT_SET, NotSet -Schema = TypeVar("Schema", bound=MealieModel) -Model = TypeVar("Model", bound=SqlAlchemyBase) -T = TypeVar("T", bound="RepositoryGeneric") - - -class RepositoryGeneric(Generic[Schema, Model]): +class RepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase]: """A Generic BaseAccess Model method to perform common operations on the database Args: - Generic ([Schema]): Represents the Pydantic Model - Generic ([Model]): Represents the SqlAlchemyModel Model + Schema: Represents the Pydantic Model + Model: Represents the SqlAlchemyModel Model """ session: Session @@ -467,7 +462,7 @@ class RepositoryGeneric(Generic[Schema, Model]): return search_filter.filter_query_by_search(query, schema, self.model) -class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]): +class GroupRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]): def __init__( self, session: Session, @@ -483,7 +478,7 @@ class GroupRepositoryGeneric(RepositoryGeneric[Schema, Model]): self._group_id = group_id if group_id else None -class HouseholdRepositoryGeneric(RepositoryGeneric[Schema, Model]): +class HouseholdRepositoryGeneric[Schema: MealieModel, Model: SqlAlchemyBase](RepositoryGeneric[Schema, Model]): def __init__( self, session: Session, diff --git a/mealie/routes/_base/controller.py b/mealie/routes/_base/controller.py index f7e13f34b..e1c03c160 100644 --- a/mealie/routes/_base/controller.py +++ b/mealie/routes/_base/controller.py @@ -6,20 +6,18 @@ See their repository for details -> https://github.com/dmontagu/fastapi-utils import inspect from collections.abc import Callable -from typing import Any, ClassVar, ForwardRef, TypeVar, cast, get_origin, get_type_hints +from typing import Any, ClassVar, ForwardRef, cast, get_origin, get_type_hints from fastapi import APIRouter, Depends from fastapi.routing import APIRoute from starlette.routing import Route, WebSocketRoute -T = TypeVar("T") - CBV_CLASS_KEY = "__cbv_class__" INCLUDE_INIT_PARAMS_KEY = "__include_init_params__" RETURN_TYPES_FUNC_KEY = "__return_types_func__" -def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]: +def controller[T](router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]: """ This function returns a decorator that converts the decorated into a class-based view for the provided router. Any methods of the decorated class that are decorated as endpoints using the router provided to this function @@ -36,7 +34,7 @@ def controller(router: APIRouter, *urls: str) -> Callable[[type[T]], type[T]]: return decorator -def _cbv(router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]: +def _cbv[T](router: APIRouter, cls: type[T], *urls: str, instance: Any | None = None) -> type[T]: """ Replaces any methods of the provided class `cls` that are endpoints of routes in `router` with updated function calls that will properly inject an instance of `cls`. diff --git a/mealie/routes/_base/mixins.py b/mealie/routes/_base/mixins.py index 95ec01e4a..e69457c78 100644 --- a/mealie/routes/_base/mixins.py +++ b/mealie/routes/_base/mixins.py @@ -1,6 +1,5 @@ from collections.abc import Callable from logging import Logger -from typing import Generic, TypeVar import sqlalchemy.exc from fastapi import HTTPException, status @@ -9,12 +8,8 @@ from pydantic import UUID4, BaseModel from mealie.repos.repository_generic import RepositoryGeneric from mealie.schema.response import ErrorResponse -C = TypeVar("C", bound=BaseModel) -R = TypeVar("R", bound=BaseModel) -U = TypeVar("U", bound=BaseModel) - -class HttpRepo(Generic[C, R, U]): +class HttpRepo[C: BaseModel, R: BaseModel, U: BaseModel]: """ The HttpRepo[C, R, U] class is a mixin class that provides a common set of methods for CRUD operations. This class is intended to be used in a composition pattern where a class has a mixin property. For example: diff --git a/mealie/schema/_mealie/mealie_model.py b/mealie/schema/_mealie/mealie_model.py index d7d70a6dc..28909527f 100644 --- a/mealie/schema/_mealie/mealie_model.py +++ b/mealie/schema/_mealie/mealie_model.py @@ -4,7 +4,7 @@ import re from collections.abc import Sequence from datetime import UTC, datetime from enum import Enum -from typing import ClassVar, Protocol, Self, TypeVar +from typing import ClassVar, Protocol, Self from humps.main import camelize from pydantic import UUID4, AliasChoices, BaseModel, ConfigDict, Field, model_validator @@ -14,8 +14,6 @@ from sqlalchemy.orm.interfaces import LoaderOption from mealie.db.models._model_base import SqlAlchemyBase -T = TypeVar("T", bound=BaseModel) - HOUR_ONLY_TZ_PATTERN = re.compile(r"[+-]\d{2}$") @@ -56,7 +54,7 @@ class MealieModel(BaseModel): @model_validator(mode="before") @classmethod - def fix_hour_only_tz(cls, data: T) -> T: + def fix_hour_only_tz[T: BaseModel](cls, data: T) -> T: """ Fixes datetimes with timezones that only have the hour portion. @@ -82,7 +80,7 @@ class MealieModel(BaseModel): Adds UTC timezone information to all datetimes in the model. The server stores everything in UTC without timezone info. """ - for field in self.model_fields: + for field in self.__class__.model_fields: val = getattr(self, field) if not isinstance(val, datetime): continue @@ -91,23 +89,25 @@ class MealieModel(BaseModel): return self - def cast(self, cls: type[T], **kwargs) -> T: + def cast[T: BaseModel](self, cls: type[T], **kwargs) -> T: """ Cast the current model to another with additional arguments. Useful for transforming DTOs into models that are saved to a database """ - create_data = {field: getattr(self, field) for field in self.model_fields if field in cls.model_fields} + create_data = { + field: getattr(self, field) for field in self.__class__.model_fields if field in cls.model_fields + } create_data.update(kwargs or {}) return cls(**create_data) - def map_to(self, dest: T) -> T: + def map_to[T: BaseModel](self, dest: T) -> T: """ Map matching values from the current model to another model. Model returned for method chaining. """ - for field in self.model_fields: - if field in dest.model_fields: + for field in self.__class__.model_fields: + if field in dest.__class__.model_fields: setattr(dest, field, getattr(self, field)) return dest @@ -117,18 +117,18 @@ class MealieModel(BaseModel): Map matching values from another model to the current model. """ - for field in src.model_fields: - if field in self.model_fields: + for field in src.__class__.model_fields: + if field in self.__class__.model_fields: setattr(self, field, getattr(src, field)) - def merge(self, src: T, replace_null=False): + def merge[T: BaseModel](self, src: T, replace_null=False): """ Replace matching values from another instance to the current instance. """ - for field in src.model_fields: + for field in src.__class__.model_fields: val = getattr(src, field) - if field in self.model_fields and (val is not None or replace_null): + if field in self.__class__.model_fields and (val is not None or replace_null): setattr(self, field, val) @classmethod diff --git a/mealie/schema/mapper.py b/mealie/schema/mapper.py index 6aee59ef6..72b58013b 100644 --- a/mealie/schema/mapper.py +++ b/mealie/schema/mapper.py @@ -1,24 +1,21 @@ -from typing import TypeVar - from pydantic import BaseModel -T = TypeVar("T", bound=BaseModel) -U = TypeVar("U", bound=BaseModel) - -def mapper(source: U, dest: T, **_) -> T: +def mapper[U: BaseModel, T: BaseModel](source: U, dest: T, **_) -> T: """ Map a source model to a destination model. Only top-level fields are mapped. """ - for field in source.model_fields: - if field in dest.model_fields: + for field in source.__class__.model_fields: + if field in dest.__class__.model_fields: setattr(dest, field, getattr(source, field)) return dest -def cast(source: U, dest: type[T], **kwargs) -> T: - create_data = {field: getattr(source, field) for field in source.model_fields if field in dest.model_fields} +def cast[U: BaseModel, T: BaseModel](source: U, dest: type[T], **kwargs) -> T: + create_data = { + field: getattr(source, field) for field in source.__class__.model_fields if field in dest.model_fields + } create_data.update(kwargs or {}) return dest(**create_data) diff --git a/mealie/schema/response/pagination.py b/mealie/schema/response/pagination.py index 67b69a103..2fb687575 100644 --- a/mealie/schema/response/pagination.py +++ b/mealie/schema/response/pagination.py @@ -1,5 +1,5 @@ import enum -from typing import Annotated, Any, Generic, TypeVar +from typing import Annotated, Any from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit from humps import camelize @@ -8,8 +8,6 @@ from pydantic_core.core_schema import ValidationInfo from mealie.schema._mealie import MealieModel -DataT = TypeVar("DataT", bound=BaseModel) - class OrderDirection(str, enum.Enum): asc = "asc" @@ -50,7 +48,7 @@ class PaginationQuery(RequestQuery): per_page: int = 50 -class PaginationBase(BaseModel, Generic[DataT]): +class PaginationBase[DataT: BaseModel](BaseModel): page: int = 1 per_page: int = 10 total: int = 0 diff --git a/mealie/schema/response/query_filter.py b/mealie/schema/response/query_filter.py index eb7651a3f..9e41490f3 100644 --- a/mealie/schema/response/query_filter.py +++ b/mealie/schema/response/query_filter.py @@ -3,7 +3,7 @@ from __future__ import annotations import re from collections import deque from enum import Enum -from typing import Any, TypeVar, cast +from typing import Any, cast from uuid import UUID import sqlalchemy as sa @@ -19,8 +19,6 @@ from mealie.db.models._model_utils.datetime import NaiveDateTime from mealie.db.models._model_utils.guid import GUID from mealie.schema._mealie.mealie_model import MealieModel -Model = TypeVar("Model", bound=SqlAlchemyBase) - class RelationalKeyword(Enum): IS = "IS" @@ -274,7 +272,7 @@ class QueryFilterBuilder: return consolidated_group_builder.self_group() @classmethod - def get_model_and_model_attr_from_attr_string( + def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase]( cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None ) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]: """ @@ -343,7 +341,7 @@ class QueryFilterBuilder: return model_attr @classmethod - def _get_filter_element( + def _get_filter_element[Model: SqlAlchemyBase]( cls, query: sa.Select, component: QueryFilterBuilderComponent, @@ -397,7 +395,7 @@ class QueryFilterBuilder: return element - def filter_query( + def filter_query[Model: SqlAlchemyBase]( self, query: sa.Select, model: type[Model], column_aliases: dict[str, sa.ColumnElement] | None = None ) -> sa.Select: """ diff --git a/mealie/schema/user/user.py b/mealie/schema/user/user.py index b05c5e8f0..b0104421a 100644 --- a/mealie/schema/user/user.py +++ b/mealie/schema/user/user.py @@ -1,6 +1,6 @@ from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import Annotated, Any, Generic, TypeVar +from typing import Annotated, Any from uuid import UUID from pydantic import UUID4, BaseModel, ConfigDict, Field, StringConstraints, field_validator @@ -20,7 +20,6 @@ from mealie.schema.response.pagination import PaginationBase from ...db.models.group import Group from ..recipe import CategoryBase -DataT = TypeVar("DataT", bound=BaseModel) DEFAULT_INTEGRATION_ID = "generic" settings = get_app_settings() @@ -102,7 +101,7 @@ class UserRatingOut(UserRatingCreate): ] -class UserRatings(BaseModel, Generic[DataT]): +class UserRatings[DataT: BaseModel](BaseModel): ratings: list[DataT] diff --git a/mealie/services/migrations/utils/database_helpers.py b/mealie/services/migrations/utils/database_helpers.py index d8e860195..80db1acd3 100644 --- a/mealie/services/migrations/utils/database_helpers.py +++ b/mealie/services/migrations/utils/database_helpers.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING from pydantic import BaseModel from slugify import slugify @@ -12,8 +12,6 @@ from mealie.schema.recipe import RecipeCategory from mealie.schema.recipe.recipe import RecipeTag from mealie.schema.recipe.recipe_category import CategoryOut, CategorySave, TagOut, TagSave -T = TypeVar("T", bound=BaseModel) - if TYPE_CHECKING: from mealie.repos.repository_generic import RepositoryGeneric @@ -23,7 +21,7 @@ class DatabaseMigrationHelpers: self.session = session self.db = db - def _get_or_set_generic( + def _get_or_set_generic[T: BaseModel]( self, accessor: RepositoryGeneric, items: Iterable[str], create_model: type[T], out_model: type[T] ) -> list[T]: """ diff --git a/mealie/services/parser_services/_base.py b/mealie/services/parser_services/_base.py index 33e5a265c..1a2be34cc 100644 --- a/mealie/services/parser_services/_base.py +++ b/mealie/services/parser_services/_base.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import TypeVar from pydantic import UUID4, BaseModel from rapidfuzz import fuzz, process @@ -17,8 +16,6 @@ from mealie.schema.recipe.recipe_ingredient import ( ) from mealie.schema.response.pagination import PaginationQuery -T = TypeVar("T", bound=BaseModel) - class DataMatcher: def __init__( @@ -83,7 +80,9 @@ class DataMatcher: return self._units_by_alias @classmethod - def find_match(cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0) -> T | None: + def find_match[T: BaseModel]( + cls, match_value: str, *, store_map: dict[str, T], fuzzy_match_threshold: int = 0 + ) -> T | None: # check for literal matches if match_value in store_map: return store_map[match_value] diff --git a/poetry.lock b/poetry.lock index d71d6515a..f3c583e65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3250,30 +3250,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.11.13" +version = "0.12.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, - {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, - {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, - {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, - {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, - {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, - {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, + {file = "ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848"}, + {file = "ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6"}, + {file = "ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2"}, + {file = "ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51"}, + {file = "ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a"}, + {file = "ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb"}, + {file = "ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0"}, + {file = "ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b"}, + {file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"}, ] [[package]] @@ -3885,4 +3885,4 @@ pgsql = ["psycopg2-binary"] [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.13" -content-hash = "2b8479e18ef741f5254b8c9d64566bf42d597cfb6564c1aa622f6a1afb117402" +content-hash = "632cd8ef199c2668bc799a1cf4f370161dc13ff7dcf76ed40f3c94a0896e304f" diff --git a/pyproject.toml b/pyproject.toml index 30aa4aaef..8ca1c5867 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ pylint = "^3.0.0" pytest = "^8.0.0" pytest-asyncio = "^1.0.0" rich = "^14.0.0" -ruff = "^0.11.0" +ruff = "^0.12.0" types-PyYAML = "^6.0.4" types-python-dateutil = "^2.8.18" types-python-slugify = "^6.0.0"