From a8164f37e2e1e45f2fc41de2383604a7a8710c1d Mon Sep 17 00:00:00 2001 From: 283375 Date: Mon, 4 Aug 2025 00:58:11 +0800 Subject: [PATCH] feat: add Version --- src/arcaea_offline/database/models/_base.py | 5 +++- src/arcaea_offline/database/models/_types.py | 24 ++++++++++++++++- src/arcaea_offline/utils/__init__.py | 5 ++++ src/arcaea_offline/utils/version.py | 27 ++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/arcaea_offline/utils/version.py diff --git a/src/arcaea_offline/database/models/_base.py b/src/arcaea_offline/database/models/_base.py index 2342dc3..8194016 100644 --- a/src/arcaea_offline/database/models/_base.py +++ b/src/arcaea_offline/database/models/_base.py @@ -4,10 +4,13 @@ from sqlalchemy import MetaData from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm.exc import DetachedInstanceError -from ._types import ForceTimezoneDateTime +from arcaea_offline.utils import Version + +from ._types import ForceTimezoneDateTime, VersionDatabaseType TYPE_ANNOTATION_MAP = { datetime: ForceTimezoneDateTime, + Version: VersionDatabaseType, } diff --git a/src/arcaea_offline/database/models/_types.py b/src/arcaea_offline/database/models/_types.py index d6224ab..33217ba 100644 --- a/src/arcaea_offline/database/models/_types.py +++ b/src/arcaea_offline/database/models/_types.py @@ -1,9 +1,11 @@ from datetime import datetime, timezone from typing import Optional -from sqlalchemy import DateTime +from sqlalchemy import DateTime, String from sqlalchemy.types import TypeDecorator +from arcaea_offline.utils import Version + class ForceTimezoneDateTime(TypeDecorator): """ @@ -26,3 +28,23 @@ class ForceTimezoneDateTime(TypeDecorator): if value is not None: value = value.replace(tzinfo=timezone.utc) return value + + +class VersionDatabaseType(TypeDecorator): + impl = String + cache_ok = True + + def process_bind_param(self, value: Optional[Version], dialect): + if value is None: + return None + + if not isinstance(value, Version): + raise ValueError("Input is not a Version instance.") + + return str(f"{value.first}.{value.second}.{value.third}") + + def process_result_value(self, value: Optional[str], dialect): + if value is None: + return None + + return Version(*(map(int, value.split(".")))) diff --git a/src/arcaea_offline/utils/__init__.py b/src/arcaea_offline/utils/__init__.py index e69de29..1993559 100644 --- a/src/arcaea_offline/utils/__init__.py +++ b/src/arcaea_offline/utils/__init__.py @@ -0,0 +1,5 @@ +from .version import Version + +__all__ = [ + "Version", +] diff --git a/src/arcaea_offline/utils/version.py b/src/arcaea_offline/utils/version.py new file mode 100644 index 0000000..56d7c1d --- /dev/null +++ b/src/arcaea_offline/utils/version.py @@ -0,0 +1,27 @@ +from typing import NamedTuple + + +class Version(NamedTuple): + first: int + second: int + third: int + + @classmethod + def from_string(cls, version_str: str): + version_str = version_str.removesuffix("c") + parts = version_str.split(".") + if len(parts) not in {2, 3}: + raise ValueError(f"Invalid version string {version_str}") + + try: + if len(parts) == 2: # noqa: PLR2004 + parts.append("0") + + first, second, third = map(int, parts) + except ValueError as e: + raise ValueError(f"Invalid version string {version_str}") from e + + return cls(first, second, third) + + def __str__(self): + return f"{self.first}.{self.second}.{self.third}"