diff --git a/jobs/fetch_article_earning_ranking_records.py b/jobs/fetch_article_earning_ranking_records.py index a044be8..765dcb3 100644 --- a/jobs/fetch_article_earning_ranking_records.py +++ b/jobs/fetch_article_earning_ranking_records.py @@ -1,45 +1,50 @@ from datetime import date, datetime, timedelta from typing import List, Optional -from beanie import Document +from bson import ObjectId +from jkit._constraints import PositiveFloat, PositiveInt from jkit.article import Article from jkit.config import CONFIG from jkit.ranking.article_earning import ArticleEarningRanking, RecordField from jkit.user import User from prefect import flow, get_run_logger from prefect.states import Completed, State -from pydantic import BaseModel, Field, PastDate, PositiveFloat, PositiveInt -from utils.db import init_db +from utils.db import DB +from utils.document_model import ( + DOCUMENT_OBJECT_CONFIG, + FIELD_OBJECT_CONFIG, + Documemt, + Field, +) from utils.job_model import Job +COLLECTION = DB.article_earning_ranking_records + -class ArticleField(BaseModel): +class ArticleField(Field, **FIELD_OBJECT_CONFIG): title: Optional[str] slug: Optional[str] -class AuthorField(BaseModel): +class AuthorField(Field, **FIELD_OBJECT_CONFIG): id: Optional[PositiveInt] slug: Optional[str] name: Optional[str] -class EarningField(BaseModel): - to_author: PositiveFloat = Field(serialization_alias="toAuthor") - to_voter: PositiveFloat = Field(serialization_alias="toVoter") +class EarningField(Field, **FIELD_OBJECT_CONFIG): + to_author: PositiveFloat + to_voter: PositiveFloat -class ArticleEarningRankingRecordModel(Document): - date: PastDate +class ArticleEarningRankingRecordDocument(Documemt, **DOCUMENT_OBJECT_CONFIG): + date: date ranking: PositiveInt article: ArticleField author: AuthorField earning: EarningField - class Settings: - name = "article_earning_ranking_records" - async def get_article_author(article_slug: str, /) -> User: article = Article.from_slug(article_slug)._as_checked() @@ -49,7 +54,7 @@ async def get_article_author(article_slug: str, /) -> User: async def process_item( item: RecordField, /, *, target_date: date -) -> ArticleEarningRankingRecordModel: +) -> ArticleEarningRankingRecordDocument: logger = get_run_logger() if item.slug: @@ -64,7 +69,8 @@ async def process_item( author_id = None author_slug = None - return ArticleEarningRankingRecordModel( + return ArticleEarningRankingRecordDocument( + _id=ObjectId(), date=target_date, ranking=item.ranking, article=ArticleField( @@ -85,19 +91,14 @@ async def process_item( @flow async def main() -> State: - logger = get_run_logger() - - await init_db([ArticleEarningRankingRecordModel]) - logger.info("初始化 ODM 模型成功") - target_date = datetime.now().date() - timedelta(days=1) - data: List[ArticleEarningRankingRecordModel] = [] + data: List[ArticleEarningRankingRecordDocument] = [] async for item in ArticleEarningRanking(target_date): processed_item = await process_item(item, target_date=target_date) data.append(processed_item) - await ArticleEarningRankingRecordModel.insert_many(data) + await COLLECTION.insert_many(x.to_dict() for x in data) return Completed(message=f"target_date={target_date}, data_count={len(data)}") diff --git a/jobs/fetch_assets_ranking_records.py b/jobs/fetch_assets_ranking_records.py index 76161ff..6f54058 100644 --- a/jobs/fetch_assets_ranking_records.py +++ b/jobs/fetch_assets_ranking_records.py @@ -1,25 +1,33 @@ from datetime import date, datetime from typing import List, Optional -from beanie import Document +from bson import ObjectId +from jkit._constraints import NonNegativeFloat, PositiveFloat, PositiveInt from jkit.config import CONFIG from jkit.exceptions import ResourceUnavailableError from jkit.ranking.assets import AssetsRanking, AssetsRankingRecord from prefect import flow, get_run_logger from prefect.states import Completed, State -from pydantic import BaseModel, NonNegativeFloat, PositiveFloat, PositiveInt -from utils.db import init_db +from utils.db import DB +from utils.document_model import ( + DOCUMENT_OBJECT_CONFIG, + FIELD_OBJECT_CONFIG, + Documemt, + Field, +) from utils.job_model import Job +COLLECTION = DB.assets_ranking_records + -class UserInfoField(BaseModel): +class UserInfoField(Field, **FIELD_OBJECT_CONFIG): id: Optional[PositiveInt] slug: Optional[str] name: Optional[str] -class AssetsRankingRecordModel(Document): +class AssetsRankingRecordDocument(Documemt, **DOCUMENT_OBJECT_CONFIG): date: date ranking: PositiveInt @@ -29,13 +37,10 @@ class AssetsRankingRecordModel(Document): user_info: UserInfoField - class Settings: - name = "assets_ranking_records" - async def process_item( item: AssetsRankingRecord, /, *, target_date: date -) -> AssetsRankingRecordModel: +) -> AssetsRankingRecordDocument: logger = get_run_logger() if item.user_info.slug: @@ -61,7 +66,8 @@ async def process_item( fp_amount = None ftn_amount = None - return AssetsRankingRecordModel( + return AssetsRankingRecordDocument( + _id=ObjectId(), date=target_date, ranking=item.ranking, fp_amount=fp_amount, @@ -77,14 +83,9 @@ async def process_item( @flow async def main() -> State: - logger = get_run_logger() - - await init_db([AssetsRankingRecordModel]) - logger.info("初始化 ODM 模型成功") - target_date = datetime.now().date() - data: List[AssetsRankingRecordModel] = [] + data: List[AssetsRankingRecordDocument] = [] async for item in AssetsRanking(): processed_item = await process_item(item, target_date=target_date) data.append(processed_item) @@ -92,7 +93,7 @@ async def main() -> State: if len(data) == 1000: break - await AssetsRankingRecordModel.insert_many(data) + await COLLECTION.insert_many(x.to_dict() for x in data) return Completed(message=f"target_date={target_date}, data_count={len(data)}") diff --git a/jobs/fetch_daily_update_ranking_records.py b/jobs/fetch_daily_update_ranking_records.py index 895ec6a..8171791 100644 --- a/jobs/fetch_daily_update_ranking_records.py +++ b/jobs/fetch_daily_update_ranking_records.py @@ -1,35 +1,41 @@ from datetime import date, datetime from typing import List -from beanie import Document +from bson import ObjectId +from jkit._constraints import PositiveInt from jkit.ranking.daily_update import DailyUpdateRanking, DailyUpdateRankingRecord -from prefect import flow, get_run_logger +from prefect import flow from prefect.states import Completed, State -from pydantic import BaseModel, PositiveInt -from utils.db import init_db +from utils.db import DB +from utils.document_model import ( + DOCUMENT_OBJECT_CONFIG, + FIELD_OBJECT_CONFIG, + Documemt, + Field, +) from utils.job_model import Job +COLLECTION = DB.daily_update_ranking_records + -class UserInfoField(BaseModel): +class UserInfoField(Field, **FIELD_OBJECT_CONFIG): slug: str name: str -class DailyUpdateRankingRecordModel(Document): +class DailyUpdateRankingRecordDocument(Documemt, **DOCUMENT_OBJECT_CONFIG): date: date ranking: PositiveInt days: PositiveInt user_info: UserInfoField - class Settings: - name = "daily_update_ranking_records" - def process_item( item: DailyUpdateRankingRecord, /, *, current_date: date -) -> DailyUpdateRankingRecordModel: - return DailyUpdateRankingRecordModel( +) -> DailyUpdateRankingRecordDocument: + return DailyUpdateRankingRecordDocument( + _id=ObjectId(), date=current_date, ranking=item.ranking, days=item.days, @@ -42,19 +48,14 @@ def process_item( @flow async def main() -> State: - logger = get_run_logger() - current_date = datetime.now().date() - await init_db([DailyUpdateRankingRecordModel]) - logger.info("初始化 ODM 模型成功") - - data: List[DailyUpdateRankingRecordModel] = [] + data: List[DailyUpdateRankingRecordDocument] = [] async for item in DailyUpdateRanking(): processed_item = process_item(item, current_date=current_date) data.append(processed_item) - await DailyUpdateRankingRecordModel.insert_many(data) + await COLLECTION.insert_many(x.to_dict() for x in data) return Completed(message=f"data_count={len(data)}") diff --git a/jobs/fetch_jianshu_lottery_win_records.py b/jobs/fetch_jianshu_lottery_win_records.py index 86d9578..b52a085 100644 --- a/jobs/fetch_jianshu_lottery_win_records.py +++ b/jobs/fetch_jianshu_lottery_win_records.py @@ -1,45 +1,51 @@ +from datetime import datetime from typing import List -from beanie import Document +from jkit._constraints import PositiveInt from jkit.jianshu_lottery import JianshuLottery, JianshuLotteryWinRecord from prefect import flow, get_run_logger from prefect.states import Completed, State -from pydantic import BaseModel, PastDatetime, PositiveInt - -from utils.db import init_db +from pymongo import DESCENDING + +from utils.db import DB +from utils.document_model import ( + DOCUMENT_OBJECT_CONFIG, + FIELD_OBJECT_CONFIG, + Documemt, + Field, +) from utils.job_model import Job +COLLECTION = DB.jianshu_lottery_win_records + -class UserInfoField(BaseModel): +class UserInfoField(Field, **FIELD_OBJECT_CONFIG): id: PositiveInt slug: str name: str -class JianshuLotteryWinRecordModel(Document): - record_id: PositiveInt - time: PastDatetime +class JianshuLotteryWinRecordDocument(Documemt, **DOCUMENT_OBJECT_CONFIG): + _id: PositiveInt + time: datetime award_name: str user_info: UserInfoField - class Settings: - name = "jianshu_lottery_win_records" - indexes = ("record_id",) - async def get_latest_stored_record_id() -> int: - latest_data = ( - await JianshuLotteryWinRecordModel.find().sort("-record_id").first_or_none() - ) - if not latest_data: + try: + latest_data = JianshuLotteryWinRecordDocument.from_dict( + await COLLECTION.find().sort("_id", DESCENDING).__anext__() + ) + except StopAsyncIteration: return 0 - return latest_data.record_id + return latest_data._id -def process_item(item: JianshuLotteryWinRecord, /) -> JianshuLotteryWinRecordModel: - return JianshuLotteryWinRecordModel( - record_id=item.id, +def process_item(item: JianshuLotteryWinRecord, /) -> JianshuLotteryWinRecordDocument: + return JianshuLotteryWinRecordDocument( + _id=item.id, time=item.time, award_name=item.award_name, user_info=UserInfoField( @@ -54,15 +60,12 @@ def process_item(item: JianshuLotteryWinRecord, /) -> JianshuLotteryWinRecordMod async def main() -> State: logger = get_run_logger() - await init_db([JianshuLotteryWinRecordModel]) - logger.info("初始化 ODM 模型成功") - stop_id = await get_latest_stored_record_id() logger.info(f"获取到最新的已存储 ID:{stop_id}") if stop_id == 0: logger.warning("数据库中没有记录") - data: List[JianshuLotteryWinRecordModel] = [] + data: List[JianshuLotteryWinRecordDocument] = [] async for item in JianshuLottery().iter_win_records(): if item.id == stop_id: break @@ -73,7 +76,7 @@ async def main() -> State: logger.warning("采集数据量达到上限") if data: - await JianshuLotteryWinRecordModel.insert_many(data) + await COLLECTION.insert_many(x.to_dict() for x in data) else: logger.info("无数据,不执行保存操作") diff --git a/jobs/fetch_jpep_ftn_trade_orders.py b/jobs/fetch_jpep_ftn_trade_orders.py index e8fd7e2..26f585c 100644 --- a/jobs/fetch_jpep_ftn_trade_orders.py +++ b/jobs/fetch_jpep_ftn_trade_orders.py @@ -1,23 +1,29 @@ from datetime import datetime -from typing import List, Optional +from typing import List, Literal, Optional -from beanie import Document -from jkit.jpep.ftn_macket import FTNMacket, FTNMacketOrderRecord -from prefect import flow, get_run_logger -from prefect.states import Completed, State -from pydantic import ( - BaseModel, +from bson import ObjectId +from jkit._constraints import ( NonNegativeInt, - PastDatetime, PositiveFloat, PositiveInt, ) +from jkit.jpep.ftn_macket import FTNMacket, FTNMacketOrderRecord +from prefect import flow +from prefect.states import Completed, State -from utils.db import init_db +from utils.db import DB +from utils.document_model import ( + DOCUMENT_OBJECT_CONFIG, + FIELD_OBJECT_CONFIG, + Documemt, + Field, +) from utils.job_model import Job +COLLECTION = DB.jpep_ftn_trade_orders -class PublisherInfoField(BaseModel): + +class PublisherInfoField(Field, **FIELD_OBJECT_CONFIG): is_anonymous: bool id: Optional[PositiveInt] name: Optional[str] @@ -25,9 +31,10 @@ class PublisherInfoField(BaseModel): credit: Optional[NonNegativeInt] -class JPEPFTNTradeOrder(Document): +class JPEPFTNTradeOrderDocument(Documemt, **DOCUMENT_OBJECT_CONFIG): fetch_time: datetime order_id: PositiveInt + type: Literal["buy", "sell"] price: PositiveFloat total_amount: PositiveInt @@ -36,13 +43,10 @@ class JPEPFTNTradeOrder(Document): minimum_trade_amount: PositiveInt traded_count: NonNegativeInt - publish_time: PastDatetime + publish_time: datetime publisher_info: PublisherInfoField - class Settings: - name = "jpep_ftn_trade_orders" - def get_fetch_time() -> datetime: current_dt = datetime.now() @@ -52,9 +56,15 @@ def get_fetch_time() -> datetime: def process_item( - item: FTNMacketOrderRecord, /, *, fetch_time: datetime -) -> JPEPFTNTradeOrder: - return JPEPFTNTradeOrder( + item: FTNMacketOrderRecord, + /, + *, + fetch_time: datetime, + type: Literal["buy", "sell"], # noqa: A002 +) -> JPEPFTNTradeOrderDocument: + return JPEPFTNTradeOrderDocument( + _id=ObjectId(), + type=type, fetch_time=fetch_time, order_id=item.id, price=item.price, @@ -76,26 +86,21 @@ def process_item( @flow async def main() -> State: - logger = get_run_logger() - fetch_time = get_fetch_time() - await init_db([JPEPFTNTradeOrder]) - logger.info("初始化 ODM 模型成功") - - buy_data: List[JPEPFTNTradeOrder] = [] + buy_data: List[JPEPFTNTradeOrderDocument] = [] async for item in FTNMacket().iter_orders(type="buy"): - processed_item = process_item(item, fetch_time=fetch_time) + processed_item = process_item(item, fetch_time=fetch_time, type="buy") buy_data.append(processed_item) - await JPEPFTNTradeOrder.insert_many(buy_data) + await COLLECTION.insert_many(x.to_dict() for x in buy_data) - sell_data: List[JPEPFTNTradeOrder] = [] + sell_data: List[JPEPFTNTradeOrderDocument] = [] async for item in FTNMacket().iter_orders(type="sell"): - processed_item = process_item(item, fetch_time=fetch_time) + processed_item = process_item(item, fetch_time=fetch_time, type="sell") sell_data.append(processed_item) - await JPEPFTNTradeOrder.insert_many(sell_data) + await COLLECTION.insert_many(x.to_dict() for x in sell_data) return Completed( message=f"fetch_time={fetch_time}, buy_data_count={len(buy_data)}, " diff --git a/poetry.lock b/poetry.lock index f2d26fe..d2ba0d2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -222,30 +222,6 @@ files = [ [package.extras] tzdata = ["tzdata"] -[[package]] -name = "beanie" -version = "1.25.0" -description = "Asynchronous Python ODM for MongoDB" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "beanie-1.25.0-py3-none-any.whl", hash = "sha256:4436ac740718ccd62b21576778679ac972359fce2938557890c576adbbf5e244"}, - {file = "beanie-1.25.0.tar.gz", hash = "sha256:f153866b9ba015274102e10a397602d088fc039b705bd806cb447c898cd2979b"}, -] - -[package.dependencies] -click = ">=7" -lazy-model = "0.2.0" -motor = ">=2.5.0,<4.0.0" -pydantic = ">=1.10,<3.0" -toml = "*" -typing-extensions = {version = ">=4.7", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Markdown (>=3.3)", "Pygments (>=2.8.0)", "jinja2 (>=3.0.3)", "mkdocs (>=1.4)", "mkdocs-material (>=9.0)", "pydoc-markdown (>=4.8)"] -queue = ["beanie-batteries-queue (>=0.2)"] -test = ["asgi-lifespan (>=1.0.1)", "dnspython (>=2.1.0)", "fastapi (>=0.100)", "flake8 (>=3)", "httpx (>=0.23.0)", "pre-commit (>=2.3.0)", "pydantic-extra-types (>=2)", "pydantic-settings (>=2)", "pydantic[email]", "pyright (>=0)", "pytest (>=6.0.0)", "pytest-asyncio (>=0.21.0)", "pytest-cov (>=2.8.1)"] - [[package]] name = "cachetools" version = "5.3.2" @@ -1074,20 +1050,6 @@ websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" [package.extras] adal = ["adal (>=1.0.2)"] -[[package]] -name = "lazy-model" -version = "0.2.0" -description = "" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "lazy-model-0.2.0.tar.gz", hash = "sha256:57c0e91e171530c4fca7aebc3ac05a163a85cddd941bf7527cc46c0ddafca47c"}, - {file = "lazy_model-0.2.0-py3-none-any.whl", hash = "sha256:5a3241775c253e36d9069d236be8378288a93d4fc53805211fd152e04cc9c342"}, -] - -[package.dependencies] -pydantic = ">=1.9.0" - [[package]] name = "lxml" version = "5.1.0" @@ -3024,4 +2986,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "a124842eabf8eeae56034567022f0ded37efc2f77172f91185aa9c54aab6e07d" +content-hash = "da6597a11cb3d8f6e587ec8aa35d1e9b0827496a9ffba20b352c03cf434e2b46" diff --git a/pyproject.toml b/pyproject.toml index 92612ed..3d5ebc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ readme = "README.md" python = "^3.8" prefect = "^2.16.0" jkit = "^3.0.0a13" -beanie = "^1.25.0" sspeedup = "^0.25.1" [tool.poetry.group.dev.dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt index a75612f..fb10d23 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,6 @@ async-timeout==4.0.3 ; python_version >= "3.8" and python_version < "3.12.0" asyncpg==0.29.0 ; python_version >= "3.8" and python_version < "4.0" attrs==23.2.0 ; python_version >= "3.8" and python_version < "4.0" backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" -beanie==1.25.0 ; python_version >= "3.8" and python_version < "4.0" cachetools==5.3.2 ; python_version >= "3.8" and python_version < "4.0" certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4.0" cffi==1.16.0 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" @@ -46,7 +45,6 @@ jsonpointer==2.4 ; python_version >= "3.8" and python_version < "4.0" jsonschema-specifications==2023.12.1 ; python_version >= "3.8" and python_version < "4.0" jsonschema==4.21.1 ; python_version >= "3.8" and python_version < "4.0" kubernetes==29.0.0 ; python_version >= "3.8" and python_version < "4.0" -lazy-model==0.2.0 ; python_version >= "3.8" and python_version < "4.0" lxml==5.1.0 ; python_version >= "3.8" and python_version < "4.0" mako==1.3.2 ; python_version >= "3.8" and python_version < "4.0" markdown-it-py==3.0.0 ; python_version >= "3.8" and python_version < "4.0" @@ -68,7 +66,6 @@ pyasn1-modules==0.3.0 ; python_version >= "3.8" and python_version < "4.0" pyasn1==0.5.1 ; python_version >= "3.8" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" pydantic-core==2.16.3 ; python_version >= "3.8" and python_version < "4.0" -pydantic==2.6.2 ; python_version >= "3.8" and python_version < "4.0" pydantic[email]==2.6.2 ; python_version >= "3.8" and python_version < "4.0" pygments==2.17.2 ; python_version >= "3.8" and python_version < "4.0" pymongo==4.6.2 ; python_version >= "3.8" and python_version < "4.0" diff --git a/requirements.txt b/requirements.txt index a5db0b0..8fb8a8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,6 @@ async-timeout==4.0.3 ; python_version >= "3.8" and python_version < "3.12.0" asyncpg==0.29.0 ; python_version >= "3.8" and python_version < "4.0" attrs==23.2.0 ; python_version >= "3.8" and python_version < "4.0" backports-zoneinfo==0.2.1 ; python_version >= "3.8" and python_version < "3.9" -beanie==1.25.0 ; python_version >= "3.8" and python_version < "4.0" cachetools==5.3.2 ; python_version >= "3.8" and python_version < "4.0" certifi==2024.2.2 ; python_version >= "3.8" and python_version < "4.0" cffi==1.16.0 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" @@ -46,7 +45,6 @@ jsonpointer==2.4 ; python_version >= "3.8" and python_version < "4.0" jsonschema-specifications==2023.12.1 ; python_version >= "3.8" and python_version < "4.0" jsonschema==4.21.1 ; python_version >= "3.8" and python_version < "4.0" kubernetes==29.0.0 ; python_version >= "3.8" and python_version < "4.0" -lazy-model==0.2.0 ; python_version >= "3.8" and python_version < "4.0" lxml==5.1.0 ; python_version >= "3.8" and python_version < "4.0" mako==1.3.2 ; python_version >= "3.8" and python_version < "4.0" markdown-it-py==3.0.0 ; python_version >= "3.8" and python_version < "4.0" @@ -67,7 +65,6 @@ pyasn1-modules==0.3.0 ; python_version >= "3.8" and python_version < "4.0" pyasn1==0.5.1 ; python_version >= "3.8" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.8" and python_version < "4.0" and platform_python_implementation != "PyPy" pydantic-core==2.16.3 ; python_version >= "3.8" and python_version < "4.0" -pydantic==2.6.2 ; python_version >= "3.8" and python_version < "4.0" pydantic[email]==2.6.2 ; python_version >= "3.8" and python_version < "4.0" pygments==2.17.2 ; python_version >= "3.8" and python_version < "4.0" pymongo==4.6.2 ; python_version >= "3.8" and python_version < "4.0" diff --git a/utils/db.py b/utils/db.py index 2d85c23..2a3e112 100644 --- a/utils/db.py +++ b/utils/db.py @@ -1,13 +1,6 @@ -from typing import List, Optional, Type - -from beanie import Document, init_beanie from motor.motor_asyncio import AsyncIOMotorClient from utils.config import CONFIG _CLIENT = AsyncIOMotorClient(CONFIG.mongodb.host, CONFIG.mongodb.port) -_DB = _CLIENT[CONFIG.mongodb.database] - - -async def init_db(models: Optional[List[Type[Document]]]) -> None: - await init_beanie(database=_DB, document_models=models) # type: ignore +DB = _CLIENT[CONFIG.mongodb.database] diff --git a/utils/document_model.py b/utils/document_model.py new file mode 100644 index 0000000..96ca77a --- /dev/null +++ b/utils/document_model.py @@ -0,0 +1,30 @@ +from typing import Any, Dict + +from bson import ObjectId +from msgspec import Struct, convert, to_builtins +from typing_extensions import Self + +FIELD_OBJECT_CONFIG = { + "kw_only": True, + "rename": "camel", +} + +DOCUMENT_OBJECT_CONFIG = { + "kw_only": True, + "rename": "camel", +} + + +class Field(Struct, **FIELD_OBJECT_CONFIG): + pass + + +class Documemt(Struct, **DOCUMENT_OBJECT_CONFIG): + _id: ObjectId + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> Self: + return convert(data, type=cls) + + def to_dict(self) -> Dict[str, Any]: + return to_builtins(self, builtin_types=(ObjectId,))