diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 127695e..fcbdcb0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.10' - uses: trim21/setup-poetry@dist/v1 - uses: trim21/install-poetry-project@dist/v1 diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 111472c..78cf34f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - uses: Trim21/setup-poetry@dist/v1 @@ -30,6 +30,7 @@ jobs: with: action: add linters: mypy + run: '' - name: mypy run: mypy --show-column-numbers chii rpc @@ -41,7 +42,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.10" - uses: trim21/setup-poetry@dist/v1 - uses: trim21/install-poetry-project@dist/v1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 359fb28..ed37947 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,4 @@ -name: release +name: Release(Docker) on: push: @@ -11,7 +11,7 @@ jobs: docker: runs-on: ubuntu-latest env: - IMAGE: "ghcr.io/${{ github.repository_owner }}/ms-timeline" + IMAGE: 'ghcr.io/${{ github.repository_owner }}/ms-timeline' concurrency: group: ${{ github.workflow }}-${{ github.sha }} @@ -26,34 +26,6 @@ jobs: username: ${{ github.actor }} password: ${{ github.token }} - - name: build base docker image - env: - TAG_HASH: "base-${{ hashFiles('poetry.lock', 'etc/*.dockerfile') }}" - run: | - echo "TAG_HASH=${TAG_HASH}" >>$GITHUB_ENV - - if ! docker pull "${IMAGE}:${TAG_HASH}"; then - echo "NEED_BUILD_BASE=true" >>$GITHUB_ENV - else - echo "NEED_BUILD_BASE=false" >>$GITHUB_ENV - fi - - - name: Build Base Docker Image (if needed) - uses: docker/build-push-action@v6 - if: ${{ fromJSON(env.NEED_BUILD_BASE) }} - with: - context: ./ - file: ./etc/base.dockerfile - provenance: false - push: true - tags: ${{ env.IMAGE }}:${{ env.TAG_HASH }} - - - run: docker tag "${IMAGE}:${TAG_HASH}" base-image - - - run: echo "SHA=$(git show --no-patch --no-notes --date=short-local --pretty='%as-%h')" >> $GITHUB_ENV - env: - TZ: UTC - - name: Docker metadata id: meta uses: docker/metadata-action@v5 @@ -63,17 +35,37 @@ jobs: type=semver,pattern=v{{version}} type=ref,event=branch - type=sha,prefix={{branch}}- - type=ref,event=branch,suffix=-${{ env.SHA }} + type=ref,event=branch,suffix=-{{ sha }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - type=raw,value=${{ env.SHA }} + # https://docs.docker.com/build/ci/github-actions/cache/#cache-mounts + - name: Go Build Cache for Docker + uses: actions/cache@v4 + with: + path: pip-wheel-cache + key: ${{ runner.os }}-pip-cache-${{ hashFiles('**/poetry.lock') }} + restore-keys: + ${{ runner.os }}-pip-cache- + + - name: inject pip-wheel-cache into docker + uses: reproducible-containers/buildkit-cache-dance@v3.1.2 + with: + cache-map: | + { + "pip-wheel-cache": "/root/.cache/pip" + } + skip-extraction: ${{ steps.cache.outputs.cache-hit }} - name: Build Final Docker Image uses: docker/build-push-action@v6 with: context: ./ provenance: false - file: ./etc/final.dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + github-token: ${{ github.token }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff5f1a2..d3c849c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,8 +19,6 @@ repos: - id: mixed-line-ending args: [--fix=lf] - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - repo: https://github.com/python-poetry/poetry rev: "1.8.3" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..093c3fe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1@sha256:865e5dd094beca432e8c0a1d5e1c465db5f998dca4e439981029b3b81fb39ed5 + +### convert poetry.lock to requirements.txt ### +FROM python:3.10-slim@sha256:80619a5316afae7045a3c13371b0ee670f39bac46ea1ed35081d2bf91d6c3dbd AS poetry + +WORKDIR /app + +ENV PIP_ROOT_USER_ACTION=ignore + +COPY requirements-poetry.txt ./ +RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked pip install -r requirements-poetry.txt + +COPY pyproject.toml poetry.lock ./ +RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked poetry export -f requirements.txt --output requirements.txt + +### final image ### +FROM python:3.10-slim@sha256:80619a5316afae7045a3c13371b0ee670f39bac46ea1ed35081d2bf91d6c3dbd + +WORKDIR /app + +ENV PYTHONPATH=/app + +COPY --from=poetry /app/requirements.txt ./requirements.txt + +ENV PIP_ROOT_USER_ACTION=ignore + +RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked pip install -U pip && \ + pip install -r requirements.txt + +WORKDIR /app + +ENTRYPOINT [ "python", "./start_grpc_server.py" ] + +COPY . ./ diff --git a/Taskfile.yaml b/Taskfile.yaml index 83048ab..d241054 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -8,8 +8,8 @@ tasks: cmds: - task --list-all - gen: - desc: Build Web Server Binary + gen-grpc: + desc: generate grpc python files generates: - ./api/**/*.py sources: @@ -23,6 +23,17 @@ tasks: --grpc_python_out=. ./proto/api/v1/timeline.proto + gen-orm: + desc: generate dataclasses from database + dotenv: + - .env + cmds: + - >- + sqlacodegen --generator dataclasses + --outfile chii/db/models.py + --noviews + 'mysql+pymysql://{{.MYSQL_USER}}:{{.MYSQL_PASS}}@{{.MYSQL_HOST}}:{{.MYSQL_PORT}}/{{.MYSQL_DB}}' + dev: poetry run watchgod start_grpc_server.main mypy: mypy --show-column-numbers chii rpc diff --git a/api/v1/timeline_pb2.py b/api/v1/timeline_pb2.py index 948b18a..0450777 100644 --- a/api/v1/timeline_pb2.py +++ b/api/v1/timeline_pb2.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: api/v1/timeline.proto +# Protobuf Python Version: 5.26.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -18,28 +19,27 @@ _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'api.v1.timeline_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - DESCRIPTOR._serialized_options = b'Z\035github.com/bangumi/server/api' - _SUBJECT.fields_by_name['type']._options = None - _SUBJECT.fields_by_name['type']._serialized_options = b'\030\001' - _SUBJECT.fields_by_name['name']._options = None - _SUBJECT.fields_by_name['name']._serialized_options = b'\030\001' - _SUBJECT.fields_by_name['name_cn']._options = None - _SUBJECT.fields_by_name['name_cn']._serialized_options = b'\030\001' - _SUBJECT.fields_by_name['image']._options = None - _SUBJECT.fields_by_name['image']._serialized_options = b'\030\001' - _SUBJECT.fields_by_name['series']._options = None - _SUBJECT.fields_by_name['series']._serialized_options = b'\030\001' - _EPISODE.fields_by_name['type']._options = None - _EPISODE.fields_by_name['type']._serialized_options = b'\030\001' - _EPISODE.fields_by_name['name']._options = None - _EPISODE.fields_by_name['name']._serialized_options = b'\030\001' - _EPISODE.fields_by_name['name_cn']._options = None - _EPISODE.fields_by_name['name_cn']._serialized_options = b'\030\001' - _EPISODE.fields_by_name['sort']._options = None - _EPISODE.fields_by_name['sort']._serialized_options = b'\030\001' +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z\035github.com/bangumi/server/api' + _globals['_SUBJECT'].fields_by_name['type']._loaded_options = None + _globals['_SUBJECT'].fields_by_name['type']._serialized_options = b'\030\001' + _globals['_SUBJECT'].fields_by_name['name']._loaded_options = None + _globals['_SUBJECT'].fields_by_name['name']._serialized_options = b'\030\001' + _globals['_SUBJECT'].fields_by_name['name_cn']._loaded_options = None + _globals['_SUBJECT'].fields_by_name['name_cn']._serialized_options = b'\030\001' + _globals['_SUBJECT'].fields_by_name['image']._loaded_options = None + _globals['_SUBJECT'].fields_by_name['image']._serialized_options = b'\030\001' + _globals['_SUBJECT'].fields_by_name['series']._loaded_options = None + _globals['_SUBJECT'].fields_by_name['series']._serialized_options = b'\030\001' + _globals['_EPISODE'].fields_by_name['type']._loaded_options = None + _globals['_EPISODE'].fields_by_name['type']._serialized_options = b'\030\001' + _globals['_EPISODE'].fields_by_name['name']._loaded_options = None + _globals['_EPISODE'].fields_by_name['name']._serialized_options = b'\030\001' + _globals['_EPISODE'].fields_by_name['name_cn']._loaded_options = None + _globals['_EPISODE'].fields_by_name['name_cn']._serialized_options = b'\030\001' + _globals['_EPISODE'].fields_by_name['sort']._loaded_options = None + _globals['_EPISODE'].fields_by_name['sort']._serialized_options = b'\030\001' _globals['_HELLOREQUEST']._serialized_start=33 _globals['_HELLOREQUEST']._serialized_end=61 _globals['_HELLORESPONSE']._serialized_start=63 diff --git a/api/v1/timeline_pb2.pyi b/api/v1/timeline_pb2.pyi index b374da0..3427306 100644 --- a/api/v1/timeline_pb2.pyi +++ b/api/v1/timeline_pb2.pyi @@ -5,37 +5,37 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti DESCRIPTOR: _descriptor.FileDescriptor class HelloRequest(_message.Message): - __slots__ = ["name"] + __slots__ = ("name",) NAME_FIELD_NUMBER: _ClassVar[int] name: str def __init__(self, name: _Optional[str] = ...) -> None: ... class HelloResponse(_message.Message): - __slots__ = ["message"] + __slots__ = ("message",) MESSAGE_FIELD_NUMBER: _ClassVar[int] message: str def __init__(self, message: _Optional[str] = ...) -> None: ... class SubjectCollectResponse(_message.Message): - __slots__ = ["ok"] + __slots__ = ("ok",) OK_FIELD_NUMBER: _ClassVar[int] ok: bool def __init__(self, ok: bool = ...) -> None: ... class SubjectProgressResponse(_message.Message): - __slots__ = ["ok"] + __slots__ = ("ok",) OK_FIELD_NUMBER: _ClassVar[int] ok: bool def __init__(self, ok: bool = ...) -> None: ... class EpisodeCollectResponse(_message.Message): - __slots__ = ["ok"] + __slots__ = ("ok",) OK_FIELD_NUMBER: _ClassVar[int] ok: bool def __init__(self, ok: bool = ...) -> None: ... class Subject(_message.Message): - __slots__ = ["id", "type", "name", "name_cn", "image", "series", "vols_total", "eps_total"] + __slots__ = ("id", "type", "name", "name_cn", "image", "series", "vols_total", "eps_total") ID_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] @@ -55,7 +55,7 @@ class Subject(_message.Message): def __init__(self, id: _Optional[int] = ..., type: _Optional[int] = ..., name: _Optional[str] = ..., name_cn: _Optional[str] = ..., image: _Optional[str] = ..., series: bool = ..., vols_total: _Optional[int] = ..., eps_total: _Optional[int] = ...) -> None: ... class Episode(_message.Message): - __slots__ = ["id", "type", "name", "name_cn", "sort"] + __slots__ = ("id", "type", "name", "name_cn", "sort") ID_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] @@ -69,7 +69,7 @@ class Episode(_message.Message): def __init__(self, id: _Optional[int] = ..., type: _Optional[int] = ..., name: _Optional[str] = ..., name_cn: _Optional[str] = ..., sort: _Optional[float] = ...) -> None: ... class SubjectCollectRequest(_message.Message): - __slots__ = ["user_id", "subject", "collection", "comment", "rate", "collection_id"] + __slots__ = ("user_id", "subject", "collection", "comment", "rate", "collection_id") USER_ID_FIELD_NUMBER: _ClassVar[int] SUBJECT_FIELD_NUMBER: _ClassVar[int] COLLECTION_FIELD_NUMBER: _ClassVar[int] @@ -85,7 +85,7 @@ class SubjectCollectRequest(_message.Message): def __init__(self, user_id: _Optional[int] = ..., subject: _Optional[_Union[Subject, _Mapping]] = ..., collection: _Optional[int] = ..., comment: _Optional[str] = ..., rate: _Optional[int] = ..., collection_id: _Optional[int] = ...) -> None: ... class EpisodeCollectRequest(_message.Message): - __slots__ = ["user_id", "last", "subject"] + __slots__ = ("user_id", "last", "subject") USER_ID_FIELD_NUMBER: _ClassVar[int] LAST_FIELD_NUMBER: _ClassVar[int] SUBJECT_FIELD_NUMBER: _ClassVar[int] @@ -95,7 +95,7 @@ class EpisodeCollectRequest(_message.Message): def __init__(self, user_id: _Optional[int] = ..., last: _Optional[_Union[Episode, _Mapping]] = ..., subject: _Optional[_Union[Subject, _Mapping]] = ...) -> None: ... class SubjectProgressRequest(_message.Message): - __slots__ = ["user_id", "subject", "eps_update", "vols_update"] + __slots__ = ("user_id", "subject", "eps_update", "vols_update") USER_ID_FIELD_NUMBER: _ClassVar[int] SUBJECT_FIELD_NUMBER: _ClassVar[int] EPS_UPDATE_FIELD_NUMBER: _ClassVar[int] diff --git a/api/v1/timeline_pb2_grpc.py b/api/v1/timeline_pb2_grpc.py index e2bb855..b91a0d4 100644 --- a/api/v1/timeline_pb2_grpc.py +++ b/api/v1/timeline_pb2_grpc.py @@ -1,9 +1,34 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings from api.v1 import timeline_pb2 as api_dot_v1_dot_timeline__pb2 +GRPC_GENERATED_VERSION = '1.63.0' +GRPC_VERSION = grpc.__version__ +EXPECTED_ERROR_RELEASE = '1.65.0' +SCHEDULED_RELEASE_DATE = 'June 25, 2024' +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + warnings.warn( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in api/v1/timeline_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + + f' This warning will become an error in {EXPECTED_ERROR_RELEASE},' + + f' scheduled for release on {SCHEDULED_RELEASE_DATE}.', + RuntimeWarning + ) + class TimeLineServiceStub(object): """Missing associated documentation comment in .proto file.""" @@ -18,22 +43,22 @@ def __init__(self, channel): '/api.v1.TimeLineService/Hello', request_serializer=api_dot_v1_dot_timeline__pb2.HelloRequest.SerializeToString, response_deserializer=api_dot_v1_dot_timeline__pb2.HelloResponse.FromString, - ) + _registered_method=True) self.SubjectCollect = channel.unary_unary( '/api.v1.TimeLineService/SubjectCollect', request_serializer=api_dot_v1_dot_timeline__pb2.SubjectCollectRequest.SerializeToString, response_deserializer=api_dot_v1_dot_timeline__pb2.SubjectCollectResponse.FromString, - ) + _registered_method=True) self.SubjectProgress = channel.unary_unary( '/api.v1.TimeLineService/SubjectProgress', request_serializer=api_dot_v1_dot_timeline__pb2.SubjectProgressRequest.SerializeToString, response_deserializer=api_dot_v1_dot_timeline__pb2.SubjectProgressResponse.FromString, - ) + _registered_method=True) self.EpisodeCollect = channel.unary_unary( '/api.v1.TimeLineService/EpisodeCollect', request_serializer=api_dot_v1_dot_timeline__pb2.EpisodeCollectRequest.SerializeToString, response_deserializer=api_dot_v1_dot_timeline__pb2.EpisodeCollectResponse.FromString, - ) + _registered_method=True) class TimeLineServiceServicer(object): @@ -78,7 +103,7 @@ def add_TimeLineServiceServicer_to_server(servicer, server): response_serializer=api_dot_v1_dot_timeline__pb2.SubjectCollectResponse.SerializeToString, ), 'SubjectProgress': grpc.unary_unary_rpc_method_handler( - servicer.SubjectProgress, + servicer.__SubjectProgress, request_deserializer=api_dot_v1_dot_timeline__pb2.SubjectProgressRequest.FromString, response_serializer=api_dot_v1_dot_timeline__pb2.SubjectProgressResponse.SerializeToString, ), @@ -108,11 +133,21 @@ def Hello(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/api.v1.TimeLineService/Hello', + return grpc.experimental.unary_unary( + request, + target, + '/api.v1.TimeLineService/Hello', api_dot_v1_dot_timeline__pb2.HelloRequest.SerializeToString, api_dot_v1_dot_timeline__pb2.HelloResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubjectCollect(request, @@ -125,11 +160,21 @@ def SubjectCollect(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/api.v1.TimeLineService/SubjectCollect', + return grpc.experimental.unary_unary( + request, + target, + '/api.v1.TimeLineService/SubjectCollect', api_dot_v1_dot_timeline__pb2.SubjectCollectRequest.SerializeToString, api_dot_v1_dot_timeline__pb2.SubjectCollectResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def SubjectProgress(request, @@ -142,11 +187,21 @@ def SubjectProgress(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/api.v1.TimeLineService/SubjectProgress', + return grpc.experimental.unary_unary( + request, + target, + '/api.v1.TimeLineService/SubjectProgress', api_dot_v1_dot_timeline__pb2.SubjectProgressRequest.SerializeToString, api_dot_v1_dot_timeline__pb2.SubjectProgressResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def EpisodeCollect(request, @@ -159,8 +214,18 @@ def EpisodeCollect(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/api.v1.TimeLineService/EpisodeCollect', + return grpc.experimental.unary_unary( + request, + target, + '/api.v1.TimeLineService/EpisodeCollect', api_dot_v1_dot_timeline__pb2.EpisodeCollectRequest.SerializeToString, api_dot_v1_dot_timeline__pb2.EpisodeCollectResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/chii/curd/base.py b/chii/curd/base.py index d621362..54a5df5 100644 --- a/chii/curd/base.py +++ b/chii/curd/base.py @@ -3,9 +3,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from chii.db import sa -from chii.db.tables import Base -T = TypeVar("T", bound=Base) +T = TypeVar("T") async def count(db: AsyncSession, *where) -> int: diff --git a/chii/db/const.py b/chii/db/const.py index 0c105a6..417a0a9 100644 --- a/chii/db/const.py +++ b/chii/db/const.py @@ -39,8 +39,8 @@ class CollectionType(IntEnum): """ wish = 1 # 想看 - doing = 2 # 看过 - collect = 3 # 在看 + done = 2 # 看过 + doing = 3 # 在看 on_hold = 4 # 搁置 dropped = 5 # 抛弃 diff --git a/chii/db/sa.py b/chii/db/sa.py index 1aae3e3..177c231 100644 --- a/chii/db/sa.py +++ b/chii/db/sa.py @@ -1,6 +1,5 @@ import time -from loguru import logger from sqlalchemy import ( CHAR, Column, @@ -21,6 +20,7 @@ ) from sqlalchemy.dialects.mysql import insert from sqlalchemy.orm import joinedload, selectinload, sessionmaker, subqueryload +from sslog import logger from chii.config import config diff --git a/chii/db/tables.py b/chii/db/tables.py index c455042..aa75423 100644 --- a/chii/db/tables.py +++ b/chii/db/tables.py @@ -1,975 +1,109 @@ import datetime -import zlib -from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Tuple, Union +import time +from dataclasses import dataclass, field +from typing import cast -from sqlalchemy import TIMESTAMP, Column, Date, Enum, Float, Index, String, Table, text +from sqlalchemy import Column, text from sqlalchemy.dialects.mysql import ( CHAR, - ENUM, INTEGER, - MEDIUMBLOB, MEDIUMINT, MEDIUMTEXT, SMALLINT, - TEXT, TINYINT, - VARCHAR, - YEAR, ) -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import registry -from chii.compat import phpseralize -from chii.compat.phpseralize import dict_to_list +reg: registry = registry() -Base = declarative_base() -metadata = Base.metadata - -class ChiiCharacter(Base): - __tablename__ = "chii_characters" - - crt_id = Column(MEDIUMINT(8), primary_key=True) - crt_name = Column(String(255, "utf8_unicode_ci"), nullable=False) - crt_role = Column( - TINYINT(4), nullable=False, index=True, comment="角色,机体,组织。。" - ) - crt_infobox: str = Column(MEDIUMTEXT, nullable=False) - crt_summary: str = Column(MEDIUMTEXT, nullable=False) - crt_img = Column(String(255, "utf8_unicode_ci"), nullable=False) - crt_comment = Column(MEDIUMINT(9), nullable=False, server_default=text("'0'")) - crt_collects = Column(MEDIUMINT(8), nullable=False) - crt_dateline = Column(INTEGER(10), nullable=False) - crt_lastpost = Column(INTEGER(11), nullable=False) - crt_lock = Column( - TINYINT(4), nullable=False, index=True, server_default=text("'0'") - ) - crt_img_anidb = Column(VARCHAR(255), nullable=False) - crt_anidb_id = Column(MEDIUMINT(8), nullable=False) - crt_ban = Column(TINYINT(3), nullable=False, index=True, server_default=text("'0'")) - crt_redirect = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - crt_nsfw = Column(TINYINT(1), nullable=False) - - -class ChiiCrtCastIndex(Base): - __tablename__ = "chii_crt_cast_index" - - crt_id = Column(MEDIUMINT(9), primary_key=True, nullable=False) - prsn_id: int = Column(MEDIUMINT(9), primary_key=True, nullable=False, index=True) - subject_id: int = Column(MEDIUMINT(9), primary_key=True, nullable=False, index=True) - subject_type_id: int = Column( - TINYINT(3), - nullable=False, - index=True, - comment="根据人物归类查询角色,动画,书籍,游戏", - ) - summary = Column( - String(255, "utf8_unicode_ci"), - nullable=False, - comment="幼年,男乱马,女乱马,变身形态,少女形态。。", - ) - - -class ChiiCrtSubjectIndex(Base): - __tablename__ = "chii_crt_subject_index" - - crt_id = Column(MEDIUMINT(9), primary_key=True, nullable=False) - subject_id = Column(MEDIUMINT(9), primary_key=True, nullable=False, index=True) - subject_type_id = Column(TINYINT(4), nullable=False, index=True) - crt_type: int = Column(TINYINT(4), nullable=False, index=True, comment="主角,配角") - ctr_appear_eps = Column( - MEDIUMTEXT, nullable=False, comment="可选,角色出场的的章节" - ) - crt_order = Column(TINYINT(3), nullable=False) - - -class ChiiEpRevision(Base): - __tablename__ = "chii_ep_revisions" - __table_args__ = (Index("rev_sid", "rev_sid", "rev_creator"),) - - ep_rev_id = Column(MEDIUMINT(8), primary_key=True) - rev_sid = Column(MEDIUMINT(8), nullable=False) - rev_eids = Column(String(255), nullable=False) - rev_ep_infobox = Column(MEDIUMTEXT, nullable=False) - rev_creator = Column(MEDIUMINT(8), nullable=False) - rev_version = Column(TINYINT(1), nullable=False, server_default=text("'0'")) - rev_dateline = Column(INTEGER(10), nullable=False) - rev_edit_summary = Column(String(200), nullable=False) - - -class ChiiEpisode(Base): - __tablename__ = "chii_episodes" - __table_args__ = (Index("ep_subject_id_2", "ep_subject_id", "ep_ban", "ep_sort"),) - - ep_id = Column(MEDIUMINT(8), primary_key=True) - ep_subject_id = Column(MEDIUMINT(8), nullable=False, index=True) - ep_sort = Column(Float, nullable=False, index=True, server_default=text("'0'")) - ep_type = Column(TINYINT(1), nullable=False) - ep_disc = Column( - TINYINT(3), - nullable=False, - index=True, - server_default=text("'0'"), - comment="碟片数", - ) - ep_name = Column(String(80), nullable=False) - ep_name_cn = Column(String(80), nullable=False) - ep_rate = Column(TINYINT(3), nullable=False) - ep_duration = Column(String(80), nullable=False) - ep_airdate = Column(String(80), nullable=False) - ep_online = Column(MEDIUMTEXT, nullable=False) - ep_comment = Column(MEDIUMINT(8), nullable=False) - ep_resources = Column(MEDIUMINT(8), nullable=False) - ep_desc = Column(MEDIUMTEXT, nullable=False) - ep_dateline = Column(INTEGER(10), nullable=False) - ep_lastpost = Column(INTEGER(10), nullable=False, index=True) - ep_lock = Column(TINYINT(3), nullable=False, server_default=text("'0'")) - ep_ban = Column(TINYINT(3), nullable=False, index=True, server_default=text("'0'")) - - -class ChiiMemberfield(Base): - __tablename__ = "chii_memberfields" - - uid = Column(MEDIUMINT(8), primary_key=True, server_default=text("'0'")) - site = Column(VARCHAR(75), nullable=False, server_default=text("''")) - location = Column(VARCHAR(30), nullable=False, server_default=text("''")) - bio = Column(TEXT, nullable=False) - privacy = Column(MEDIUMTEXT, nullable=False) - blocklist = Column(MEDIUMTEXT, nullable=False) - - -class ChiiMember(Base): - __tablename__ = "chii_members" - - uid: int = Column(MEDIUMINT(8), primary_key=True) - username = Column(CHAR(15), nullable=False, unique=True, server_default=text("''")) - nickname = Column(String(30), nullable=False) - avatar: str = Column(VARCHAR(255), nullable=False) - groupid = Column(SMALLINT(6), nullable=False, server_default=text("'0'")) - regdate = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - lastvisit = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - lastactivity = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - lastpost = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - dateformat = Column(CHAR(10), nullable=False, server_default=text("''")) - timeformat = Column(TINYINT(1), nullable=False, server_default=text("'0'")) - timeoffset = Column(CHAR(4), nullable=False, server_default=text("''")) - newpm = Column(TINYINT(1), nullable=False, server_default=text("'0'")) - new_notify = Column( - SMALLINT(6), nullable=False, server_default=text("'0'"), comment="新提醒" - ) - sign = Column(VARCHAR(255), nullable=False) - - -class ChiiOauthAccessToken(Base): - __tablename__ = "chii_oauth_access_tokens" - - access_token = Column(String(40, "utf8_unicode_ci"), primary_key=True) - client_id = Column(String(80, "utf8_unicode_ci"), nullable=False) - user_id: str = Column(String(80, "utf8_unicode_ci")) - expires = Column( - TIMESTAMP, - nullable=False, - server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), - ) - scope = Column(String(4000, "utf8_unicode_ci")) - - -t_chii_person_alias = Table( - "chii_person_alias", - metadata, - Column("prsn_cat", ENUM("prsn", "crt"), nullable=False), - Column("prsn_id", MEDIUMINT(9), nullable=False, index=True), - Column("alias_name", String(255, "utf8_unicode_ci"), nullable=False), - Column("alias_type", TINYINT(4), nullable=False), - Column("alias_key", String(10, "utf8_unicode_ci"), nullable=False), - Index("prsn_cat", "prsn_cat", "prsn_id"), -) - - -class ChiiPersonCollect(Base): - __tablename__ = "chii_person_collects" - __table_args__ = ( - Index("prsn_clt_cat", "prsn_clt_cat", "prsn_clt_mid"), - {"comment": "人物收藏"}, - ) - - prsn_clt_id = Column(MEDIUMINT(8), primary_key=True) - prsn_clt_cat = Column(Enum("prsn", "crt"), nullable=False) - prsn_clt_mid = Column(MEDIUMINT(8), nullable=False, index=True) - prsn_clt_uid = Column(MEDIUMINT(8), nullable=False, index=True) - prsn_clt_dateline = Column(INTEGER(10), nullable=False) - - -class ChiiPersonCsIndex(Base): - __tablename__: ClassVar[str] = "chii_person_cs_index" - __table_args__: ClassVar[dict[str, Any]] = { - "comment": "subjects' credits/creator & staff (c&s)index" - } - - prsn_type = Column(ENUM("prsn", "crt"), primary_key=True, nullable=False) - prsn_id = Column( - MEDIUMINT(9), - primary_key=True, - nullable=False, - index=True, - ) - prsn_position: int = Column( - SMALLINT(5), - primary_key=True, - nullable=False, - index=True, - comment="监督,原案,脚本,..", - ) - subject_id = Column( - MEDIUMINT(9), - primary_key=True, - nullable=False, - index=True, - ) - subject_type_id: int = Column(TINYINT(4), nullable=False, index=True) - summary = Column(MEDIUMTEXT, nullable=False) - prsn_appear_eps = Column(MEDIUMTEXT, nullable=False, comment="可选,人物参与的章节") - - -class ChiiPersonField(Base): - __tablename__ = "chii_person_fields" - __table_args__: ClassVar[dict[str, Any]] = {"extend_existing": True} - - prsn_id = Column(INTEGER(8), primary_key=True, nullable=False, index=True) - prsn_cat = Column(ENUM("prsn", "crt"), nullable=False) - gender = Column(TINYINT(4), nullable=False) - bloodtype = Column(TINYINT(4), nullable=False) - birth_year = Column(YEAR(4), nullable=False) - birth_mon = Column(TINYINT(2), nullable=False) - birth_day = Column(TINYINT(2), nullable=False) - __mapper_args__: ClassVar[dict[str, Any]] = { - "polymorphic_on": prsn_cat, - "polymorphic_identity": "prsn", - } - - -class ChiiCharacterField(ChiiPersonField): - __mapper_args__: ClassVar[dict[str, Any]] = {"polymorphic_identity": "crt"} - - -t_chii_person_relationship = Table( - "chii_person_relationship", - metadata, - Column("prsn_type", ENUM("prsn", "crt"), nullable=False), - Column("prsn_id", MEDIUMINT(9), nullable=False), - Column("relat_prsn_type", ENUM("prsn", "crt"), nullable=False), - Column("relat_prsn_id", MEDIUMINT(9), nullable=False), - Column( - "relat_type", SMALLINT(6), nullable=False, comment="任职于,从属,聘用,嫁给," - ), - Index("relat_prsn_type", "relat_prsn_type", "relat_prsn_id"), - Index("prsn_type", "prsn_type", "prsn_id"), -) - - -class ChiiPerson(Base): - __tablename__ = "chii_persons" - __table_args__: ClassVar[dict[str, Any]] = {"comment": "(现实)人物表"} - - prsn_id = Column(MEDIUMINT(8), primary_key=True) - prsn_name = Column(String(255, "utf8_unicode_ci"), nullable=False) - prsn_type = Column( - TINYINT(4), nullable=False, index=True, comment="个人,公司,组合" - ) - prsn_infobox: str = Column(MEDIUMTEXT, nullable=False) - prsn_producer = Column(TINYINT(1), nullable=False, index=True) - prsn_mangaka = Column(TINYINT(1), nullable=False, index=True) - prsn_artist = Column(TINYINT(1), nullable=False, index=True) - prsn_seiyu = Column(TINYINT(1), nullable=False, index=True) - prsn_writer = Column( - TINYINT(4), - nullable=False, - index=True, - server_default=text("'0'"), - comment="作家", - ) - prsn_illustrator = Column( - TINYINT(4), - nullable=False, - index=True, - server_default=text("'0'"), - comment="绘师", - ) - prsn_actor = Column(TINYINT(1), nullable=False, index=True, comment="演员") - prsn_summary: str = Column(MEDIUMTEXT, nullable=False) - prsn_img = Column(String(255, "utf8_unicode_ci"), nullable=False) - prsn_img_anidb = Column(VARCHAR(255), nullable=False) - prsn_comment = Column(MEDIUMINT(9), nullable=False) - prsn_collects = Column(MEDIUMINT(8), nullable=False) - prsn_dateline = Column(INTEGER(10), nullable=False) - prsn_lastpost = Column(INTEGER(11), nullable=False) - prsn_lock = Column(TINYINT(4), nullable=False, index=True) - prsn_anidb_id = Column(MEDIUMINT(8), nullable=False) - prsn_ban = Column( - TINYINT(3), nullable=False, index=True, server_default=text("'0'") - ) - prsn_redirect = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - prsn_nsfw = Column(TINYINT(1), nullable=False) - - -class ChiiRevHistory(Base): - __tablename__ = "chii_rev_history" - __table_args__ = ( - Index("rev_crt_id", "rev_type", "rev_mid"), - Index("rev_id", "rev_id", "rev_type", "rev_creator"), - ) - - rev_id = Column(MEDIUMINT(8), primary_key=True) - rev_type = Column(TINYINT(3), nullable=False, comment="条目,角色,人物") - rev_mid = Column(MEDIUMINT(8), nullable=False, comment="对应条目,人物的ID") - rev_text_id = Column(MEDIUMINT(9), nullable=False) - rev_dateline = Column(INTEGER(10), nullable=False) - rev_creator = Column(MEDIUMINT(8), nullable=False, index=True) - rev_edit_summary = Column(String(200, "utf8_unicode_ci"), nullable=False) - - -class GzipPHPSerializedBlob(MEDIUMBLOB): - def bind_processor(self, dialect): - raise NotImplementedError("write to db is not supported now") - - @staticmethod - def load_array(d: List[Tuple[Union[int, str], Any]]): - for i, (k, v) in enumerate(d): - if isinstance(k, int): - d[i] = (str(k), v) - return dict(d) - - @staticmethod - def loads(b: bytes): - return phpseralize.loads( - zlib.decompress(b, -zlib.MAX_WBITS), - array_hook=GzipPHPSerializedBlob.load_array, - ) - - def result_processor(self, dialect, coltype): - loads = self.loads - - def process(value): - if value is None: - return None - return loads(value) - - return process - - def compare_values(self, x, y): - if self.comparator: - return self.comparator(x, y) - return x == y - - -class ChiiRevText(Base): - __tablename__ = "chii_rev_text" - - rev_text_id = Column(MEDIUMINT(9), primary_key=True) - rev_text = Column(GzipPHPSerializedBlob, nullable=False) - - -t_chii_subject_alias = Table( - "chii_subject_alias", - metadata, - Column("subject_id", INTEGER(10), nullable=False, index=True), - Column("alias_name", String(255), nullable=False), - Column( - "subject_type_id", - TINYINT(3), - nullable=False, - server_default=text("'0'"), - comment="所属条目的类型", - ), - Column( - "alias_type", - TINYINT(3), - nullable=False, - server_default=text("'0'"), - comment="是别名还是条目名", - ), - Column("alias_key", VARCHAR(10), nullable=False), -) - - -class ChiiSubjectField(Base): - __tablename__ = "chii_subject_fields" - __table_args__ = ( - Index("query_date", "field_sid", "field_date"), - Index("field_year_mon", "field_year", "field_mon"), - ) - - field_sid = Column(MEDIUMINT(8), primary_key=True) - field_tid = Column( - SMALLINT(6), nullable=False, index=True, server_default=text("'0'") - ) - field_tags = Column(MEDIUMTEXT, nullable=False) - field_rate_1 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_2 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_3 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_4 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_5 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_6 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_7 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_8 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_9 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_rate_10 = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - field_airtime = Column(TINYINT(1), nullable=False, index=True) - field_rank = Column( - INTEGER(10), nullable=False, index=True, server_default=text("'0'") - ) - field_year = Column(YEAR(4), nullable=False, index=True, comment="放送年份") - field_mon = Column(TINYINT(2), nullable=False, comment="放送月份") - field_week_day = Column(TINYINT(1), nullable=False, comment="放送日(星期X)") - # 对于默认的零值 '0000-00-00' 会被解析成字符串。 - # 非零值会被处理成 `datetime.date` - field_date = Column(Date, nullable=False, index=True, comment="放送日期") - field_redirect = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - - def rating(self): - scores = self.scores() - total = 0 - total_count = 0 - for key, value in scores.items(): - total += int(key) * value - total_count += value - if total_count != 0: - score = round(total / total_count, 1) - else: - score = 0 - - return { - "rank": self.field_rank, - "score": score, - "count": scores, - "total": total_count, - } - - def scores(self): - return { - "1": self.field_rate_1, - "2": self.field_rate_2, - "3": self.field_rate_3, - "4": self.field_rate_4, - "5": self.field_rate_5, - "6": self.field_rate_6, - "7": self.field_rate_7, - "8": self.field_rate_8, - "9": self.field_rate_9, - "10": self.field_rate_10, +@reg.mapped +@dataclass(kw_only=True) +class ChiiTimeline: + __tablename__ = "chii_timeline" + __sa_dataclass_metadata_key__ = "sa" + + id: int = field( + init=False, metadata={"sa": Column("tml_id", INTEGER(10), primary_key=True)} + ) + uid: int = field( + metadata={ + "sa": Column( + "tml_uid", + MEDIUMINT(8), + nullable=False, + index=True, + server_default=text("'0'"), + ) } - - def tags(self) -> List[dict]: - if not self.field_tags: - return [] - - # defaults to utf-8 - tags_deserialized = dict_to_list(phpseralize.loads(self.field_tags.encode())) - - return [ - {"name": tag["tag_name"], "count": tag["result"]} - for tag in tags_deserialized - if tag["tag_name"] is not None # remove tags like { "tag_name": None } - ] - - -class ChiiSubjectRelations(Base): - """ - 这个表带有 comment,也没有主键,所以生成器用的是 `Table` 而不是现在的class。 - """ - - __tablename__ = "chii_subject_relations" - __table_args__ = ( - Index( - "rlt_relation_type", - "rlt_relation_type", - "rlt_subject_id", - "rlt_related_subject_id", - ), - Index( - "rlt_subject_id", - "rlt_subject_id", - "rlt_related_subject_id", - "rlt_vice_versa", - unique=True, - ), - Index( - "rlt_related_subject_type_id", "rlt_related_subject_type_id", "rlt_order" - ), - ) - rlt_subject_id = Column( - "rlt_subject_id", - MEDIUMINT(8), - nullable=False, - comment="关联主 ID", - ) - rlt_subject_type_id = Column( - "rlt_subject_type_id", TINYINT(3), nullable=False, index=True - ) - rlt_relation_type: int = Column( - "rlt_relation_type", SMALLINT(5), nullable=False, comment="关联类型" - ) - rlt_related_subject_id = Column( - "rlt_related_subject_id", - MEDIUMINT(8), - nullable=False, - comment="关联目标 ID", - ) - rlt_related_subject_type_id: int = Column( - "rlt_related_subject_type_id", - TINYINT(3), - nullable=False, - comment="关联目标类型", - ) - rlt_vice_versa = Column("rlt_vice_versa", TINYINT(1), nullable=False) - rlt_order = Column("rlt_order", TINYINT(3), nullable=False, comment="关联排序") - - __mapper_args__: ClassVar[dict[str, Any]] = { - "primary_key": [rlt_subject_id, rlt_related_subject_id, rlt_vice_versa] - } - - -class ChiiSubjectRevision(Base): - __tablename__ = "chii_subject_revisions" - __table_args__ = ( - Index("rev_subject_id", "rev_subject_id", "rev_creator"), - Index("rev_creator", "rev_creator", "rev_id"), - ) - - rev_id = Column(MEDIUMINT(8), primary_key=True) - rev_type = Column( - TINYINT(3), - nullable=False, - index=True, - server_default=text("'1'"), - comment="修订类型", - ) - rev_subject_id = Column(MEDIUMINT(8), nullable=False) - rev_type_id = Column(SMALLINT(6), nullable=False, server_default=text("'0'")) - rev_creator = Column(MEDIUMINT(8), nullable=False) - rev_dateline = Column( - INTEGER(10), nullable=False, index=True, server_default=text("'0'") - ) - rev_name = Column(String(80), nullable=False) - rev_name_cn = Column(String(80), nullable=False) - rev_field_infobox = Column(MEDIUMTEXT, nullable=False) - rev_field_summary = Column(MEDIUMTEXT, nullable=False) - rev_vote_field = Column(MEDIUMTEXT, nullable=False) - rev_field_eps = Column(MEDIUMINT(8), nullable=False) - rev_edit_summary = Column(String(200), nullable=False) - rev_platform = Column(SMALLINT(6), nullable=False) - - -class ChiiSubject(Base): - __tablename__ = "chii_subjects" - __table_args__ = ( - Index( - "order_by_name", - "subject_ban", - "subject_type_id", - "subject_series", - "subject_platform", - "subject_name", - ), - Index( - "browser", - "subject_ban", - "subject_type_id", - "subject_series", - "subject_platform", - ), - Index("subject_idx_cn", "subject_idx_cn", "subject_type_id"), - ) - - subject_id = Column(MEDIUMINT(8), primary_key=True) - subject_type_id = Column( - SMALLINT(6), nullable=False, index=True, server_default=text("'0'") - ) - subject_name = Column(String(80), nullable=False, index=True) - subject_name_cn = Column(String(80), nullable=False, index=True) - subject_uid = Column(String(20), nullable=False, comment="isbn / imdb") - subject_creator = Column(MEDIUMINT(8), nullable=False, index=True) - subject_dateline = Column(INTEGER(10), nullable=False, server_default=text("'0'")) - subject_image = Column(String(255), nullable=False) - subject_platform = Column( - SMALLINT(6), nullable=False, index=True, server_default=text("'0'") - ) - field_infobox = Column(MEDIUMTEXT, nullable=False) - field_summary = Column(MEDIUMTEXT, nullable=False, comment="summary") - field_5 = Column(MEDIUMTEXT, nullable=False, comment="author summary") - field_volumes = Column( - MEDIUMINT(8), nullable=False, server_default=text("'0'"), comment="卷数" - ) - field_eps: int = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - subject_wish: int = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - subject_collect: int = Column( - MEDIUMINT(8), nullable=False, server_default=text("'0'") - ) - subject_doing: int = Column( - MEDIUMINT(8), nullable=False, server_default=text("'0'") - ) - subject_on_hold: int = Column( - MEDIUMINT(8), nullable=False, server_default=text("'0'"), comment="搁置人数" - ) - subject_dropped: int = Column( - MEDIUMINT(8), nullable=False, server_default=text("'0'"), comment="抛弃人数" - ) - subject_series = Column( - TINYINT(1), nullable=False, index=True, server_default=text("'0'") - ) - subject_series_entry = Column( - MEDIUMINT(8), nullable=False, index=True, server_default=text("'0'") - ) - subject_idx_cn = Column(String(1), nullable=False) - subject_airtime = Column(TINYINT(1), nullable=False, index=True) - subject_nsfw = Column(TINYINT(1), nullable=False, index=True) - subject_ban = Column( - TINYINT(1), nullable=False, index=True, server_default=text("'0'") - ) - - @property - def locked(self) -> bool: - return self.subject_ban == 2 - - @property - def ban(self) -> bool: - return self.subject_ban == 1 - - @classmethod - def with_default_value( - cls, - subject_id=1, - subject_type_id=1, - subject_platform=0, - subject_image="", - field_infobox="", - field_summary="", - subject_name="name", - subject_name_cn="name_cn", - subject_ban=0, - subject_nsfw=0, - field_volumes=0, - field_eps=0, - subject_wish=0, - subject_collect=0, - subject_doing=0, - subject_on_hold=0, - subject_dropped=0, - field_redirect=0, - field_rate_1=0, - field_rate_2=0, - field_rate_3=0, - field_rate_4=0, - field_rate_5=0, - field_rate_6=0, - field_rate_7=0, - field_rate_8=0, - field_rate_9=0, - field_rate_10=0, - field_rank=0, - ): - """a method to get a instance with all field has a default value""" - return ChiiSubject( - subject_id=subject_id, - subject_type_id=subject_type_id, - subject_platform=subject_platform, - subject_image=subject_image, - field_infobox=field_infobox, - field_summary=field_summary, - subject_name=subject_name, - subject_name_cn=subject_name_cn, - subject_ban=subject_ban, - subject_nsfw=subject_nsfw, - field_volumes=field_volumes, - field_eps=field_eps, - subject_wish=subject_wish, - subject_collect=subject_collect, - subject_doing=subject_doing, - subject_on_hold=subject_on_hold, - subject_dropped=subject_dropped, - fields=ChiiSubjectField( - field_redirect=field_redirect, - field_rate_1=field_rate_1, - field_rate_2=field_rate_2, - field_rate_3=field_rate_3, - field_rate_4=field_rate_4, - field_rate_5=field_rate_5, - field_rate_6=field_rate_6, - field_rate_7=field_rate_7, - field_rate_8=field_rate_8, - field_rate_9=field_rate_9, - field_rate_10=field_rate_10, - field_rank=field_rank, - ), - ) - - -class ChiiSubjectInterest(Base): - __tablename__ = "chii_subject_interests" - __table_args__ = ( - Index("user_collects", "interest_subject_type", "interest_uid"), - Index( - "tag_subject_id", "interest_subject_type", "interest_type", "interest_uid" - ), - Index( - "subject_lasttouch", - "interest_subject_id", - "interest_private", - "interest_lasttouch", - ), - Index( - "user_collect_type", - "interest_subject_type", - "interest_type", - "interest_uid", - "interest_private", - "interest_collect_dateline", - ), - Index( - "subject_collect", - "interest_subject_id", - "interest_type", - "interest_private", - "interest_collect_dateline", - ), - Index( - "subject_comment", - "interest_subject_id", - "interest_has_comment", - "interest_private", - "interest_lasttouch", - ), - Index("interest_id", "interest_uid", "interest_private"), - Index( - "user_collect_latest", - "interest_subject_type", - "interest_type", - "interest_uid", - "interest_private", - ), - Index( - "top_subject", - "interest_subject_id", - "interest_subject_type", - "interest_doing_dateline", - ), - Index( - "subject_rate", "interest_subject_id", "interest_rate", "interest_private" - ), - Index("interest_type_2", "interest_type", "interest_uid"), - Index( - "interest_uid_2", "interest_uid", "interest_private", "interest_lasttouch" - ), - Index("user_interest", "interest_uid", "interest_subject_id", unique=True), - Index("interest_subject_id", "interest_subject_id", "interest_type"), - ) - - id = Column("interest_id", INTEGER(10), primary_key=True) - user_id = Column("interest_uid", MEDIUMINT(8), nullable=False, index=True) - subject_id = Column("interest_subject_id", MEDIUMINT(8), nullable=False, index=True) - subject_type = Column( - "interest_subject_type", - SMALLINT(6), - nullable=False, - index=True, - server_default=text("'0'"), ) - rate = Column( - "interest_rate", - TINYINT(3), - nullable=False, - index=True, - server_default=text("'0'"), + cat: int = field( + metadata={"sa": Column("tml_cat", SMALLINT(6), nullable=False, index=True)} ) - type = Column( - "interest_type", - TINYINT(1), - nullable=False, - index=True, - server_default=text("'0'"), - ) - has_comment = Column("interest_has_comment", TINYINT(1), nullable=False, default=0) - comment = Column("interest_comment", MEDIUMTEXT, nullable=False, default="") - tag: str = Column("interest_tag", MEDIUMTEXT, nullable=False, default="") - ep_status = Column( - "interest_ep_status", - MEDIUMINT(8), - nullable=False, - server_default=text("'0'"), + type: int = field( + metadata={ + "sa": Column( + "tml_type", SMALLINT(6), nullable=False, server_default=text("'0'") + ) + } ) - vol_status = Column( - "interest_vol_status", - MEDIUMINT(8), - nullable=False, - comment="卷数", + related: str = field( + default="0", + metadata={ + "sa": Column( + "tml_related", + CHAR(255), + nullable=False, + server_default=text("'0'"), + default=0, + ) + }, + ) + memo: str = field( + default="", metadata={"sa": Column("tml_memo", MEDIUMTEXT, nullable=False)} + ) + img: str = field( + default="", + metadata={"sa": Column("tml_img", MEDIUMTEXT, nullable=False, default="")}, + ) + batch: int = field( + metadata={"sa": Column("tml_batch", TINYINT(3), nullable=False, index=True)} + ) + source: int = field( default=0, - ) - wish_dateline = Column( - "interest_wish_dateline", INTEGER(10), nullable=False, default=0 - ) - doing_dateline = Column( - "interest_doing_dateline", INTEGER(10), nullable=False, default=0 - ) - collect_dateline = Column( - "interest_collect_dateline", INTEGER(10), nullable=False, index=True, default=0 - ) - on_hold_dateline = Column( - "interest_on_hold_dateline", INTEGER(10), nullable=False, default=0 - ) - dropped_dateline = Column( - "interest_dropped_dateline", INTEGER(10), nullable=False, default=0 - ) - last_touch = Column( - "interest_lasttouch", - INTEGER(10), - nullable=False, - index=True, - server_default=text("'0'"), - ) - private = Column( - "interest_private", TINYINT(1), nullable=False, index=True, default=0 - ) - - -class ChiiIndex(Base): - __tablename__ = "chii_index" - __table_args__ = ( - Index("mid", "idx_id"), - Index("idx_ban", "idx_ban"), - Index("idx_type", "idx_type"), - Index("idx_uid", "idx_uid"), - Index("idx_collects", "idx_collects"), - ) - idx_id = Column( - MEDIUMINT(8), comment="自动id", primary_key=True, autoincrement=True - ) - idx_type = Column(TINYINT(3), nullable=False, server_default=text("'0'")) - idx_title = Column(VARCHAR(80), nullable=False, comment="标题") - idx_desc = Column(MEDIUMTEXT, nullable=False, comment="简介") - idx_replies = Column( - MEDIUMINT(8), nullable=False, server_default="'0'", comment="回复数" - ) - idx_subject_total = Column( - MEDIUMINT(8), nullable=False, server_default="'0'", comment="内含条目总数" - ) - idx_collects = Column( - MEDIUMINT(8), nullable=False, server_default="'0'", comment="收藏数" - ) - idx_stats = Column(MEDIUMTEXT, nullable=False) - idx_dateline = Column(INTEGER(10), nullable=False, comment="创建时间") - idx_lasttouch = Column(INTEGER(10), nullable=False) - idx_uid = Column(MEDIUMINT(8), nullable=False, comment="创建人UID") - idx_ban = Column(TINYINT(1), nullable=False, server_default="'0'") - - -class ChiiIndexCollects(Base): - __tablename__ = "chii_index_collects" - __table_args__ = ( - Index("idx_clt_mid", "idx_clt_mid", "idx_clt_uid"), - {"comment": "目录收藏"}, - ) - idx_clt_id = Column(MEDIUMINT(8), primary_key=True, autoincrement=True) - idx_clt_mid = Column(MEDIUMINT(8), nullable=False, comment="目录ID") - idx_clt_uid = Column(MEDIUMINT(8), nullable=False, comment="用户UID") - idx_clt_dateline = Column(INTEGER(10), nullable=False) - - -class ChiiIndexComments(Base): - __tablename__ = "chii_index_comments" - __table_args__ = ( - Index("idx_pst_mid", "idx_pst_mid"), - Index("idx_pst_related", "idx_pst_related"), - Index("idx_pst_uid", "idx_pst_uid"), - ) - idx_pst_id = Column(MEDIUMINT(8), primary_key=True, autoincrement=True) - idx_pst_mid = Column(MEDIUMINT(8), nullable=False) - idx_pst_uid = Column(MEDIUMINT(8), nullable=False) - idx_pst_related = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - idx_pst_dateline = Column(INTEGER(10), nullable=False) - idx_pst_content = Column(MEDIUMTEXT, nullable=False) - - -class ChiiIndexRelated(Base): - __tablename__ = "chii_index_related" - __table_args__ = ( - Index("idx_rlt_rid", "idx_rlt_rid", "idx_rlt_type"), - Index("idx_rlt_sid", "idx_rlt_rid", "idx_rlt_sid"), - Index("idx_rlt_sid_2", "idx_rlt_sid"), - Index("index_rlt_cat", "idx_rlt_cat"), - Index( - "idx_order", "idx_rlt_rid", "idx_rlt_cat", "idx_rlt_order", "idx_rlt_sid" - ), - {"comment": "目录关联表"}, - ) - idx_rlt_id = Column(MEDIUMINT(8), primary_key=True, autoincrement=True) - idx_rlt_cat = Column(TINYINT(3), nullable=False) - idx_rlt_rid = Column(MEDIUMINT(8), nullable=False, comment="关联目录") - idx_rlt_type = Column(SMALLINT(6), nullable=False, comment="关联条目类型") - idx_rlt_sid = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - idx_rlt_order = Column(MEDIUMINT(8), nullable=False, server_default=text("'0'")) - idx_rlt_comment = Column(MEDIUMTEXT, nullable=False) - idx_rlt_dateline = Column(INTEGER(10), nullable=False) - - -class ChiiTimeline(Base): - __tablename__ = "chii_timeline" - __table_args__ = (Index("query_tml_cat", "tml_uid", "tml_cat"),) - - if TYPE_CHECKING: - - def __init__( - self, - uid: int, - cat: int, - type: int, - related: str, - memo: str, - batch: int, - img: str = "", - source: Optional[int] = None, - replies: int = 0, - id: Optional[int] = None, - dateline: Optional[int] = None, - ): ... - - id = Column("tml_id", INTEGER(10), primary_key=True) - uid = Column( - "tml_uid", MEDIUMINT(8), nullable=False, index=True, server_default=text("'0'") - ) - cat: int = Column("tml_cat", SMALLINT(6), nullable=False, index=True) - type: int = Column( - "tml_type", SMALLINT(6), nullable=False, server_default=text("'0'") - ) - related = Column( - "tml_related", CHAR(255), nullable=False, server_default=text("'0'"), default=0 - ) - memo: str = Column("tml_memo", MEDIUMTEXT, nullable=False) - #: deprecated - img: str = Column("tml_img", MEDIUMTEXT, nullable=False, default="") - batch = Column("tml_batch", TINYINT(3), nullable=False, index=True) - source = Column( - "tml_source", - TINYINT(3), - nullable=False, - server_default=text("'0'"), - comment="更新来源", - default=5, - ) - replies = Column( - "tml_replies", MEDIUMINT(8), nullable=False, comment="回复数", default=0 - ) - dateline: int = Column( - "tml_dateline", - INTEGER(10), - nullable=False, - server_default=text("'0'"), - default=lambda: int(datetime.datetime.now().timestamp()), - ) - - -class ChiiUsergroup(Base): - __tablename__ = "chii_usergroup" - - usr_grp_id = Column(MEDIUMINT(8), primary_key=True) - usr_grp_name = Column(VARCHAR(255), nullable=False) - usr_grp_perm = Column(MEDIUMTEXT, nullable=False) - usr_grp_dateline = Column(INTEGER(10), nullable=False) + metadata={ + "sa": Column( + "tml_source", + TINYINT(3), + nullable=False, + server_default=text("'0'"), + comment="更新来源", + default=5, + ) + }, + ) + replies: int = field( + default=0, + metadata={ + "sa": Column( + "tml_replies", MEDIUMINT(8), nullable=False, comment="回复数", default=0 + ) + }, + ) + dateline: int = field( + default_factory=lambda: int(time.time()), + metadata={ + "sa": Column( + "tml_dateline", + INTEGER(10), + nullable=False, + server_default=text("'0'"), + default=lambda: int(datetime.datetime.now().timestamp()), + ) + }, + ) + + +# type helper for ChiiTimeline.uid.desc() +ChiiTimeline_column_id: Column[int] = cast(Column[int], ChiiTimeline.id) +ChiiTimeline_column_uid: Column[int] = cast(Column[int], ChiiTimeline.uid) diff --git a/chii/models/__init__.py b/chii/models/__init__.py deleted file mode 100644 index 5a12fdb..0000000 --- a/chii/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .subject import Subject -from .user import Avatar, PublicUser, User - -__all__ = ["PublicUser", "User", "Avatar", "Subject"] diff --git a/chii/models/subject.py b/chii/models/subject.py deleted file mode 100644 index c0ac8da..0000000 --- a/chii/models/subject.py +++ /dev/null @@ -1,57 +0,0 @@ -import datetime -from typing import Optional - -from pydantic import BaseModel, Field - - -class Subject(BaseModel): - id: int - type: int - name: str = "" - name_cn: str = "" - summary: str = "" - nsfw: bool - date: Optional[str] # air date in `YYYY-MM-DD` format" - platform: int # TV, Web, 欧美剧, PS4... - image: str = "" - infobox: str = "" - - redirect: int - - ban: int - - @property - def banned(self) -> bool: - """redirected/merged subject""" - return self.ban == 1 - - @property - def locked(self) -> bool: - return self.ban == 2 - - -class Ep(BaseModel): - id: int = Field(alias="ep_id") - subject_id: int = Field(alias="ep_subject_id") - sort: float = Field(alias="ep_sort") - type: int = Field(alias="ep_type") - disc: int = Field(0, alias="ep_disc") - name: str = Field(alias="ep_name") - name_cn: str = Field(alias="ep_name_cn") - duration: str = Field(alias="ep_duration") - airdate: str = Field(alias="ep_airdate") - online: str = Field(alias="ep_online") - comment: int = Field(alias="ep_comment") - desc: str = Field(alias="ep_desc") - dateline: datetime.datetime = Field(alias="ep_dateline") - lastpost: datetime.datetime = Field(alias="ep_lastpost") - lock: bool = Field(alias="ep_lock") - ban: bool = Field(alias="ep_ban") - - class Config: - orm_mode = True - - -class SubjectCharacter(BaseModel): - id: int - relation: str diff --git a/chii/models/user.py b/chii/models/user.py deleted file mode 100644 index a506d55..0000000 --- a/chii/models/user.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import datetime, timedelta - -from pydantic import BaseModel - -from chii.permission import Role, UserGroup - - -class Avatar(BaseModel): - large: str - medium: str - small: str - - @classmethod - def from_db_record(cls, s: str): - """default user user avatar https://lain.bgm.tv/pic/user/l/""" - if not s: - s = "icon.jpg" - return cls( - large="https://lain.bgm.tv/pic/user/l/" + s, - medium="https://lain.bgm.tv/pic/user/m/" + s, - small="https://lain.bgm.tv/pic/user/s/" + s, - ) - - -class PublicUser(BaseModel): - id: int - username: str - nickname: str - avatar: Avatar - - -class User(Role, BaseModel): - """private authorized user""" - - id: int - username: str - nickname: str - group_id: UserGroup - registration_date: datetime - sign: str = "" - avatar: Avatar = Avatar.from_db_record("") - - def allow_nsfw(self) -> bool: - allow_date = self.registration_date + timedelta(days=60) - return datetime.utcnow().astimezone() > allow_date - - def get_user_id(self) -> int: - return self.id diff --git a/chii/subject.py b/chii/subject.py index a9b0df3..ca52df7 100644 --- a/chii/subject.py +++ b/chii/subject.py @@ -1,6 +1,7 @@ import enum +@enum.unique class SubjectType(enum.IntEnum): """条目类型 - `1` 为 书籍 diff --git a/chii/timeline/__init__.py b/chii/timeline/__init__.py index 801418b..1053978 100644 --- a/chii/timeline/__init__.py +++ b/chii/timeline/__init__.py @@ -1,10 +1,8 @@ -from typing import Any, Dict, Optional +from typing import Dict, Optional from pydantic import BaseModel -from chii.compat import phpseralize -from chii.db.const import IntEnum -from chii.db.tables import ChiiTimeline +from chii.db.const import CollectionType, IntEnum from chii.subject import SubjectType @@ -60,42 +58,40 @@ class TimelineCat(IntEnum): Doujin = 9 -class Timeline(BaseModel): - type: int - cat: int - id: int - memo: Any - - SUBJECT_TYPE_MAP: Dict[int, Dict[int, int]] = { - SubjectType.book: {1: 1, 2: 5, 3: 9, 4: 13, 5: 14}, - SubjectType.anime: {1: 2, 2: 6, 3: 10, 4: 13, 5: 14}, - SubjectType.music: {1: 3, 2: 7, 3: 11, 4: 13, 5: 14}, - SubjectType.game: {1: 4, 2: 8, 3: 12, 4: 13, 5: 14}, - SubjectType.real: {1: 2, 2: 6, 3: 10, 4: 13, 5: 14}, + SubjectType.book: { + CollectionType.wish: 1, + CollectionType.done: 5, + CollectionType.doing: 9, + CollectionType.on_hold: 13, + CollectionType.dropped: 14, + }, + SubjectType.anime: { + CollectionType.wish: 2, + CollectionType.done: 6, + CollectionType.doing: 10, + CollectionType.on_hold: 13, + CollectionType.dropped: 14, + }, + SubjectType.music: { + CollectionType.wish: 3, + CollectionType.done: 7, + CollectionType.doing: 11, + CollectionType.on_hold: 13, + CollectionType.dropped: 14, + }, + SubjectType.game: { + CollectionType.wish: 4, + CollectionType.done: 8, + CollectionType.doing: 12, + CollectionType.on_hold: 13, + CollectionType.dropped: 14, + }, + SubjectType.real: { + CollectionType.wish: 2, + CollectionType.done: 6, + CollectionType.doing: 10, + CollectionType.on_hold: 13, + CollectionType.dropped: 14, + }, } - - -def parseMemo(cat: int, type: int, batch: bool, memo: str): - if cat == TimelineCat.Relation and type in [2, 3, 4]: - return phpseralize.loads(memo) - - if cat == TimelineCat.Say: - if type == 2: - return phpseralize.loads(memo) - return phpseralize.loads(memo) - return None - - -def parseTimeLine(tl: ChiiTimeline) -> Timeline: - memo = parseMemo(tl.cat, tl.type, bool(tl.batch), tl.memo) - - if not memo: - raise ValueError( - f"unexpected timeline cat ${tl.cat} type ${tl.type} ${tl.memo}" - ) - - if tl.cat == TimelineCat.Relation and type in [2, 3, 4]: - return Timeline(id=tl.id, cat=tl.cat, type=tl.type, memo=memo) - - raise ValueError("unknown timeline") diff --git a/etc/base.dockerfile b/etc/base.dockerfile deleted file mode 100644 index c73fc6e..0000000 --- a/etc/base.dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -### convert poetry.lock to requirements.txt ### -FROM python:3.11-slim AS poetry - -WORKDIR /app -COPY . ./ -COPY pyproject.toml poetry.lock ./ - -RUN pip install poetry &&\ - poetry export -f requirements.txt --output requirements.txt - -### final image ### -FROM python:3.11-slim - -WORKDIR /app - -ENV PYTHONPATH=/app - -COPY --from=poetry /app/requirements.txt ./requirements.txt - -RUN pip install -r requirements.txt --no-cache-dir - -WORKDIR /app - -ENTRYPOINT [ "python", "./start_grpc_server.py" ] diff --git a/etc/final.dockerfile b/etc/final.dockerfile deleted file mode 100644 index 0b4fb0c..0000000 --- a/etc/final.dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM base-image - -COPY . ./ diff --git a/poetry.lock b/poetry.lock index a0c90a4..ecfd73d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -695,77 +695,83 @@ files = [ [[package]] name = "greenlet" -version = "3.1.0" +version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8"}, - {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca"}, - {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc"}, - {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d"}, - {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25"}, - {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682"}, - {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1"}, - {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99"}, - {file = "greenlet-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54"}, - {file = "greenlet-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345"}, - {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6"}, - {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc"}, - {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6"}, - {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f"}, - {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19"}, - {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a"}, - {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b"}, - {file = "greenlet-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9"}, - {file = "greenlet-3.1.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27"}, - {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303"}, - {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf"}, - {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc"}, - {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f"}, - {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a"}, - {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665"}, - {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811"}, - {file = "greenlet-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b"}, - {file = "greenlet-3.1.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd"}, - {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca"}, - {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64"}, - {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b"}, - {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989"}, - {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17"}, - {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5"}, - {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484"}, - {file = "greenlet-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0"}, - {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0"}, - {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6"}, - {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a"}, - {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df"}, - {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637"}, - {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954"}, - {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3"}, - {file = "greenlet-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00"}, - {file = "greenlet-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6"}, - {file = "greenlet-3.1.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33"}, - {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f"}, - {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7"}, - {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09"}, - {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491"}, - {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b"}, - {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d"}, - {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0"}, - {file = "greenlet-3.1.0-cp38-cp38-win32.whl", hash = "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2"}, - {file = "greenlet-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910"}, - {file = "greenlet-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b"}, - {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7"}, - {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501"}, - {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39"}, - {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a"}, - {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28"}, - {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b"}, - {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8"}, - {file = "greenlet-3.1.0-cp39-cp39-win32.whl", hash = "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc"}, - {file = "greenlet-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97"}, - {file = "greenlet-3.1.0.tar.gz", hash = "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0"}, + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, ] [package.extras] @@ -963,24 +969,6 @@ files = [ {file = "libphpserialize-0.0.8.tar.gz", hash = "sha256:ad72e7ff47ddad6d576e7ff7b1ce26d0d4f0d2c0c5a578cb10c4181c39b34856"}, ] -[[package]] -name = "loguru" -version = "0.7.2" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = ">=3.5" -files = [ - {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, - {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] - [[package]] name = "more-itertools" version = "10.5.0" @@ -1676,21 +1664,25 @@ files = [ [[package]] name = "sqlacodegen" -version = "2.3.0" +version = "3.0.0rc5" description = "Automatic model code generator for SQLAlchemy" optional = false -python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,>=2.7" +python-versions = ">=3.8" files = [ - {file = "sqlacodegen-2.3.0-py2.py3-none-any.whl", hash = "sha256:6122465c0842b2f50c04efcf0ea5f882663a50cd205391d006f2475981269ab5"}, - {file = "sqlacodegen-2.3.0.tar.gz", hash = "sha256:f6ea38f815b330013548bb1f90158672f773d0b1fe97f61c117d291a70ab27de"}, + {file = "sqlacodegen-3.0.0rc5-py3-none-any.whl", hash = "sha256:1fd84a6fc4bff701f396016b5d639a2cdf264ff433ac7a37755909e13d2240d5"}, + {file = "sqlacodegen-3.0.0rc5.tar.gz", hash = "sha256:7dbc0e4c6e1cdc91d016192942d66eb8922bc4080a76d6e03866ba4ef8eb56f6"}, ] [package.dependencies] -inflect = ">=0.2.0" -SQLAlchemy = ">=0.9.0" +inflect = ">=4.0.0" +SQLAlchemy = ">=2.0.23" [package.extras] -test = ["mysql-connector-python", "psycopg2-binary", "pytest", "pytest-cov"] +citext = ["sqlalchemy-citext (>=1.7.0)"] +geoalchemy2 = ["geoalchemy2 (>=0.11.1)"] +pgvector = ["pgvector (>=0.2.4)"] +sqlmodel = ["sqlmodel (>=0.0.12)"] +test = ["coverage (>=7)", "mysql-connector-python", "psycopg2-binary", "pytest (>=7.4)"] [[package]] name = "sqlalchemy" @@ -1780,6 +1772,41 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "sslog" +version = "0.0.0a43" +description = "opinionated logger based on structlog" +optional = false +python-versions = "~=3.8" +files = [ + {file = "sslog-0.0.0a43-py3-none-any.whl", hash = "sha256:e529f081118af8e529508222d4d0ac1a209f9c82add7b06fa32a8e1a214ca782"}, + {file = "sslog-0.0.0a43.tar.gz", hash = "sha256:3f1849634f95dbefa9a54c6549a9fb456ff820e446f72ba197ffa7e2b7115725"}, +] + +[package.dependencies] +structlog = "24.4.0" +typing-extensions = "*" + +[package.extras] +dev = ["colorama", "mypy", "pre-commit", "rich"] + +[[package]] +name = "structlog" +version = "24.4.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, + {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, +] + +[package.extras] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + [[package]] name = "tomli" version = "2.0.1" @@ -1882,20 +1909,6 @@ files = [ [package.dependencies] anyio = ">=3.0.0,<4" -[[package]] -name = "win32-setctime" -version = "1.1.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -files = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, -] - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - [[package]] name = "yarl" version = "1.11.1" @@ -2004,4 +2017,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8017aab3a89f1b37c093c6b4825c9646dedc14826f912d23c1f81c85d29fdd3c" +content-hash = "d1688c4bfe2467dd85f606e397cb34e078023f86b5e4de0cd6520d0b4f1c16f4" diff --git a/pyproject.toml b/pyproject.toml index 92a0c6f..68c1061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,15 +3,11 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.poetry] -name = "chii" -version = "0.0.4" -description = "" -authors = [] +package-mode = false [tool.poetry.dependencies] python = "^3.10" # dependencies -loguru = "==0.7.2" SQLAlchemy = { extras = ["mypy", "asyncio"], version = "2.0.35" } grpcio = "1.66.1" grpcio-tools = "1.66.1" @@ -23,9 +19,10 @@ etcd3-py = "0.1.6" aiohttp = "3.10.5" pydantic-settings = "2.5.2" typing-extensions = '4.12.2' +sslog = "^0.0.0a43" [tool.poetry.group.dev.dependencies] -sqlacodegen = "2.3.0" +sqlacodegen = "3.0.0rc5" # tests coverage = { version = "==7.6.1", extras = ["toml"] } pytest = "==8.3.3" @@ -68,7 +65,7 @@ plugins = ['sqlalchemy.ext.mypy.plugin', 'pydantic.mypy'] [tool.black] target_version = ['py310'] -extend-exclude="api/v1" +extend-exclude = "api/v1" [tool.ruff] extend-exclude = [".venv", "api"] diff --git a/requirements-poetry.txt b/requirements-poetry.txt new file mode 100644 index 0000000..6e5e09a --- /dev/null +++ b/requirements-poetry.txt @@ -0,0 +1,2 @@ +poetry==1.8.3 +poetry-plugin-export==1.8.0 diff --git a/rpc/timeline_service.py b/rpc/timeline_service.py index 8de957c..d05e164 100644 --- a/rpc/timeline_service.py +++ b/rpc/timeline_service.py @@ -5,8 +5,8 @@ import phpserialize as php import pydantic from grpc import RpcContext -from loguru import logger from sqlalchemy.orm import Session +from sslog import logger from api.v1 import timeline_pb2_grpc from api.v1.timeline_pb2 import ( @@ -22,7 +22,7 @@ from chii.compat import phpseralize from chii.config import config from chii.db import sa -from chii.db.tables import ChiiTimeline +from chii.db.tables import ChiiTimeline, ChiiTimeline_column_id, ChiiTimeline_column_uid from chii.timeline import ( SUBJECT_TYPE_MAP, ProgressMemo, @@ -43,37 +43,39 @@ def Hello(self, request: HelloRequest, context) -> HelloResponse: print(f"{config.node_id} rpc hello {request.name}") return HelloResponse(message=f"{config.node_id}: hello {request.name}") - @logger.catch(reraise=True) def SubjectCollect( - self, request: SubjectCollectRequest, context: RpcContext + self, req: SubjectCollectRequest, context: RpcContext ) -> SubjectCollectResponse: """ cat 3 看过/读过/抛弃了... https://github.com/bangumi/dev-docs/blob/master/Timeline.md#cat_sbj_collect-条目收藏 """ - tlType = SUBJECT_TYPE_MAP[request.subject.type][request.collection] - if config.debug: - print(request) + + with logger.catch(msg="exception in SubjectCollect"): + return self.__subject_collect(req) + + def __subject_collect(self, req: SubjectCollectRequest) -> SubjectCollectResponse: + tlType = SUBJECT_TYPE_MAP[req.subject.type][req.collection] + logger.debug("request: {!r}", req) with self.SessionMaker.begin() as session: - tl: Optional[ChiiTimeline] = session.scalar( - sa.get( - ChiiTimeline, - ChiiTimeline.uid == request.user_id, - order=ChiiTimeline.id.desc(), - ) + tl: Optional[ChiiTimeline] = ( + session.query(ChiiTimeline) + .where(ChiiTimeline_column_uid == req.user_id) + .order_by(ChiiTimeline_column_id.desc()) + .limit(1) ) if tl and tl.dateline >= int(time.time() - 10 * 60): logger.info("find previous timeline, merging") if tl.cat == TimelineCat.Subject and tl.type == tlType: - self.merge_previous_timeline(session, tl, request) + self.merge_previous_timeline(session, tl, req) return SubjectCollectResponse(ok=True) logger.info( "missing previous timeline or timeline type mismatch, create a new timeline" ) - self.create_subject_collection_timeline(session, request, tlType) + self.create_subject_collection_timeline(session, req, tlType) session.commit() return SubjectCollectResponse(ok=True) @@ -141,7 +143,6 @@ def create_subject_collection_timeline( ) ) - @logger.catch(reraise=True) def EpisodeCollect( self, req: EpisodeCollectRequest, context ) -> EpisodeCollectResponse: @@ -149,6 +150,13 @@ def EpisodeCollect( cat 4 type 2 "看过 ep2 ${subject name}" """ + with logger.catch(msg="exception in EpisodeCollectRequest"): + return self.__episode_collect(req) + + def __episode_collect(self, req: EpisodeCollectRequest) -> EpisodeCollectResponse: + """ + cat 4 type 2 "看过 ep2 ${subject name}" + """ tlType = 2 memo = ProgressMemo( ep_id=req.last.id, @@ -163,13 +171,13 @@ def EpisodeCollect( print(req) with self.SessionMaker.begin() as session: - tl: Optional[ChiiTimeline] = session.scalar( - sa.get( - ChiiTimeline, - ChiiTimeline.uid == req.user_id, - order=ChiiTimeline.id.desc(), - ) + tl: Optional[ChiiTimeline] = ( + session.query(ChiiTimeline) + .where(ChiiTimeline_column_uid == req.user_id) + .order_by(ChiiTimeline_column_id.desc()) + .limit(1) ) + if tl and tl.dateline >= int(time.time() - 15 * 60): logger.info("find previous timeline, updating") if ( @@ -197,13 +205,22 @@ def EpisodeCollect( return EpisodeCollectResponse(ok=True) - @logger.catch(reraise=True) def SubjectProgress( self, req: SubjectProgressRequest, context ) -> SubjectProgressResponse: """ cat 4 type 0 """ + + with logger.catch(msg="exception in SubjectProgress"): + return self.__subject_progress(req) + + def __subject_progress( + self, req: SubjectProgressRequest + ) -> SubjectProgressResponse: + """ + cat 4 type 0 + """ tlType = 0 if config.debug: @@ -220,13 +237,13 @@ def SubjectProgress( ) with self.SessionMaker.begin() as session: - tl: Optional[ChiiTimeline] = session.scalar( - sa.get( - ChiiTimeline, - ChiiTimeline.uid == req.user_id, - order=ChiiTimeline.id.desc(), - ) + tl: Optional[ChiiTimeline] = ( + session.query(ChiiTimeline) + .where(ChiiTimeline_column_uid == req.user_id) + .order_by(ChiiTimeline_column_id.desc()) + .limit(1) ) + if tl and tl.dateline >= int(time.time() - 15 * 60): logger.info("find previous timeline, updating") if ( diff --git a/rpc/timeline_service_test.py b/rpc/timeline_service_test.py index 5fc0f0c..310bf84 100644 --- a/rpc/timeline_service_test.py +++ b/rpc/timeline_service_test.py @@ -1,4 +1,13 @@ +from typing import Optional + +from sqlalchemy import select + from api.v1.timeline_pb2 import HelloRequest +from chii.db import sa +from chii.db.const import CollectionType +from chii.db.tables import ChiiTimeline, ChiiTimeline_column_id, ChiiTimeline_column_uid +from chii.subject import SubjectType +from chii.timeline import SUBJECT_TYPE_MAP, TimelineCat from rpc.timeline_service import TimeLineService @@ -8,3 +17,28 @@ def test_Hello(): .Hello(HelloRequest(name="nn"), None) .message.endswith(": hello nn") ) + + +SessionMaker = sa.sync_session_maker() + + +def test_get() -> None: + with SessionMaker.begin() as session: + tl: Optional[ChiiTimeline] = session.scalar( + select(ChiiTimeline) + .where(ChiiTimeline_column_uid == 204) + .order_by(ChiiTimeline_column_id.desc()) + .limit(1) + ) + + assert tl + assert tl.uid == 204 + + session.add( + ChiiTimeline( + uid=1, + cat=TimelineCat.Wiki, + type=SUBJECT_TYPE_MAP[SubjectType.anime][CollectionType.wish], + batch=False, + ) + ) diff --git a/scripts/process_timeline.py b/scripts/process_timeline.py deleted file mode 100644 index 5e0bdf8..0000000 --- a/scripts/process_timeline.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Dict, List, Union - -from loguru import logger -from pydantic import parse_obj_as -from sqlalchemy.orm import Session - -from chii import timeline -from chii.compat import phpseralize -from chii.db import sa -from chii.db.tables import ChiiTimeline - -step = 100 - - -@logger.catch() -def main() -> None: - SessionMaker = sa.sync_session_maker() - with SessionMaker() as session: - max_tml_id = get_max_timeline_id(session) - last_id = 0 - - while True: - tls: List[ChiiTimeline] = session.scalars( - sa.select(ChiiTimeline) - .where(ChiiTimeline.tml_id >= last_id) - .limit(step) - .order_by(ChiiTimeline.tml_id.asc()) - ) - - for tl in tls: - last_id = tl.tml_id - timeline.parseTimeLine(tl) - # if tl.tml_memo: - - if tl.tml_img: - try: - img = phpseralize.loads(tl.img.encode()) - except Exception as e: - print("image", e) - continue - - parse_obj_as(Union[Dict[int, timeline.Image], timeline.Image], img) - - if last_id >= max_tml_id: - break - - -def get_max_timeline_id(session: Session): - return session.scalar( - sa.select(ChiiTimeline.tml_id).order_by(ChiiTimeline.tml_id.desc()).limit(1) - ) - - -if __name__ == "__main__": - print("start") - main() diff --git a/start_grpc_server.py b/start_grpc_server.py index 538c8e7..b10020e 100644 --- a/start_grpc_server.py +++ b/start_grpc_server.py @@ -9,7 +9,7 @@ import grpc from etcd3 import Lease from etcd3.utils import retry -from loguru import logger +from sslog import logger from api.v1 import timeline_pb2_grpc from chii.config import config