From b6f4335f5b977d7e42d6d6fc04df1389312039bb Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Mon, 2 Sep 2024 18:33:26 +0300 Subject: [PATCH 01/26] moving core methods to models --- tests/test_core.py | 188 +++------------------------- yepcord/rest_api/routes/auth.py | 8 +- yepcord/rest_api/routes/guilds.py | 2 +- yepcord/rest_api/routes/users.py | 5 +- yepcord/rest_api/routes/users_me.py | 46 +++---- yepcord/yepcord/core.py | 68 +--------- yepcord/yepcord/models/session.py | 13 +- yepcord/yepcord/models/user.py | 84 +++++++++++++ 8 files changed, 141 insertions(+), 273 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 2a6a820..527d1c3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -109,20 +109,6 @@ async def test_getUser_fail(): assert user is None, f"User with id {VARS['user_id'] + 1} doesn't exists: must return None!" -@pt.mark.asyncio -async def test_createSession_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - session = await testCore.createSession(VARS["user_id"]) - assert session is not None - - -@pt.mark.asyncio -async def test_createSession_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - session = await testCore.createSession(Snowflake.makeId()) - assert session is None - - def changeUserData(data: UserData) -> None: data.birth = "2001-01-01" if data.birth == "2000-01-01" else "2000-01-01" data.username = f"{EMAIL_ID}_Changed username {randint(0, 1000)}" @@ -138,110 +124,24 @@ def changeUserData(data: UserData) -> None: data.banner_color = randint(1, 10000) -@pt.mark.asyncio -async def test_getUserProfile_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - _user = await User.get(id=VARS["user_id"]) - user = await testCore.getUserProfile(VARS["user_id"], _user) - assert user is not None and user == _user - - -@pt.mark.asyncio -async def test_getUserProfile_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - with pt.raises(InvalidDataErr): - await testCore.getUserProfile(VARS["user_id"] + 1, await User.get(id=VARS["user_id"])) - - -@pt.mark.asyncio -async def test_checkUserPassword_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - assert await testCore.checkUserPassword(user, "test_passw0rd") - - -@pt.mark.asyncio -async def test_checkUserPassword_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - assert not await testCore.checkUserPassword(user, "wrong_password") - - -@pt.mark.asyncio -async def test_changeUserDiscriminator_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - assert await testCore.changeUserDiscriminator(user, randint(1, 9999)) - - -@pt.mark.asyncio +@pt.mark.asyncio # May be removed, but not removing now because other tests depend on VARS["user_id"] + 100000 async def test_changeUserDiscriminator_fail(testCore: Coroutine[Any, Any, Core]): testCore = await testCore - # Register 2 users with same nickname - session1 = await testCore.register(VARS["user_id"] + 100000, "Test", f"{EMAIL_ID}_test1@yepcord.ml", "password", - "2000-01-01") - session2 = await testCore.register(VARS["user_id"] + 200000, "Test", f"{EMAIL_ID}_test2@yepcord.ml", "password", - "2000-01-01") - user1 = await User.y.get(session1.user.id) - user2 = await User.y.get(session2.user.id) - userdata2 = await user2.userdata - - # Try to change first user discriminator to second user discriminator - assert not await testCore.changeUserDiscriminator(user1, userdata2.discriminator, True) - with pt.raises(InvalidDataErr): - await testCore.changeUserDiscriminator(user1, userdata2.discriminator, False) + await testCore.register(VARS["user_id"] + 100000, "Test", f"{EMAIL_ID}_test1@yepcord.ml", "password", + "2000-01-01") + await testCore.register(VARS["user_id"] + 200000, "Test", f"{EMAIL_ID}_test2@yepcord.ml", "password", + "2000-01-01") -@pt.mark.asyncio -async def test_changeUserName_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - await testCore.changeUserName(user, f"{EMAIL_ID}_UserName") - userdata = await user.data - assert userdata.username == f"{EMAIL_ID}_UserName" - - -@pt.mark.asyncio -async def test_changeUserName_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - - # Insert in userdata table 9999 users to trigger USERNAME_TOO_MANY_USERS error +@pt.mark.asyncio # May be removed, but not removing now because other tests depend on VARS["user_id"] + 100000 + d +async def test_changeUserName_fail(): username = f"NoDiscriminatorsLeft_{Snowflake.makeId()}" - users = [] - userdatas = [] - for d in range(1, 10000): - _id = VARS["user_id"] + 100000 + d - users.append((_id, f"test_user_{_id}@test.yepcord.ml")) - userdatas.append((_id, _id, date(2000, 1, 1), username, d)) - - conn = connections.get("default") - if conn.capabilities.dialect == "mysql": - await conn.execute_many("INSERT INTO `user`(`id`, `email`, `password`) VALUES (%s, %s, \"123456\")", users) - await conn.execute_many( - "INSERT INTO `userdata`(`id`, `user_id`, `birth`, `username`, `discriminator`, `flags`, `public_flags`) " - "VALUES (%s, %s, %s, %s, %s, 0, 0)", - userdatas - ) - elif conn.capabilities.dialect == "sqlite": - await conn.execute_many("INSERT INTO `user`(`id`, `email`, `password`) VALUES (?, ?, \"123456\")", users) - await conn.execute_many( - "INSERT INTO `userdata`(`id`, `user_id`, `birth`, `username`, `discriminator`, `flags`, `public_flags`) " - "VALUES (?, ?, ?, ?, ?, 0, 0)", - userdatas - ) - elif conn.capabilities.dialect == "postgres": - await conn.execute_many("INSERT INTO \"user\" (id, email, password) VALUES ($1, $2, '123456')", users) - await conn.execute_many( - "INSERT INTO userdata(id, user_id, birth, username, discriminator, flags, public_flags) " - "VALUES ($1, $2, $3, $4, $5, 0, 0)", - userdatas - ) - - user = await User.y.get(VARS["user_id"]) - with pt.raises(InvalidDataErr): - await testCore.changeUserName(user, username) + for d in range(1, 10): + user_id = VARS["user_id"] + 100000 + d + user = await User.create(id=user_id, email=f"test_user_{user_id}@test.yepcord.ml", password="123456") + await UserData.create(id=user_id, user=user, birth=date(2000, 1, 1), username=username, discriminator=d) @pt.mark.asyncio @@ -342,18 +242,9 @@ async def test_delRelationship_success(): @pt.mark.asyncio -async def test_changeUserPassword_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_logoutUser_success(): user = await User.y.get(VARS["user_id"]) - await testCore.changeUserPassword(user, "test_password123") - user = await User.y.get(VARS["user_id"]) - assert await testCore.checkUserPassword(user, "test_password123") - - -@pt.mark.asyncio -async def test_logoutUser_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - session = await testCore.createSession(VARS["user_id"]) + session = await Session.Y.create(user) assert await Session.get_or_none(id=session.id) is not None await session.delete() assert await Session.get_or_none(id=session.id) is None @@ -380,38 +271,6 @@ async def test_getMfa(): await settings.save(update_fields=["mfa"]) -@pt.mark.asyncio -async def test_setBackupCodes_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.get(id=VARS["user_id"]) - codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] - await testCore.setBackupCodes(user, codes) - db_codes = [code.code for code in await testCore.getBackupCodes(user)] - assert len(db_codes) == 10 - assert set(codes) == set(db_codes) - - -@pt.mark.asyncio -async def test_clearBackupCodes_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.get(id=VARS["user_id"]) - await testCore.clearBackupCodes(user) - codes = await testCore.getBackupCodes(user) - assert len(codes) == 0 - - -@pt.mark.asyncio -async def test_getBackupCodes_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.get(id=VARS["user_id"]) - assert len(await testCore.getBackupCodes(user)) == 0 - codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] - await testCore.setBackupCodes(user, codes) - assert len(db_codes := await testCore.getBackupCodes(user)) == 10 - db_codes = [code.code for code in db_codes] - assert set(codes) == set(db_codes) - - @pt.mark.asyncio async def test_getMfaFromTicket_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore @@ -421,7 +280,7 @@ async def test_getMfaFromTicket_success(testCore: Coroutine[Any, Any, Core]): await settings.save(update_fields=["mfa"]) try: - await testCore.login(user.email, "test_password123") + await testCore.login(user.email, "test_passw0rd") assert False except MfaRequiredErr as e: ticket = b64encode(dumps([e.uid, "login"])) + f".{e.sid}.{e.sig}" @@ -454,25 +313,6 @@ async def test_verifyUserMfaNonce(): del VARS["mfa_nonce"] -@pt.mark.asyncio -async def test_useMfaCode_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - codes = list(await testCore.getBackupCodes(user)) - assert await testCore.useMfaCode(user, codes[0].code) - codes[0].used = True - db_codes = await testCore.getBackupCodes(user) - assert len(db_codes) == 10 - assert set(codes) == set(db_codes) - - -@pt.mark.asyncio -async def test_useMfaCode_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - user = await User.y.get(VARS["user_id"]) - assert not await testCore.useMfaCode(user, "invalid_code") - - @pt.mark.asyncio async def test_getDMChannelOrCreate_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index 51df7ee..8936b21 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -68,9 +68,9 @@ async def login_with_mfa(data: MfaLogin): code = code.replace("-", "").replace(" ", "") user = await User.y.get(mfa.uid) if code not in mfa.getCodes(): - if not (len(code) == 8 and await getCore().useMfaCode(user, code)): + if not (len(code) == 8 and await user.use_backup_code(code)): raise InvalidDataErr(400, Errors.make(60008)) - sess = await getCore().createSession(user.id) + sess = await Session.Y.create(user) sett = await user.settings return { "token": sess.token, @@ -89,7 +89,7 @@ async def logout(session: Session = DepSession): async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User = DepUser): if not (password := data.password): raise InvalidDataErr(400, Errors.make(50018)) - if not await getCore().checkUserPassword(user, password): + if not user.check_password(password): raise InvalidDataErr(400, Errors.make(50018)) nonce = await getCore().generateUserMfaNonce(user) await getCore().sendMfaChallengeEmail(user, nonce[0]) @@ -115,4 +115,4 @@ async def verify_email(data: VerifyEmail): user = await getCore().getUserByEmail(email) await getCore().verifyEmail(user, data.token) await getGw().dispatch(UserUpdateEvent(user, await user.data, await user.settings), [user.id]) - return {"token": (await getCore().createSession(user.id)).token, "user_id": str(user.id)} + return {"token": (await Session.Y.create(user)).token, "user_id": str(user.id)} diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index d56eb89..dc1d997 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -684,7 +684,7 @@ async def delete_guild(data: GuildDelete, user: User = DepUser, guild: Guild = D if not data.code: raise InvalidDataErr(400, Errors.make(60008)) if data.code not in mfa.getCodes(): - if not (len(data.code) == 8 and await getCore().useMfaCode(user, data.code)): + if not (len(data.code) == 8 and await user.use_backup_code(data.code)): raise InvalidDataErr(400, Errors.make(60008)) role_ids = [role.id for role in await getCore().getRoles(guild, True)] diff --git a/yepcord/rest_api/routes/users.py b/yepcord/rest_api/routes/users.py index 3dcaf56..c21431c 100644 --- a/yepcord/rest_api/routes/users.py +++ b/yepcord/rest_api/routes/users.py @@ -23,15 +23,14 @@ from ...yepcord.models import User # Base path is /api/vX/users -users = YBlueprint('users', __name__) +users = YBlueprint("users", __name__) @users.get("//profile", qs_cls=UserProfileQuery) async def get_user_profile(query_args: UserProfileQuery, target_user: str, user: User = DepUser): if target_user == "@me": target_user = user.id - target_user = int(target_user) - target_user = await getCore().getUserProfile(target_user, user) + target_user = await user.get_another_user(int(target_user)) return await target_user.profile_json( user, query_args.with_mutual_guilds, query_args.mutual_friends_count, query_args.guild_id ) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index d97df7b..e0b3b57 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -57,21 +57,21 @@ async def update_me(data: UserUpdate, user: User = DepUser): discrim = data.discriminator if data.discriminator and data.discriminator != userdata.discriminator else None username = data.username if data.username and data.username != userdata.username else None if discrim or username or data.new_password is not None or data.email is not None: - if data.password is not None and not await getCore().checkUserPassword(user, data.password): + if data.password is not None and not user.check_password(data.password): raise InvalidDataErr(400, Errors.make(50035, {"password": { "code": "PASSWORD_DOES_NOT_MATCH", "message": "Passwords does not match." }})) data.password = None if username: - await getCore().changeUserName(user, username) + await user.change_username(username) data.username = None if discrim: - if not await getCore().changeUserDiscriminator(user, discrim, bool(username)): + if not await user.change_discriminator(discrim, bool(username)): await userdata.refresh_from_db(fields=["username", "discriminator"]) return await userdata.ds_json_full() data.discriminator = None if data.new_password is not None: - await getCore().changeUserPassword(user, data.new_password) + await user.change_password(data.new_password) data.new_password = None if data.email is not None: await getCore().changeUserEmail(user, data.email) @@ -276,7 +276,7 @@ async def enable_mfa(data: MfaEnable, session: Session = DepSession): settings = await user.settings if settings.mfa is not None: raise InvalidDataErr(404, Errors.make(60001)) - if not (password := data.password) or not await getCore().checkUserPassword(user, password): + if not (password := data.password) or not user.check_password(password): raise InvalidDataErr(400, Errors.make(50018)) if not (secret := data.secret): raise InvalidDataErr(400, Errors.make(60005)) @@ -287,12 +287,13 @@ async def enable_mfa(data: MfaEnable, session: Session = DepSession): raise InvalidDataErr(400, Errors.make(60008)) settings.mfa = secret await settings.save(update_fields=["mfa"]) - codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] - await getCore().setBackupCodes(user, codes) + codes = [ + {"user_id": str(user.id), "code": code, "consumed": False} + for code in await user.create_backup_codes() + ] await execute_after(getGw().dispatch(UserUpdateEvent(user, await user.data, settings), [user.id]), 1.5) - codes = [{"user_id": str(user.id), "code": code, "consumed": False} for code in codes] await session.delete() - session = await getCore().createSession(user) + session = await Session.Y.create(user) return {"token": session.token, "backup_codes": codes} @@ -307,14 +308,14 @@ async def disable_mfa(data: MfaDisable, session: Session = DepSession): mfa = await user.mfa code = code.replace("-", "").replace(" ", "") if code not in mfa.getCodes(): - if not (len(code) == 8 and await getCore().useMfaCode(user, code)): + if not (len(code) == 8 and await user.use_backup_code(code)): raise InvalidDataErr(400, Errors.make(60008)) settings.mfa = None await settings.save(update_fields=["mfa"]) - await getCore().clearBackupCodes(user) + await user.clear_backup_codes() await getGw().dispatch(UserUpdateEvent(user, await user.data, settings), [user.id]) await session.delete() - session = await getCore().createSession(user) + session = await Session.Y.create(user) return {"token": session.token} @@ -326,17 +327,16 @@ async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): raise InvalidDataErr(400, Errors.make(50035, {"key": { "code": "BASE_TYPE_REQUIRED", "message": "This field is required" }})) - reg = data.regenerate - await getCore().verifyUserMfaNonce(user, nonce, reg) + regenerate = data.regenerate + await getCore().verifyUserMfaNonce(user, nonce, regenerate) if await getCore().mfaNonceToCode(nonce) != key: raise InvalidDataErr(400, Errors.make(60011)) - if reg: - codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] - await getCore().setBackupCodes(user, codes) - codes = [{"user_id": str(user.id), "code": code, "consumed": False} for code in codes] - else: - codes = [code.ds_json() for code in await getCore().getBackupCodes(user)] - return {"backup_codes": codes} + + codes = await user.create_backup_codes() if regenerate else await user.get_backup_codes() + return {"backup_codes": [ + {"user_id": str(user.id), "code": code, "consumed": False} + for code in codes + ]} @users_me.put("/relationships/", body_cls=RelationshipPut) @@ -422,7 +422,7 @@ async def new_dm_channel(data: DmChannelCreate, user: User = DepUser): @users_me.post("/delete", body_cls=DeleteRequest) async def delete_user(data: DeleteRequest, user: User = DepUser): - if not await getCore().checkUserPassword(user, data.password): + if not user.check_password(data.password): raise InvalidDataErr(400, Errors.make(50018)) if await getCore().getUserOwnedGuilds(user) or await getCore().getUserOwnedGroups(user): raise InvalidDataErr(400, Errors.make(40011)) @@ -490,7 +490,7 @@ async def remote_auth_finish(data: RemoteAuthFinish, user: User = DepUser): if ra_session is None: raise InvalidDataErr(404, Errors.make(10012)) - session = await getCore().createSession(user) + session = await Session.Y.create(user) if ra_session.version == 2: await ra_session.update(v2_session=session, expires_at=time_plus_150s()) else: diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index f18dc27..eab009e 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -120,50 +120,7 @@ async def login(self, email: str, password: str) -> Session: _sid = urandom(12) sid = int.from_bytes(_sid, "big") raise MfaRequiredErr(user.id, b64encode(_sid), self.generateMfaTicketSignature(user, sid)) - return await self.createSession(user.id) - - async def createSession(self, user: Union[int, User]) -> Optional[Session]: - if not isinstance(user, User) and (user := await User.get_or_none(id=user, deleted=False)) is None: - return - sig = self.generateSessionSignature() - return await Session.create(id=Snowflake.makeId(), user=user, signature=sig) - - async def getUserProfile(self, uid: int, current_user: User) -> User: - # TODO: check for relationship, mutual guilds or mutual friends - if not (user := await User.y.get(uid, False)): - raise InvalidDataErr(404, Errors.make(10013)) - return user - - async def checkUserPassword(self, user: User, password: str) -> bool: - return checkpw(self.prepPassword(password, user.id), user.password.encode("utf8")) - - async def changeUserDiscriminator(self, user: User, discriminator: int, changed_username: bool = False) -> bool: - data = await user.data - username = data.username - if await User.y.getByUsername(username, discriminator): - if changed_username: - return False - raise InvalidDataErr(400, Errors.make(50035, {"username": { - "code": "USERNAME_TOO_MANY_USERS", - "message": "This discriminator already used by someone. Please enter something else." - }})) - data.discriminator = discriminator - await data.save(update_fields=["discriminator"]) - return True - - async def changeUserName(self, user: User, username: str) -> None: - data = await user.data - discriminator = data.discriminator - if await User.y.getByUsername(username, discriminator): - discriminator = await self.getRandomDiscriminator(username) - if discriminator is None: - raise InvalidDataErr(400, Errors.make(50035, {"username": { - "code": "USERNAME_TOO_MANY_USERS", - "message": "This name is used by too many users. Please enter something else or try again." - }})) - data.username = username - data.discriminator = discriminator - await data.save(update_fields=["username", "discriminator"]) + return await Session.Y.create(user) async def getRelationships(self, user: User, with_data=False) -> list[dict]: rels = [] @@ -198,22 +155,6 @@ async def getRelatedUsers(self, user: User, only_ids=False) -> list: users.append(data.ds_json) return users - async def changeUserPassword(self, user: User, new_password: str) -> None: - user.password = self.hashPassword(user.id, new_password) - await user.save(update_fields=["password"]) - - async def setBackupCodes(self, user: User, codes: list[str]) -> None: - await self.clearBackupCodes(user) - await MfaCode.bulk_create([ - MfaCode(user=user, code=code) for code in codes - ]) - - async def clearBackupCodes(self, user: User) -> None: - await MfaCode.filter(user=user).delete() - - async def getBackupCodes(self, user: User) -> list[MfaCode]: - return await MfaCode.filter(user=user).select_related("user").limit(10).all() - def generateMfaTicketSignature(self, user: User, session_id: int) -> str: payload = { "user_id": user.id, @@ -258,13 +199,6 @@ async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> if nonce_type != payload["type"]: raise InvalidDataErr(400, Errors.make(60011)) - async def useMfaCode(self, user: User, code: str) -> bool: - if (code := await MfaCode.get_or_none(user=user, code=code, used=False)) is None: - return False - code.used = True - await code.save(update_fields=["used"]) - return True - async def getChannel(self, channel_id: Optional[int]) -> Optional[Channel]: if channel_id is None: return diff --git a/yepcord/yepcord/models/session.py b/yepcord/yepcord/models/session.py index f156109..bf74547 100644 --- a/yepcord/yepcord/models/session.py +++ b/yepcord/yepcord/models/session.py @@ -17,16 +17,27 @@ """ from __future__ import annotations -from typing import Optional + +from os import urandom +from typing import Optional, Union from tortoise import fields import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model +from ..snowflake import Snowflake from ..utils import b64encode, int_size, b64decode +class SessionUtils: + @classmethod + async def create(cls, user: models.User) -> Optional[models.Session]: + return await Session.create(id=Snowflake.makeId(), user=user, signature=b64encode(urandom(32))) + + class Session(Model): + Y = SessionUtils + id: int = SnowflakeField(pk=True) user: models.User = fields.ForeignKeyField("models.User") signature: str = fields.CharField(max_length=128) diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index c7de462..7a311c1 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -19,15 +19,27 @@ from __future__ import annotations from datetime import datetime +from random import randint, choice from typing import Optional +from bcrypt import checkpw, hashpw, gensalt from tortoise import fields import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model from ..classes.other import MFA +from ..config import Config from ..ctx import getCore +from ..errors import InvalidDataErr, Errors from ..snowflake import Snowflake +from ..utils import int_size + + +async def _get_free_discriminator(login: str) -> Optional[int]: + for _ in range(5): + discriminator = randint(1, 9999) + if not await User.y.getByUsername(login, discriminator): + return discriminator class UserUtils: @@ -42,6 +54,10 @@ async def getByUsername(username: str, discriminator: int) -> Optional[User]: if data is not None: return data.user + @staticmethod + def prepare_password(password: str, user_id: int) -> bytes: + return password.encode("utf8") + user_id.to_bytes(int_size(user_id), "big").replace(b"\x00", b'') + class User(Model): y = UserUtils @@ -115,3 +131,71 @@ async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, data["user"]["bot"] = True return data + + async def get_another_user(self, user_id: int) -> User: + # TODO: check for relationship, mutual guilds or mutual friends + if (user := await User.y.get(user_id, False)) is None: # TODO: add test for nonexistent user + raise InvalidDataErr(404, Errors.make(10013)) + return user + + def check_password(self, password: str) -> bool: + return checkpw(self.y.prepare_password(password, self.id), self.password.encode("utf8")) + + def hash_new_password(self, password: str) -> str: + password = self.y.prepare_password(password, self.id) + return hashpw(password, gensalt(Config.BCRYPT_ROUNDS)).decode("utf8") + + async def change_password(self, new_password: str) -> None: + self.password = self.hash_new_password(new_password) + await self.save(update_fields=["password"]) + + async def change_username(self, username: str) -> None: + data = await self.data + discriminator = data.discriminator + if await User.y.getByUsername(username, discriminator): + discriminator = await _get_free_discriminator(username) + if discriminator is None: + raise InvalidDataErr(400, Errors.make(50035, {"username": { + "code": "USERNAME_TOO_MANY_USERS", + "message": "This name is used by too many users. Please enter something else or try again." + }})) + data.username = username + data.discriminator = discriminator + await data.save(update_fields=["username", "discriminator"]) + + async def change_discriminator(self, new_discriminator: int, username_changed: bool = False) -> bool: + data = await self.data + username = data.username + if await self.y.getByUsername(username, new_discriminator): + if username_changed: + return False + raise InvalidDataErr(400, Errors.make(50035, {"username": { + "code": "USERNAME_TOO_MANY_USERS", + "message": "This discriminator already used by someone. Please enter something else." + }})) + data.discriminator = new_discriminator + await data.save(update_fields=["discriminator"]) + return True + + async def create_backup_codes(self) -> list[str]: + codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] + + await self.clear_backup_codes() + await models.MfaCode.bulk_create([ + models.MfaCode(user=self, code=code) for code in codes + ]) + + return codes + + async def clear_backup_codes(self) -> None: + await models.MfaCode.filter(user=self).delete() + + async def get_backup_codes(self) -> list[str]: + return [code.code async for code in models.MfaCode.filter(user=self).limit(10)] + + async def use_backup_code(self, code: str) -> bool: + if (code := await models.MfaCode.get_or_none(user=self, code=code, used=False)) is None: + return False + code.used = True + await code.save(update_fields=["used"]) + return True From b7e83a59500d355932000e65879fb98aae806a0e Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 3 Sep 2024 19:20:32 +0300 Subject: [PATCH 02/26] move all methods that had one usage to usage place --- yepcord/rest_api/dependencies.py | 7 +- yepcord/rest_api/routes/applications.py | 6 +- yepcord/rest_api/routes/auth.py | 13 +- yepcord/rest_api/routes/channels.py | 62 +++++++--- yepcord/rest_api/routes/guilds.py | 41 +++++-- yepcord/rest_api/routes/users_me.py | 12 +- yepcord/yepcord/core.py | 150 ++---------------------- yepcord/yepcord/models/channel.py | 4 +- yepcord/yepcord/models/guild.py | 7 +- yepcord/yepcord/models/guild_event.py | 2 +- yepcord/yepcord/models/message.py | 5 +- 11 files changed, 121 insertions(+), 188 deletions(-) diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index 9ee2aa2..c1aa0d4 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -122,9 +122,12 @@ async def depMessageO( async def depInvite(invite: Optional[str] = None) -> Invite: try: invite_id = int.from_bytes(b64decode(invite), "big") - if not (inv := await getCore().getInvite(invite_id)): + invite = await ( + Invite.get_or_none(id=invite_id) + .select_related("channel", "channel__guild", "inviter", "channel__guild__owner", "channel__owner") + ) + if not invite: raise ValueError - invite = inv except ValueError: if not (invite := await getCore().getVanityCodeInvite(invite)): raise InvalidDataErr(404, Errors.make(10006)) diff --git a/yepcord/rest_api/routes/applications.py b/yepcord/rest_api/routes/applications.py index fcf34e1..f54add9 100644 --- a/yepcord/rest_api/routes/applications.py +++ b/yepcord/rest_api/routes/applications.py @@ -35,8 +35,10 @@ @applications.get("/", strict_slashes=False) async def get_applications(user: User = DepUser): - apps = await getCore().getApplications(user) - return [await app.ds_json() for app in apps] + return [ + await app.ds_json() + for app in await Application.filter(owner=user, deleted=False).select_related("owner") + ] @applications.post("/", strict_slashes=False, body_cls=CreateApplication) diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index 8936b21..b6d73f9 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -25,6 +25,7 @@ from ..utils import captcha from ..y_blueprint import YBlueprint from ...gateway.events import UserUpdateEvent +from ...yepcord.classes.other import EmailMsg from ...yepcord.ctx import getCore, getGw from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.models import Session, User @@ -92,7 +93,17 @@ async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User if not user.check_password(password): raise InvalidDataErr(400, Errors.make(50018)) nonce = await getCore().generateUserMfaNonce(user) - await getCore().sendMfaChallengeEmail(user, nonce[0]) + + code = await getCore().mfaNonceToCode(nonce[0]) + await EmailMsg( + user.email, + f"Your one-time verification key is {code}", + f"It looks like you're trying to view your account's backup codes.\n" + f"This verification key expires in 10 minutes. This key is extremely sensitive, treat it like a " + f"password and do not share it with anyone.\n" + f"Enter it in the app to unlock your backup codes:\n{code}" + ).send() + return {"nonce": nonce[0], "regenerate_nonce": nonce[1]} diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 1519d87..d88f600 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -39,7 +39,8 @@ from ...yepcord.enums import GuildPermissions, MessageType, ChannelType, WebhookType, GUILD_CHANNELS, MessageFlags from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.models import User, Channel, Message, ReadState, Emoji, PermissionOverwrite, Webhook, ThreadMember, \ - ThreadMetadata, AuditLogEntry, Relationship, ApplicationCommand, Integration, Bot, Role + ThreadMetadata, AuditLogEntry, Relationship, ApplicationCommand, Integration, Bot, Role, HiddenDmChannel, Invite, \ + Reaction from ...yepcord.snowflake import Snowflake from ...yepcord.utils import getImage, b64encode, b64decode @@ -116,7 +117,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha @channels.delete("/", allow_bots=True) async def delete_channel(user: User = DepUser, channel: Channel = DepChannel): if channel.type == ChannelType.DM: - await getCore().hideDmChannel(user, channel) + await HiddenDmChannel.get_or_create(user=user, channel=channel) await getGw().dispatch(DMChannelDeleteEvent(await channel.ds_json()), user_ids=[user.id]) return await channel.ds_json() elif channel.type == ChannelType.GROUP_DM: @@ -319,7 +320,11 @@ async def pin_message(user: User = DepUser, channel: Channel = DepChannel, messa member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.VIEW_CHANNEL, channel=channel) if not message.pinned: - await getCore().pinMessage(message) + if await Message.filter(pinned_timestamp__not_isnull=True, channel=message.channel).count() >= 50: + raise InvalidDataErr(400, Errors.make(30003)) + message.pinned_timestamp = datetime.now() + await message.save(update_fields=["pinned_timestamp"]) + await getGw().sendPinsUpdateEvent(channel) message_ref = {"message_id": str(message.id), "channel_id": str(channel.id)} if channel.guild: @@ -351,11 +356,16 @@ async def unpin_message(user: User = DepUser, channel: Channel = DepChannel, mes async def get_pinned_messages(user: User = DepUser, channel: Channel = DepChannel): if channel.guild: member = await getCore().getGuildMember(channel.guild, user.id) - await member.checkPermission(GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, - channel=channel) - messages = await getCore().getPinnedMessages(channel) - messages = [await message.ds_json() for message in messages] - return messages + await member.checkPermission( + GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, + channel=channel, + ) + + return [ + await message.ds_json() + for message in await Message.filter(pinned_timestamp__not_isnull=True, channel=channel) + .select_related("channel", "author", "guild") + ] @channels.put("//messages//reactions//@me", allow_bots=True) @@ -371,7 +381,7 @@ async def add_message_reaction(reaction: str, user: User = DepUser, channel: Cha "emoji": None if not isinstance(reaction, Emoji) else reaction, "emoji_name": reaction if isinstance(reaction, str) else reaction.name } - await getCore().addReaction(message, user, **emoji) + await Reaction.get_or_create(user=user, message=message, **emoji) await getGw().dispatch(MessageReactionAddEvent(user.id, message.id, channel.id, emoji), channel=channel, permissions=GuildPermissions.VIEW_CHANNEL) return "", 204 @@ -390,7 +400,7 @@ async def remove_message_reaction(reaction: str, user: User = DepUser, channel: "emoji": None if not isinstance(reaction, Emoji) else reaction, "emoji_name": reaction if isinstance(reaction, str) else reaction.name } - await getCore().removeReaction(message, user, **emoji) + await Reaction.filter(user=user, message=message, **emoji).delete() await getGw().dispatch(MessageReactionRemoveEvent(user.id, message.id, channel.id, emoji), channel=channel, permissions=GuildPermissions.VIEW_CHANNEL) return "", 204 @@ -405,11 +415,17 @@ async def get_message_reactions(query_args: GetReactionsQuery, reaction: str, us GuildPermissions.VIEW_CHANNEL, channel=channel) if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): raise InvalidDataErr(400, Errors.make(10014)) - emoji_data = { - "emoji": None if not isinstance(reaction, Emoji) else reaction, - "emoji_name": reaction if isinstance(reaction, str) else reaction.name - } - return await getCore().getReactedUsersJ(message, query_args.limit, **emoji_data) + emoji = None if not isinstance(reaction, Emoji) else reaction + emoji_name = reaction if isinstance(reaction, str) else reaction.name + + reactions = await Reaction.filter( + message=message, emoji=emoji, emoji_name=emoji_name + ).limit(query_args.limit).select_related("user") + + return [ + (await reaction.user.data).ds_json + for reaction in reactions + ] @channels.get("//messages/search", qs_cls=SearchQuery) @@ -430,7 +446,9 @@ async def create_invite(data: InviteCreate, user: User = DepUser, channel: Chann if channel.guild: member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.CREATE_INSTANT_INVITE) - invite = await getCore().createInvite(channel, user, **data.model_dump()) + invite = await Invite.create( + id=Snowflake.makeId(), channel=channel, inviter=user, **data.model_dump(include={"max_age", "max_uses"}), + ) if channel.guild: entry = await AuditLogEntry.utils.invite_create(user, invite) await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=channel.guild.id, @@ -501,8 +519,11 @@ async def get_channel_invites(user: User = DepUser, channel: Channel = DepChanne if not (member := await getCore().getGuildMember(channel.guild, user.id)): raise InvalidDataErr(403, Errors.make(50001)) await member.checkPermission(GuildPermissions.VIEW_CHANNEL, channel=channel) - invites = await getCore().getChannelInvites(channel) - return [await invite.ds_json() for invite in invites] + return [ + await invite.ds_json() + for invite in await Invite.filter(channel=channel, vanity_code__isnull=True) + .select_related("channel__guild", "inviter") + ] @channels.post("//webhooks", body_cls=WebhookCreate, allow_bots=True) @@ -527,7 +548,10 @@ async def get_channel_webhooks(user: User = DepUser, channel: Channel = DepChann member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) - return [await webhook.ds_json() for webhook in await getCore().getChannelWebhooks(channel)] + return [ + await webhook.ds_json() + for webhook in await Webhook.filter(channel=channel).select_related("channel", "channel__guild", "user") + ] @channels.post("//messages//threads", body_cls=CreateThread, allow_bots=True) diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index dc1d997..df47c7c 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -23,6 +23,7 @@ from async_timeout import timeout from quart import request, current_app from tortoise.expressions import Q +from tortoise.functions import Count from ..dependencies import DepUser, DepGuild, DepGuildMember, DepGuildTemplate, DepRole from ..models.guilds import GuildCreate, GuildUpdate, TemplateCreate, TemplateUpdate, EmojiCreate, EmojiUpdate, \ @@ -42,7 +43,7 @@ ScheduledEventEntityType from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.models import User, Guild, GuildMember, GuildTemplate, Emoji, Channel, PermissionOverwrite, UserData, \ - Role, Invite, Sticker, GuildEvent, AuditLogEntry, Integration, ApplicationCommand + Role, Invite, Sticker, GuildEvent, AuditLogEntry, Integration, ApplicationCommand, Webhook, GuildBan from ...yepcord.snowflake import Snowflake from ...yepcord.utils import getImage, b64decode, validImage, imageType @@ -260,9 +261,10 @@ async def create_channel(data: ChannelCreate, user: User = DepUser, guild: Guild @guilds.get("//invites", allow_bots=True) async def get_guild_invites(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) - invites = await getCore().getGuildInvites(guild) - invites = [await invite.ds_json() for invite in invites] - return invites + return [ + await invite.ds_json() + for invite in await Invite.filter(channel__guild=guild).select_related("channel", "channel__guild", "inviter") + ] @guilds.get("//premium/subscriptions", allow_bots=True) @@ -325,7 +327,7 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: raise InvalidDataErr(403, Errors.make(50013)) if await getCore().getGuildBan(guild, user_id) is not None: return "", 204 - reason = request.headers.get("x-audit-log-reason") + reason = request.headers.get("x-audit-log-reason", "") if target_member is not None: await getGw().dispatchUnsub([target_member.user.id], guild.id) for role in await target_member.roles.all(): @@ -333,12 +335,12 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: await target_member.delete() if target_member.user.is_bot: await process_bot_kick(user, target_member) - await getCore().banGuildMember(target_member, reason) + await GuildBan.create(user=target_member.user, guild=guild, reason=reason) target_user = target_member.user else: if (target_user := await User.y.get(user_id, False)) is None: raise InvalidDataErr(404, Errors.make(10013)) - await getCore().banGuildUser(target_user, guild, reason) + await GuildBan.create(user=target_user, guild=guild, reason=reason) target_user_data = await target_user.data if target_member is not None: await getGw().dispatch(GuildMemberRemoveEvent(guild.id, target_user_data.ds_json), user_ids=[user_id]) @@ -369,7 +371,10 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: @guilds.get("//bans", allow_bots=True) async def get_guild_bans(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.BAN_MEMBERS) - return [await ban.ds_json() for ban in await getCore().getGuildBans(guild)] + return [ + await ban.ds_json() + for ban in await GuildBan.filter(guild=guild).select_related("user", "guild") + ] @guilds.delete("//bans/", allow_bots=True) @@ -377,7 +382,7 @@ async def unban_member(user_id: int, user: User = DepUser, guild: Guild = DepGui member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.BAN_MEMBERS) target_user_data: UserData = await UserData.get(id=user_id).select_related("user") - await getCore().removeGuildBan(guild, target_user_data.user) + await GuildBan.filter(guild=guild, user=target_user_data.user).delete() await getGw().dispatch(GuildBanRemoveEvent(guild.id, target_user_data.ds_json), guild_id=guild.id, permissions=GuildPermissions.BAN_MEMBERS) @@ -512,13 +517,20 @@ async def get_connections_configuration(role: int, member: GuildMember = DepGuil @guilds.get("//roles/member-counts", allow_bots=True) async def get_role_member_count(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_ROLES) - return await getCore().getRolesMemberCounts(guild) + counts = {} + for role in await Role.filter(guild=guild).select_related("guildmembers").annotate(m=Count("guildmembers")): + counts[role.id] = role.m + return counts @guilds.get("//roles//member-ids", allow_bots=True) async def get_role_members(member: GuildMember = DepGuildMember, role: Role = DepRole): await member.checkPermission(GuildPermissions.MANAGE_ROLES) - return [str(member_id) for member_id in await getCore().getRoleMemberIds(role)] + return [ + str(member_id) + for member_id in await role.guildmembers.all().select_related("user").limit(100) + .values_list("user__id", flat=True) + ] @guilds.patch("//roles//members", body_cls=AddRoleMembers, allow_bots=True) @@ -532,7 +544,7 @@ async def add_role_members(data: AddRoleMembers, user: User = DepUser, guild: Gu members = {} for member_id in data.member_ids: target_member = await getCore().getGuildMember(guild, member_id) - if not await getCore().memberHasRole(target_member, role): + if not await target_member.roles.filter(id=role.id).exists(): await target_member.roles.add(role) target_member_json = await target_member.ds_json() await getGw().dispatch(GuildMemberUpdateEvent(guild.id, target_member_json), guild_id=guild.id) @@ -702,7 +714,10 @@ async def delete_guild(data: GuildDelete, user: User = DepUser, guild: Guild = D @guilds.get("//webhooks", allow_bots=True) async def get_guild_webhooks(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) - return [await webhook.ds_json() for webhook in await getCore().getWebhooks(guild)] + return [ + await webhook.ds_json() + for webhook in await Webhook.filter(channel__guild=guild).select_related("channel", "channel__guild", "user") + ] @guilds.get("//stickers", allow_bots=True) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index e0b3b57..f932dde 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -33,10 +33,10 @@ from ...yepcord.classes.other import MFA from ...yepcord.config import Config from ...yepcord.ctx import getCore, getCDNStorage, getGw -from ...yepcord.enums import RelationshipType +from ...yepcord.enums import RelationshipType, ChannelType from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.models import User, UserSettingsProto, FrecencySettings, UserNote, Session, UserData, Guild, \ - GuildMember, RemoteAuthSession, Relationship, Authorization, Bot, ConnectedAccount + GuildMember, RemoteAuthSession, Relationship, Authorization, Bot, ConnectedAccount, GuildEvent, Channel from ...yepcord.models.remote_auth_session import time_plus_150s from ...yepcord.proto import FrecencyUserSettings, PreloadedUserSettings from ...yepcord.utils import execute_after, validImage, getImage @@ -255,7 +255,7 @@ async def get_relationships(user: User = DepUser): async def get_notes(target_uid: int, user: User = DepUser): if not (target_user := await User.y.get(target_uid, False)): raise InvalidDataErr(404, Errors.make(10013)) - if not (note := await getCore().getUserNote(user, target_user)): + if not (note := await UserNote.get_or_none(user=user, target=target_user).select_related("user", "target")): raise InvalidDataErr(404, Errors.make(10013)) return note.ds_json() @@ -424,7 +424,7 @@ async def new_dm_channel(data: DmChannelCreate, user: User = DepUser): async def delete_user(data: DeleteRequest, user: User = DepUser): if not user.check_password(data.password): raise InvalidDataErr(400, Errors.make(50018)) - if await getCore().getUserOwnedGuilds(user) or await getCore().getUserOwnedGroups(user): + if await Guild.exists(owner=user) or await Channel.exists(owner=user, type=ChannelType.GROUP_DM): raise InvalidDataErr(400, Errors.make(40011)) await getCore().deleteUser(user) await getGw().dispatch(UserDeleteEvent(user.id), user_ids=[user.id]) @@ -437,7 +437,9 @@ async def get_scheduled_events(query_args: GetScheduledEventsQuery, user: User = for guild_id in query_args.guild_ids[:5]: if not await GuildMember.get_or_none(guild__id=guild_id, user__id=user.id): raise InvalidDataErr(403, Errors.make(50001)) - for event_id in await getCore().getSubscribedGuildEventIds(user, guild_id): + for event_id in await GuildEvent.filter( + guild__id=guild_id, subscribers__user__id__in=[user.id] + ).values_list("id", flat=True): events.append({ "guild_scheduled_event_id": str(event_id), "user_id": str(user.id) diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index eab009e..aad36bb 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -40,10 +40,9 @@ from .config import Config from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, MfaRequiredErr, Errors -from .models import User, UserData, UserSettings, Session, Relationship, Channel, Message, ReadState, UserNote, \ - Attachment, FrecencySettings, Emoji, Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, \ - PermissionOverwrite, GuildBan, AuditLogEntry, Webhook, HiddenDmChannel, MfaCode, Role, GuildEvent, \ - ThreadMetadata, ThreadMember, Application +from .models import User, UserData, UserSettings, Session, Relationship, Channel, Message, ReadState, FrecencySettings, \ + Emoji, Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, PermissionOverwrite, GuildBan, AuditLogEntry, \ + Webhook, HiddenDmChannel, MfaCode, Role, GuildEvent, ThreadMember from .snowflake import Snowflake from .storage import getStorage from .utils import b64encode, b64decode, int_size, NoneType @@ -306,12 +305,6 @@ async def getReadStatesJ(self, user: User) -> list: states.append(await st.ds_json()) return states - async def getUserNote(self, user: User, target: User) -> Optional[UserNote]: - return await UserNote.get_or_none(user=user, target=target).select_related("user", "target") - - async def getAttachment(self, attachment_id: int) -> Optional[Attachment]: - return await Attachment.get_or_none(id=attachment_id) - async def getUserByChannelId(self, channel_id: int, user_id: int) -> Optional[User]: if not (channel := await self.getChannel(channel_id)): return None @@ -356,7 +349,7 @@ async def verifyEmail(self, user: User, token: str) -> None: async def getUserByEmail(self, email: str) -> Optional[User]: return await User.get_or_none(email=email) - async def changeUserEmail(self, user: User, email: str) -> None: + async def changeUserEmail(self, user: User, email: str) -> None: # TODO email = email.lower() if user.email == email: return @@ -365,15 +358,6 @@ async def changeUserEmail(self, user: User, email: str) -> None: "message": "Email address already registered."}})) await user.update(email=email, verified=False) - async def sendMfaChallengeEmail(self, user: User, nonce: str) -> None: - code = await self.mfaNonceToCode(nonce) - await EmailMsg(user.email, - f"Your one-time verification key is {code}", - f"It looks like you're trying to view your account's backup codes.\n" - f"This verification key expires in 10 minutes. This key is extremely sensitive, treat it like a " - f"password and do not share it with anyone.\n" - f"Enter it in the app to unlock your backup codes:\n{code}").send() - async def mfaNonceToCode(self, nonce: str) -> Optional[str]: if not (payload := JWT.decode(nonce, self.key)): return @@ -389,26 +373,10 @@ async def createDMGroupChannel(self, user: User, recipients: list[User], name: O await channel.recipients.add(recipient) return channel - async def pinMessage(self, message: Message) -> None: - if await Message.filter(pinned_timestamp__not_isnull=True, channel=message.channel).count() >= 50: - raise InvalidDataErr(400, Errors.make(30003)) - message.pinned_timestamp = datetime.now() - await message.save(update_fields=["pinned_timestamp"]) - async def getLastPinnedMessage(self, channel: Channel) -> Optional[Message]: return await (Message.filter(pinned_timestamp__not_isnull=True, channel=channel).order_by("-pinned_timestamp") .limit(1).get_or_none()) - async def getPinnedMessages(self, channel: Channel) -> list[Message]: - return await (Message.filter(pinned_timestamp__not_isnull=True, channel=channel) - .select_related("channel", "author", "guild").all()) - - async def addReaction(self, message: Message, user: User, emoji: Emoji, emoji_name: str) -> Reaction: - return (await Reaction.get_or_create(user=user, message=message, emoji=emoji, emoji_name=emoji_name))[0] - - async def removeReaction(self, message: Message, user: User, emoji: Emoji, emoji_name: str) -> None: - await Reaction.filter(user=user, message=message, emoji=emoji, emoji_name=emoji_name).delete() - async def getMessageReactionsJ(self, message: Message, user: Union[User, int]) -> list: if isinstance(user, User): user = user.id @@ -428,15 +396,6 @@ async def getMessageReactionsJ(self, message: Message, user: Union[User, int]) - }) return reactions - async def getReactedUsersJ(self, message: Message, limit: int, emoji: Emoji, emoji_name: str) -> list[dict]: - users = [] - reactions = await (Reaction.filter(message=message, emoji=emoji, emoji_name=emoji_name).limit(limit) - .select_related("user").all()) - for reaction in reactions: - data = await reaction.user.data - users.append(data.ds_json) - return users - async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[list[Message], int]: filter_args = {"channel": channel} if "author_id" in search_filter: @@ -461,16 +420,6 @@ async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[l count = await query.count() return messages, count - async def createInvite(self, channel: Channel, inviter: User, max_age: int = 86400, max_uses: int = 0) -> Invite: - return await Invite.create( - id=Snowflake.makeId(), channel=channel, inviter=inviter, max_age=max_age, max_uses=max_uses - ) - - async def getInvite(self, invite_id: int) -> Optional[Invite]: - return await (Invite.get_or_none(id=invite_id) - .select_related("channel", "channel__guild", "inviter", "channel__guild__owner", - "channel__owner")) - @atomic() async def createGuild(self, guild_id: int, user: User, name: str, icon: str = None) -> Guild: guild = await Guild.create(id=guild_id, owner=user, name=name, icon=icon) @@ -578,9 +527,6 @@ async def getGuildMember(self, guild: Guild, user_id: int) -> Optional[GuildMemb async def getGuildMembers(self, guild: Guild) -> list[GuildMember]: return await GuildMember.filter(guild=guild).select_related("user").all() - async def getGuildMembersIds(self, guild: Guild) -> list[int]: - return [member.user.id for member in await self.getGuildMembers(guild)] - async def getGuildChannels(self, guild: Guild) -> list[Channel]: return await Channel.filter(guild=guild) \ .exclude(type__in=[ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD])\ @@ -613,23 +559,9 @@ async def getEmojiByReaction(self, reaction: str) -> Optional[Emoji]: return return emoji if emoji.name == name else None - async def getGuildInvites(self, guild: Guild) -> list[Invite]: - return await Invite.filter(channel__guild=guild).select_related("channel", "channel__guild", "inviter").all() - - async def banGuildMember(self, member: GuildMember, reason: str = None) -> None: - if reason is None: reason = "" - await GuildBan.create(user=member.user, guild=member.guild, reason=reason) - - async def banGuildUser(self, user: User, guild: Guild, reason: str = None) -> None: - if reason is None: reason = "" - await GuildBan.create(user=user, guild=guild, reason=reason) - async def getGuildBan(self, guild: Guild, user_id: int) -> Optional[GuildMember]: return await GuildBan.get_or_none(guild=guild, user__id=user_id) - async def getGuildBans(self, guild: Guild) -> list[GuildBan]: - return await GuildBan.filter(guild=guild).select_related("user", "guild").all() - async def bulkDeleteGuildMessagesFromBanned( self, guild: Guild, user_id: int, after_id: int ) -> dict[Channel, list[int]]: @@ -647,12 +579,6 @@ async def bulkDeleteGuildMessagesFromBanned( return result - async def getRolesMemberCounts(self, guild: Guild) -> dict[int, int]: - counts = {} - for role in await Role.filter(guild=guild).select_related("guildmembers").annotate(m=Count("guildmembers")).all(): - counts[role.id] = role.m - return counts - async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[str, str]]: user_guilds_member = await GuildMember.filter(user=user).select_related("guild").all() user_guild_ids = [member.guild.id for member in user_guilds_member] @@ -669,9 +595,6 @@ async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[st return mutual_guilds_json - async def getAttachments(self, message: Message) -> list[Attachment]: - return await Attachment.filter(message=message).select_related("channel").all() - async def getMemberRolesIds(self, member: GuildMember, include_default: bool = False) -> list[int]: roles_ids = [role.id for role in await member.roles.all()] if include_default: @@ -701,13 +624,6 @@ async def getMemberRoles(self, member: GuildMember, include_default: bool = Fals roles.sort(key=lambda r: r.position) return roles - async def removeGuildBan(self, guild: Guild, user: User) -> None: - await GuildBan.filter(guild=guild, user=user).delete() - - async def getRoleMemberIds(self, role: Role) -> list[int]: - role = await Role.get(id=role.id) - return [member.user.id for member in await role.guildmembers.all().select_related("user").limit(100)] - async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids: list[int]) -> list[GuildMember]: # noinspection PyUnresolvedReferences return await GuildMember.filter( @@ -716,9 +632,6 @@ async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) ).select_related("user").limit(limit).all() - async def memberHasRole(self, member: GuildMember, role: Role) -> bool: - return await member.roles.filter(id=role.id).exists() - async def getPermissionOverwrite(self, channel: Channel, target: Union[Role, User]) -> Optional[PermissionOverwrite]: kw = {"target_role": target} if isinstance(target, Role) else {"target_user": target} return await (PermissionOverwrite.get_or_none(channel=channel, **kw) @@ -732,10 +645,6 @@ async def getPermissionOverwriteUnk(self, channel: Channel, target_id: int) -> O async def getPermissionOverwrites(self, channel: Channel) -> list[PermissionOverwrite]: return await PermissionOverwrite.filter(channel=channel).select_related("target_role", "target_user").all() - async def deletePermissionOverwrite(self, channel: Channel, target: Union[Role, User]) -> None: - if (overwrite := await self.getPermissionOverwrite(channel, target)) is not None: - await overwrite.delete() - async def getOverwritesForMember(self, channel: Channel, member: GuildMember) -> list[PermissionOverwrite]: roles = await self.getMemberRoles(member, True) roles.sort(key=lambda r: r.position) @@ -748,15 +657,6 @@ async def getOverwritesForMember(self, channel: Channel, member: GuildMember) -> result.append(overwrite) return result - async def getChannelInvites(self, channel: Channel) -> list[Invite]: - #await Invite.Meta.database.execute( - # query="DELETE FROM `invites` WHERE channel=:channel_id AND `max_age` > 0 AND " - # "`max_age` + (((`id` >> 22) + :sf_epoch) / 1000) < :current_time;", - # values={"channel_id": channel.id, "current_time": int(time()), "sf_epoch": Snowflake.EPOCH} - #) - return await (Invite.filter(channel=channel, vanity_code__isnull=True) - .select_related("channel__guild", "inviter").all()) - async def getVanityCodeInvite(self, code: str) -> Optional[Invite]: if code is None: return return await Invite.get_or_none(vanity_code=code) @@ -787,18 +687,9 @@ async def setTemplateDirty(self, guild: Guild) -> None: template.is_dirty = True await template.save(update_fields=["is_dirty"]) - async def getWebhooks(self, guild: Guild) -> list[Webhook]: - return await Webhook.filter(channel__guild=guild).select_related("channel", "channel__guild", "user").all() - - async def getChannelWebhooks(self, channel: Channel) -> list[Webhook]: - return await Webhook.filter(channel=channel).select_related("channel", "channel__guild", "user").all() - async def getWebhook(self, webhook_id: int) -> Optional[Webhook]: return await Webhook.get_or_none(id=webhook_id).select_related("channel", "channel__guild", "user") - async def hideDmChannel(self, user: User, channel: Channel) -> None: - await HiddenDmChannel.get_or_create(user=user, channel=channel) - async def unhideDmChannel(self, user: User, channel: Channel) -> None: await HiddenDmChannel.filter(user=user, channel=channel).delete() @@ -811,12 +702,6 @@ async def getGuildStickers(self, guild: Guild) -> list[Sticker]: async def getSticker(self, sticker_id: int) -> Optional[Sticker]: return await Sticker.get_or_none(id=sticker_id).select_related("guild", "user") - async def getUserOwnedGuilds(self, user: User) -> list[Guild]: - return await Guild.filter(owner=user).all() - - async def getUserOwnedGroups(self, user: User) -> list[Channel]: - return await Channel.filter(owner=user, type=ChannelType.GROUP_DM).all() - async def deleteUser(self, user: User) -> None: await user.update(deleted=True, email=f"deleted_{user.id}@yepcord.ml", password="") data = await user.data @@ -831,34 +716,21 @@ async def deleteUser(self, user: User) -> None: await Invite.filter(inviter=user).delete() await ReadState.filter(user=user).delete() - async def getGuildEventUserCount(self, event: GuildEvent) -> int: - return await event.subscribers.filter().count() - async def getGuildEvent(self, event_id: int) -> Optional[GuildEvent]: return await GuildEvent.get_or_none(id=event_id).select_related("channel", "guild", "creator") async def getGuildEvents(self, guild: Guild) -> list[GuildEvent]: return await GuildEvent.filter(guild=guild).select_related("channel", "guild", "creator").all() - async def getSubscribedGuildEventIds(self, user: User, guild_id: int) -> list[int]: - events_ids = [] - for member in await GuildMember.filter(user=user, guild__id=guild_id).all(): - events_ids.extend(await member.guildevents.filter().values_list("id", flat=True)) - return events_ids - - async def getThreadMetadata(self, thread: Channel) -> Optional[ThreadMetadata]: - return await ThreadMetadata.get_or_none(channel=thread) - - async def getThreadMembersCount(self, thread: Channel) -> int: - return await ThreadMember.filter(channel=thread).count() + #async def getSubscribedGuildEventIds(self, user: User, guild_id: int) -> list[int]: + # events_ids = [] + # for member in await GuildMember.filter(user=user, guild__id=guild_id).all(): + # events_ids.extend(await member.guildevents.filter().values_list("id", flat=True)) + # return events_ids async def getThreadMembers(self, thread: Channel, limit: int = 100) -> list[ThreadMember]: return await ThreadMember.filter(channel=thread).select_related("user").limit(limit).all() - async def getGuildMemberThreads(self, guild: Guild, user_id: int) -> list[Channel]: - return await (ThreadMember.filter(guild=guild, user__id=user_id) - .select_related("channel", "user", "guild").all()) - async def getThreadMember(self, thread: Channel, user_id: int) -> Optional[ThreadMember]: return await ThreadMember.get_or_none(channel=thread, user__id=user_id) @@ -881,10 +753,6 @@ def getLanguageCode(self, ip: str, default: str="en-US") -> str: return country_to_language.get(country_code, default) - async def getApplications(self, user: User) -> list[Application]: - return await (Application.filter(owner=user, deleted=False).select_related("bots", "bots__user", "owner") - .all()) - ctx._getCore = lambda: Core.getInstance() ctx._getCDNStorage = lambda: getStorage() diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index d071b36..052cd24 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -154,9 +154,9 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "owner_id": str(self.owner.id), "name": self.name, "last_message_id": last_message_id, - "thread_metadata": (await getCore().getThreadMetadata(self)).ds_json(), + "thread_metadata": (await models.ThreadMetadata.get(channel=self)).ds_json(), "message_count": message_count, - "member_count": await getCore().getThreadMembersCount(self), + "member_count": await models.ThreadMember.filter(channel=self).count(), "rate_limit_per_user": self.rate_limit, "flags": self.flags, "total_message_sent": message_count, diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index daa5b9d..0427e70 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -127,7 +127,12 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo if for_gateway or user_id: member = await getCore().getGuildMember(self, user_id) data["joined_at"] = member.joined_at.strftime("%Y-%m-%dT%H:%M:%S.000000+00:00") - data["threads"] = [thread.ds_json() for thread in await getCore().getGuildMemberThreads(self, user_id)] + data["threads"] = [ + thread.ds_json() + for thread in await models.ThreadMember.filter(guild=self, user__id=user_id).select_related( + "channel", "user", "guild" + ) + ] if for_gateway or with_channels: data["channels"] = [await channel.ds_json() for channel in await getCore().getGuildChannels(self)] if with_members: diff --git a/yepcord/yepcord/models/guild_event.py b/yepcord/yepcord/models/guild_event.py index 6bec683..9514933 100644 --- a/yepcord/yepcord/models/guild_event.py +++ b/yepcord/yepcord/models/guild_event.py @@ -73,5 +73,5 @@ async def ds_json(self, with_user: bool = False, with_user_count: bool = False) creator = await self.creator.data data["creator"] = creator.ds_json if with_user_count: - data["user_count"] = await getCore().getGuildEventUserCount(self) + data["user_count"] = await self.subscribers.filter().count() return data diff --git a/yepcord/yepcord/models/message.py b/yepcord/yepcord/models/message.py index a1fe3f6..832e582 100644 --- a/yepcord/yepcord/models/message.py +++ b/yepcord/yepcord/models/message.py @@ -90,7 +90,10 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: "stickers": self.stickers, "tts": False, "sticker_ids": [sticker["id"] for sticker in self.stickers], - "attachments": [attachment.ds_json() for attachment in await getCore().getAttachments(self)], + "attachments": [ + attachment.ds_json() + for attachment in await models.Attachment.filter(message=self).select_related("channel") + ], } if self.guild: data["guild_id"] = str(self.guild.id) data["mention_everyone"] = ("@everyone" in self.content or "@here" in self.content) if self.content else None From 7940a720d8f4655ca5e9d6600d993b9fdb93c116 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Wed, 4 Sep 2024 14:33:01 +0300 Subject: [PATCH 03/26] move login and register --- tests/test_core.py | 98 ++++++++++++++++----------------- yepcord/rest_api/routes/auth.py | 19 ++++--- yepcord/yepcord/core.py | 70 ++--------------------- yepcord/yepcord/models/user.py | 63 +++++++++++++++++++-- 4 files changed, 119 insertions(+), 131 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 527d1c3..4838ef9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -19,12 +19,12 @@ from asyncio import get_event_loop from datetime import date from json import dumps -from random import randint, choice +from random import randint from typing import Coroutine, Any import pytest as pt import pytest_asyncio -from tortoise import Tortoise, connections +from tortoise import Tortoise from yepcord.yepcord.config import Config, ConfigModel from yepcord.yepcord.core import Core @@ -64,37 +64,35 @@ async def _setup_db(): @pt.mark.asyncio -async def test_register_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - session = await testCore.register(VARS["user_id"], "Test Login", f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd", - "2000-01-01") +async def test_register_success(): + user = await User.y.register("Test Login", f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd", "2000-01-01") + VARS["user_id"] = user.id + session = await Session.Y.create(user) + assert session is not None, "Account not registered: maybe you using already used database?" - await core.register(VARS["user_id"] + 200001, "Test", f"{EMAIL_ID}_test2_1@yepcord.ml", "password", "2000-01-01") + user = await User.y.register("Test", f"{EMAIL_ID}_test2_1@yepcord.ml", "password", "2000-01-01") + VARS[f"user_id_200001"] = user.id @pt.mark.asyncio -async def test_register_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_register_fail(): with pt.raises(InvalidDataErr): - await testCore.register(VARS["user_id"], "Test Login 2", f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd", - "2000-01-01") + await User.y.register("Test Login 2", f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd", "2000-01-01") @pt.mark.asyncio -async def test_login_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - session = await testCore.login(f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd") +async def test_login_success(): + session = await User.y.login(f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd") assert session is not None, "Session not created: ???" @pt.mark.asyncio -async def test_login_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_login_fail(): with pt.raises(InvalidDataErr): - await testCore.login(f"{EMAIL_ID}_test@yepcord.ml", "wrong_password") + await User.y.login(f"{EMAIL_ID}_test@yepcord.ml", "wrong_password") with pt.raises(InvalidDataErr): - await testCore.login(f"{EMAIL_ID}_test123@yepcord.ml", "test_passw0rd") + await User.y.login(f"{EMAIL_ID}_test123@yepcord.ml", "test_passw0rd") @pt.mark.asyncio @@ -125,13 +123,11 @@ def changeUserData(data: UserData) -> None: @pt.mark.asyncio # May be removed, but not removing now because other tests depend on VARS["user_id"] + 100000 -async def test_changeUserDiscriminator_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - - await testCore.register(VARS["user_id"] + 100000, "Test", f"{EMAIL_ID}_test1@yepcord.ml", "password", - "2000-01-01") - await testCore.register(VARS["user_id"] + 200000, "Test", f"{EMAIL_ID}_test2@yepcord.ml", "password", - "2000-01-01") +async def test_changeUserDiscriminator_fail(): + user = await User.y.register("Test", f"{EMAIL_ID}_test1@yepcord.ml", "password", "2000-01-01") + VARS["user_id_100000"] = user.id + user = await User.y.register("Test", f"{EMAIL_ID}_test2@yepcord.ml", "password", "2000-01-01") + VARS["user_id_200000"] = user.id @pt.mark.asyncio # May be removed, but not removing now because other tests depend on VARS["user_id"] + 100000 + d @@ -160,22 +156,22 @@ async def test_getUserByUsername_fail(): @pt.mark.asyncio async def test_checkRelationShipAvailable_success(): - user1 = await User.get(id=VARS["user_id"] + 100000) - user2 = await User.get(id=VARS["user_id"] + 200000) + user1 = await User.get(id=VARS["user_id_100000"]) + user2 = await User.get(id=VARS["user_id_200000"]) assert await Relationship.utils.available(user1, user2) @pt.mark.asyncio async def test_reqRelationship_success(): - user1 = await User.get(id=VARS["user_id"] + 100000) - user2 = await User.get(id=VARS["user_id"] + 200000) + user1 = await User.get(id=VARS["user_id_100000"]) + user2 = await User.get(id=VARS["user_id_200000"]) assert await Relationship.utils.request(user2, user1) @pt.mark.asyncio async def test_checkRelationShipAvailable_fail(): - user1 = await User.get(id=VARS["user_id"] + 100000) - user2 = await User.get(id=VARS["user_id"] + 200000) + user1 = await User.get(id=VARS["user_id_100000"]) + user2 = await User.get(id=VARS["user_id_200000"]) with pt.raises(InvalidDataErr): await Relationship.utils.available(user1, user2, raise_=True) @@ -183,11 +179,11 @@ async def test_checkRelationShipAvailable_fail(): @pt.mark.asyncio async def test_getRelationships_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore - user = await User.get(id=VARS["user_id"] + 100000) + user = await User.get(id=VARS["user_id_100000"]) rels = await testCore.getRelationships(user) assert len(rels) == 1 assert rels[0]["type"] == 3 - assert rels[0]["user_id"] == str(VARS["user_id"] + 200000) + assert rels[0]["user_id"] == str(VARS["user_id_200000"]) @pt.mark.asyncio @@ -200,45 +196,45 @@ async def test_getRelationships_fail(testCore: Coroutine[Any, Any, Core]): @pt.mark.asyncio async def test_getRelationship_success(): - assert await Relationship.utils.exists(await User.get(id=VARS["user_id"] + 100000), - await User.get(id=VARS["user_id"] + 200000)) + assert await Relationship.utils.exists(await User.get(id=VARS["user_id_100000"]), + await User.get(id=VARS["user_id_200000"])) @pt.mark.asyncio async def test_getRelationship_fail(): assert not await Relationship.utils.exists(await User.get(id=VARS["user_id"] + 100001), - await User.get(id=VARS["user_id"] + 200000)) + await User.get(id=VARS["user_id_200000"])) assert not await Relationship.utils.exists(await User.get(id=VARS["user_id"] + 100001), - await User.get(id=VARS["user_id"] + 200001)) + await User.get(id=VARS[f"user_id_200001"])) @pt.mark.asyncio async def test_getRelatedUsers_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore - user = await User.get(id=VARS["user_id"] + 100000) + user = await User.get(id=VARS["user_id_100000"]) users = await testCore.getRelatedUsers(user, True) assert len(users) == 1 - assert users[0] == VARS["user_id"] + 200000 + assert users[0] == VARS["user_id_200000"] @pt.mark.asyncio async def test_accRelationship_success(): - user = await User.get(id=VARS["user_id"] + 100000) - user2 = await User.get(id=VARS["user_id"] + 200000) + user = await User.get(id=VARS["user_id_100000"]) + user2 = await User.get(id=VARS["user_id_200000"]) await Relationship.utils.accept(user2, user) - rel = await Relationship.utils.get(await User.get(id=VARS["user_id"] + 100000), - await User.get(id=VARS["user_id"] + 200000)) + rel = await Relationship.utils.get(await User.get(id=VARS["user_id_100000"]), + await User.get(id=VARS["user_id_200000"])) assert rel is not None assert rel.type == RelationshipType.FRIEND @pt.mark.asyncio async def test_delRelationship_success(): - user = await User.get(id=VARS["user_id"] + 100000) - user2 = await User.get(id=VARS["user_id"] + 200000) + user = await User.get(id=VARS["user_id_100000"]) + user2 = await User.get(id=VARS["user_id_200000"]) await Relationship.utils.delete(user, user2) - assert not await Relationship.utils.exists(await User.get(id=VARS["user_id"] + 100000), - await User.get(id=VARS["user_id"] + 200000)) + assert not await Relationship.utils.exists(await User.get(id=VARS["user_id_100000"]), + await User.get(id=VARS["user_id_200000"])) @pt.mark.asyncio @@ -280,7 +276,7 @@ async def test_getMfaFromTicket_success(testCore: Coroutine[Any, Any, Core]): await settings.save(update_fields=["mfa"]) try: - await testCore.login(user.email, "test_passw0rd") + await User.y.login(user.email, "test_passw0rd") assert False except MfaRequiredErr as e: ticket = b64encode(dumps([e.uid, "login"])) + f".{e.sid}.{e.sig}" @@ -316,8 +312,8 @@ async def test_verifyUserMfaNonce(): @pt.mark.asyncio async def test_getDMChannelOrCreate_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore - user1 = await User.y.get(VARS["user_id"] + 100000) - user2 = await User.y.get(VARS["user_id"] + 200000) + user1 = await User.y.get(VARS["user_id_100000"]) + user2 = await User.y.get(VARS["user_id_200000"]) channel = await testCore.getDMChannelOrCreate(user1, user2) channel2 = await testCore.getDMChannelOrCreate(user1, user2) assert channel is not None @@ -381,7 +377,7 @@ def test_config(): @pt.mark.asyncio async def test_gw_channel_filter(): user = await User.y.get(VARS["user_id"]) - user2 = await User.y.get(VARS["user_id"] + 100000) + user2 = await User.y.get(VARS["user_id_100000"]) guild = await Guild.create(owner=user, name="test") await GuildMember.create(guild=guild, user=user) role = await Role.create(id=guild.id, name="@everyone", guild=guild) diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index b6d73f9..3e1c110 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -39,21 +39,22 @@ @auth.post("/register", body_cls=Register) @captcha async def register(data: Register): - loc = getCore().getLanguageCode(request.remote_addr, request.accept_languages.best_match(LOCALES, "en-US")) - sess = await getCore().register(Snowflake.makeId(), data.username, data.email, data.password, data.date_of_birth, - loc) - return {"token": sess.token} + locale = getCore().getLanguageCode(request.remote_addr, request.accept_languages.best_match(LOCALES, "en-US")) + user = await User.y.register(data.username, data.email, data.password, data.date_of_birth, locale) + session = await Session.Y.create(user) + + return {"token": session.token} @auth.post("/login", body_cls=Login) @captcha async def login(data: Login): - sess = await getCore().login(data.login, data.password) - user = sess.user - sett = await user.settings + session = await User.y.login(data.login, data.password) + user = session.user + settings = await user.settings return { - "token": sess.token, - "user_settings": {"locale": sett.locale, "theme": sett.theme}, + "token": session.token, + "user_settings": {"locale": settings.locale, "theme": settings.theme}, "user_id": str(user.id) } diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index aad36bb..80d4605 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -19,7 +19,6 @@ from asyncio import get_event_loop # noinspection PyPackageRequirements from contextvars import Context -from datetime import datetime from hashlib import sha256 from hmac import new from json import loads as jloads, dumps as jdumps @@ -29,7 +28,6 @@ from typing import Optional, Union import maxminddb -from bcrypt import hashpw, gensalt, checkpw from tortoise.expressions import Q, Subquery from tortoise.functions import Count from tortoise.transactions import atomic @@ -39,13 +37,13 @@ from .classes.singleton import Singleton from .config import Config from .enums import ChannelType, GUILD_CHANNELS -from .errors import InvalidDataErr, MfaRequiredErr, Errors -from .models import User, UserData, UserSettings, Session, Relationship, Channel, Message, ReadState, FrecencySettings, \ - Emoji, Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, PermissionOverwrite, GuildBan, AuditLogEntry, \ +from .errors import InvalidDataErr, Errors +from .models import User, UserSettings, Session, Relationship, Channel, Message, ReadState, FrecencySettings, Emoji, \ + Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, PermissionOverwrite, GuildBan, AuditLogEntry, \ Webhook, HiddenDmChannel, MfaCode, Role, GuildEvent, ThreadMember from .snowflake import Snowflake from .storage import getStorage -from .utils import b64encode, b64decode, int_size, NoneType +from .utils import b64encode, b64decode, NoneType from ..gateway.events import DMChannelCreateEvent @@ -55,72 +53,12 @@ def __init__(self, key=None): self.key = key if key and len(key) >= 16 and isinstance(key, bytes) else urandom(32) self.ipdb = None - def prepPassword(self, password: str, uid: int) -> bytes: - """ - Prepares user password for hashing - :param password: - :param uid: - :return: - """ - password = password.encode("utf8") - password += uid.to_bytes(int_size(uid), "big") - return password.replace(b"\x00", b'') - - def hashPassword(self, uid: int, password: str) -> str: - password = self.prepPassword(password, uid) - return hashpw(password, gensalt(Config.BCRYPT_ROUNDS)).decode("utf8") - - def generateSessionSignature(self) -> str: - return b64encode(urandom(32)) - async def getRandomDiscriminator(self, login: str) -> Optional[int]: for _ in range(5): d = randint(1, 9999) if not await User.y.getByUsername(login, d): return d - @atomic() - async def register(self, uid: int, login: str, email: Optional[str], password: str, birth: str, - locale: str = "en-US", - invite: Optional[str] = None) -> Session: - birth = datetime.strptime(birth, "%Y-%m-%d") - - email = email.lower() - if await self.getUserByEmail(email): - raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", - "message": "Email address already registered."}})) - password = self.hashPassword(uid, password) - signature = self.generateSessionSignature() - - discriminator = await self.getRandomDiscriminator(login) - if discriminator is None: - raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "USERNAME_TOO_MANY_USERS", - "message": "Too many users have this username, " - "please try another."}})) - - user = await User.create(id=uid, email=email, password=password) - await UserData.create(id=uid, user=user, birth=birth, username=login, discriminator=discriminator) - await UserSettings.create(id=uid, user=user, locale=locale) - - session = await Session.create(id=Snowflake.makeId(), user=user, signature=signature) - await self.sendVerificationEmail(user) - return session - - async def login(self, email: str, password: str) -> Session: - email = email.strip().lower() - user = await User.get_or_none(email=email) - if not user or not checkpw(self.prepPassword(password, user.id), user.password.encode("utf8")): - raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "INVALID_LOGIN", - "message": "Invalid login or password."}, - "password": {"code": "INVALID_LOGIN", - "message": "Invalid login or password."}})) - settings = await user.settings - if settings.mfa: - _sid = urandom(12) - sid = int.from_bytes(_sid, "big") - raise MfaRequiredErr(user.id, b64encode(_sid), self.generateMfaTicketSignature(user, sid)) - return await Session.Y.create(user) - async def getRelationships(self, user: User, with_data=False) -> list[dict]: rels = [] rel: Relationship diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 7a311c1..0bb2a76 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -19,20 +19,22 @@ from __future__ import annotations from datetime import datetime +from os import urandom from random import randint, choice from typing import Optional from bcrypt import checkpw, hashpw, gensalt from tortoise import fields +from tortoise.transactions import atomic import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model from ..classes.other import MFA from ..config import Config from ..ctx import getCore -from ..errors import InvalidDataErr, Errors +from ..errors import InvalidDataErr, Errors, MfaRequiredErr from ..snowflake import Snowflake -from ..utils import int_size +from ..utils import int_size, b64encode async def _get_free_discriminator(login: str) -> Optional[int]: @@ -58,6 +60,58 @@ async def getByUsername(username: str, discriminator: int) -> Optional[User]: def prepare_password(password: str, user_id: int) -> bytes: return password.encode("utf8") + user_id.to_bytes(int_size(user_id), "big").replace(b"\x00", b'') + @staticmethod + def hash_password(password: str, user_id: int) -> str: + password = UserUtils.prepare_password(password, user_id) + return hashpw(password, gensalt(Config.BCRYPT_ROUNDS)).decode("utf8") + + @staticmethod + @atomic() + async def register( + login: str, email: Optional[str], password: str, birth: str, locale: str = "en-US", + invite: Optional[str] = None + ) -> User: + user_id = Snowflake.makeId() + + birth = datetime.strptime(birth, "%Y-%m-%d") + + email = email.lower() + if await User.exists(email=email): + raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", + "message": "Email address already registered."}})) + + discriminator = await _get_free_discriminator(login) + if discriminator is None: + raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "USERNAME_TOO_MANY_USERS", + "message": "Too many users have this username, " + "please try another."}})) + + user = await User.create(id=user_id, email=email, password=UserUtils.hash_password(password, user_id)) + await models.UserData.create(id=user_id, user=user, birth=birth, username=login, discriminator=discriminator) + await models.UserSettings.create(id=user_id, user=user, locale=locale) + + await getCore().sendVerificationEmail(user) + return user + + @staticmethod + async def login(email: str, password: str) -> models.Session: + email = email.strip().lower() + user = await User.get_or_none(email=email) + if not user or not checkpw(UserUtils.prepare_password(password, user.id), user.password.encode("utf8")): + raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "INVALID_LOGIN", + "message": "Invalid login or password."}, + "password": {"code": "INVALID_LOGIN", + "message": "Invalid login or password."}})) + settings = await user.settings + if settings.mfa: + sid = urandom(12) + raise MfaRequiredErr( + user.id, + b64encode(sid), + getCore().generateMfaTicketSignature(user, int.from_bytes(sid, "big")), + ) + return await models.Session.Y.create(user) + class User(Model): y = UserUtils @@ -142,8 +196,7 @@ def check_password(self, password: str) -> bool: return checkpw(self.y.prepare_password(password, self.id), self.password.encode("utf8")) def hash_new_password(self, password: str) -> str: - password = self.y.prepare_password(password, self.id) - return hashpw(password, gensalt(Config.BCRYPT_ROUNDS)).decode("utf8") + return self.y.hash_password(password, self.id) async def change_password(self, new_password: str) -> None: self.password = self.hash_new_password(new_password) @@ -191,7 +244,7 @@ async def clear_backup_codes(self) -> None: await models.MfaCode.filter(user=self).delete() async def get_backup_codes(self) -> list[str]: - return [code.code async for code in models.MfaCode.filter(user=self).limit(10)] + return [code.code for code in await models.MfaCode.filter(user=self).limit(10)] async def use_backup_code(self, code: str) -> bool: if (code := await models.MfaCode.get_or_none(user=self, code=code, used=False)) is None: From f658d83cd4bda463a2d1d2f680b0cf188ebb2e71 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Wed, 4 Sep 2024 15:16:56 +0300 Subject: [PATCH 04/26] move some channel, guild member and role methods --- tests/api/utils.py | 9 +- yepcord/rest_api/routes/auth.py | 1 - yepcord/rest_api/routes/channels.py | 4 +- yepcord/rest_api/routes/invites.py | 4 +- yepcord/rest_api/routes/users.py | 1 - yepcord/rest_api/routes/users_me.py | 3 +- yepcord/rest_api/y_blueprint.py | 2 +- yepcord/yepcord/core.py | 128 +++++++++---------------- yepcord/yepcord/models/channel.py | 8 ++ yepcord/yepcord/models/guild_event.py | 3 +- yepcord/yepcord/models/guild_member.py | 15 +-- yepcord/yepcord/models/invite.py | 7 ++ yepcord/yepcord/models/session.py | 2 +- yepcord/yepcord/models/user.py | 19 +++- yepcord/yepcord/mq_broker.py | 6 +- yepcord/yepcord/utils.py | 2 +- 16 files changed, 103 insertions(+), 111 deletions(-) diff --git a/tests/api/utils.py b/tests/api/utils.py index d9a4e44..4ed79e1 100644 --- a/tests/api/utils.py +++ b/tests/api/utils.py @@ -3,7 +3,7 @@ from contextlib import asynccontextmanager from datetime import datetime, timedelta from hashlib import sha256 -from typing import Optional, Union +from typing import Optional, Union, Awaitable, Callable from quart.typing import TestWebsocketConnectionProtocol @@ -16,6 +16,9 @@ TestClientType = _app.test_client_class +#User.y.hash_password = lambda password, user_id: password +#User.check_password = lambda self, password: self.password == password + async def create_user(app: TestClientType, email: str, password: str, username: str, *, exp_code: int=200) -> Optional[str]: response = await app.post('/api/v9/auth/register', json={ @@ -390,7 +393,7 @@ def __init__(self, token: str): self.heartbeatTask: Optional[asyncio.Task] = None self.mainTask: Optional[asyncio.Task] = None - self.handlers = { + self.handlers: dict[int, Callable[[TestWebsocketConnectionProtocol, Optional[dict]], Awaitable[None]]] = { GatewayOp.HELLO: self.handle_hello } self.listeners: list[GatewayClient.EventListener] = [] @@ -405,7 +408,7 @@ async def run(self, ws: TestWebsocketConnectionProtocol, task=True) -> None: return while self.running: - msg = await ws.receive_json() + msg: dict = await ws.receive_json() if msg["op"] in self.handlers: await self.handlers[msg["op"]](ws, msg.get("data")) diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index 3e1c110..b1fe07a 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -29,7 +29,6 @@ from ...yepcord.ctx import getCore, getGw from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.models import Session, User -from ...yepcord.snowflake import Snowflake from ...yepcord.utils import LOCALES, b64decode # Base path is /api/vX/auth diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index d88f600..010bb52 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -183,8 +183,8 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): if channel.type == ChannelType.DM: other_user = await channel.other_user(user) - if await getCore().isDmChannelHidden(other_user, channel): - await getCore().unhideDmChannel(other_user, channel) + if await channel.dm_is_hidden(other_user): + await channel.dm_unhide(other_user) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": other_user.id}), user_ids=[other_user.id]) await getCore().sendMessage(message) diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index a0dab53..7012888 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -66,7 +66,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): user_ids=[recipient.id for recipient in recipients]) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": user.id}), user_ids=[user.id]) - await getCore().useInvite(invite) + await invite.use() elif channel.type in (ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE, ChannelType.GUILD_NEWS): inv = await invite.ds_json() for excl in ["max_age", "max_uses", "created_at"]: # Remove excluded fields @@ -88,7 +88,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=sys_channel, permissions=GuildPermissions.VIEW_CHANNEL) - await getCore().useInvite(invite) + await invite.use() await getGw().dispatchSub([user.id], guild_id=guild.id) return inv diff --git a/yepcord/rest_api/routes/users.py b/yepcord/rest_api/routes/users.py index c21431c..1f58916 100644 --- a/yepcord/rest_api/routes/users.py +++ b/yepcord/rest_api/routes/users.py @@ -19,7 +19,6 @@ from ..dependencies import DepUser from ..models.users import UserProfileQuery from ..y_blueprint import YBlueprint -from ...yepcord.ctx import getCore from ...yepcord.models import User # Base path is /api/vX/users diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index f932dde..e2d9815 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -17,7 +17,6 @@ """ from base64 import b64encode as _b64encode, b64decode as _b64decode -from random import choice from time import time from typing import Union, Optional @@ -426,7 +425,7 @@ async def delete_user(data: DeleteRequest, user: User = DepUser): raise InvalidDataErr(400, Errors.make(50018)) if await Guild.exists(owner=user) or await Channel.exists(owner=user, type=ChannelType.GROUP_DM): raise InvalidDataErr(400, Errors.make(40011)) - await getCore().deleteUser(user) + await user.y_delete() await getGw().dispatch(UserDeleteEvent(user.id), user_ids=[user.id]) return "", 204 diff --git a/yepcord/rest_api/y_blueprint.py b/yepcord/rest_api/y_blueprint.py index b44221d..392e7a5 100644 --- a/yepcord/rest_api/y_blueprint.py +++ b/yepcord/rest_api/y_blueprint.py @@ -17,7 +17,7 @@ """ from functools import wraps -from typing import Any, Callable, Optional, Awaitable +from typing import Any, Callable, Optional from fast_depends import inject from flask.sansio.scaffold import T_route, setupmethod diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index 80d4605..19a6a3c 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -38,9 +38,9 @@ from .config import Config from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors -from .models import User, UserSettings, Session, Relationship, Channel, Message, ReadState, FrecencySettings, Emoji, \ +from .models import User, Relationship, Channel, Message, ReadState, Emoji, \ Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, PermissionOverwrite, GuildBan, AuditLogEntry, \ - Webhook, HiddenDmChannel, MfaCode, Role, GuildEvent, ThreadMember + Webhook, Role, GuildEvent, ThreadMember from .snowflake import Snowflake from .storage import getStorage from .utils import b64encode, b64decode, NoneType @@ -63,15 +63,14 @@ async def getRelationships(self, user: User, with_data=False) -> list[dict]: rels = [] rel: Relationship for rel in await (Relationship.filter(Q(from_user=user) | Q(to_user=user)) - .select_related("from_user", "to_user").all()): + .select_related("from_user", "to_user")): if (rel_json := await rel.ds_json(user, with_data)) is not None: rels.append(rel_json) return rels async def getRelatedUsers(self, user: User, only_ids=False) -> list: users = [] - for r in await (Relationship.filter(Q(from_user=user) | Q(to_user=user)).select_related("from_user", "to_user") - .all()): + for r in await Relationship.filter(Q(from_user=user) | Q(to_user=user)).select_related("from_user", "to_user"): other_user = r.other_user(user) if only_ids: users.append(other_user.id) @@ -163,8 +162,8 @@ async def getDMChannelOrCreate(self, user1: User, user2: User) -> Channel: await channel.recipients.add(user2) return await Channel.get(id=channel.id) - if await self.isDmChannelHidden(user1, channel): - await self.unhideDmChannel(user1, channel) + if await channel.dm_is_hidden(user1): + await channel.dm_unhide(user1) await ctx.getGw().dispatch(DMChannelCreateEvent(channel), user_ids=[user1.id]) return await self.setLastMessageIdForChannel(channel) @@ -180,9 +179,11 @@ async def getChannelMessagesCount(self, channel: Channel) -> int: return await Message.filter(channel=channel).count() async def getPrivateChannels(self, user: User, with_hidden: bool = False) -> list[Channel]: - channels = await Channel.filter(recipients__id=user.id).select_related("owner").all() + channels = await Channel.filter(recipients__id=user.id).select_related("owner") channels = [ - channel for channel in channels if not with_hidden and not await self.isDmChannelHidden(user, channel) + channel + for channel in channels + if not with_hidden and not await channel.dm_is_hidden(user) ] return [await self.setLastMessageIdForChannel(channel) for channel in channels] @@ -192,7 +193,7 @@ async def getChannelMessages(self, channel: Channel, limit: int, before: int = 0 if before: id_filter["id__lt"] = before messages = await (Message.filter(channel=channel, ephemeral=False, **id_filter) .select_related(*Message.DEFAULT_RELATED) - .order_by("-id").limit(limit).all()) + .order_by("-id").limit(limit)) return messages async def getMessage(self, channel: Channel, message_id: int) -> Optional[Message]: @@ -239,7 +240,7 @@ async def setReadState(self, user: User, channel: Channel, count: int, last: int async def getReadStatesJ(self, user: User) -> list: states = [] st: ReadState - for st in await ReadState.filter(user=user).select_related("channel", "user").all(): + for st in await ReadState.filter(user=user).select_related("channel", "user"): states.append(await st.ds_json()) return states @@ -250,7 +251,7 @@ async def getUserByChannelId(self, channel_id: int, user_id: int) -> Optional[Us async def getUserByChannel(self, channel: Channel, user_id: int) -> Optional[User]: if channel.type in (ChannelType.DM, ChannelType.GROUP_DM): - if await Channel.filter(id=channel.id, recipients__id__in=[user_id]).select_related("recipients").exists(): + if await Channel.exists(id=channel.id, recipients__id__in=[user_id]): return await User.y.get(user_id) elif channel.type in GUILD_CHANNELS: return await self.getGuildMember(channel.guild, user_id) @@ -294,7 +295,9 @@ async def changeUserEmail(self, user: User, email: str) -> None: # TODO if await self.getUserByEmail(email): raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", "message": "Email address already registered."}})) - await user.update(email=email, verified=False) + user.email = email + user.verified = False + await user.save() async def mfaNonceToCode(self, nonce: str) -> Optional[str]: if not (payload := JWT.decode(nonce, self.key)): @@ -354,7 +357,8 @@ async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[l .order_by("-id").limit(25)) if "offset" in search_filter: query = query.offset(search_filter["offset"]) - messages = await query.all() + + messages = await query count = await query.count() return messages, count @@ -456,22 +460,22 @@ async def getRoles(self, guild: Guild, exclude_default=False) -> list[Role]: query = Role.filter(guild=guild).select_related("guild") if exclude_default: query = query.exclude(id=guild.id) - return await query.all() + return await query async def getGuildMember(self, guild: Guild, user_id: int) -> Optional[GuildMember]: return await GuildMember.get_or_none(guild=guild, user__id=user_id).select_related("user", "guild", "guild__owner") async def getGuildMembers(self, guild: Guild) -> list[GuildMember]: - return await GuildMember.filter(guild=guild).select_related("user").all() + return await GuildMember.filter(guild=guild).select_related("user") async def getGuildChannels(self, guild: Guild) -> list[Channel]: return await Channel.filter(guild=guild) \ .exclude(type__in=[ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD])\ - .select_related("guild", "parent").all() + .select_related("guild", "parent") async def getUserGuilds(self, user: User) -> list[Guild]: - return [member.guild for member in await GuildMember.filter(user=user).select_related("guild", "guild__owner").all()] + return [member.guild for member in await GuildMember.filter(user=user).select_related("guild", "guild__owner")] async def getGuildMemberCount(self, guild: Guild) -> int: return await GuildMember.filter(guild=guild).count() @@ -480,7 +484,7 @@ async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") async def getEmojis(self, guild_id: int) -> list[Emoji]: - return await Emoji.filter(guild__id=guild_id).select_related("user").all() + return await Emoji.filter(guild__id=guild_id).select_related("user") async def getEmoji(self, emoji_id: int) -> Optional[Emoji]: return await Emoji.get_or_none(id=emoji_id).select_related("guild") @@ -497,14 +501,14 @@ async def getEmojiByReaction(self, reaction: str) -> Optional[Emoji]: return return emoji if emoji.name == name else None - async def getGuildBan(self, guild: Guild, user_id: int) -> Optional[GuildMember]: + async def getGuildBan(self, guild: Guild, user_id: int) -> Optional[GuildBan]: return await GuildBan.get_or_none(guild=guild, user__id=user_id) async def bulkDeleteGuildMessagesFromBanned( self, guild: Guild, user_id: int, after_id: int ) -> dict[Channel, list[int]]: messages = await (Message.filter(guild=guild, author__id=user_id, id__gt=after_id).select_related("channel") - .limit(500).all()) + .limit(500)) result = {} messages_ids = [] for message in messages: @@ -518,11 +522,11 @@ async def bulkDeleteGuildMessagesFromBanned( return result async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[str, str]]: - user_guilds_member = await GuildMember.filter(user=user).select_related("guild").all() + user_guilds_member = await GuildMember.filter(user=user).select_related("guild") user_guild_ids = [member.guild.id for member in user_guilds_member] user_guilds_member = {member.guild.id: member for member in user_guilds_member} - current_user_guilds_member = await GuildMember.filter(user=current_user).select_related("guild").all() + current_user_guilds_member = await GuildMember.filter(user=current_user).select_related("guild") current_user_guild_ids = [member.guild.id for member in current_user_guilds_member] mutual_guilds_ids = set(user_guild_ids) & set(current_user_guild_ids) @@ -533,12 +537,6 @@ async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[st return mutual_guilds_json - async def getMemberRolesIds(self, member: GuildMember, include_default: bool = False) -> list[int]: - roles_ids = [role.id for role in await member.roles.all()] - if include_default: - roles_ids.append(member.guild.id) - return roles_ids - async def setMemberRolesFromList(self, member: GuildMember, roles: list[Role]) -> tuple[list, list]: current_roles = await member.roles.all() add = [] @@ -554,21 +552,14 @@ async def setMemberRolesFromList(self, member: GuildMember, roles: list[Role]) - return add, remove - async def getMemberRoles(self, member: GuildMember, include_default: bool = False) -> list[Role]: - roles = await member.roles.all() - if include_default: - roles.append(await Role.get(id=member.guild.id)) - - roles.sort(key=lambda r: r.position) - return roles - + # noinspection PyUnusedLocal async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids: list[int]) -> list[GuildMember]: # noinspection PyUnresolvedReferences return await GuildMember.filter( Q(guild=guild) & (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) #& #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) - ).select_related("user").limit(limit).all() + ).select_related("user").limit(limit) async def getPermissionOverwrite(self, channel: Channel, target: Union[Role, User]) -> Optional[PermissionOverwrite]: kw = {"target_role": target} if isinstance(target, Role) else {"target_user": target} @@ -581,34 +572,27 @@ async def getPermissionOverwriteUnk(self, channel: Channel, target_id: int) -> O .select_related("channel", "channel__guild", "target_role", "target_user")) async def getPermissionOverwrites(self, channel: Channel) -> list[PermissionOverwrite]: - return await PermissionOverwrite.filter(channel=channel).select_related("target_role", "target_user").all() + return await PermissionOverwrite.filter(channel=channel).select_related("target_role", "target_user") async def getOverwritesForMember(self, channel: Channel, member: GuildMember) -> list[PermissionOverwrite]: - roles = await self.getMemberRoles(member, True) - roles.sort(key=lambda r: r.position) - roles = {role.id: role for role in roles} + role_ids = set(await member.roles.all().values_list("id", flat=True)) + role_ids.add(member.guild.id) + overwrites = await self.getPermissionOverwrites(channel) overwrites.sort(key=lambda r: r.type) - result = [] - for overwrite in overwrites: - if overwrite.target.id in roles or overwrite.target == member.user: - result.append(overwrite) - return result + return [ + overwrite + for overwrite in overwrites + if overwrite.target.id in role_ids or overwrite.target == member.user + ] async def getVanityCodeInvite(self, code: str) -> Optional[Invite]: if code is None: return return await Invite.get_or_none(vanity_code=code) - async def useInvite(self, invite: Invite) -> None: - if 0 < invite.max_uses <= invite.uses + 1: - await invite.delete() - else: - invite.uses += 1 - await invite.save(update_fields=["uses"]) - async def getAuditLogEntries(self, guild: Guild, limit: int, before: Optional[int] = None) -> list[AuditLogEntry]: before = {} if before is None else {"id__lt": before} - return await AuditLogEntry.filter(guild=guild, **before).select_related("guild", "user").limit(limit).all() + return await AuditLogEntry.filter(guild=guild, **before).select_related("guild", "user").limit(limit) async def getGuildTemplate(self, guild: Guild) -> Optional[GuildTemplate]: return await GuildTemplate.get_or_none(guild=guild).select_related("creator", "guild") @@ -628,51 +612,25 @@ async def setTemplateDirty(self, guild: Guild) -> None: async def getWebhook(self, webhook_id: int) -> Optional[Webhook]: return await Webhook.get_or_none(id=webhook_id).select_related("channel", "channel__guild", "user") - async def unhideDmChannel(self, user: User, channel: Channel) -> None: - await HiddenDmChannel.filter(user=user, channel=channel).delete() - - async def isDmChannelHidden(self, user: User, channel: Channel) -> bool: - return await HiddenDmChannel.filter(user=user, channel=channel).exists() - async def getGuildStickers(self, guild: Guild) -> list[Sticker]: - return await Sticker.filter(guild=guild).select_related("guild", "user").all() + return await Sticker.filter(guild=guild).select_related("guild", "user") async def getSticker(self, sticker_id: int) -> Optional[Sticker]: return await Sticker.get_or_none(id=sticker_id).select_related("guild", "user") - async def deleteUser(self, user: User) -> None: - await user.update(deleted=True, email=f"deleted_{user.id}@yepcord.ml", password="") - data = await user.data - await data.update(discriminator=0, username=f"Deleted User {hex(user.id)[2:]}", avatar=None, public_flags=0, - avatar_decoration=None) - await Session.filter(user=user).delete() - await Relationship.filter(Q(from_user=user) | Q(to_user=user)).delete() - await MfaCode.filter(user=user).delete() - await GuildMember.filter(user=user).delete() - await UserSettings.filter(user=user).delete() - await FrecencySettings.filter(user=user).delete() - await Invite.filter(inviter=user).delete() - await ReadState.filter(user=user).delete() - async def getGuildEvent(self, event_id: int) -> Optional[GuildEvent]: return await GuildEvent.get_or_none(id=event_id).select_related("channel", "guild", "creator") async def getGuildEvents(self, guild: Guild) -> list[GuildEvent]: - return await GuildEvent.filter(guild=guild).select_related("channel", "guild", "creator").all() - - #async def getSubscribedGuildEventIds(self, user: User, guild_id: int) -> list[int]: - # events_ids = [] - # for member in await GuildMember.filter(user=user, guild__id=guild_id).all(): - # events_ids.extend(await member.guildevents.filter().values_list("id", flat=True)) - # return events_ids + return await GuildEvent.filter(guild=guild).select_related("channel", "guild", "creator") async def getThreadMembers(self, thread: Channel, limit: int = 100) -> list[ThreadMember]: - return await ThreadMember.filter(channel=thread).select_related("user").limit(limit).all() + return await ThreadMember.filter(channel=thread).select_related("user").limit(limit) async def getThreadMember(self, thread: Channel, user_id: int) -> Optional[ThreadMember]: return await ThreadMember.get_or_none(channel=thread, user__id=user_id) - def getLanguageCode(self, ip: str, default: str="en-US") -> str: + def getLanguageCode(self, ip: str, default: str = "en-US") -> str: if self.ipdb is None and not os.path.exists("other/ip_database.mmdb"): return default if self.ipdb is None: diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 052cd24..2c55b56 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -179,3 +179,11 @@ async def other_user(self, current_user: models.User) -> Optional[models.User]: if self.type != ChannelType.DM: return return await self.recipients.filter(~Q(id=current_user.id)).get_or_none() + + async def dm_is_hidden(self, for_user: models.User) -> bool: + return await models.HiddenDmChannel.exists(user=for_user, channel=self) + + async def dm_unhide(self, for_user: models.User) -> None: + if self.type != ChannelType.DM: + return + await models.HiddenDmChannel.filter(user=for_user, channel=self).delete() diff --git a/yepcord/yepcord/models/guild_event.py b/yepcord/yepcord/models/guild_event.py index 9514933..69afe77 100644 --- a/yepcord/yepcord/models/guild_event.py +++ b/yepcord/yepcord/models/guild_event.py @@ -22,9 +22,8 @@ from tortoise import fields import yepcord.yepcord.models as models -from ..ctx import getCore -from ..enums import ScheduledEventEntityType from ._utils import SnowflakeField, Model +from ..enums import ScheduledEventEntityType class GuildEvent(Model): diff --git a/yepcord/yepcord/models/guild_member.py b/yepcord/yepcord/models/guild_member.py index 8733993..4589965 100644 --- a/yepcord/yepcord/models/guild_member.py +++ b/yepcord/yepcord/models/guild_member.py @@ -116,7 +116,7 @@ async def ds_json(self, with_user=True) -> dict: "is_pending": False, "pending": False, "premium_since": self.user.created_at.strftime("%Y-%m-%dT%H:%M:%S.000000+00:00"), - "roles": [str(role) for role in await getCore().getMemberRolesIds(self)], + "roles": [str(role_id) for role_id in await self.roles.all().values_list("id", flat=True)], "mute": self.mute, "deaf": self.deaf } @@ -130,13 +130,16 @@ async def ds_json(self, with_user=True) -> dict: def perm_checker(self) -> PermissionsChecker: return PermissionsChecker(self) - @property - async def roles_w_default(self) -> list[models.Role]: - return await getCore().getMemberRoles(self, True) + async def get_roles(self, with_default: bool = True) -> list[models.Role]: + roles = await self.roles.all() + if with_default: + roles.append(await models.Role.get(id=self.guild.id)) + + return sorted(roles, key=lambda r: r.position) @property async def top_role(self) -> models.Role: - roles = await self.roles_w_default + roles = await self.get_roles() return roles[-1] async def checkPermission(self, *check_permissions, channel: Optional[models.Channel] = None) -> None: @@ -147,6 +150,6 @@ async def permissions(self) -> int: if self.user == self.guild.owner: return 562949953421311 permissions = 0 - for role in await self.roles_w_default: + for role in await self.get_roles(): permissions |= role.permissions return permissions diff --git a/yepcord/yepcord/models/invite.py b/yepcord/yepcord/models/invite.py index 75b56f2..1727b61 100644 --- a/yepcord/yepcord/models/invite.py +++ b/yepcord/yepcord/models/invite.py @@ -111,3 +111,10 @@ async def ds_json(self, with_counts: bool=False) -> dict: data["code"] = self.vanity_code return data + + async def use(self) -> None: + if 0 < self.max_uses <= self.uses + 1: + await self.delete() + else: + self.uses += 1 + await self.save(update_fields=["uses"]) diff --git a/yepcord/yepcord/models/session.py b/yepcord/yepcord/models/session.py index bf74547..2690b9f 100644 --- a/yepcord/yepcord/models/session.py +++ b/yepcord/yepcord/models/session.py @@ -19,7 +19,7 @@ from __future__ import annotations from os import urandom -from typing import Optional, Union +from typing import Optional from tortoise import fields diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 0bb2a76..408c165 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -25,6 +25,7 @@ from bcrypt import checkpw, hashpw, gensalt from tortoise import fields +from tortoise.expressions import Q from tortoise.transactions import atomic import yepcord.yepcord.models as models @@ -65,6 +66,7 @@ def hash_password(password: str, user_id: int) -> str: password = UserUtils.prepare_password(password, user_id) return hashpw(password, gensalt(Config.BCRYPT_ROUNDS)).decode("utf8") + # noinspection PyUnusedLocal @staticmethod @atomic() async def register( @@ -97,7 +99,7 @@ async def register( async def login(email: str, password: str) -> models.Session: email = email.strip().lower() user = await User.get_or_none(email=email) - if not user or not checkpw(UserUtils.prepare_password(password, user.id), user.password.encode("utf8")): + if not user or not user.check_password(password): raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "INVALID_LOGIN", "message": "Invalid login or password."}, "password": {"code": "INVALID_LOGIN", @@ -186,6 +188,7 @@ async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, return data + # noinspection PyMethodMayBeStatic async def get_another_user(self, user_id: int) -> User: # TODO: check for relationship, mutual guilds or mutual friends if (user := await User.y.get(user_id, False)) is None: # TODO: add test for nonexistent user @@ -252,3 +255,17 @@ async def use_backup_code(self, code: str) -> bool: code.used = True await code.save(update_fields=["used"]) return True + + async def y_delete(self) -> None: + await self.update(deleted=True, email=f"deleted_{self.id}@yepcord.ml", password="") + data = await self.data + await data.update(discriminator=0, username=f"Deleted User {hex(self.id)[2:]}", avatar=None, public_flags=0, + avatar_decoration=None) + await models.Session.filter(user=self).delete() + await models.Relationship.filter(Q(from_user=self) | Q(to_user=self)).delete() + await models.MfaCode.filter(user=self).delete() + await models.GuildMember.filter(user=self).delete() + await models.UserSettings.filter(user=self).delete() + await models.FrecencySettings.filter(user=self).delete() + await models.Invite.filter(inviter=self).delete() + await models.ReadState.filter(user=self).delete() diff --git a/yepcord/yepcord/mq_broker.py b/yepcord/yepcord/mq_broker.py index 3e058ae..5757f46 100644 --- a/yepcord/yepcord/mq_broker.py +++ b/yepcord/yepcord/mq_broker.py @@ -74,7 +74,7 @@ async def wait_forever(): self._server = None # pragma: no cover async def run(self): - asyncio.get_event_loop().create_task(self._run()) + _ = asyncio.get_event_loop().create_task(self._run()) async with timeout(5): await self._run_event.wait() self._run_event.clear() @@ -126,7 +126,7 @@ async def _handle_message(self, message: str) -> None: # pragma: no cover return channel = data["channel"] for handler in self._handlers.get(channel, []): - asyncio.get_running_loop().create_task(handler(data["message"])) + _ = asyncio.get_running_loop().create_task(handler(data["message"])) async def start(self) -> None: if self._connection is not None and not self._connection.closed: # pragma: no cover @@ -134,7 +134,7 @@ async def start(self) -> None: self._reinitialize() await self._try_run_server() - asyncio.get_running_loop().create_task(self._run_client()) + _ = asyncio.get_running_loop().create_task(self._run_client()) await self._run_event.wait() self._run_event.clear() diff --git a/yepcord/yepcord/utils.py b/yepcord/yepcord/utils.py index b6f77e8..2dc4be5 100644 --- a/yepcord/yepcord/utils.py +++ b/yepcord/yepcord/utils.py @@ -76,7 +76,7 @@ async def _wait_exec(coro_, seconds_): await asleep(seconds_) await coro_ - get_event_loop().create_task(_wait_exec(coro, seconds)) + _ = get_event_loop().create_task(_wait_exec(coro, seconds)) ping_regex = rcompile(r'<@((?:!|&)?\d{17,32})>') From 8d84fb2bd4e37b7fa3ded5d3500f09e3fbf671b1 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Thu, 5 Sep 2024 16:22:55 +0300 Subject: [PATCH 05/26] move guild creation methods, channel-message related methods --- tests/cdn/test_storage_and_cdn.py | 11 +- tests/test_core.py | 6 +- yepcord/rest_api/dependencies.py | 2 +- yepcord/rest_api/routes/applications.py | 4 +- yepcord/rest_api/routes/auth.py | 6 +- yepcord/rest_api/routes/channels.py | 28 ++-- yepcord/rest_api/routes/guilds.py | 23 +-- yepcord/rest_api/routes/interactions.py | 2 +- yepcord/rest_api/routes/invites.py | 6 +- yepcord/rest_api/routes/oauth2.py | 5 +- yepcord/rest_api/routes/users_me.py | 2 +- yepcord/rest_api/routes/webhooks.py | 1 - yepcord/rest_api/utils.py | 4 +- yepcord/yepcord/core.py | 205 +++-------------------- yepcord/yepcord/enums.py | 2 +- yepcord/yepcord/models/channel.py | 36 +++- yepcord/yepcord/models/guild.py | 101 ++++++++++- yepcord/yepcord/models/guild_template.py | 9 +- yepcord/yepcord/models/interaction.py | 2 +- yepcord/yepcord/models/invite.py | 7 +- yepcord/yepcord/models/message.py | 11 +- yepcord/yepcord/models/user.py | 29 +++- yepcord/yepcord/utils.py | 8 +- 23 files changed, 243 insertions(+), 267 deletions(-) diff --git a/tests/cdn/test_storage_and_cdn.py b/tests/cdn/test_storage_and_cdn.py index 382f631..5d9082e 100644 --- a/tests/cdn/test_storage_and_cdn.py +++ b/tests/cdn/test_storage_and_cdn.py @@ -27,7 +27,7 @@ from yepcord.yepcord.config import Config from yepcord.yepcord.core import Core from yepcord.yepcord.enums import StickerFormat, StickerType, ChannelType -from yepcord.yepcord.models import User, Sticker, Emoji, Channel, Message, Attachment +from yepcord.yepcord.models import User, Sticker, Emoji, Channel, Message, Attachment, Guild from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.storage import getStorage, _Storage from yepcord.yepcord.utils import getImage, b64decode @@ -243,9 +243,10 @@ async def test_sticker(storage: _Storage): client: TestClientType = app.test_client() user = await User.create(id=Snowflake.makeId(), email=f"test_{Snowflake.makeId()}@yepcord.ml", password="") - guild = await core.createGuild(Snowflake.makeId(), user, "test") - sticker = await Sticker.create(id=Snowflake.makeId(), guild=guild, name="test", user=user, - type=StickerType.GUILD, format=StickerFormat.PNG) + guild = await Guild.Y.create(user, "test") + sticker = await Sticker.create( + id=Snowflake.makeId(), guild=guild, name="test", user=user, type=StickerType.GUILD, format=StickerFormat.PNG + ) sticker_hash = await storage.setStickerFromBytesIO(sticker.id, getImage(YEP_IMAGE)) assert sticker_hash == "sticker" @@ -290,7 +291,7 @@ async def test_emoji(storage: _Storage): client: TestClientType = app.test_client() user = await User.create(id=Snowflake.makeId(), email=f"test_{Snowflake.makeId()}@yepcord.ml", password="") - guild = await core.createGuild(Snowflake.makeId(), user, "test") + guild = await Guild.Y.create(user, "test") emoji = await Emoji.create(id=Snowflake.makeId(), name="test", user=user, guild=guild) emoji_info = await storage.setEmojiFromBytesIO(emoji.id, getImage(YEP_IMAGE)) assert not emoji_info["animated"] diff --git a/tests/test_core.py b/tests/test_core.py index 4838ef9..063bb06 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -32,7 +32,7 @@ from yepcord.yepcord.errors import InvalidDataErr, MfaRequiredErr from yepcord.yepcord.gateway_dispatcher import GatewayDispatcher from yepcord.yepcord.models import User, UserData, Session, Relationship, Guild, Channel, Role, PermissionOverwrite, \ - GuildMember + GuildMember, Message from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.utils import b64decode, b64encode @@ -342,14 +342,14 @@ async def test_getChannel_fail(testCore: Coroutine[Any, Any, Core]): async def test_getLastMessageIdForChannel_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore channel = await testCore.getChannel(VARS["channel_id"]) - assert channel.last_message_id is None or channel.last_message_id > 0 + assert await channel.get_last_message_id() is None or await channel.get_last_message_id() > 0 @pt.mark.asyncio async def test_getChannelMessagesCount_success(testCore: Coroutine[Any, Any, Core]): testCore = await testCore channel = await testCore.getChannel(VARS["channel_id"]) - assert await testCore.getChannelMessagesCount(channel) == 0 + assert await Message.filter(channel=channel).count() == 0 @pt.mark.asyncio diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index c1aa0d4..ce60a39 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -106,7 +106,7 @@ async def depMessageO( if webhook: message = await Message.get_or_none(id=message, webhook_id=webhook.id).select_related(*Message.DEFAULT_RELATED) elif channel is not None and user is not None: - message = await getCore().getMessage(channel, message) + message = await channel.get_message(message) else: raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) diff --git a/yepcord/rest_api/routes/applications.py b/yepcord/rest_api/routes/applications.py index f54add9..a04edba 100644 --- a/yepcord/rest_api/routes/applications.py +++ b/yepcord/rest_api/routes/applications.py @@ -45,10 +45,10 @@ async def get_applications(user: User = DepUser): async def create_application(data: CreateApplication, user: User = DepUser): app_id = Snowflake.makeId() name = username = data.name - disc = await getCore().getRandomDiscriminator(username) + disc = await User.y.get_free_discriminator(username) if disc is None: username = f"{username}{app_id}" - disc = await getCore().getRandomDiscriminator(username) + disc = await User.y.get_free_discriminator(username) app = await Application.create(id=app_id, owner=user, name=name) bot_user = await User.create(id=app_id, email=f"bot_{app_id}", password="", is_bot=True) diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index b1fe07a..a758e7c 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -19,6 +19,7 @@ from json import loads as jloads from quart import request +from tortoise.exceptions import DoesNotExist from ..dependencies import DepSession, DepUser from ..models.auth import Register, Login, MfaLogin, ViewBackupCodes, VerifyEmail @@ -118,12 +119,11 @@ async def resend_verification_email(user: User = DepUser): async def verify_email(data: VerifyEmail): if not data.token: raise InvalidDataErr(400, Errors.make(50035, {"token": {"code": "TOKEN_INVALID", "message": "Invalid token."}})) - # noinspection PyPep8 try: email = jloads(b64decode(data.token.split(".")[0]).decode("utf8"))["email"] - except: + user = await User.get(email=email, verified=False) + except (ValueError, DoesNotExist): raise InvalidDataErr(400, Errors.make(50035, {"token": {"code": "TOKEN_INVALID", "message": "Invalid token."}})) - user = await getCore().getUserByEmail(email) await getCore().verifyEmail(user, data.token) await getGw().dispatch(UserUpdateEvent(user, await user.data, await user.settings), [user.id]) return {"token": (await Session.Y.create(user)).token, "user_id": str(user.id)} diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 010bb52..4ccd8bf 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -94,12 +94,10 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha if "name" in changed: message = await Message.create(id=Snowflake.makeId(), channel=channel, author=user, type=MessageType.CHANNEL_NAME_CHANGE, content=channel.name) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) if "icon" in changed: message = await Message.create(id=Snowflake.makeId(), channel=channel, author=user, type=MessageType.CHANNEL_ICON_CHANGE, content="") - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) elif channel.type in GUILD_CHANNELS: if "parent" in changes: @@ -123,7 +121,6 @@ async def delete_channel(user: User = DepUser, channel: Channel = DepChannel): elif channel.type == ChannelType.GROUP_DM: message = Message(id=Snowflake.makeId(), author=user, channel=channel, content="", type=MessageType.RECIPIENT_REMOVE, extra_data={"user": user.id}) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=channel) await channel.recipients.remove(user) await getGw().dispatch(ChannelRecipientRemoveEvent(channel.id, (await user.data).ds_json), @@ -163,7 +160,7 @@ async def get_messages(query_args: GetMessagesQuery, user: User = DepUser, chann if channel.guild is not None: member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) - messages = await channel.messages(**query_args.model_dump()) + messages = await channel.get_messages(**query_args.model_dump()) messages = [await message.ds_json(user_id=user.id) for message in messages] return messages @@ -187,7 +184,6 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): await channel.dm_unhide(other_user) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": other_user.id}), user_ids=[other_user.id]) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel, permissions=GuildPermissions.VIEW_CHANNEL) await getCore().setReadState(user, channel, 0, message.id) @@ -244,10 +240,13 @@ async def send_message_ack(data: MessageAck, message: int, user: User = DepUser, await getCore().setReadState(user, channel, ct, message.id) await getGw().sendMessageAck(user.id, channel.id, message.id, ct, True) else: - ct = len(await getCore().getChannelMessages(channel, 99, channel.last_message_id, message.id)) - await getCore().setReadState(user, channel, ct, message.id) - await getGw().dispatch(MessageAckEvent({"version_id": 1, "message_id": str(message.id), - "channel_id": str(channel.id)}), user_ids=[user.id]) + count = await Message.filter( + channel=channel, ephemeral=False, id__gt=message.id, id__lt=await channel.get_last_message_id(), + ).count() + await getCore().setReadState(user, channel, count, message.id) + await getGw().dispatch(MessageAckEvent({ + "version_id": 1, "message_id": str(message.id), "channel_id": str(channel.id), + }), user_ids=[user.id]) return {"token": None} @@ -280,9 +279,10 @@ async def add_recipient(target_user: int, user: User = DepUser, channel: Channel elif channel.type == ChannelType.GROUP_DM: recipients = await channel.recipients.all() if target_user not in recipients and len(recipients) < 10: - message = await Message.create(id=Snowflake.makeId(), author=user, channel=channel, content="", - type=MessageType.RECIPIENT_ADD, extra_data={"user": target_user.id}) - await getCore().sendMessage(message) + message = await Message.create( + id=Snowflake.makeId(), author=user, channel=channel, content="", type=MessageType.RECIPIENT_ADD, + extra_data={"user": target_user.id} + ) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) await channel.recipients.add(target_user) target_user_data = await target_user.data @@ -304,7 +304,6 @@ async def delete_recipient(target_user: int, user: User = DepUser, channel: Chan if target_user in recipients: msg = await Message.create(id=Snowflake.makeId(), author=user, channel=channel, content="", type=MessageType.RECIPIENT_REMOVE, extra_data={"user": target_user.id}) - await getCore().sendMessage(msg) await getGw().dispatch(MessageCreateEvent(await msg.ds_json()), channel=msg.channel) await channel.recipients.remove(target_user) target_user_data = await target_user.data @@ -334,7 +333,6 @@ async def pin_message(user: User = DepUser, channel: Channel = DepChannel, messa message_reference=message_ref, guild=channel.guild ) - await getCore().sendMessage(msg) await getGw().dispatch(MessageCreateEvent(await msg.ds_json()), channel=msg.channel, permissions=GuildPermissions.VIEW_CHANNEL) return "", 204 @@ -583,8 +581,6 @@ async def create_thread(data: CreateThread, user: User = DepUser, channel: Chann await message.update(thread=thread, flags=message.flags | MessageFlags.HAS_THREAD) await getGw().dispatch(MessageUpdateEvent(await message.ds_json()), channel=message.channel, permissions=GuildPermissions.VIEW_CHANNEL) - await getCore().sendMessage(thread_message) - await getCore().sendMessage(thread_create_message) return await thread.ds_json() diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index df47c7c..264e1cb 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -53,12 +53,13 @@ @guilds.post("/", strict_slashes=False, body_cls=GuildCreate, allow_bots=True) async def create_guild(data: GuildCreate, user: User = DepUser): - guild_id = Snowflake.makeId() + guild = await Guild.Y.create(user, data.name) if data.icon: img = getImage(data.icon) - if h := await getCDNStorage().setGuildIconFromBytesIO(guild_id, img): - data.icon = h - guild = await getCore().createGuild(guild_id, user, **data.model_dump(exclude_defaults=True)) + if icon := await getCDNStorage().setGuildIconFromBytesIO(guild.id, img): + guild.icon = icon + await guild.save(update_fields=["icon"]) + await getGw().dispatch(GuildCreateEvent( await guild.ds_json(user_id=user.id, with_members=True, with_channels=True) ), user_ids=[user.id]) @@ -325,7 +326,7 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: target_member = await getCore().getGuildMember(guild, user_id) if target_member is not None and not await member.perm_checker.canKickOrBan(target_member): raise InvalidDataErr(403, Errors.make(50013)) - if await getCore().getGuildBan(guild, user_id) is not None: + if await GuildBan.exists(guild=guild, user__id=user_id): return "", 204 reason = request.headers.get("x-audit-log-reason", "") if target_member is not None: @@ -672,13 +673,13 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use except ValueError: raise InvalidDataErr(404, Errors.make(10057)) - guild_id = Snowflake.makeId() - if data.icon: - img = getImage(data.icon) - if h := await getCDNStorage().setGuildIconFromBytesIO(guild_id, img): - data.icon = h + guild = await Guild.Y.create_from_template(user, template, data.name) + + if data.icon and (img := getImage(data.icon)) is not None: + if icon := await getCDNStorage().setGuildIconFromBytesIO(guild.id, img): + guild.icon = icon + await guild.save(update_fields=["icon"]) - guild = await getCore().createGuildFromTemplate(guild_id, user, template, data.name, data.icon) await getGw().dispatch(GuildCreateEvent( await guild.ds_json(user_id=user.id, with_members=True, with_channels=True) ), user_ids=[user.id]) diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index 4f654f7..b42d16c 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -145,7 +145,7 @@ async def create_interaction(user: User = DepUser): message = None target_member = None if command.type == ApplicationCommandType.MESSAGE and \ - (message := await getCore().getMessage(channel, data.data.target_id)) is None: + (message := await channel.get_message(data.data.target_id)) is None: raise InvalidDataErr(404, Errors.make(10008)) if command.type == ApplicationCommandType.USER and \ (target_member := await getCore().getGuildMember(guild, data.data.target_id)) is None: diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index 7012888..42776b2 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -24,7 +24,7 @@ from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import ChannelType, GuildPermissions, MessageType from ...yepcord.errors import InvalidDataErr, Errors -from ...yepcord.models import Invite, User, Message, GuildMember +from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan from ...yepcord.snowflake import Snowflake # Base path is /api/vX/invites @@ -61,7 +61,6 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): await channel.recipients.add(user) await getGw().dispatch(ChannelRecipientAddEvent(channel.id, (await user.data).ds_json), user_ids=[recipient.id for recipient in recipients]) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), user_ids=[recipient.id for recipient in recipients]) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": user.id}), @@ -74,7 +73,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): del inv[excl] if not await getCore().getGuildMember(channel.guild, user.id): guild = channel.guild - if await getCore().getGuildBan(guild, user.id) is not None: + if await GuildBan.exists(guild=guild, user=user): raise InvalidDataErr(403, Errors.make(40007)) inv["new_member"] = True await GuildMember.create(id=Snowflake.makeId(), user=user, guild=guild) @@ -85,7 +84,6 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): id=Snowflake.makeId(), author=user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild ) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=sys_channel, permissions=GuildPermissions.VIEW_CHANNEL) await invite.use() diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index b32ed97..19050c3 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -31,7 +31,7 @@ from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType from ...yepcord.errors import Errors, InvalidDataErr from ...yepcord.models import User, Guild, GuildMember, Message, Role, AuditLogEntry, Application, Bot, Authorization, \ - Integration + Integration, GuildBan from ...yepcord.snowflake import Snowflake from ...yepcord.utils import b64decode @@ -108,7 +108,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori await member.checkPermission(GuildPermissions.MANAGE_GUILD) bot = await Bot.get(id=application.id).select_related("user") - if (ban := await getCore().getGuildBan(guild, bot.user.id)) is not None: + if (ban := await GuildBan.get_or_none(guild=guild, user=bot.user)) is not None: await ban.delete() bot_userdata = await bot.user.userdata @@ -139,7 +139,6 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori id=Snowflake.makeId(), author=bot.user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild ) - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=sys_channel, permissions=GuildPermissions.VIEW_CHANNEL) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index e2d9815..92e2555 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -73,7 +73,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): await user.change_password(data.new_password) data.new_password = None if data.email is not None: - await getCore().changeUserEmail(user, data.email) + await user.change_email(data.email) await getCore().sendVerificationEmail(user) data.email = None if data.avatar != "" and data.avatar is not None: diff --git a/yepcord/rest_api/routes/webhooks.py b/yepcord/rest_api/routes/webhooks.py index 8800ea4..66ab23b 100644 --- a/yepcord/rest_api/routes/webhooks.py +++ b/yepcord/rest_api/routes/webhooks.py @@ -113,7 +113,6 @@ async def post_webhook_message(query_args: WebhookMessageCreateQuery, webhook: i message = await processMessage(await request.get_json(), channel, None, WebhookMessageCreate, webhook) message_json = await message.ds_json() - await getCore().sendMessage(message) await getGw().dispatch(MessageCreateEvent(message_json), channel=channel, permissions=GuildPermissions.VIEW_CHANNEL) if query_args.wait: diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 1d84564..9b63d4b 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -61,7 +61,7 @@ async def _getMessage(user: User, channel: Channel, message_id: int) -> Message: raise InvalidDataErr(404, Errors.make(10003)) if not user: raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) - if not message_id or not (message := await getCore().getMessage(channel, message_id)): + if not message_id or not (message := await channel.get_message(message_id)): raise InvalidDataErr(404, Errors.make(10008)) return message @@ -202,7 +202,7 @@ async def process_stickers(sticker_ids: list[int]): async def validate_reply(data: MessageCreate, channel: Channel) -> int: message_type = MessageType.DEFAULT if data.message_reference: - data.validate_reply(channel, await getCore().getMessage(channel, data.message_reference.message_id)) + data.validate_reply(channel, await channel.get_message(data.message_reference.message_id)) if data.message_reference: message_type = MessageType.REPLY return message_type diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index 19a6a3c..e6fe4d9 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -16,21 +16,17 @@ along with this program. If not, see . """ import os.path -from asyncio import get_event_loop # noinspection PyPackageRequirements -from contextvars import Context from hashlib import sha256 from hmac import new from json import loads as jloads, dumps as jdumps from os import urandom -from random import randint from time import time from typing import Optional, Union import maxminddb from tortoise.expressions import Q, Subquery from tortoise.functions import Count -from tortoise.transactions import atomic from . import ctx from .classes.other import EmailMsg, JWT, MFA @@ -38,12 +34,11 @@ from .config import Config from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors -from .models import User, Relationship, Channel, Message, ReadState, Emoji, \ - Invite, Guild, GuildMember, GuildTemplate, Reaction, Sticker, PermissionOverwrite, GuildBan, AuditLogEntry, \ - Webhook, Role, GuildEvent, ThreadMember +from .models import User, Relationship, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, \ + Reaction, Sticker, PermissionOverwrite, AuditLogEntry, Webhook, Role, GuildEvent, ThreadMember from .snowflake import Snowflake from .storage import getStorage -from .utils import b64encode, b64decode, NoneType +from .utils import b64encode, b64decode from ..gateway.events import DMChannelCreateEvent @@ -53,17 +48,10 @@ def __init__(self, key=None): self.key = key if key and len(key) >= 16 and isinstance(key, bytes) else urandom(32) self.ipdb = None - async def getRandomDiscriminator(self, login: str) -> Optional[int]: - for _ in range(5): - d = randint(1, 9999) - if not await User.y.getByUsername(login, d): - return d - async def getRelationships(self, user: User, with_data=False) -> list[dict]: rels = [] rel: Relationship - for rel in await (Relationship.filter(Q(from_user=user) | Q(to_user=user)) - .select_related("from_user", "to_user")): + for rel in await Relationship.filter(Q(from_user=user) | Q(to_user=user)).select_related("from_user", "to_user"): if (rel_json := await rel.ds_json(user, with_data)) is not None: rels.append(rel_json) return rels @@ -138,9 +126,7 @@ async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> async def getChannel(self, channel_id: Optional[int]) -> Optional[Channel]: if channel_id is None: return - if (channel := await Channel.get_or_none(id=channel_id).select_related("guild", "owner", "parent")) is None: - return - return await self.setLastMessageIdForChannel(channel) + return await Channel.get_or_none(id=channel_id).select_related("guild", "owner", "parent") async def getDChannel(self, user1: User, user2: User) -> Optional[Channel]: return await Channel.get_or_none( @@ -165,69 +151,37 @@ async def getDMChannelOrCreate(self, user1: User, user2: User) -> Channel: if await channel.dm_is_hidden(user1): await channel.dm_unhide(user1) await ctx.getGw().dispatch(DMChannelCreateEvent(channel), user_ids=[user1.id]) - return await self.setLastMessageIdForChannel(channel) - async def getLastMessageId(self, channel: Channel) -> Optional[int]: - if (last_message := await Message.filter(channel=channel).group_by("-id").first()) is not None: - return last_message.id - - async def setLastMessageIdForChannel(self, channel: Channel) -> Channel: - channel.last_message_id = await self.getLastMessageId(channel) return channel - async def getChannelMessagesCount(self, channel: Channel) -> int: - return await Message.filter(channel=channel).count() - async def getPrivateChannels(self, user: User, with_hidden: bool = False) -> list[Channel]: - channels = await Channel.filter(recipients__id=user.id).select_related("owner") - channels = [ + return [ channel - for channel in channels + for channel in await Channel.filter(recipients__id=user.id).select_related("owner") if not with_hidden and not await channel.dm_is_hidden(user) ] - return [await self.setLastMessageIdForChannel(channel) for channel in channels] - - async def getChannelMessages(self, channel: Channel, limit: int, before: int = 0, after: int = 0) -> list[Message]: - id_filter = {} - if after: id_filter["id__gt"] = after - if before: id_filter["id__lt"] = before - messages = await (Message.filter(channel=channel, ephemeral=False, **id_filter) - .select_related(*Message.DEFAULT_RELATED) - .order_by("-id").limit(limit)) - return messages - - async def getMessage(self, channel: Channel, message_id: int) -> Optional[Message]: - if not message_id: return - return await (Message.get_or_none(channel=channel, id=message_id) - .select_related(*Message.DEFAULT_RELATED)) - - async def sendMessage(self, message: Message) -> Message: - async def _addToReadStates(): - users = await self.getRelatedUsersToChannel(message.channel, False) - if message.author.id in users: - users.remove(message.author.id) - for user in users: - read_state, _ = await ReadState.get_or_create( - user=user, channel=message.channel, defaults={"last_read_id": message.id, "count": 0} - ) - read_state.count += 1 - await read_state.save(update_fields=["count"]) - - Context().run(get_event_loop().create_task, _addToReadStates()) - return message - - async def getRelatedUsersToChannel(self, channel: Union[Channel, int], ids: bool = True) -> list[Union[int, User]]: - if isinstance(channel, int): - channel = await Channel.get_or_none(id=channel).select_related("guild") - if not channel: - return [] + + #async def sendMessage(self, message: Message) -> Message: + # async def _addToReadStates(): # TODO: recalculate read states when requested by user + # users = await self.getRelatedUsersToChannel(message.channel) + # if message.author in users: + # users.remove(message.author) + # for user in users: + # read_state, _ = await ReadState.get_or_create( + # user=user, channel=message.channel, defaults={"last_read_id": message.id, "count": 0} + # ) + # read_state.count += 1 + # await read_state.save(update_fields=["count"]) + + # return message + + async def getRelatedUsersToChannelCount(self, channel: Channel) -> int: if channel.type in [ChannelType.DM, ChannelType.GROUP_DM]: - if ids: return [recipient.id for recipient in await channel.recipients.all()] - return await channel.recipients.all() + return await channel.recipients.filter().count() elif channel.type in GUILD_CHANNELS: - return [member.user.id if ids else member.user for member in await self.getGuildMembers(channel.guild)] + return await GuildMember.filter(guild=channel.guild).count() elif channel.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): - return [member.user.id if ids else member.user for member in await self.getThreadMembers(channel)] + return await ThreadMember.filter(channel=channel).count() async def setReadState(self, user: User, channel: Channel, count: int, last: int) -> None: read_state, _ = await ReadState.get_or_create( @@ -285,20 +239,6 @@ async def verifyEmail(self, user: User, token: str) -> None: user.verified = True await user.save(update_fields=["verified"]) - async def getUserByEmail(self, email: str) -> Optional[User]: - return await User.get_or_none(email=email) - - async def changeUserEmail(self, user: User, email: str) -> None: # TODO - email = email.lower() - if user.email == email: - return - if await self.getUserByEmail(email): - raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", - "message": "Email address already registered."}})) - user.email = email - user.verified = False - await user.save() - async def mfaNonceToCode(self, nonce: str) -> Optional[str]: if not (payload := JWT.decode(nonce, self.key)): return @@ -307,7 +247,7 @@ async def mfaNonceToCode(self, nonce: str) -> Optional[str]: return signature.replace("-", "").replace("_", "")[:8].upper() async def createDMGroupChannel(self, user: User, recipients: list[User], name: Optional[str] = None) -> Channel: - if user.id not in recipients: + if user not in recipients: recipients.append(user) channel = await Channel.create(id=Snowflake.makeId(), type=ChannelType.GROUP_DM, name=name, owner=user) for recipient in recipients: @@ -362,94 +302,6 @@ async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[l count = await query.count() return messages, count - @atomic() - async def createGuild(self, guild_id: int, user: User, name: str, icon: str = None) -> Guild: - guild = await Guild.create(id=guild_id, owner=user, name=name, icon=icon) - await Role.create(id=guild.id, guild=guild, name="@everyone") - - text_category = await Channel.create( - id=Snowflake.makeId(), type=ChannelType.GUILD_CATEGORY, guild=guild, name="Text Channels", position=0, - flags=0, rate_limit=0 - ) - voice_category = await Channel.create( - id=Snowflake.makeId(), type=ChannelType.GUILD_CATEGORY, guild=guild, name="Voice Channels", position=0, - flags=0, rate_limit=0 - ) - system_channel = await Channel.create( - id=Snowflake.makeId(), type=ChannelType.GUILD_TEXT, guild=guild, name="general", position=0, flags=0, - parent=text_category, rate_limit=0 - ) - await Channel.create( - id=Snowflake.makeId(), type=ChannelType.GUILD_VOICE, guild=guild, name="General", position=0, flags=0, - parent=voice_category, bitrate=64000, user_limit=0, rate_limit=0 - ) - guild.system_channel = system_channel.id - await guild.save(update_fields=["system_channel"]) - - await GuildMember.create(id=Snowflake.makeId(), user=user, guild=guild) - - return guild - - @atomic() - async def createGuildFromTemplate(self, guild_id: int, user: User, template: GuildTemplate, name: Optional[str], - icon: Optional[str]) -> Guild: - serialized = template.serialized_guild - name = serialized["name"] = name or serialized["name"] - guild = await Guild.create(id=guild_id, owner=user, name=name) - - serialized["icon"] = icon - replaced_ids: dict[Union[int, NoneType], Union[int, NoneType]] = {None: None, 0: guild_id} - channels = {} - roles = {} - - for role in serialized["roles"]: - if role["id"] not in replaced_ids: - replaced_ids[role["id"]] = Snowflake.makeId() - role["id"] = replaced_ids[role["id"]] - roles[role["id"]] = await Role.create(guild=guild, **role) - - for channel in serialized["channels"]: - if channel["id"] not in replaced_ids: - replaced_ids[channel["id"]] = Snowflake.makeId() - channel["id"] = channel_id = replaced_ids[channel["id"]] - channel["parent"] = channels.get(replaced_ids.get(channel["parent_id"], None), None) - channel["rate_limit"] = channel["rate_limit_per_user"] - channel["default_auto_archive"] = channel["default_auto_archive_duration"] - - del channel["parent_id"] - del channel["rate_limit_per_user"] - del channel["default_auto_archive_duration"] - - del channel["available_tags"] - del channel["template"] - del channel["default_reaction_emoji"] - del channel["default_thread_rate_limit_per_user"] - del channel["default_sort_order"] - del channel["default_forum_layout"] - - permission_overwrites = channel["permission_overwrites"] - del channel["permission_overwrites"] - - channels[channel_id] = await Channel.create(guild=guild, **channel) - for overwrite in permission_overwrites: - overwrite["target_role"] = roles[replaced_ids[overwrite["id"]]] - overwrite["channel"] = channels[channel_id] - del overwrite["id"] - await PermissionOverwrite.create(**overwrite) - - serialized["afk_channel"] = replaced_ids.get(serialized["afk_channel_id"], None) - serialized["system_channel"] = replaced_ids.get(serialized["system_channel_id"], None) - del serialized["afk_channel_id"] - del serialized["system_channel_id"] - - del serialized["roles"] - del serialized["channels"] - - await guild.update(**serialized) - await GuildMember.create(id=Snowflake.makeId(), user=user, guild=guild) - - return guild - async def getRole(self, role_id: int, guild: Optional[Guild] = None) -> Role: q = {"id": role_id} if guild is not None: @@ -501,9 +353,6 @@ async def getEmojiByReaction(self, reaction: str) -> Optional[Emoji]: return return emoji if emoji.name == name else None - async def getGuildBan(self, guild: Guild, user_id: int) -> Optional[GuildBan]: - return await GuildBan.get_or_none(guild=guild, user__id=user_id) - async def bulkDeleteGuildMessagesFromBanned( self, guild: Guild, user_id: int, after_id: int ) -> dict[Channel, list[int]]: diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index f6b8d6d..96c3f50 100644 --- a/yepcord/yepcord/enums.py +++ b/yepcord/yepcord/enums.py @@ -92,7 +92,7 @@ class ChannelType(E): GUILD_DIRECTORY = 14 -GUILD_CHANNELS = (ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE, ChannelType.GUILD_CATEGORY, ChannelType.GUILD_NEWS) +GUILD_CHANNELS = {ChannelType.GUILD_TEXT, ChannelType.GUILD_VOICE, ChannelType.GUILD_CATEGORY, ChannelType.GUILD_NEWS} class MessageType(E): diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 2c55b56..51b8ee5 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -52,14 +52,18 @@ class Channel(Model): default_auto_archive: Optional[int] = fields.IntField(null=True, default=None) flags: Optional[int] = fields.IntField(null=True, default=0) - last_message_id: int = None + _last_message_id: int = None + + async def get_last_message_id(self) -> int: + if self._last_message_id is None and \ + (last_message := await models.Message.filter(channel=self).group_by("-id").first()) is not None: + self._last_message_id = last_message.id + + return self._last_message_id async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: - if self.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): - self.last_message_id = await getCore().getLastMessageId(self) - if self.last_message_id is None: - await getCore().setLastMessageIdForChannel(self) - last_message_id = str(self.last_message_id) if self.last_message_id is not None else None + last_message_id = await self.get_last_message_id() + last_message_id = str(last_message_id) if last_message_id is not None else None recipients = [] if self.type in (ChannelType.DM, ChannelType.GROUP_DM): recipients = await (self.recipients.all() if not user_id else self.recipients.filter(~Q(id=user_id)).all()) @@ -147,7 +151,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "nsfw": self.nsfw } elif self.type == ChannelType.GUILD_PUBLIC_THREAD: - message_count = await getCore().getChannelMessagesCount(self) + message_count = await models.Message.filter(channel=self).count() data: dict = base_data | { "guild_id": str(self.guild.id), "parent_id": str(self.parent.id) if self.parent else None, @@ -172,8 +176,19 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: return data - async def messages(self, limit: int=50, before: int=0, after: int=0) -> list[models.Message]: - return await getCore().getChannelMessages(self, limit, before, after) + async def get_messages(self, limit: int = 50, before: int = 0, after: int = 0) -> list[models.Message]: + id_filter = {} + if after: + id_filter["id__gt"] = after + if before: + id_filter["id__lt"] = before + + return await ( + models.Message.filter(channel=self, ephemeral=False, **id_filter) + .select_related(*models.Message.DEFAULT_RELATED) + .order_by("-id") + .limit(limit) + ) async def other_user(self, current_user: models.User) -> Optional[models.User]: if self.type != ChannelType.DM: @@ -187,3 +202,6 @@ async def dm_unhide(self, for_user: models.User) -> None: if self.type != ChannelType.DM: return await models.HiddenDmChannel.filter(user=for_user, channel=self).delete() + + async def get_message(self, message_id: int) -> Optional[models.Message]: + return await models.Message.Y.get(self, message_id) diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index 0427e70..ab373ff 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -15,19 +15,116 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ +from __future__ import annotations from time import time -from typing import Optional +from typing import Optional, Union from tortoise import fields +from tortoise.transactions import atomic from ..ctx import getCore import yepcord.yepcord.models as models -from ..enums import Locales +from ..enums import Locales, ChannelType from ._utils import SnowflakeField, Model, ChoicesValidator +from ..snowflake import Snowflake + + +class GuildUtils: + @staticmethod + @atomic() + async def create(owner: models.User, name: str) -> models.Guild: + guild = await Guild.create(id=Snowflake.makeId(), owner=owner, name=name) + await models.Role.create(id=guild.id, guild=guild, name="@everyone") + + text_category = await models.Channel.create( + id=Snowflake.makeId(), type=ChannelType.GUILD_CATEGORY, guild=guild, name="Text Channels", position=0, + flags=0, rate_limit=0 + ) + voice_category = await models.Channel.create( + id=Snowflake.makeId(), type=ChannelType.GUILD_CATEGORY, guild=guild, name="Voice Channels", position=0, + flags=0, rate_limit=0 + ) + system_channel = await models.Channel.create( + id=Snowflake.makeId(), type=ChannelType.GUILD_TEXT, guild=guild, name="general", position=0, flags=0, + parent=text_category, rate_limit=0 + ) + await models.Channel.create( + id=Snowflake.makeId(), type=ChannelType.GUILD_VOICE, guild=guild, name="General", position=0, flags=0, + parent=voice_category, bitrate=64000, user_limit=0, rate_limit=0 + ) + guild.system_channel = system_channel.id + await guild.save(update_fields=["system_channel"]) + + await models.GuildMember.create(id=Snowflake.makeId(), user=owner, guild=guild) + + return guild + + @staticmethod + @atomic() + async def create_from_template( + owner: models.User, template: models.GuildTemplate, name: Optional[str] + ) -> models.Guild: + serialized = template.serialized_guild + name = serialized["name"] = name or serialized["name"] + guild = await Guild.create(id=Snowflake.makeId(), owner=owner, name=name) + + replaced_ids: dict[Union[int, None], Union[int, None]] = {None: None, 0: guild.id} + channels = {} + roles = {} + + for role in serialized["roles"]: + if role["id"] not in replaced_ids: + replaced_ids[role["id"]] = Snowflake.makeId() + role["id"] = replaced_ids[role["id"]] + roles[role["id"]] = await models.Role.create(guild=guild, **role) + + for channel in serialized["channels"]: + if channel["id"] not in replaced_ids: + replaced_ids[channel["id"]] = Snowflake.makeId() + channel["id"] = channel_id = replaced_ids[channel["id"]] + channel["parent"] = channels.get(replaced_ids.get(channel["parent_id"], None), None) + channel["rate_limit"] = channel["rate_limit_per_user"] + channel["default_auto_archive"] = channel["default_auto_archive_duration"] + + del channel["parent_id"] + del channel["rate_limit_per_user"] + del channel["default_auto_archive_duration"] + + del channel["available_tags"] + del channel["template"] + del channel["default_reaction_emoji"] + del channel["default_thread_rate_limit_per_user"] + del channel["default_sort_order"] + del channel["default_forum_layout"] + + permission_overwrites = channel["permission_overwrites"] + del channel["permission_overwrites"] + + channels[channel_id] = await models.Channel.create(guild=guild, **channel) + for overwrite in permission_overwrites: + overwrite["target_role"] = roles[replaced_ids[overwrite["id"]]] + overwrite["channel"] = channels[channel_id] + del overwrite["id"] + await models.PermissionOverwrite.create(**overwrite) + + serialized["afk_channel"] = replaced_ids.get(serialized["afk_channel_id"], None) + serialized["system_channel"] = replaced_ids.get(serialized["system_channel_id"], None) + del serialized["afk_channel_id"] + del serialized["system_channel_id"] + + del serialized["roles"] + del serialized["channels"] + + await guild.update(**serialized) + await models.GuildMember.create(id=Snowflake.makeId(), user=owner, guild=guild) + + return guild class Guild(Model): + Y = GuildUtils + id: int = SnowflakeField(pk=True) owner: models.User = fields.ForeignKeyField("models.User") name: str = fields.CharField(max_length=64) diff --git a/yepcord/yepcord/models/guild_template.py b/yepcord/yepcord/models/guild_template.py index fb0148a..984d378 100644 --- a/yepcord/yepcord/models/guild_template.py +++ b/yepcord/yepcord/models/guild_template.py @@ -21,13 +21,12 @@ from tortoise import fields +import yepcord.yepcord.models as models +from ._utils import SnowflakeField, Model from ..ctx import getCore from ..enums import ChannelType -from ._utils import SnowflakeField, Model from ..snowflake import Snowflake -from ..utils import b64encode, int_size, NoneType - -import yepcord.yepcord.models as models +from ..utils import b64encode, int_size class GuildTemplate(Model): @@ -71,7 +70,7 @@ async def ds_json(self) -> dict: @staticmethod async def serialize_guild(guild: models.Guild) -> dict: - replaced_ids: dict[Union[int, NoneType], Union[int, NoneType]] = {None: None} + replaced_ids: dict[Union[int, None], Union[int, None]] = {None: None} last_replaced_id = 0 serialized_roles = [] serialized_channels = [] diff --git a/yepcord/yepcord/models/interaction.py b/yepcord/yepcord/models/interaction.py index 1309db9..d74a75a 100644 --- a/yepcord/yepcord/models/interaction.py +++ b/yepcord/yepcord/models/interaction.py @@ -78,7 +78,7 @@ async def ds_json(self, with_user=False, with_token=False, resolved: dict = None data["channel_id"] = str(self.channel.id) data["channel"] = await self.channel.ds_json() - if self.message_id is not None and (message := await getCore().getMessage(self.channel, self.message_id)): + if self.message_id is not None and (message := await self.channel.get_message(self.message_id)): data["message"] = await message.ds_json() if self.locale is not None: diff --git a/yepcord/yepcord/models/invite.py b/yepcord/yepcord/models/invite.py index 1727b61..d38a0b8 100644 --- a/yepcord/yepcord/models/invite.py +++ b/yepcord/yepcord/models/invite.py @@ -67,12 +67,11 @@ async def ds_json(self, with_counts: bool=False) -> dict: } if with_counts: - related_users = await getCore().getRelatedUsersToChannel(self.channel.id, ids=False) - data["approximate_member_count"] = len(related_users) + data["approximate_member_count"] = await getCore().getRelatedUsersToChannelCount(self.channel) if self.channel.type == ChannelType.GROUP_DM: data["channel"]["recipients"] = [ - {"username": (await rel_user.data).username} - for rel_user in related_users + {"username": (await recipient.data).username} + for recipient in await self.channel.recipients.all() ] if self.channel.type == ChannelType.GROUP_DM: diff --git a/yepcord/yepcord/models/message.py b/yepcord/yepcord/models/message.py index 832e582..243b84e 100644 --- a/yepcord/yepcord/models/message.py +++ b/yepcord/yepcord/models/message.py @@ -30,7 +30,16 @@ from ..utils import ping_regex +class MessageUtils: + @staticmethod + async def get(channel: models.Channel, message_id: int) -> Optional[Message]: + if not message_id: return + return await Message.get_or_none(channel=channel, id=message_id).select_related(*Message.DEFAULT_RELATED) + + class Message(Model): + Y = MessageUtils + id: int = SnowflakeField(pk=True) channel: models.Channel = fields.ForeignKeyField("models.Channel", on_delete=fields.SET_NULL, null=True, related_name="channel") @@ -127,7 +136,7 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: ref_channel = await getCore().getChannel(int(self.message_reference["channel_id"])) ref_message = None if ref_channel: - ref_message = await getCore().getMessage(ref_channel, int(self.message_reference["message_id"])) + ref_message = await ref_channel.get_message(int(self.message_reference["message_id"])) if ref_message: ref_message.message_reference = {} data["referenced_message"] = await ref_message.ds_json() if ref_message else None if self.nonce is not None: diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 408c165..e309f66 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -38,13 +38,6 @@ from ..utils import int_size, b64encode -async def _get_free_discriminator(login: str) -> Optional[int]: - for _ in range(5): - discriminator = randint(1, 9999) - if not await User.y.getByUsername(login, discriminator): - return discriminator - - class UserUtils: @staticmethod async def get(user_id: int, allow_deleted: bool = True) -> Optional[User]: @@ -82,7 +75,7 @@ async def register( raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", "message": "Email address already registered."}})) - discriminator = await _get_free_discriminator(login) + discriminator = await User.y.get_free_discriminator(login) if discriminator is None: raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "USERNAME_TOO_MANY_USERS", "message": "Too many users have this username, " @@ -114,6 +107,13 @@ async def login(email: str, password: str) -> models.Session: ) return await models.Session.Y.create(user) + @staticmethod + async def get_free_discriminator(login: str) -> Optional[int]: + for _ in range(5): + discriminator = randint(1, 9999) + if not await User.y.getByUsername(login, discriminator): + return discriminator + class User(Model): y = UserUtils @@ -209,7 +209,7 @@ async def change_username(self, username: str) -> None: data = await self.data discriminator = data.discriminator if await User.y.getByUsername(username, discriminator): - discriminator = await _get_free_discriminator(username) + discriminator = await self.y.get_free_discriminator(username) if discriminator is None: raise InvalidDataErr(400, Errors.make(50035, {"username": { "code": "USERNAME_TOO_MANY_USERS", @@ -233,6 +233,17 @@ async def change_discriminator(self, new_discriminator: int, username_changed: b await data.save(update_fields=["discriminator"]) return True + async def change_email(self, new_email: str) -> None: + new_email = new_email.lower() + if self.email == new_email: + return + if await User.exists(email=new_email): + raise InvalidDataErr(400, Errors.make(50035, {"email": {"code": "EMAIL_ALREADY_REGISTERED", + "message": "Email address already registered."}})) + self.email = new_email + self.verified = False + await self.save() + async def create_backup_codes(self) -> list[str]: codes = ["".join([choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(8)]) for _ in range(10)] diff --git a/yepcord/yepcord/utils.py b/yepcord/yepcord/utils.py index 2dc4be5..5255279 100644 --- a/yepcord/yepcord/utils.py +++ b/yepcord/yepcord/utils.py @@ -98,10 +98,10 @@ def int_size(i: int) -> int: return (i.bit_length() + 7) // 8 -LOCALES = ["bg", "cs", "da", "de", "el", "en-GB", "es-ES", "fi", "fr", "hi", "hr", "hu", "it", "ja", "ko", "lt", "nl", - "no", "pl", "pt-BR", "ro", "ru", "sv-SE", "th", "tr", "uk", "vi", "zh-CN", "zh-TW", "en-US"] - -NoneType = type(None) +LOCALES = [ + "bg", "cs", "da", "de", "el", "en-GB", "es-ES", "fi", "fr", "hi", "hr", "hu", "it", "ja", "ko", "lt", "nl", + "no", "pl", "pt-BR", "ro", "ru", "sv-SE", "th", "tr", "uk", "vi", "zh-CN", "zh-TW", "en-US", +] def freeze(obj: Any) -> Any: From 688f4af07eb49b68eac78e6934dcd2b486037a32 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Thu, 5 Sep 2024 17:51:31 +0300 Subject: [PATCH 06/26] move guild methods; remove Ctx class --- yepcord/cli.py | 4 +- yepcord/gateway/events.py | 6 +- yepcord/gateway/gateway.py | 6 +- yepcord/rest_api/models/applications.py | 3 +- yepcord/rest_api/models/guilds.py | 7 ++- yepcord/rest_api/routes/applications.py | 2 +- yepcord/rest_api/routes/channels.py | 17 +++--- yepcord/rest_api/routes/guilds.py | 54 +++++++++++------- yepcord/rest_api/routes/hypesquad.py | 4 +- yepcord/rest_api/routes/oauth2.py | 6 +- yepcord/rest_api/routes/users_me.py | 2 +- yepcord/rest_api/utils.py | 4 +- yepcord/yepcord/core.py | 70 ++---------------------- yepcord/yepcord/ctx.py | 50 +++-------------- yepcord/yepcord/gateway_dispatcher.py | 23 +++++--- yepcord/yepcord/models/channel.py | 50 +++++++++++++++-- yepcord/yepcord/models/guild.py | 43 +++++++++++++-- yepcord/yepcord/models/guild_member.py | 4 +- yepcord/yepcord/models/guild_template.py | 7 +-- yepcord/yepcord/models/readstate.py | 3 +- yepcord/yepcord/models/user.py | 6 ++ 21 files changed, 192 insertions(+), 179 deletions(-) diff --git a/yepcord/cli.py b/yepcord/cli.py index 55c9254..60df3be 100644 --- a/yepcord/cli.py +++ b/yepcord/cli.py @@ -46,7 +46,9 @@ def migrate(config: str, location: str = None) -> None: async def _migrate(): command = Command({ "connections": {"default": Config.DB_CONNECT_STRING}, - "apps": {"models": {"models": ["yepcord.yepcord.models", "aerich.models"], "default_connection": "default"}}, + "apps": {"models": { + "models": ["yepcord.yepcord.models", "aerich.models"], "default_connection": "default", + }}, }, location=location or Config.MIGRATIONS_DIR) await command.init() if Path(command.location).exists(): diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 379df0d..30aa708 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -103,8 +103,10 @@ async def json(self) -> dict: "flags": 0, }, "users": await self.core.getRelatedUsers(self.user), - "guilds": [await guild.ds_json(user_id=self.user.id, for_gateway=True, with_channels=True) - for guild in await self.core.getUserGuilds(self.user)], + "guilds": [ + await guild.ds_json(user_id=self.user.id, for_gateway=True, with_channels=True) + for guild in await self.user.get_guilds() + ], "session_id": self.client.sid, "presences": [], # TODO "relationships": await self.core.getRelationships(self.user), diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index cd44d54..0614b99 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -93,7 +93,7 @@ async def handle_IDENTIFY(self, data: dict) -> None: await self.gateway.authenticated(self, self.cached_presence) await self.esend(ReadyEvent(session.user, self, getCore())) if not session.user.is_bot: - guild_ids = [guild.id for guild in await getCore().getUserGuilds(session.user)] + guild_ids = [guild.id for guild in await session.user.get_guilds()] await self.esend(ReadySupplementalEvent(await self.gateway.getFriendsPresences(self.user_id), guild_ids)) @require_auth @@ -221,7 +221,9 @@ async def sendToGuild(self, event: RawDispatchEvent, guild_id: int, exclude_user await self._send(client, event) sent.add(client) - async def sendToRoles(self, event: RawDispatchEvent, role_ids: list[int], exclude_users: set[int], sent: set) -> None: + async def sendToRoles( + self, event: RawDispatchEvent, role_ids: list[int], exclude_users: set[int], sent: set[GatewayClient] + ) -> None: for role_id in role_ids: for client in self.gw.store.get(role_id=role_id): if client.user_id in exclude_users or client in sent: diff --git a/yepcord/rest_api/models/applications.py b/yepcord/rest_api/models/applications.py index 6ae618e..50e127b 100644 --- a/yepcord/rest_api/models/applications.py +++ b/yepcord/rest_api/models/applications.py @@ -95,7 +95,8 @@ def validate_options(cls, value: Optional[list[CommandOption]]) -> Optional[list if len(not_sub) != 0 and len(not_sub) != len(value): raise InvalidDataErr(400, Errors.make(50035, {"options": { "code": "APPLICATION_COMMAND_OPTIONS_TYPE_INVALID", - "message": "Sub-command and sub-command group option types are mutually exclusive to all other types"}})) + "message": "Sub-command and sub-command group option types are mutually exclusive to all other types", + }})) return value def __init__(self, **kwargs): diff --git a/yepcord/rest_api/models/guilds.py b/yepcord/rest_api/models/guilds.py index d0bb56f..a59a9a2 100644 --- a/yepcord/rest_api/models/guilds.py +++ b/yepcord/rest_api/models/guilds.py @@ -307,8 +307,11 @@ def to_json(self, channel_type: int) -> dict: return self.model_dump(include={"name", "type", "position"}, exclude_defaults=True) elif channel_type == ChannelType.GUILD_TEXT: return self.model_dump( - include={"name", "type", "position", "topic", "nsfw", "rate_limit", "parent_id", "default_auto_archive"}, - exclude_defaults=True) + include={ + "name", "type", "position", "topic", "nsfw", "rate_limit", "parent_id", "default_auto_archive" + }, + exclude_defaults=True + ) elif channel_type == ChannelType.GUILD_VOICE: return self.model_dump(include={"name", "type", "position", "nsfw", "bitrate", "user_limit", "parent_id", "video_quality_mode"}, exclude_defaults=True) diff --git a/yepcord/rest_api/routes/applications.py b/yepcord/rest_api/routes/applications.py index a04edba..1b48d1d 100644 --- a/yepcord/rest_api/routes/applications.py +++ b/yepcord/rest_api/routes/applications.py @@ -22,7 +22,7 @@ from ..models.applications import CreateApplication, UpdateApplication, UpdateApplicationBot, GetCommandsQS, \ CreateCommand from ..y_blueprint import YBlueprint -from ...yepcord.ctx import getCore, getCDNStorage +from ...yepcord.ctx import getCDNStorage from ...yepcord.enums import ApplicationCommandType from ...yepcord.models import User, UserData, UserSettings, Application, Bot, gen_secret_key, gen_token_secret, \ ApplicationCommand, Guild diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 4ccd8bf..8cd06c2 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -465,7 +465,7 @@ async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, if target is None or (isinstance(target, Role) and target.guild != channel.guild): return "", 204 await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.MANAGE_ROLES, channel=channel) - old_overwrite = await getCore().getPermissionOverwrite(channel, target) + old_overwrite = await channel.get_permission_overwrite(target) if old_overwrite is not None: await old_overwrite.update(**data.model_dump()) overwrite = old_overwrite @@ -493,7 +493,7 @@ async def delete_permission_overwrite(target_id: int, user: User = DepUser, chan if not (member := await getCore().getGuildMember(channel.guild, user.id)): raise InvalidDataErr(403, Errors.make(50001)) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.MANAGE_ROLES, channel=channel) - overwrite = await getCore().getPermissionOverwriteUnk(channel, target_id) + overwrite = await channel.get_permission_overwrite(target_id) if overwrite is None: return "", 204 @@ -553,8 +553,9 @@ async def get_channel_webhooks(user: User = DepUser, channel: Channel = DepChann @channels.post("//messages//threads", body_cls=CreateThread, allow_bots=True) -async def create_thread(data: CreateThread, user: User = DepUser, channel: Channel = DepChannel, - message: Message = DepMessage): +async def create_thread( + data: CreateThread, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage +): if not channel.guild: raise InvalidDataErr(403, Errors.make(50003)) member = await getCore().getGuildMember(channel.guild, user.id) @@ -564,11 +565,11 @@ async def create_thread(data: CreateThread, user: User = DepUser, channel: Chann name=data.name, owner=user, parent=channel, flags=0) thread_member = await ThreadMember.create(id=Snowflake.makeId(), user=user, channel=thread, guild=channel.guild) - thread_message = await Message.create( + await Message.create( id=Snowflake.makeId(), channel=thread, author=user, content="", type=MessageType.THREAD_STARTER_MESSAGE, message_reference={"message_id": message.id, "channel_id": channel.id, "guild_id": channel.guild.id} ) - thread_create_message = await Message.create( + await Message.create( id=Snowflake.makeId(), channel=channel, author=user, content=thread.name, type=MessageType.THREAD_CREATED, message_reference={"message_id": message.id, "channel_id": channel.id, "guild_id": channel.guild.id} ) @@ -586,7 +587,9 @@ async def create_thread(data: CreateThread, user: User = DepUser, channel: Chann @channels.get("//application-commands/search", qs_cls=CommandsSearchQS) -async def search_application_commands(query_args: CommandsSearchQS, user: User = DepUser, channel: Channel = DepChannel): +async def search_application_commands( + query_args: CommandsSearchQS, user: User = DepUser, channel: Channel = DepChannel +): dm_user = None if (channel.type not in (*GUILD_CHANNELS, ChannelType.DM) or (channel.type == ChannelType.DM and not (dm_user := await channel.other_user(user)).is_bot)): diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index 264e1cb..a7e0a0a 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -159,8 +159,10 @@ async def update_guild_template(data: TemplateUpdate, member: GuildMember = DepG @guilds.get("//emojis", allow_bots=True) async def get_guild_emojis(guild: Guild = DepGuild): - emojis = await getCore().getEmojis(guild.id) - return [await emoji.ds_json(with_user=True) for emoji in emojis] + return [ + await emoji.ds_json(with_user=True) + for emoji in await guild.get_emojis() + ] @guilds.post("//emojis", body_cls=EmojiCreate, allow_bots=True) @@ -218,8 +220,7 @@ async def update_channels_positions(guild: Guild = DepGuild, member: GuildMember if not data: return "", 204 data = ChannelsPositionsChangeList(changes=data) - channels = await getCore().getGuildChannels(guild) - channels = {channel.id: channel for channel in channels} + channels = {channel.id: channel for channel in await guild.get_channels()} for change in data.changes: if not (channel := channels.get(change.id)): continue @@ -311,7 +312,9 @@ async def kick_member(user_id: int, user: User = DepUser, guild: Guild = DepGuil await target_member.delete() if target_member.user.is_bot: await process_bot_kick(user, target_member) - await getGw().dispatch(GuildMemberRemoveEvent(guild.id, (await target_member.user.data).ds_json), user_ids=[user_id]) + await getGw().dispatch( + GuildMemberRemoveEvent(guild.id, (await target_member.user.data).ds_json), user_ids=[user_id] + ) await getGw().dispatch(GuildDeleteEvent(guild.id), user_ids=[target_member.id]) entry = await AuditLogEntry.utils.member_kick(user, target_member) await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, @@ -404,7 +407,7 @@ async def get_guild_integrations(query_args: GetIntegrationsQS, guild: Guild = D @guilds.get("//roles", allow_bots=True) async def get_roles(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_ROLES) - return [role.ds_json() for role in await getCore().getRoles(guild, True)] + return [role.ds_json() for role in await guild.get_roles(True)] @guilds.post("//roles", body_cls=RoleCreate, allow_bots=True) @@ -459,8 +462,7 @@ async def update_role(data: RoleUpdate, user: User = DepUser, guild: Guild = Dep async def update_roles_positions(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_ROLES) roles_data = await request.get_json() - roles = await getCore().getRoles(guild, exclude_default=True) - roles = {role.id: role for role in roles} + roles = {role.id: role for role in await guild.get_roles(True)} if not await member.perm_checker.canChangeRolesPositions(roles_data, list(roles.values())): raise InvalidDataErr(403, Errors.make(50013)) @@ -483,8 +485,10 @@ async def update_roles_positions(guild: Guild = DepGuild, member: GuildMember = await getCore().setTemplateDirty(guild) - roles = await getCore().getRoles(guild) - return [role.ds_json() for role in roles] + return [ + role.ds_json() + for role in await guild.get_roles() + ] @guilds.delete("//roles/", allow_bots=True) @@ -565,7 +569,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs if data.roles is not None: await member.checkPermission(GuildPermissions.MANAGE_ROLES) roles = [int(role) for role in data.roles] - guild_roles = {role.id: role for role in await getCore().getRoles(guild, exclude_default=True)} + guild_roles = {role.id: role for role in await guild.get_roles(True)} roles = [guild_roles[role_id] for role_id in roles if role_id in guild_roles] user_top_role = await member.top_role for role in roles: @@ -634,7 +638,7 @@ async def update_vanity_url(data: SetVanityUrl, user: User = DepUser, guild: Gui await guild.save(update_fields=["vanity_url_code"]) channel = await getCore().getChannel(guild.system_channel) if guild.system_channel is not None else None if channel is None: - channel = (await getCore().getGuildChannels(guild))[0] + channel = (await guild.get_channels())[0] await Invite.create(id=Snowflake.makeId(), channel=channel, inviter=guild.owner, vanity_code=data.code) await getGw().dispatch(GuildUpdateEvent(await guild.ds_json(user_id=user.id)), guild_id=guild.id) return {"code": guild.vanity_url_code} @@ -643,13 +647,14 @@ async def update_vanity_url(data: SetVanityUrl, user: User = DepUser, guild: Gui @guilds.get("//audit-logs", qs_cls=GetAuditLogsQuery, allow_bots=True) async def get_audit_logs(query_args: GetAuditLogsQuery, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) - entries = await getCore().getAuditLogEntries(guild, **query_args.model_dump()) + entries = await guild.get_audit_logs(**query_args.model_dump()) userdatas = {} for entry in entries: target_id = entry.target_id - if target_id and target_id not in userdatas: - if (data := await UserData.get_or_none(id=target_id).select_related("user")) is not None: - userdatas[target_id] = data + if not target_id or target_id in userdatas: + continue + if (data := await UserData.get_or_none(id=target_id).select_related("user")) is not None: + userdatas[target_id] = data userdatas = list(userdatas.values()) return { @@ -700,7 +705,7 @@ async def delete_guild(data: GuildDelete, user: User = DepUser, guild: Guild = D if not (len(data.code) == 8 and await user.use_backup_code(data.code)): raise InvalidDataErr(400, Errors.make(60008)) - role_ids = [role.id for role in await getCore().getRoles(guild, True)] + role_ids = [role.id for role in await guild.get_roles(True)] await guild.delete() await getGw().dispatch(GuildDeleteEvent(guild.id), user_ids=[user.id]) @@ -723,7 +728,10 @@ async def get_guild_webhooks(guild: Guild = DepGuild, member: GuildMember = DepG @guilds.get("//stickers", allow_bots=True) async def get_guild_stickers(guild: Guild = DepGuild): - return [await sticker.ds_json() for sticker in await getCore().getGuildStickers(guild)] + return [ + await sticker.ds_json() + for sticker in await guild.get_stickers() + ] @guilds.post("//stickers", allow_bots=True) @@ -821,8 +829,10 @@ async def get_scheduled_event(query_args: GetScheduledEvent, event_id: int, guil @guilds.get("//scheduled-events", qs_cls=GetScheduledEvent, allow_bots=True) async def get_scheduled_events(query_args: GetScheduledEvent, guild: Guild = DepGuild): - events = await getCore().getGuildEvents(guild) - return [await event.ds_json(with_user_count=query_args.with_user_count) for event in events] + return [ + await event.ds_json(with_user_count=query_args.with_user_count) + for event in await guild.get_events() + ] @guilds.patch("//scheduled-events/", body_cls=UpdateScheduledEvent, allow_bots=True) @@ -847,7 +857,9 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil valid_transition = True if event.status == ScheduledEventStatus.SCHEDULED: - if new_status not in (ScheduledEventStatus.SCHEDULED, ScheduledEventStatus.ACTIVE, ScheduledEventStatus.CANCELED): + if new_status not in { + ScheduledEventStatus.SCHEDULED, ScheduledEventStatus.ACTIVE, ScheduledEventStatus.CANCELED + }: valid_transition = False elif (event.status == ScheduledEventStatus.ACTIVE and new_status != ScheduledEventStatus.COMPLETED) \ and event.status != new_status: diff --git a/yepcord/rest_api/routes/hypesquad.py b/yepcord/rest_api/routes/hypesquad.py index 2ff6182..a3fd090 100644 --- a/yepcord/rest_api/routes/hypesquad.py +++ b/yepcord/rest_api/routes/hypesquad.py @@ -33,7 +33,9 @@ async def api_hypesquad_online(data: HypesquadHouseChange, user: User = DepUser): userdata = await user.data flags = BitFlags(userdata.public_flags, UserFlags) - for f in (UserFlags.HYPESQUAD_ONLINE_HOUSE_1, UserFlags.HYPESQUAD_ONLINE_HOUSE_2, UserFlags.HYPESQUAD_ONLINE_HOUSE_3): + for f in ( + UserFlags.HYPESQUAD_ONLINE_HOUSE_1, UserFlags.HYPESQUAD_ONLINE_HOUSE_2, UserFlags.HYPESQUAD_ONLINE_HOUSE_3 + ): flags.remove(f) flags.add(getattr(UserFlags, f"HYPESQUAD_ONLINE_HOUSE_{data.house_id}")) userdata.public_flags = flags.value diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index 19050c3..f6dbf3b 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -24,8 +24,8 @@ from ..models.oauth2 import AppAuthorizeGetQs, ExchangeCode, AppAuthorizePostQs, AppAuthorizePost from ..utils import captcha from ..y_blueprint import YBlueprint -from ...gateway.events import GuildCreateEvent, MessageCreateEvent, GuildAuditLogEntryCreateEvent, GuildRoleCreateEvent, \ - IntegrationCreateEvent +from ...gateway.events import GuildCreateEvent, MessageCreateEvent, GuildAuditLogEntryCreateEvent, \ + GuildRoleCreateEvent, IntegrationCreateEvent from ...yepcord.config import Config from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType @@ -75,7 +75,7 @@ async def guild_ds_json(g: Guild) -> dict: result["bot"] = (await bot.user.userdata).ds_json result["application"]["bot_public"] = bot.bot_public result["application"]["bot_require_code_grant"] = bot.bot_require_code_grant - result["guilds"] = [await guild_ds_json(guild) for guild in await getCore().getUserGuilds(user)] + result["guilds"] = [await guild_ds_json(guild) for guild in await user.get_guilds()] return result diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 92e2555..b5882c4 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -535,4 +535,4 @@ async def ds_json(guild: Guild) -> dict: "permissions": str(await member.permissions), } - return [await ds_json(guild) for guild in await getCore().getUserGuilds(user)] + return [await ds_json(guild) for guild in await user.get_guilds()] diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 9b63d4b..117071e 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -121,7 +121,9 @@ async def processMessageData(data: Optional[dict], channel: Channel) -> tuple[di content = get_content() total_size += len(content) if total_size > 1024 * 1024 * 100: raise InvalidDataErr(400, Errors.make(50045)) - content_type = file.content_type.strip() if file.content_type else from_buffer(content[:1024], mime=True) + content_type = ( + file.content_type.strip() if file.content_type else from_buffer(content[:1024], mime=True) + ) metadata = {} if content_type.startswith("image/"): img = Image.open(file) diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index e6fe4d9..827bd90 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -16,7 +16,6 @@ along with this program. If not, see . """ import os.path -# noinspection PyPackageRequirements from hashlib import sha256 from hmac import new from json import loads as jloads, dumps as jdumps @@ -35,7 +34,7 @@ from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors from .models import User, Relationship, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, \ - Reaction, Sticker, PermissionOverwrite, AuditLogEntry, Webhook, Role, GuildEvent, ThreadMember + Reaction, Sticker, Webhook, Role, GuildEvent, ThreadMember from .snowflake import Snowflake from .storage import getStorage from .utils import b64encode, b64decode @@ -51,7 +50,9 @@ def __init__(self, key=None): async def getRelationships(self, user: User, with_data=False) -> list[dict]: rels = [] rel: Relationship - for rel in await Relationship.filter(Q(from_user=user) | Q(to_user=user)).select_related("from_user", "to_user"): + for rel in await Relationship.filter( + Q(from_user=user) | Q(to_user=user) + ).select_related("from_user", "to_user"): if (rel_json := await rel.ds_json(user, with_data)) is not None: rels.append(rel_json) return rels @@ -254,10 +255,6 @@ async def createDMGroupChannel(self, user: User, recipients: list[User], name: O await channel.recipients.add(recipient) return channel - async def getLastPinnedMessage(self, channel: Channel) -> Optional[Message]: - return await (Message.filter(pinned_timestamp__not_isnull=True, channel=channel).order_by("-pinned_timestamp") - .limit(1).get_or_none()) - async def getMessageReactionsJ(self, message: Message, user: Union[User, int]) -> list: if isinstance(user, User): user = user.id @@ -308,12 +305,6 @@ async def getRole(self, role_id: int, guild: Optional[Guild] = None) -> Role: q["guild"] = guild return await Role.get_or_none(**q).select_related("guild") - async def getRoles(self, guild: Guild, exclude_default=False) -> list[Role]: - query = Role.filter(guild=guild).select_related("guild") - if exclude_default: - query = query.exclude(id=guild.id) - return await query - async def getGuildMember(self, guild: Guild, user_id: int) -> Optional[GuildMember]: return await GuildMember.get_or_none(guild=guild, user__id=user_id).select_related("user", "guild", "guild__owner") @@ -321,23 +312,12 @@ async def getGuildMember(self, guild: Guild, user_id: int) -> Optional[GuildMemb async def getGuildMembers(self, guild: Guild) -> list[GuildMember]: return await GuildMember.filter(guild=guild).select_related("user") - async def getGuildChannels(self, guild: Guild) -> list[Channel]: - return await Channel.filter(guild=guild) \ - .exclude(type__in=[ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD])\ - .select_related("guild", "parent") - - async def getUserGuilds(self, user: User) -> list[Guild]: - return [member.guild for member in await GuildMember.filter(user=user).select_related("guild", "guild__owner")] - async def getGuildMemberCount(self, guild: Guild) -> int: return await GuildMember.filter(guild=guild).count() async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") - async def getEmojis(self, guild_id: int) -> list[Emoji]: - return await Emoji.filter(guild__id=guild_id).select_related("user") - async def getEmoji(self, emoji_id: int) -> Optional[Emoji]: return await Emoji.get_or_none(id=emoji_id).select_related("guild") @@ -410,39 +390,10 @@ async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) ).select_related("user").limit(limit) - async def getPermissionOverwrite(self, channel: Channel, target: Union[Role, User]) -> Optional[PermissionOverwrite]: - kw = {"target_role": target} if isinstance(target, Role) else {"target_user": target} - return await (PermissionOverwrite.get_or_none(channel=channel, **kw) - .select_related("channel", "channel__guild", "target_role", "target_user")) - - async def getPermissionOverwriteUnk(self, channel: Channel, target_id: int) -> Optional[PermissionOverwrite]: - q = Q(target_role__id=target_id) | Q(target_user__id=target_id) - return await (PermissionOverwrite.filter(channel=channel).get_or_none(q) - .select_related("channel", "channel__guild", "target_role", "target_user")) - - async def getPermissionOverwrites(self, channel: Channel) -> list[PermissionOverwrite]: - return await PermissionOverwrite.filter(channel=channel).select_related("target_role", "target_user") - - async def getOverwritesForMember(self, channel: Channel, member: GuildMember) -> list[PermissionOverwrite]: - role_ids = set(await member.roles.all().values_list("id", flat=True)) - role_ids.add(member.guild.id) - - overwrites = await self.getPermissionOverwrites(channel) - overwrites.sort(key=lambda r: r.type) - return [ - overwrite - for overwrite in overwrites - if overwrite.target.id in role_ids or overwrite.target == member.user - ] - async def getVanityCodeInvite(self, code: str) -> Optional[Invite]: if code is None: return return await Invite.get_or_none(vanity_code=code) - async def getAuditLogEntries(self, guild: Guild, limit: int, before: Optional[int] = None) -> list[AuditLogEntry]: - before = {} if before is None else {"id__lt": before} - return await AuditLogEntry.filter(guild=guild, **before).select_related("guild", "user").limit(limit) - async def getGuildTemplate(self, guild: Guild) -> Optional[GuildTemplate]: return await GuildTemplate.get_or_none(guild=guild).select_related("creator", "guild") @@ -461,21 +412,12 @@ async def setTemplateDirty(self, guild: Guild) -> None: async def getWebhook(self, webhook_id: int) -> Optional[Webhook]: return await Webhook.get_or_none(id=webhook_id).select_related("channel", "channel__guild", "user") - async def getGuildStickers(self, guild: Guild) -> list[Sticker]: - return await Sticker.filter(guild=guild).select_related("guild", "user") - async def getSticker(self, sticker_id: int) -> Optional[Sticker]: return await Sticker.get_or_none(id=sticker_id).select_related("guild", "user") async def getGuildEvent(self, event_id: int) -> Optional[GuildEvent]: return await GuildEvent.get_or_none(id=event_id).select_related("channel", "guild", "creator") - async def getGuildEvents(self, guild: Guild) -> list[GuildEvent]: - return await GuildEvent.filter(guild=guild).select_related("channel", "guild", "creator") - - async def getThreadMembers(self, thread: Channel, limit: int = 100) -> list[ThreadMember]: - return await ThreadMember.filter(channel=thread).select_related("user").limit(limit) - async def getThreadMember(self, thread: Channel, user_id: int) -> Optional[ThreadMember]: return await ThreadMember.get_or_none(channel=thread, user__id=user_id) @@ -499,5 +441,5 @@ def getLanguageCode(self, ip: str, default: str = "en-US") -> str: return country_to_language.get(country_code, default) -ctx._getCore = lambda: Core.getInstance() -ctx._getCDNStorage = lambda: getStorage() +ctx._get_core = Core.getInstance +ctx._get_storage = getStorage diff --git a/yepcord/yepcord/ctx.py b/yepcord/yepcord/ctx.py index 6d10b00..0483011 100644 --- a/yepcord/yepcord/ctx.py +++ b/yepcord/yepcord/ctx.py @@ -18,9 +18,7 @@ from __future__ import annotations -# noinspection PyPackageRequirements -from contextvars import ContextVar, copy_context -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Callable, cast if TYPE_CHECKING: # pragma: no cover from .core import Core @@ -28,52 +26,18 @@ from .gateway_dispatcher import GatewayDispatcher -class _Ctx: - _CTX = ContextVar("ctx") - _instance = None - - def __new__(cls, *args, **kwargs): - if not isinstance(cls._instance, cls): - cls._instance = super(_Ctx, cls).__new__(cls) - return cls._instance - - def get(self, item, default=None): - self._init() - return self.__class__._CTX.get().get(item, default) - - def set(self, key, value): - self._init() - self.__class__._CTX.get()[key] = value - - def _init(self): - v = self.__class__._CTX - if v not in copy_context(): - v.set({}) - return self - - def __setitem__(self, key, value): - self.set(key, value) - - -Ctx = _Ctx() - - -def _getCore(): pass +_get_core: Callable[[], Optional[Core]] = lambda: None +_get_storage: Callable[[], Optional[_Storage]] = lambda: None +_get_gw: Callable[[], Optional[GatewayDispatcher]] = lambda: None def getCore() -> Core: - return Ctx.get("CORE") or _getCore() - - -def _getCDNStorage(): pass + return cast(Core, _get_core()) def getCDNStorage() -> _Storage: - return Ctx.get("STORAGE") or _getCDNStorage() - - -def _getGw(): pass + return cast(_Storage, _get_storage()) def getGw() -> GatewayDispatcher: - return Ctx.get("GW") or _getGw() + return cast(GatewayDispatcher, _get_gw()) diff --git a/yepcord/yepcord/gateway_dispatcher.py b/yepcord/yepcord/gateway_dispatcher.py index 8ecc6f3..fa7a13e 100644 --- a/yepcord/yepcord/gateway_dispatcher.py +++ b/yepcord/yepcord/gateway_dispatcher.py @@ -23,6 +23,7 @@ from tortoise.expressions import RawSQL +from . import ctx from .classes.singleton import Singleton from .enums import ChannelType from .errors import InvalidDataErr @@ -106,18 +107,23 @@ async def sendMessageAck(self, uid: int, channel_id: int, message_id: int, menti async def sendPinsUpdateEvent(self, channel: Channel) -> None: ts = datetime(year=1970, month=1, day=1) - if message := await c.getCore().getLastPinnedMessage(channel): + if message := await channel.get_last_pinned_message(): ts = message.pinned_timestamp ts = ts.strftime("%Y-%m-%dT%H:%M:%S+00:00") await self.dispatch(ChannelPinsUpdateEvent(channel.id, ts), **(await self.getChannelFilter(channel))) async def sendGuildEmojisUpdateEvent(self, guild: Guild) -> None: - emojis = [await emoji.ds_json() for emoji in await c.getCore().getEmojis(guild.id)] + emojis = [ + await emoji.ds_json() + for emoji in await guild.get_emojis() + ] await self.dispatch(GuildEmojisUpdate(guild.id, emojis), guild_id=guild.id) async def sendStickersUpdateEvent(self, guild: Guild) -> None: - stickers = await c.getCore().getGuildStickers(guild) - stickers = [await sticker.ds_json() for sticker in stickers] + stickers = [ + await sticker.ds_json() + for sticker in await guild.get_stickers() + ] await self.dispatch(StickersUpdateEvent(guild.id, stickers), guild_id=guild.id) async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict: @@ -125,11 +131,11 @@ async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict return {"user_ids": await channel.recipients.all().values_list("id", flat=True)} await channel.fetch_related("guild", "guild__owner") - roles = {role.id: role.permissions for role in await c.getCore().getRoles(channel.guild)} + roles = {role.id: role.permissions for role in await channel.guild.get_roles()} user_ids = set() excluded_user_ids = set() - overwrites = await c.getCore().getPermissionOverwrites(channel) + overwrites = await channel.get_permission_overwrites() for overwrite in overwrites: if overwrite.type == 0: role_id = overwrite.target_role.id @@ -138,7 +144,7 @@ async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict roles[role_id] &= ~overwrite.deny roles[role_id] |= overwrite.allow else: - if not (member := await c.getCore().getGuildMember(channel.guild, overwrite.target_user.id)): + if not (member := await ctx.getCore().getGuildMember(channel.guild, overwrite.target_user.id)): continue try: await member.checkPermission(permissions, channel=channel) @@ -162,5 +168,4 @@ async def getRolesByPermissions(self, guild_id: int, permissions: int = 0) -> li .filter(perms=permissions).values_list("id", flat=True) -import yepcord.yepcord.ctx as c -c._getGw = lambda: GatewayDispatcher.getInstance() +ctx._get_gw = GatewayDispatcher.getInstance diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 51b8ee5..f4b55f1 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Union from tortoise import fields from tortoise.expressions import Q @@ -98,7 +98,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: return base_data | { "position": self.position, "permission_overwrites": [ - overwrite.ds_json() for overwrite in await getCore().getPermissionOverwrites(self) + overwrite.ds_json() for overwrite in await self.get_permission_overwrites() ], "parent_id": str(self.parent.id) if self.parent else None, "name": self.name, @@ -111,7 +111,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "rate_limit_per_user": self.rate_limit, "position": self.position, "permission_overwrites": [ - overwrite.ds_json() for overwrite in await getCore().getPermissionOverwrites(self) + overwrite.ds_json() for overwrite in await self.get_permission_overwrites() ], "parent_id": str(self.parent.id) if self.parent else None, "name": self.name, @@ -127,7 +127,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "rate_limit_per_user": self.rate_limit, "position": self.position, "permission_overwrites": [ - overwrite.ds_json() for overwrite in await getCore().getPermissionOverwrites(self) + overwrite.ds_json() for overwrite in await self.get_permission_overwrites() ], "parent_id": str(self.parent.id) if self.parent else None, "name": self.name, @@ -141,7 +141,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "topic": self.topic, "position": self.position, "permission_overwrites": [ - overwrite.ds_json() for overwrite in await getCore().getPermissionOverwrites(self) + overwrite.ds_json() for overwrite in await self.get_permission_overwrites() ], "parent_id": str(self.parent.id) if self.parent else None, "name": self.name, @@ -164,7 +164,10 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: "rate_limit_per_user": self.rate_limit, "flags": self.flags, "total_message_sent": message_count, - "member_ids_preview": [str(member.user.id) for member in await getCore().getThreadMembers(self, 10)] + "member_ids_preview": [ + str(member.user.id) + for member in await self.get_thread_members() + ], } if user_id and (member := await getCore().getThreadMember(self, user_id)) is not None: data["member"] = { @@ -205,3 +208,38 @@ async def dm_unhide(self, for_user: models.User) -> None: async def get_message(self, message_id: int) -> Optional[models.Message]: return await models.Message.Y.get(self, message_id) + + async def get_last_pinned_message(self) -> Optional[models.Message]: + return await models.Message.filter( + pinned_timestamp__not_isnull=True, channel=self + ).order_by("-pinned_timestamp").first() + + async def get_thread_members(self, limit: int = 10) -> list[models.ThreadMember]: + if self.type not in {ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD}: + return [] + return await models.ThreadMember.filter(channel=self).select_related("user").limit(limit) + + async def get_permission_overwrite( + self, target: Union[models.Role, models.User, int] + ) -> Optional[models.PermissionOverwrite]: + query = Q(channel=self) + if isinstance(target, models.Role): + query &= Q(target_role=target) + elif isinstance(target, models.User): + query &= Q(target_user=target) + else: + query &= (Q(target_role__id=target) | Q(target_user__id=target)) + + return await models.PermissionOverwrite.get_or_none(query).select_related( + "channel", "channel__guild", "target_role", "target_user" + ) + + async def get_permission_overwrites( + self, target: Optional[models.GuildMember] = None + ) -> list[models.PermissionOverwrite]: + query = models.PermissionOverwrite.filter(channel=self).select_related("target_role", "target_user") + if target is not None: + role_ids = [target.guild.id, *(await target.roles.all().values_list("id", flat=True))] + query = query.filter(Q(target_role__id__in=role_ids) | Q(target_user__id=target.user.id)).order_by("type") + + return await query diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index ab373ff..48f9f03 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -156,9 +156,12 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo data = { "id": str(self.id), "version": int(time() * 1000), # What is this? - "stickers": [await sticker.ds_json() for sticker in await getCore().getGuildStickers(self)], + "stickers": [ + await sticker.ds_json() + for sticker in await self.get_stickers() + ], "stage_instances": [], - "roles": [role.ds_json() for role in await getCore().getRoles(self)], + "roles": [role.ds_json() for role in await self.get_roles()], "properties": { "afk_timeout": self.afk_timeout, "splash": self.splash, @@ -210,8 +213,14 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo "member_count": await getCore().getGuildMemberCount(self), "lazy": True, "large": False, - "guild_scheduled_events": [await event.ds_json() for event in await getCore().getGuildEvents(self)], - "emojis": [await emoji.ds_json(False) for emoji in await getCore().getEmojis(self.id)], + "guild_scheduled_events": [ + await event.ds_json() + for event in await self.get_events() + ], + "emojis": [ + await emoji.ds_json(False) + for emoji in await self.get_emojis() + ], "data_mode": "full", "application_command_counts": [], } @@ -231,8 +240,32 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo ) ] if for_gateway or with_channels: - data["channels"] = [await channel.ds_json() for channel in await getCore().getGuildChannels(self)] + data["channels"] = [await channel.ds_json() for channel in await self.get_channels()] if with_members: data["members"] = [await member.ds_json() for member in await getCore().getGuildMembers(self)] return data + + async def get_roles(self, exclude_default: bool = False) -> list[models.Role]: + query = models.Role.filter(guild=self).select_related("guild") + if exclude_default: + query = query.exclude(id=self.id) + return await query + + async def get_channels(self) -> list[models.Channel]: + return await models.Channel.filter(guild=self) \ + .exclude(type__in=[ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD]) \ + .select_related("guild", "parent") + + async def get_emojis(self) -> list[models.Emoji]: + return await models.Emoji.filter(guild=self).select_related("user") + + async def get_audit_logs(self, limit: int, before: Optional[int] = None) -> list[models.AuditLogEntry]: + before = {} if before is None else {"id__lt": before} + return await models.AuditLogEntry.filter(guild=self, **before).select_related("guild", "user").limit(limit) + + async def get_stickers(self) -> list[models.Sticker]: + return await models.Sticker.filter(guild=self).select_related("guild", "user") + + async def get_events(self) -> list[models.GuildEvent]: + return await models.GuildEvent.filter(guild=self).select_related("channel", "guild", "creator") diff --git a/yepcord/yepcord/models/guild_member.py b/yepcord/yepcord/models/guild_member.py index 4589965..f3b799c 100644 --- a/yepcord/yepcord/models/guild_member.py +++ b/yepcord/yepcord/models/guild_member.py @@ -23,7 +23,6 @@ from tortoise import fields -from ..ctx import getCore from ..enums import GuildPermissions from ..errors import InvalidDataErr, Errors from ._utils import SnowflakeField, Model @@ -45,8 +44,7 @@ def _check(perms: int, perm: int) -> bool: if _check(permissions, GuildPermissions.ADMINISTRATOR): return if channel: - overwrites = await getCore().getOverwritesForMember(channel, self.member) - for overwrite in overwrites: + for overwrite in await channel.get_permission_overwrites(self.member): permissions &= ~overwrite.deny permissions |= overwrite.allow diff --git a/yepcord/yepcord/models/guild_template.py b/yepcord/yepcord/models/guild_template.py index 984d378..9850eaa 100644 --- a/yepcord/yepcord/models/guild_template.py +++ b/yepcord/yepcord/models/guild_template.py @@ -23,7 +23,6 @@ import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model -from ..ctx import getCore from ..enums import ChannelType from ..snowflake import Snowflake from ..utils import b64encode, int_size @@ -76,7 +75,7 @@ async def serialize_guild(guild: models.Guild) -> dict: serialized_channels = [] # Serialize roles - roles = await getCore().getRoles(guild) + roles = await guild.get_roles() roles.sort(key=lambda r: r.id) for role in roles: if role.managed: @@ -96,11 +95,11 @@ async def serialize_guild(guild: models.Guild) -> dict: }) # Serialize channels - channels = await getCore().getGuildChannels(guild) + channels = await guild.get_channels() channels.sort(key=lambda ch: (int(ch.type == ChannelType.GUILD_CATEGORY), ch.id), reverse=True) for channel in channels: serialized_permission_overwrites = [] - for overwrite in await getCore().getPermissionOverwrites(channel): + for overwrite in await channel.get_permission_overwrites(): if overwrite.type == 0: # Overwrite for role role_id = replaced_ids[overwrite.target.id] if role_id is None: diff --git a/yepcord/yepcord/models/readstate.py b/yepcord/yepcord/models/readstate.py index a1aea3c..9127d97 100644 --- a/yepcord/yepcord/models/readstate.py +++ b/yepcord/yepcord/models/readstate.py @@ -18,7 +18,6 @@ from tortoise import fields -from ..ctx import getCore import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model @@ -36,7 +35,7 @@ class Meta: ) async def ds_json(self) -> dict: - last_pin = await getCore().getLastPinnedMessage(self.channel) + last_pin = await self.channel.get_last_pinned_message() last_pin_ts = last_pin.pinned_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00") if last_pin is not None else None return { "mention_count": self.count, diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index e309f66..acc3197 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -280,3 +280,9 @@ async def y_delete(self) -> None: await models.FrecencySettings.filter(user=self).delete() await models.Invite.filter(inviter=self).delete() await models.ReadState.filter(user=self).delete() + + async def get_guilds(self) -> list[models.Guild]: + return [ + member.guild for member in + await models.GuildMember.filter(user=self).select_related("guild", "guild__owner") + ] From d8a8145f3e456541ba8f0d989394a677f8803be1 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Thu, 5 Sep 2024 20:10:27 +0300 Subject: [PATCH 07/26] refactor errors --- yepcord/rest_api/dependencies.py | 27 +++++----- yepcord/rest_api/models/users_me.py | 8 +-- yepcord/rest_api/routes/auth.py | 14 ++--- yepcord/rest_api/routes/channels.py | 63 +++++++++++----------- yepcord/rest_api/routes/guilds.py | 56 ++++++++++---------- yepcord/rest_api/routes/interactions.py | 27 +++++----- yepcord/rest_api/routes/invites.py | 8 +-- yepcord/rest_api/routes/oauth2.py | 10 ++-- yepcord/rest_api/routes/users_me.py | 66 ++++++++++++----------- yepcord/rest_api/routes/webhooks.py | 18 +++---- yepcord/rest_api/utils.py | 17 +++--- yepcord/yepcord/classes/connections.py | 6 +-- yepcord/yepcord/core.py | 6 +-- yepcord/yepcord/ctx.py | 8 +-- yepcord/yepcord/errors.py | 70 +++++++++++++++++++++++++ yepcord/yepcord/models/guild_member.py | 4 +- yepcord/yepcord/models/relationship.py | 6 +-- yepcord/yepcord/models/user.py | 4 +- 18 files changed, 248 insertions(+), 170 deletions(-) diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index ce60a39..1477660 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -25,7 +25,8 @@ from yepcord.rest_api.utils import getSessionFromToken from yepcord.yepcord.ctx import getCore -from yepcord.yepcord.errors import InvalidDataErr, Errors +from yepcord.yepcord.errors import InvalidDataErr, Errors, UnknownApplication, UnknownInvite, UnknownMessage, \ + UnknownRole, UnknownGuildTemplate, MissingAccess, Unauthorized from yepcord.yepcord.models import Session, Authorization, Bot, User, Channel, Message, Webhook, Invite, Guild, \ GuildMember, Role, GuildTemplate, Application, Interaction from yepcord.yepcord.snowflake import Snowflake @@ -70,7 +71,7 @@ async def depChannelO(channel_id: Optional[int] = None, user: User = Depends(dep if (channel := await getCore().getChannel(channel_id)) is None: return if not await getCore().getUserByChannel(channel, user.id): - raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) + raise Unauthorized return channel @@ -108,7 +109,7 @@ async def depMessageO( elif channel is not None and user is not None: message = await channel.get_message(message) else: - raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) + raise Unauthorized if message is not None: return message @@ -130,7 +131,7 @@ async def depInvite(invite: Optional[str] = None) -> Invite: raise ValueError except ValueError: if not (invite := await getCore().getVanityCodeInvite(invite)): - raise InvalidDataErr(404, Errors.make(10006)) + raise UnknownInvite return invite @@ -139,7 +140,7 @@ async def depGuildO(guild: Optional[int] = None, user: User = Depends(depUser()) if (guild := await getCore().getGuild(guild)) is None: return if not await GuildMember.filter(guild=guild, user=user).exists(): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess return guild @@ -153,7 +154,7 @@ async def depGuildMember(guild: Guild = Depends(depGuild), user: User = Depends( async def depRole(role: int, guild: Guild = Depends(depGuild)) -> Role: if not role or not (role := await getCore().getRole(role, guild)): - raise InvalidDataErr(404, Errors.make(10011)) + raise UnknownRole return role @@ -163,43 +164,43 @@ async def depGuildTemplate(template: str, guild: Guild = Depends(depGuild)) -> G if not (template := await getCore().getGuildTemplateById(template_id, guild)): raise ValueError except ValueError: - raise InvalidDataErr(404, Errors.make(10057)) + raise UnknownGuildTemplate return template async def depApplication(application_id: int, user: User = Depends(depUser())) -> Application: if user.is_bot and application_id != user.id: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication kw = {"id": application_id, "deleted": False} if not user.is_bot: kw["owner"] = user if (app := await Application.get_or_none(**kw).select_related("owner")) is None: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication return app async def depInteraction(interaction: int, token: str) -> Interaction: if not token.startswith("int___"): - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication interaction = await (Interaction.get_or_none(id=interaction) .select_related("application", "user", "channel", "command")) if interaction is None or interaction.ds_token != token: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication return interaction async def depInteractionW(application_id: int, token: str) -> Message: if not (inter := await Interaction.from_token(f"int___{token}")) or inter.application.id != application_id: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication message = await Message.get_or_none(interaction=inter, id__gt=Snowflake.fromTimestamp(time() - 15 * 60)) \ .select_related(*Message.DEFAULT_RELATED) if message is None: - raise InvalidDataErr(404, Errors.make(10008)) + raise UnknownMessage return message diff --git a/yepcord/rest_api/models/users_me.py b/yepcord/rest_api/models/users_me.py index 74c6ff7..6b0c064 100644 --- a/yepcord/rest_api/models/users_me.py +++ b/yepcord/rest_api/models/users_me.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, Field, field_validator from typing import Optional, List -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import InvalidDataErr, Errors, UnknownToken from ...yepcord.utils import b64decode @@ -169,7 +169,7 @@ def validate_fingerprint(cls, value: Optional[str]) -> Optional[str]: try: assert len(b64decode(value)) == 32 except (ValueError, AssertionError): - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken return value @@ -183,7 +183,7 @@ def validate_handshake_token(cls, value: str) -> str: try: int(value) except ValueError: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken return value @@ -196,7 +196,7 @@ def validate_handshake_token(cls, value: str) -> str: try: int(value) except ValueError: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken return value diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index a758e7c..a0545dd 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -28,7 +28,7 @@ from ...gateway.events import UserUpdateEvent from ...yepcord.classes.other import EmailMsg from ...yepcord.ctx import getCore, getGw -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import InvalidDataErr, Errors, PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket from ...yepcord.models import Session, User from ...yepcord.utils import LOCALES, b64decode @@ -62,16 +62,16 @@ async def login(data: Login): @auth.post("/mfa/totp", body_cls=MfaLogin) async def login_with_mfa(data: MfaLogin): if not (ticket := data.ticket): - raise InvalidDataErr(400, Errors.make(60006)) + raise Invalid2FaAuthTicket if not (code := data.code): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode if not (mfa := await getCore().getMfaFromTicket(ticket)): - raise InvalidDataErr(400, Errors.make(60006)) + raise Invalid2FaAuthTicket code = code.replace("-", "").replace(" ", "") user = await User.y.get(mfa.uid) if code not in mfa.getCodes(): if not (len(code) == 8 and await user.use_backup_code(code)): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode sess = await Session.Y.create(user) sett = await user.settings return { @@ -90,9 +90,9 @@ async def logout(session: Session = DepSession): @auth.post("/verify/view-backup-codes-challenge", body_cls=ViewBackupCodes) async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User = DepUser): if not (password := data.password): - raise InvalidDataErr(400, Errors.make(50018)) + raise PasswordDoesNotMatch if not user.check_password(password): - raise InvalidDataErr(400, Errors.make(50018)) + raise PasswordDoesNotMatch nonce = await getCore().generateUserMfaNonce(user) code = await getCore().mfaNonceToCode(nonce[0]) diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 8cd06c2..03e926b 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -37,7 +37,9 @@ WebhooksUpdateEvent, ThreadCreateEvent, ThreadMemberUpdateEvent, MessageAckEvent, GuildAuditLogEntryCreateEvent from ...yepcord.ctx import getCore, getCDNStorage, getGw from ...yepcord.enums import GuildPermissions, MessageType, ChannelType, WebhookType, GUILD_CHANNELS, MessageFlags -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import UnknownMessage, UnknownUser, UnknownEmoji, UnknownInteraction, \ + MaxPinsReached, MissingPermissions, CannotSendToThisUser, CannotExecuteOnDM, CannotEditAnotherUserMessage, \ + MissingAccess from ...yepcord.models import User, Channel, Message, ReadState, Emoji, PermissionOverwrite, Webhook, ThreadMember, \ ThreadMetadata, AuditLogEntry, Relationship, ApplicationCommand, Integration, Bot, Role, HiddenDmChannel, Invite, \ Reaction @@ -60,7 +62,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha if channel.type == ChannelType.GROUP_DM: changes = data.to_json(channel.type) if "owner_id" in changes and channel.owner != user: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions elif "owner_id" in changes: new_owner = await User.get_or_none(id=changes["owner_id"]) if new_owner in await channel.recipients.all(): @@ -170,7 +172,7 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): if channel.type == ChannelType.DM: oth = await channel.other_user(user) if await Relationship.utils.is_blocked(oth, user): - raise InvalidDataErr(403, Errors.make(50007)) + raise CannotSendToThisUser elif channel.guild: member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.SEND_MESSAGES, GuildPermissions.VIEW_CHANNEL, @@ -195,12 +197,11 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): @channels.delete("//messages/", allow_bots=True) async def delete_message(user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if message.author != user: - if channel.type in GUILD_CHANNELS: - member = await getCore().getGuildMember(channel.guild, user.id) - await member.checkPermission(GuildPermissions.MANAGE_MESSAGES, GuildPermissions.VIEW_CHANNEL, - GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) - else: - raise InvalidDataErr(403, Errors.make(50003)) + if channel.type not in GUILD_CHANNELS: + raise CannotExecuteOnDM + member = await getCore().getGuildMember(channel.guild, user.id) + await member.checkPermission(GuildPermissions.MANAGE_MESSAGES, GuildPermissions.VIEW_CHANNEL, + GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) guild_id = channel.guild.id if channel.guild else None await message.delete() await getGw().dispatch(MessageDeleteEvent(message.id, channel.id, guild_id), channel=channel, @@ -212,7 +213,7 @@ async def delete_message(user: User = DepUser, channel: Channel = DepChannel, me async def edit_message(data: MessageUpdate, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if message.author != user: - raise InvalidDataErr(403, Errors.make(50005)) + raise CannotEditAnotherUserMessage if channel.guild: member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.SEND_MESSAGES, GuildPermissions.VIEW_CHANNEL, @@ -229,7 +230,7 @@ async def get_message(user: User = DepUser, channel: Channel = DepChannel, messa member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) if message.ephemeral and message.author != user: - raise InvalidDataErr(404, Errors.make(10008)) + raise UnknownMessage return await message.ds_json(user_id=user.id) @@ -268,9 +269,9 @@ async def send_typing_event(user: User = DepUser, channel: Channel = DepChannel) @channels.put("//recipients/") async def add_recipient(target_user: int, user: User = DepUser, channel: Channel = DepChannel): if (target_user := await User.y.get(target_user, False)) is None: - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser if channel.type not in (ChannelType.DM, ChannelType.GROUP_DM): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions if channel.type == ChannelType.DM: recipients = await channel.recipients.filter(~Q(id=user.id)).all() recipients.append(target_user) @@ -296,9 +297,9 @@ async def add_recipient(target_user: int, user: User = DepUser, channel: Channel @channels.delete("//recipients/") async def delete_recipient(target_user: int, user: User = DepUser, channel: Channel = DepChannel): if channel.type not in (ChannelType.GROUP_DM,): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions if channel.owner != user or target_user == user.id: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions target_user = await User.y.get(target_user, False) recipients = await channel.recipients.all() if target_user in recipients: @@ -320,7 +321,7 @@ async def pin_message(user: User = DepUser, channel: Channel = DepChannel, messa await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.VIEW_CHANNEL, channel=channel) if not message.pinned: if await Message.filter(pinned_timestamp__not_isnull=True, channel=message.channel).count() >= 50: - raise InvalidDataErr(400, Errors.make(30003)) + raise MaxPinsReached message.pinned_timestamp = datetime.now() await message.save(update_fields=["pinned_timestamp"]) @@ -374,7 +375,7 @@ async def add_message_reaction(reaction: str, user: User = DepUser, channel: Cha await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): - raise InvalidDataErr(400, Errors.make(10014)) + raise UnknownEmoji emoji = { "emoji": None if not isinstance(reaction, Emoji) else reaction, "emoji_name": reaction if isinstance(reaction, str) else reaction.name @@ -393,7 +394,7 @@ async def remove_message_reaction(reaction: str, user: User = DepUser, channel: await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): - raise InvalidDataErr(400, Errors.make(10014)) + raise UnknownEmoji emoji = { "emoji": None if not isinstance(reaction, Emoji) else reaction, "emoji_name": reaction if isinstance(reaction, str) else reaction.name @@ -412,7 +413,7 @@ async def get_message_reactions(query_args: GetReactionsQuery, reaction: str, us await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): - raise InvalidDataErr(400, Errors.make(10014)) + raise UnknownEmoji emoji = None if not isinstance(reaction, Emoji) else reaction emoji_name = reaction if isinstance(reaction, str) else reaction.name @@ -458,9 +459,9 @@ async def create_invite(data: InviteCreate, user: User = DepUser, channel: Chann async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, target_id: int, user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM if not (member := await getCore().getGuildMember(channel.guild, user.id)): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess target = await getCore().getRole(target_id) if data.type == 0 else await User.get_or_none(id=target_id) if target is None or (isinstance(target, Role) and target.guild != channel.guild): return "", 204 @@ -489,9 +490,9 @@ async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, @channels.delete("//permissions/", allow_bots=True) async def delete_permission_overwrite(target_id: int, user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM if not (member := await getCore().getGuildMember(channel.guild, user.id)): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.MANAGE_ROLES, channel=channel) overwrite = await channel.get_permission_overwrite(target_id) @@ -513,9 +514,9 @@ async def delete_permission_overwrite(target_id: int, user: User = DepUser, chan @channels.get("//invites", allow_bots=True) async def get_channel_invites(user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM if not (member := await getCore().getGuildMember(channel.guild, user.id)): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess await member.checkPermission(GuildPermissions.VIEW_CHANNEL, channel=channel) return [ await invite.ds_json() @@ -527,7 +528,7 @@ async def get_channel_invites(user: User = DepUser, channel: Channel = DepChanne @channels.post("//webhooks", body_cls=WebhookCreate, allow_bots=True) async def create_webhook(data: WebhookCreate, user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) @@ -542,7 +543,7 @@ async def create_webhook(data: WebhookCreate, user: User = DepUser, channel: Cha @channels.get("//webhooks", allow_bots=True) async def get_channel_webhooks(user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) @@ -557,7 +558,7 @@ async def create_thread( data: CreateThread, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage ): if not channel.guild: - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.CREATE_PUBLIC_THREADS, channel=channel) @@ -593,7 +594,7 @@ async def search_application_commands( dm_user = None if (channel.type not in (*GUILD_CHANNELS, ChannelType.DM) or (channel.type == ChannelType.DM and not (dm_user := await channel.other_user(user)).is_bot)): - raise InvalidDataErr(403, Errors.make(50003)) + raise CannotExecuteOnDM try: cursor = loads(b64decode(query_args.cursor)) if query_args.cursor else [0] @@ -656,9 +657,9 @@ async def get_message_interaction(user: User = DepUser, channel: Channel = DepCh member = await getCore().getGuildMember(channel.guild, user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) if message.ephemeral and message.author != user: - raise InvalidDataErr(404, Errors.make(10008)) + raise UnknownMessage if not message.interaction: - raise InvalidDataErr(404, Errors.make(10062)) + raise UnknownInteraction return await message.interaction.get_command_info() | { "application_command": None, "options": (message.interaction.data or {}).get("options", []) diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index a7e0a0a..7427c7b 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -41,7 +41,9 @@ from ...yepcord.ctx import getCore, getCDNStorage, getGw from ...yepcord.enums import GuildPermissions, StickerType, StickerFormat, ScheduledEventStatus, ChannelType, \ ScheduledEventEntityType -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import InvalidDataErr, Errors, UnknownRole, UnknownUser, UnknownGuildTemplate, UnknownSticker, \ + UnknownGuildEvent, UnknownEmoji, CanHaveOneTemplate, MissingPermissions, InvalidRole, Invalid2FaCode, \ + CannotSendEmptyMessage, InvalidAsset from ...yepcord.models import User, Guild, GuildMember, GuildTemplate, Emoji, Channel, PermissionOverwrite, UserData, \ Role, Invite, Sticker, GuildEvent, AuditLogEntry, Integration, ApplicationCommand, Webhook, GuildBan from ...yepcord.snowflake import Snowflake @@ -120,7 +122,7 @@ async def create_guild_template(data: TemplateCreate, user: User = DepUser, guil member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) if await getCore().getGuildTemplate(guild): - raise InvalidDataErr(400, Errors.make(30031)) + raise CanHaveOneTemplate template: GuildTemplate = await GuildTemplate.create( id=Snowflake.makeId(), guild=guild, name=data.name, description=data.description, creator=user, @@ -187,7 +189,7 @@ async def update_guild_emoji(data: EmojiUpdate, emoji: int, guild: Guild = DepGu member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) if (emoji := await getCore().getEmoji(emoji)) is None or emoji.guild != guild: - raise InvalidDataErr(400, Errors.make(10014)) + raise UnknownEmoji await emoji.update(**data.model_dump(exclude_defaults=True)) await getGw().sendGuildEmojisUpdateEvent(guild) @@ -245,7 +247,7 @@ async def create_channel(data: ChannelCreate, user: User = DepUser, guild: Guild for overwrite in data.permission_overwrites: target = await getCore().getRole(overwrite.id) if data.type == 0 else await User.get_or_none(id=overwrite.id) if target is None: - raise InvalidDataErr(404, Errors.make(10011 if data.type == 0 else 10013)) + raise (UnknownRole if data.type == 0 else UnknownUser) kw = {"target_role": target} if isinstance(target, Role) else {"target_user": target} await PermissionOverwrite.create(**overwrite.model_dump(), channel=channel, **kw) @@ -305,7 +307,7 @@ async def kick_member(user_id: int, user: User = DepUser, guild: Guild = DepGuil if not (target_member := await getCore().getGuildMember(guild, user_id)): return "", 204 if not await member.perm_checker.canKickOrBan(target_member): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions await getGw().dispatchUnsub([target_member.user.id], guild.id) for role in await target_member.roles.all(): await getGw().dispatchUnsub([target_member.user.id], role_id=role.id) @@ -328,7 +330,7 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: await member.checkPermission(GuildPermissions.BAN_MEMBERS) target_member = await getCore().getGuildMember(guild, user_id) if target_member is not None and not await member.perm_checker.canKickOrBan(target_member): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions if await GuildBan.exists(guild=guild, user__id=user_id): return "", 204 reason = request.headers.get("x-audit-log-reason", "") @@ -343,7 +345,7 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: target_user = target_member.user else: if (target_user := await User.y.get(user_id, False)) is None: - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser await GuildBan.create(user=target_user, guild=guild, reason=reason) target_user_data = await target_user.data if target_member is not None: @@ -465,7 +467,7 @@ async def update_roles_positions(guild: Guild = DepGuild, member: GuildMember = roles = {role.id: role for role in await guild.get_roles(True)} if not await member.perm_checker.canChangeRolesPositions(roles_data, list(roles.values())): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions roles_data = RolesPositionsChangeList(changes=roles_data) @@ -496,7 +498,7 @@ async def delete_role(user: User = DepUser, guild: Guild = DepGuild, member: Gui role: Role = DepRole): await member.checkPermission(GuildPermissions.MANAGE_ROLES) if role.managed: - raise InvalidDataErr(400, Errors.make(50028)) + raise InvalidRole await role.delete() await getGw().dispatch(GuildRoleDeleteEvent(guild.id, role.id), guild_id=guild.id, permissions=GuildPermissions.MANAGE_ROLES) @@ -543,9 +545,9 @@ async def add_role_members(data: AddRoleMembers, user: User = DepUser, guild: Gu member: GuildMember = DepGuildMember, role: Role = DepRole): await member.checkPermission(GuildPermissions.MANAGE_ROLES) if role.managed: - raise InvalidDataErr(400, Errors.make(50028)) + raise InvalidRole if role.id == guild.id or (role.position >= (await member.top_role).position and user != guild.owner): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions members = {} for member_id in data.member_ids: target_member = await getCore().getGuildMember(guild, member_id) @@ -574,7 +576,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs user_top_role = await member.top_role for role in roles: if guild_roles[role.id].position >= user_top_role.position and member.user != guild.owner: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions add, remove = await getCore().setMemberRolesFromList(target_member, roles) for role_id in add: await getGw().dispatchSub([target_user], role_id=role_id) @@ -589,7 +591,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs ) if data.avatar != "": if target_member != member: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions img = data.avatar if img is not None: img = getImage(img) @@ -676,7 +678,7 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use if not (template := await getCore().getGuildTemplateById(template_id)): raise ValueError except ValueError: - raise InvalidDataErr(404, Errors.make(10057)) + raise UnknownGuildTemplate guild = await Guild.Y.create_from_template(user, template, data.name) @@ -696,14 +698,14 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use @guilds.post("//delete", body_cls=GuildDelete, allow_bots=True) async def delete_guild(data: GuildDelete, user: User = DepUser, guild: Guild = DepGuild): if user != guild.owner: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions if mfa := await user.mfa: if not data.code: - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode if data.code not in mfa.getCodes(): if not (len(data.code) == 8 and await user.use_backup_code(data.code)): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode role_ids = [role.id for role in await guild.get_roles(True)] @@ -738,14 +740,14 @@ async def get_guild_stickers(guild: Guild = DepGuild): async def upload_guild_stickers(user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) if request.content_length is not None and request.content_length > 1024 * 512: - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage async with timeout(current_app.config["BODY_TIMEOUT"]): if not (file := (await request.files).get("file")): - raise InvalidDataErr(400, Errors.make(50046)) + raise InvalidAsset data = CreateSticker(**dict(await request.form)) sticker_b = BytesIO(getattr(file, "getvalue", file.read)()) if sticker_b.getbuffer().nbytes > 1024 * 512 or not (img := getImage(sticker_b)) or not validImage(img): - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage sticker_type = getattr(StickerFormat, str(imageType(img)).upper(), StickerFormat.PNG) sticker_id = Snowflake.makeId() @@ -766,7 +768,7 @@ async def update_guild_sticker(data: UpdateSticker, sticker_id: int, guild: Guil member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) if not (sticker := await getCore().getSticker(sticker_id)) or sticker.guild != guild: - raise InvalidDataErr(404, Errors.make(10060)) + raise UnknownSticker await sticker.update(**data.model_dump(exclude_defaults=True)) await getGw().sendStickersUpdateEvent(guild) return await sticker.ds_json() @@ -776,7 +778,7 @@ async def update_guild_sticker(data: UpdateSticker, sticker_id: int, guild: Guil async def delete_guild_sticker(sticker_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) if not (sticker := await getCore().getSticker(sticker_id)) or sticker.guild != guild: - raise InvalidDataErr(404, Errors.make(10060)) + raise UnknownSticker await sticker.delete() await getGw().sendStickersUpdateEvent(guild) @@ -822,7 +824,7 @@ async def create_scheduled_event(data: CreateEvent, user: User = DepUser, guild: @guilds.get("//scheduled-events/", qs_cls=GetScheduledEvent, allow_bots=True) async def get_scheduled_event(query_args: GetScheduledEvent, event_id: int, guild: Guild = DepGuild): if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: - raise InvalidDataErr(404, Errors.make(10070)) + raise UnknownGuildEvent return await event.ds_json(with_user_count=query_args.with_user_count) @@ -840,7 +842,7 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EVENTS) if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: - raise InvalidDataErr(404, Errors.make(10070)) + raise UnknownGuildEvent if (img := data.image) or img is None: if img is not None: @@ -881,7 +883,7 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil async def subscribe_to_scheduled_event(event_id: int, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: - raise InvalidDataErr(404, Errors.make(10070)) + raise UnknownGuildEvent await event.subscribers.add(member) await getGw().dispatch(ScheduledEventUserAddEvent(user.id, event_id, guild.id), @@ -897,7 +899,7 @@ async def subscribe_to_scheduled_event(event_id: int, user: User = DepUser, guil async def unsubscribe_from_scheduled_event(event_id: int, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: - raise InvalidDataErr(404, Errors.make(10070)) + raise UnknownGuildEvent if await event.subscribers.filter(user__id=user.id).get_or_none() is not None: await event.subscribers.remove(member) @@ -911,7 +913,7 @@ async def unsubscribe_from_scheduled_event(event_id: int, user: User = DepUser, async def delete_scheduled_event(event_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EVENTS) if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: - raise InvalidDataErr(404, Errors.make(10070)) + raise UnknownGuildEvent await event.delete() await getGw().dispatch(GuildScheduledEventDeleteEvent(await event.ds_json()), guild_id=guild.id) diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index b42d16c..49f958f 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -27,7 +27,8 @@ from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import GuildPermissions, InteractionStatus, MessageFlags, InteractionCallbackType, \ ApplicationCommandOptionType, MessageType, ApplicationCommandType -from ...yepcord.errors import Errors, InvalidDataErr +from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, UnknownMessage, \ + UnknownUser, MissingAccess, CannotSendEmptyMessage, InteractionAlreadyAck, Unauthorized from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild from ...yepcord.models.interaction import Interaction from ...yepcord.snowflake import Snowflake @@ -122,34 +123,34 @@ def validate_options(user_options: list[InteractionOption], bot_options: list[di async def create_interaction(user: User = DepUser): data = InteractionCreate(**(await get_multipart_json())) if (application := await Application.get_or_none(id=data.application_id, deleted=False)) is None: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication guild = None channel = await getCore().getChannel(data.channel_id) if data.guild_id: if (guild := await getCore().getGuild(data.guild_id)) is None: - raise InvalidDataErr(404, Errors.make(10004)) + raise UnknownGuild if (member := await getCore().getGuildMember(guild, user.id)) is None: - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess P = GuildPermissions await member.checkPermission(P.VIEW_CHANNEL, P.USE_APPLICATION_COMMANDS, channel=channel) if channel.guild != guild: - raise InvalidDataErr(404, Errors.make(10003)) + raise UnknownChannel if not await getCore().getUserByChannel(channel, user.id): - raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) + raise Unauthorized if guild is not None: - if (await Integration.get_or_none(guild=guild, application=application)) is None: - raise InvalidDataErr(404, Errors.make(10002)) + if not await Integration.exists(guild=guild, application=application): + raise UnknownApplication command_query = Q(id=data.data.id, application=application) & (Q(guild=guild) | Q(guild=None)) if (command := await ApplicationCommand.get_or_none(command_query)) is None: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication message = None target_member = None if command.type == ApplicationCommandType.MESSAGE and \ (message := await channel.get_message(data.data.target_id)) is None: - raise InvalidDataErr(404, Errors.make(10008)) + raise UnknownMessage if command.type == ApplicationCommandType.USER and \ (target_member := await getCore().getGuildMember(guild, data.data.target_id)) is None: - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser if data.data.version != command.version or data.data.type != command.type or data.data.name != command.name: raise InvalidDataErr(400, Errors.make(50035, {"data": { @@ -223,10 +224,10 @@ async def respond_to_interaction(data: InteractionRespond, interaction: Interact T = InteractionCallbackType d = data.data if interaction.status != InteractionStatus.PENDING: - raise InvalidDataErr(400, Errors.make(40060)) + raise InteractionAlreadyAck if data.type == T.CHANNEL_MESSAGE_WITH_SOURCE: if not d.content: - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage flags = d.flags & MessageFlags.EPHEMERAL await send_interaction_response(interaction, flags, d.content) elif data.type == T.DEFFERED_CHANNEL_MESSAGE_WITH_SOURCE: diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index 42776b2..4777b58 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -23,7 +23,7 @@ InviteDeleteEvent from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import ChannelType, GuildPermissions, MessageType -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import UnknownInvite, UserBanned, MissingAccess from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan from ...yepcord.snowflake import Snowflake @@ -47,7 +47,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): if channel.type == ChannelType.GROUP_DM: recipients = await channel.recipients.all() if user not in recipients and len(recipients) >= 10: - raise InvalidDataErr(404, Errors.make(10006)) + raise UnknownInvite inv = await invite.ds_json() for excl in ["max_age", "created_at"]: # Remove excluded fields if excl in inv: @@ -74,7 +74,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): if not await getCore().getGuildMember(channel.guild, user.id): guild = channel.guild if await GuildBan.exists(guild=guild, user=user): - raise InvalidDataErr(403, Errors.make(40007)) + raise UserBanned inv["new_member"] = True await GuildMember.create(id=Snowflake.makeId(), user=user, guild=guild) await getGw().dispatch(GuildCreateEvent(await guild.ds_json(user_id=user.id)), user_ids=[user.id]) @@ -95,7 +95,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): async def delete_invite(user: User = DepUser, invite: Invite = DepInvite): if invite.channel.guild: if (member := await getCore().getGuildMember(invite.channel.guild, user.id)) is None: - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_GUILD) await invite.delete() await getGw().dispatch(InviteDeleteEvent(invite), guild_id=invite.channel.guild.id) diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index f6dbf3b..07bea2c 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -29,7 +29,7 @@ from ...yepcord.config import Config from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType -from ...yepcord.errors import Errors, InvalidDataErr +from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownGuild, MissingAccess from ...yepcord.models import User, Guild, GuildMember, Message, Role, AuditLogEntry, Application, Bot, Authorization, \ Integration, GuildBan from ...yepcord.snowflake import Snowflake @@ -47,7 +47,7 @@ async def get_application_authorization_info(query_args: AppAuthorizeGetQs, user {"scope": {"code": "SCOPE_INVALID", "message": "Invalid scope"}})) if (application := await Application.get_or_none(id=query_args.client_id, deleted=False)) is None: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication bot = await Bot.get(id=application.id).select_related("user") result = { @@ -89,7 +89,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori {"scope": {"code": "SCOPE_INVALID", "message": "Invalid scope"}})) if (application := await Application.get_or_none(id=query_args.client_id, deleted=False)) is None: - raise InvalidDataErr(404, Errors.make(10002)) + raise UnknownApplication if not data.authorize: return {"location": f"{query_args.redirect_uri}?error=access_denied&error_description=" @@ -101,10 +101,10 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori f"Guild+not+specified."} if (guild := await getCore().getGuild(data.guild_id)) is None: - raise InvalidDataErr(404, Errors.make(10004)) + raise UnknownGuild if not (member := await getCore().getGuildMember(guild, user.id)): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_GUILD) bot = await Bot.get(id=application.id).select_related("user") diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index b5882c4..c3ccbd6 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -33,7 +33,9 @@ from ...yepcord.config import Config from ...yepcord.ctx import getCore, getCDNStorage, getGw from ...yepcord.enums import RelationshipType, ChannelType -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import InvalidDataErr, Errors, UnknownToken, UnknownUser, UnknownConnection, UnknownDiscordTag, \ + AlreadyFriends, MalformedUserSettings, Already2Fa, PasswordDoesNotMatch, Invalid2FaSecret, Invalid2FaCode, \ + NotYet2Fa, InvalidKey, InvalidGuild, InvalidRecipient, MustTransferGuildsBeforeDelete, MissingAccess from ...yepcord.models import User, UserSettingsProto, FrecencySettings, UserNote, Session, UserData, Guild, \ GuildMember, RemoteAuthSession, Relationship, Authorization, Bot, ConnectedAccount, GuildEvent, Channel from ...yepcord.models.remote_auth_session import time_plus_150s @@ -155,7 +157,7 @@ async def update_protobuf_settings(data: SettingsProtoUpdate, user: User = DepUs proto = PreloadedUserSettings() proto.ParseFromString(_b64decode(data.settings.encode("utf8"))) except ValueError: - raise InvalidDataErr(400, Errors.make(50104)) + raise MalformedUserSettings settings = await user.settings settings_proto = UserSettingsProto(settings) await settings_proto.update(proto) @@ -180,7 +182,7 @@ async def update_protobuf_frecency_settings(data: SettingsProtoUpdate, user: Use proto_new = FrecencyUserSettings() proto_new.ParseFromString(_b64decode(data.settings.encode("utf8"))) except ValueError: - raise InvalidDataErr(400, Errors.make(50104)) + raise MalformedUserSettings fsettings, _ = await FrecencySettings.get_or_create(id=user.id, user=user, settings="") proto = fsettings.to_proto() if fsettings else FrecencyUserSettings() proto.MergeFrom(proto_new) @@ -206,7 +208,7 @@ async def edit_connection(service: str, ext_id: str, data: EditConnection, user: connection = await ConnectedAccount.get_or_none(user=user, service_id=ext_id, type=service, verified=True) if connection is None: - raise InvalidDataErr(404, Errors.make(10017)) + raise UnknownConnection await connection.update(**data.model_dump(exclude_none=True)) @@ -223,7 +225,7 @@ async def delete_connection(service: str, ext_id: str, user: User = DepUser): connection = await ConnectedAccount.get_or_none(user=user, service_id=ext_id, type=service, verified=True) if connection is None: - raise InvalidDataErr(404, Errors.make(10017)) + raise UnknownConnection await connection.delete() await getGw().dispatch(UserConnectionsUpdate(connection), user_ids=[user.id]) @@ -234,9 +236,9 @@ async def delete_connection(service: str, ext_id: str, user: User = DepUser): @users_me.post("/relationships", body_cls=RelationshipRequest) async def new_relationship(data: RelationshipRequest, user: User = DepUser): if not (target_user := await User.y.getByUsername(**data.model_dump())): - raise InvalidDataErr(400, Errors.make(80004)) + raise UnknownDiscordTag if target_user == user: - raise InvalidDataErr(400, Errors.make(80007)) + raise AlreadyFriends await Relationship.utils.request(user, target_user) await getGw().dispatch(RelationshipAddEvent(user.id, await user.userdata, 3), [target_user.id]) @@ -253,16 +255,16 @@ async def get_relationships(user: User = DepUser): @users_me.get("/notes/", allow_bots=True) async def get_notes(target_uid: int, user: User = DepUser): if not (target_user := await User.y.get(target_uid, False)): - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser if not (note := await UserNote.get_or_none(user=user, target=target_user).select_related("user", "target")): - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser return note.ds_json() @users_me.put("/notes/", body_cls=PutNote, allow_bots=True) async def set_notes(data: PutNote, target_uid: int, user: User = DepUser): if not (target_user := await User.y.get(target_uid, False)): - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser if data.note: note, _ = await UserNote.get_or_create(user=user, target=target_user, defaults={"text": data.note}) await getGw().dispatch(UserNoteUpdateEvent(target_uid, data.note), user_ids=[user.id]) @@ -274,16 +276,16 @@ async def enable_mfa(data: MfaEnable, session: Session = DepSession): user = session.user settings = await user.settings if settings.mfa is not None: - raise InvalidDataErr(404, Errors.make(60001)) + raise Already2Fa if not (password := data.password) or not user.check_password(password): - raise InvalidDataErr(400, Errors.make(50018)) + raise PasswordDoesNotMatch if not (secret := data.secret): - raise InvalidDataErr(400, Errors.make(60005)) + raise Invalid2FaSecret mfa = MFA(secret, user.id) if not mfa.valid: - raise InvalidDataErr(400, Errors.make(60005)) + raise Invalid2FaSecret if not (code := data.code) or code not in mfa.getCodes(): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode settings.mfa = secret await settings.save(update_fields=["mfa"]) codes = [ @@ -299,16 +301,16 @@ async def enable_mfa(data: MfaEnable, session: Session = DepSession): @users_me.post("/mfa/totp/disable", body_cls=MfaDisable) async def disable_mfa(data: MfaDisable, session: Session = DepSession): if not (code := data.code): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode user = session.user settings = await user.settings if settings.mfa is None: - raise InvalidDataErr(404, Errors.make(60002)) + raise NotYet2Fa mfa = await user.mfa code = code.replace("-", "").replace(" ", "") if code not in mfa.getCodes(): if not (len(code) == 8 and await user.use_backup_code(code)): - raise InvalidDataErr(400, Errors.make(60008)) + raise Invalid2FaCode settings.mfa = None await settings.save(update_fields=["mfa"]) await user.clear_backup_codes() @@ -321,7 +323,7 @@ async def disable_mfa(data: MfaDisable, session: Session = DepSession): @users_me.post("/mfa/codes-verification", body_cls=MfaCodesVerification) async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): if not (nonce := data.nonce): - raise InvalidDataErr(400, Errors.make(60011)) + raise InvalidKey if not (key := data.key): raise InvalidDataErr(400, Errors.make(50035, {"key": { "code": "BASE_TYPE_REQUIRED", "message": "This field is required" @@ -329,7 +331,7 @@ async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): regenerate = data.regenerate await getCore().verifyUserMfaNonce(user, nonce, regenerate) if await getCore().mfaNonceToCode(nonce) != key: - raise InvalidDataErr(400, Errors.make(60011)) + raise InvalidKey codes = await user.create_backup_codes() if regenerate else await user.get_backup_codes() return {"backup_codes": [ @@ -341,7 +343,7 @@ async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): @users_me.put("/relationships/", body_cls=RelationshipPut) async def accept_relationship_or_block(data: RelationshipPut, user_id: int, user: User = DepUser): if not (target_user_data := await UserData.get_or_none(id=user_id).select_related("user")): - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser if not data.type or data.type == 1: from_user = target_user_data.user @@ -385,7 +387,7 @@ async def api_users_me_harvest(): @users_me.delete("/guilds/", allow_bots=True) async def leave_guild(user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): if user == guild.owner: - raise InvalidDataErr(400, Errors.make(50055)) + raise InvalidGuild await getGw().dispatchUnsub([user.id], guild.id) for role in await member.roles.all(): await getGw().dispatchUnsub([user.id], role_id=role.id) @@ -408,7 +410,7 @@ async def new_dm_channel(data: DmChannelCreate, user: User = DepUser): recipients.remove(user.id) recipients_users = [await User.y.get(recipient) for recipient in recipients] if None in recipients_users: - raise InvalidDataErr(400, Errors.make(50033)) + raise InvalidRecipient if len(recipients) == 1: channel = await getCore().getDMChannelOrCreate(user, recipients_users[0]) elif len(recipients) == 0: @@ -422,9 +424,9 @@ async def new_dm_channel(data: DmChannelCreate, user: User = DepUser): @users_me.post("/delete", body_cls=DeleteRequest) async def delete_user(data: DeleteRequest, user: User = DepUser): if not user.check_password(data.password): - raise InvalidDataErr(400, Errors.make(50018)) + raise PasswordDoesNotMatch if await Guild.exists(owner=user) or await Channel.exists(owner=user, type=ChannelType.GROUP_DM): - raise InvalidDataErr(400, Errors.make(40011)) + raise MustTransferGuildsBeforeDelete await user.y_delete() await getGw().dispatch(UserDeleteEvent(user.id), user_ids=[user.id]) return "", 204 @@ -435,7 +437,7 @@ async def get_scheduled_events(query_args: GetScheduledEventsQuery, user: User = events = [] for guild_id in query_args.guild_ids[:5]: if not await GuildMember.get_or_none(guild__id=guild_id, user__id=user.id): - raise InvalidDataErr(403, Errors.make(50001)) + raise MissingAccess for event_id in await GuildEvent.filter( guild__id=guild_id, subscribers__user__id__in=[user.id] ).values_list("id", flat=True): @@ -453,7 +455,7 @@ async def remote_auth_login(data: RemoteAuthLogin, user: Optional[User] = DepUse ra_session = await RemoteAuthSession.get_or_none(fingerprint=data.fingerprint, user=None, expires_at__gt=int(time())) if ra_session is None: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken await ra_session.update(user=user) userdata = await user.userdata @@ -467,7 +469,7 @@ async def remote_auth_login(data: RemoteAuthLogin, user: Optional[User] = DepUse elif data.ticket is not None: ticket = Session.extract_token(data.ticket) if ticket is None: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken user_id, session_id, fingerprint = ticket @@ -475,13 +477,13 @@ async def remote_auth_login(data: RemoteAuthLogin, user: Optional[User] = DepUse fingerprint=fingerprint, expires_at__gt=int(time()), v2_session__id=session_id, user__id=user_id ) if ra_session is None: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken await ra_session.delete() return {"encrypted_token": ra_session.v2_encrypted_token} - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken @users_me.post("/remote-auth/finish", body_cls=RemoteAuthFinish) @@ -489,7 +491,7 @@ async def remote_auth_finish(data: RemoteAuthFinish, user: User = DepUser): ra_session = await RemoteAuthSession.get_or_none(id=int(data.handshake_token), expires_at__gt=int(time()), user=user, v2_session=None) if ra_session is None: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken session = await Session.Y.create(user) if ra_session.version == 2: @@ -510,7 +512,7 @@ async def remote_auth_cancel(data: RemoteAuthCancel, user: User = DepUser): ra_session = await RemoteAuthSession.get_or_none(id=int(data.handshake_token), expires_at__gt=int(time()), user=user) if ra_session is None: - raise InvalidDataErr(404, Errors.make(10012)) + raise UnknownToken await getGw().dispatchRA("cancel", {"fingerprint": ra_session.fingerprint}) diff --git a/yepcord/rest_api/routes/webhooks.py b/yepcord/rest_api/routes/webhooks.py index 66ab23b..c492693 100644 --- a/yepcord/rest_api/routes/webhooks.py +++ b/yepcord/rest_api/routes/webhooks.py @@ -28,7 +28,7 @@ from ...gateway.events import MessageCreateEvent, WebhooksUpdateEvent, MessageUpdateEvent from ...yepcord.ctx import getCore, getCDNStorage, getGw from ...yepcord.enums import GuildPermissions, MessageFlags -from ...yepcord.errors import InvalidDataErr, Errors +from ...yepcord.errors import UnknownWebhook, MissingPermissions, CannotSendEmptyMessage from ...yepcord.models import User, Channel, Message, Webhook from ...yepcord.utils import getImage @@ -43,7 +43,7 @@ async def delete_webhook(webhook: int, user: Optional[User] = DepUserO, token: O if webhook.token != token: guild = webhook.channel.guild if user is None or not (member := await getCore().getGuildMember(guild, user.id)): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) await webhook.delete() await getGw().dispatch(WebhooksUpdateEvent(webhook.channel.guild.id, webhook.channel.id), @@ -56,12 +56,12 @@ async def delete_webhook(webhook: int, user: Optional[User] = DepUserO, token: O @webhooks.patch("//", body_cls=WebhookUpdate, allow_bots=True) async def edit_webhook(data: WebhookUpdate, webhook: int, user: Optional[User] = DepUserO, token: Optional[str]=None): if not (webhook := await getCore().getWebhook(webhook)): - raise InvalidDataErr(404, Errors.make(10015)) + raise UnknownWebhook channel = webhook.channel guild = channel.guild if webhook.token != token: if user is None or not (member := await getCore().getGuildMember(guild, user.id)): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) if data.channel_id: if user is None: @@ -92,11 +92,11 @@ async def edit_webhook(data: WebhookUpdate, webhook: int, user: Optional[User] = @webhooks.get("//", allow_bots=True) async def get_webhook(webhook: int, user: Optional[User] = DepUserO, token: Optional[str]=None): if not (webhook := await getCore().getWebhook(webhook)): - raise InvalidDataErr(404, Errors.make(10015)) + raise UnknownWebhook if webhook.token != token: guild = webhook.channel.guild if user is None or not (member := await getCore().getGuildMember(guild, user.id)): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) return await webhook.ds_json() @@ -105,9 +105,9 @@ async def get_webhook(webhook: int, user: Optional[User] = DepUserO, token: Opti @webhooks.post("//", qs_cls=WebhookMessageCreateQuery) async def post_webhook_message(query_args: WebhookMessageCreateQuery, webhook: int, token: str): if not (webhook := await getCore().getWebhook(webhook)): - raise InvalidDataErr(404, Errors.make(10015)) + raise UnknownWebhook if webhook.token != token: - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions channel = webhook.channel message = await processMessage(await request.get_json(), channel, None, WebhookMessageCreate, webhook) @@ -152,7 +152,7 @@ async def interaction_followup_create(message: Message = DepInteractionW): await validate_reply(data, channel) stickers_data = await process_stickers(data.sticker_ids) if not data.content and not data.embeds and not attachments and not stickers_data["stickers"]: - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage data_json = data.to_json() | stickers_data | {"flags": message.flags & ~MessageFlags.LOADING} await message.update(**data_json) diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 117071e..d3917a0 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -31,7 +31,8 @@ from ..yepcord.config import Config from ..yepcord.ctx import getCore, getCDNStorage from ..yepcord.enums import MessageType -from ..yepcord.errors import Errors, InvalidDataErr +from ..yepcord.errors import Errors, InvalidDataErr, UnknownChannel, UnknownMessage, InvalidFormBody, \ + FileExceedsMaxSize, CannotSendEmptyMessage from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message from ..yepcord.snowflake import Snowflake @@ -58,11 +59,11 @@ async def getSessionFromToken(token: str) -> Optional[Union[Session, Authorizati async def _getMessage(user: User, channel: Channel, message_id: int) -> Message: if not channel: - raise InvalidDataErr(404, Errors.make(10003)) + raise UnknownChannel if not user: raise InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) if not message_id or not (message := await channel.get_message(message_id)): - raise InvalidDataErr(404, Errors.make(10008)) + raise UnknownMessage return message @@ -96,14 +97,14 @@ async def get_multipart_json() -> dict: raise ValueError return loads(form["payload_json"]) except (ValueError, KeyError): - raise InvalidDataErr(400, Errors.make(50035)) + raise InvalidFormBody async def processMessageData(data: Optional[dict], channel: Channel) -> tuple[dict, list[Attachment]]: attachments = [] if data is None: # Multipart request if request.content_length is not None and request.content_length > 1024 * 1024 * 100: - raise InvalidDataErr(400, Errors.make(50045)) + raise FileExceedsMaxSize async with timeout(current_app.config["BODY_TIMEOUT"]): files = list((await request.files).values()) data = await get_multipart_json() @@ -120,7 +121,7 @@ async def processMessageData(data: Optional[dict], channel: Channel) -> tuple[di get_content = getattr(file, "getvalue", file.read) content = get_content() total_size += len(content) - if total_size > 1024 * 1024 * 100: raise InvalidDataErr(400, Errors.make(50045)) + if total_size > 1024 * 1024 * 100: raise FileExceedsMaxSize content_type = ( file.content_type.strip() if file.content_type else from_buffer(content[:1024], mime=True) ) @@ -139,7 +140,7 @@ async def processMessageData(data: Optional[dict], channel: Channel) -> tuple[di not data.get("embeds") and \ not data.get("attachments") and \ not data.get("sticker_ids"): - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage return data, attachments @@ -228,7 +229,7 @@ async def processMessage(data: dict, channel: Channel, author: Optional[User], v message_type = await validate_reply(data, channel) stickers_data = await process_stickers(data.sticker_ids) if not data.content and not data.embeds and not attachments and not stickers_data["stickers"]: - raise InvalidDataErr(400, Errors.make(50006)) + raise CannotSendEmptyMessage data_json = data.to_json() if webhook is not None: diff --git a/yepcord/yepcord/classes/connections.py b/yepcord/yepcord/classes/connections.py index 2ca0416..eca8be7 100644 --- a/yepcord/yepcord/classes/connections.py +++ b/yepcord/yepcord/classes/connections.py @@ -23,7 +23,7 @@ from httpx import AsyncClient from yepcord.yepcord.config import Config -from yepcord.yepcord.errors import InvalidDataErr, Errors +from yepcord.yepcord.errors import InvalidDataErr, Errors, Error0 from yepcord.yepcord.models import User, ConnectedAccount @@ -85,7 +85,7 @@ async def exchange_code(cls, code: str) -> Optional[str]: url, kwargs = cls.exchange_code_req(code, settings) resp = await cl.post(url, **kwargs) if resp.status_code >= 400 or "error" in (j := resp.json()): - raise InvalidDataErr(400, Errors.make(0)) + raise Error0 return j["access_token"] @@ -99,7 +99,7 @@ async def get_user_info(cls, access_token: str) -> dict: async with AsyncClient() as cl: resp = await cl.get(url, **kwargs) if resp.status_code >= 400: # pragma: no cover - raise InvalidDataErr(400, Errors.make(0)) + raise Error0 return resp.json() diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index 827bd90..25649e9 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -32,7 +32,7 @@ from .classes.singleton import Singleton from .config import Config from .enums import ChannelType, GUILD_CHANNELS -from .errors import InvalidDataErr, Errors +from .errors import InvalidDataErr, Errors, InvalidKey from .models import User, Relationship, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, \ Reaction, Sticker, Webhook, Role, GuildEvent, ThreadMember from .snowflake import Snowflake @@ -119,10 +119,10 @@ async def generateUserMfaNonce(self, user: User) -> tuple[str, str]: async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> None: if not (payload := JWT.decode(nonce, self.key)) or payload["user_id"] != user.id: - raise InvalidDataErr(400, Errors.make(60011)) + raise InvalidKey nonce_type = "normal" if not regenerate else "regenerate" if nonce_type != payload["type"]: - raise InvalidDataErr(400, Errors.make(60011)) + raise InvalidKey async def getChannel(self, channel_id: Optional[int]) -> Optional[Channel]: if channel_id is None: diff --git a/yepcord/yepcord/ctx.py b/yepcord/yepcord/ctx.py index 0483011..4fd8106 100644 --- a/yepcord/yepcord/ctx.py +++ b/yepcord/yepcord/ctx.py @@ -18,7 +18,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Callable, cast +from typing import TYPE_CHECKING, Optional, Callable if TYPE_CHECKING: # pragma: no cover from .core import Core @@ -32,12 +32,12 @@ def getCore() -> Core: - return cast(Core, _get_core()) + return _get_core() def getCDNStorage() -> _Storage: - return cast(_Storage, _get_storage()) + return _get_storage() def getGw() -> GatewayDispatcher: - return cast(GatewayDispatcher, _get_gw()) + return _get_gw() diff --git a/yepcord/yepcord/errors.py b/yepcord/yepcord/errors.py index b4ca01a..c9b28f2 100644 --- a/yepcord/yepcord/errors.py +++ b/yepcord/yepcord/errors.py @@ -70,6 +70,7 @@ class _Errors: err_40007 = "The user is banned from this guild." err_40011 = "You must transfer ownership of any owned guilds before deleting your account" + err_40060 = "Interaction has already been acknowledged" err_50001 = "Missing Access" err_50003 = "Cannot execute action on a DM channel" @@ -140,3 +141,72 @@ def from_pydantic(exc: ValidationError): Errors = _Errors() + + +class UnknownX(InvalidDataErr): + def __init__(self, error_code: int): + super().__init__(404, Errors.make(error_code)) + + +class BadRequest(InvalidDataErr): + def __init__(self, error_code: int): + super().__init__(400, Errors.make(error_code)) + + +class Forbidden(InvalidDataErr): + def __init__(self, error_code: int): + super().__init__(403, Errors.make(error_code)) + + +UnknownApplication = UnknownX(10002) +UnknownChannel = UnknownX(10003) +UnknownGuild = UnknownX(10004) +UnknownInvite = UnknownX(10006) +UnknownMessage = UnknownX(10008) +UnknownRole = UnknownX(10011) +UnknownToken = UnknownX(10012) +UnknownUser = UnknownX(10013) +UnknownEmoji = BadRequest(10014) +UnknownWebhook = UnknownX(10015) +UnknownConnection = UnknownX(10017) +UnknownGuildTemplate = UnknownX(10057) +UnknownSticker = UnknownX(10060) +UnknownInteraction = UnknownX(10062) +UnknownGuildEvent = UnknownX(10070) + +MaxPinsReached = BadRequest(30003) +MaxEmojisReached = BadRequest(30008) +CanHaveOneTemplate = BadRequest(30031) + +UserBanned = Forbidden(40007) +MustTransferGuildsBeforeDelete = BadRequest(40011) +InteractionAlreadyAck = BadRequest(40060) + +MissingAccess = Forbidden(50001) +CannotExecuteOnDM = Forbidden(50003) +CannotEditAnotherUserMessage = Forbidden(50005) +CannotSendEmptyMessage = BadRequest(50006) +CannotSendToThisUser = Forbidden(50007) +MissingPermissions = Forbidden(50013) +PasswordDoesNotMatch = BadRequest(50018) +InvalidRole = BadRequest(50028) +InvalidRecipient = BadRequest(50033) +InvalidFormBody = BadRequest(50035) +FileExceedsMaxSize = BadRequest(50045) +InvalidAsset = BadRequest(50046) +InvalidGuild = BadRequest(50055) +MalformedUserSettings = BadRequest(50104) + +Already2Fa = UnknownX(60001) +NotYet2Fa = UnknownX(60002) +Invalid2FaSecret = BadRequest(60005) +Invalid2FaAuthTicket = BadRequest(60006) +Invalid2FaCode = BadRequest(60008) +InvalidKey = BadRequest(60011) + +FriendRequestsDisabled = Forbidden(80000) +UnknownDiscordTag = BadRequest(80004) +AlreadyFriends = BadRequest(80007) + +Unauthorized = InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) +Error0 = InvalidDataErr(400, Errors.make(0)) diff --git a/yepcord/yepcord/models/guild_member.py b/yepcord/yepcord/models/guild_member.py index f3b799c..be3df2d 100644 --- a/yepcord/yepcord/models/guild_member.py +++ b/yepcord/yepcord/models/guild_member.py @@ -24,7 +24,7 @@ from tortoise import fields from ..enums import GuildPermissions -from ..errors import InvalidDataErr, Errors +from ..errors import MissingPermissions from ._utils import SnowflakeField, Model from ..snowflake import Snowflake import yepcord.yepcord.models as models @@ -50,7 +50,7 @@ def _check(perms: int, perm: int) -> bool: for permission in check_permissions: if not _check(permissions, permission): - raise InvalidDataErr(403, Errors.make(50013)) + raise MissingPermissions async def canKickOrBan(self, target_member: GuildMember) -> bool: if self.member == target_member: diff --git a/yepcord/yepcord/models/relationship.py b/yepcord/yepcord/models/relationship.py index 1e85332..c20bed8 100644 --- a/yepcord/yepcord/models/relationship.py +++ b/yepcord/yepcord/models/relationship.py @@ -23,7 +23,7 @@ from tortoise import fields from ..enums import RelationshipType, RelTypeDiscord -from ..errors import InvalidDataErr, Errors +from ..errors import AlreadyFriends from ._utils import ChoicesValidator, SnowflakeField, Model import yepcord.yepcord.models as models @@ -41,13 +41,13 @@ async def get(user1: models.User, user2: models.User) -> Relationship: async def available(from_user: models.User, to_user: models.User, *, raise_: bool=False) -> bool: available = not await RelationshipUtils.exists(from_user, to_user) # TODO: check for to_user settings if not available and raise_: - raise InvalidDataErr(400, Errors.make(80007)) + raise AlreadyFriends return available @staticmethod async def request(from_user: models.User, to_user: models.User) -> Relationship: if not await RelationshipUtils.available(from_user, to_user): - raise InvalidDataErr(400, Errors.make(80007)) + raise AlreadyFriends return await Relationship.create(from_user=from_user, to_user=to_user, type=RelationshipType.PENDING) @staticmethod diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index acc3197..793ebe1 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -33,7 +33,7 @@ from ..classes.other import MFA from ..config import Config from ..ctx import getCore -from ..errors import InvalidDataErr, Errors, MfaRequiredErr +from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser from ..snowflake import Snowflake from ..utils import int_size, b64encode @@ -192,7 +192,7 @@ async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, async def get_another_user(self, user_id: int) -> User: # TODO: check for relationship, mutual guilds or mutual friends if (user := await User.y.get(user_id, False)) is None: # TODO: add test for nonexistent user - raise InvalidDataErr(404, Errors.make(10013)) + raise UnknownUser return user def check_password(self, password: str) -> bool: From 159a34a1ac90bdbf61b38d4d872e5b00f9c446ca Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 6 Sep 2024 16:21:04 +0300 Subject: [PATCH 08/26] move relationship, some channel and messages methods --- poetry.lock | 62 ++++++++++- pyproject.toml | 4 + tests/api/test_applications.py | 2 +- tests/cdn/test_storage_and_cdn.py | 2 +- tests/test_core.py | 50 ++++----- yepcord/gateway/events.py | 23 ++-- yepcord/gateway/gateway.py | 22 ++-- yepcord/gateway/main.py | 5 +- yepcord/rest_api/dependencies.py | 2 +- yepcord/rest_api/main.py | 1 - yepcord/rest_api/routes/channels.py | 8 +- yepcord/rest_api/routes/guilds.py | 12 +-- yepcord/rest_api/routes/interactions.py | 6 +- yepcord/rest_api/routes/invites.py | 4 +- yepcord/rest_api/routes/oauth2.py | 4 +- yepcord/rest_api/routes/users_me.py | 18 ++-- yepcord/yepcord/core.py | 133 ++---------------------- yepcord/yepcord/models/channel.py | 57 +++++++++- yepcord/yepcord/models/guild_member.py | 15 +++ yepcord/yepcord/models/message.py | 27 ++++- yepcord/yepcord/models/relationship.py | 2 +- yepcord/yepcord/models/user.py | 29 ++++++ 22 files changed, 275 insertions(+), 213 deletions(-) diff --git a/poetry.lock b/poetry.lock index a43f5bd..ae340c2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1291,6 +1291,17 @@ aiohttp = ["aiohttp"] fast-parse = ["fast-mail-parser"] nkeys = ["nkeys"] +[[package]] +name = "objprint" +version = "0.2.3" +description = "A library that can print Python objects in human readable format" +optional = false +python-versions = ">=3.6" +files = [ + {file = "objprint-0.2.3-py3-none-any.whl", hash = "sha256:1721e6f97bae5c5b86c2716a0d45a9dd2c9a4cd9f52cfc8a0dfbe801805554cb"}, + {file = "objprint-0.2.3.tar.gz", hash = "sha256:73d0ad5a7c3151fce634c8892e5c2a050ccae3b1a353bf1316f08b7854da863b"}, +] + [[package]] name = "orjson" version = "3.10.7" @@ -2121,6 +2132,55 @@ files = [ docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +[[package]] +name = "viztracer" +version = "0.16.3" +description = "A debugging and profiling tool that can trace and visualize python code execution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "viztracer-0.16.3-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:3a39fcee0ffd35639b75565afce9a48ea92a4386f4c2f4a2752b4ec068f273cb"}, + {file = "viztracer-0.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c223312ecf4adb2271ca4ea84b546b278ec2dbe15b5ace5aa630cb07c2b53d4f"}, + {file = "viztracer-0.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1320a6ebb89d691a1eced48f5d842d5d459d79bf85ad4fbf004e46bbf417c0d5"}, + {file = "viztracer-0.16.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a96df402baacdca90c23559c4632e517db2e91628d191a7670d95809960ca8e"}, + {file = "viztracer-0.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff04b25518eac932fec618c8297794c9894ac75db543b1c872cea945ae986feb"}, + {file = "viztracer-0.16.3-cp310-cp310-win32.whl", hash = "sha256:8f56dad6111e1f3a0bd1f6de541d87c0c145fd88d4fd2b613b0c0d1f538b1a29"}, + {file = "viztracer-0.16.3-cp310-cp310-win_amd64.whl", hash = "sha256:59c32b87b4c5ba25b78dcb993b255512106b669042ce2b31bb7b6a92edc43d67"}, + {file = "viztracer-0.16.3-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:cca0a017635f6ade7cd73474bcdc19b2730cdc468f338912afe749234c6ebe30"}, + {file = "viztracer-0.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d6a938c197f4f723ad2f8d7b8e1aa0449e249dd4702e67445aaf42392786378"}, + {file = "viztracer-0.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8721e1fd177b4abff0cbee141a31d6b819867833c92866f238815b321178c6df"}, + {file = "viztracer-0.16.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73c0e88ecc68572599405cca54f52d7d9b7dc811992bf8edbee1860486627542"}, + {file = "viztracer-0.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73697d43ed42f6e5110166387c3b4d76b4e181bf70822bac3f7f3e39b12be21e"}, + {file = "viztracer-0.16.3-cp311-cp311-win32.whl", hash = "sha256:dac628f38a23154a184bd5f37e215d8ada83e33892593036112584dec83b7369"}, + {file = "viztracer-0.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:13df50118ff0e3266c32b66f0ff26df202f7c4d9583a550dc537098303141cec"}, + {file = "viztracer-0.16.3-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:4f07b263abe9ade8390253eebdd4658eb44288e98d1b7b9afae42a54d15197f1"}, + {file = "viztracer-0.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f4b0b59cd97f7d6fb37766105cb062d042df87648670f560a3d730050026087"}, + {file = "viztracer-0.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a2f9b2bf4f8e051cf9ee70bd7e6ff91c33239bcd01fa37005423915bfccaa64"}, + {file = "viztracer-0.16.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69971a9b24801259511545ec2a3b5a4f90733f64fca799268fb6d64d72c9e93e"}, + {file = "viztracer-0.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2f208a89c79a87ba563ee39f035df924a0109c4a2300a8098181e25f1f8afea"}, + {file = "viztracer-0.16.3-cp312-cp312-win32.whl", hash = "sha256:bf23e06d8f9a870711a3441123c364ac6a5d0caa288c6acdd116212f81e46b2a"}, + {file = "viztracer-0.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:594e9dabf5cca584654d6813844be0377ae292e8dd507a013e9e427fd10d6fbd"}, + {file = "viztracer-0.16.3-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:5dc505dd2a607a8d9d2c0c488dc297593a3cca2227a4c31630d20fa0e4b27087"}, + {file = "viztracer-0.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50b7876bcb1762767878422fe4ba0ff46854ddf2d494b8b236b595da889156b"}, + {file = "viztracer-0.16.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f17cf5d67112b236bc3501f563beaa2a542121c0d8d2087b4e9a9d24763394d"}, + {file = "viztracer-0.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f4d5732e1a7f3f1fa4bee0f53f17155002fabef97d620bec774158bac7bd701"}, + {file = "viztracer-0.16.3-cp38-cp38-win32.whl", hash = "sha256:e79d7e55fffd32cbb49466f96233f292602c732832f9a9cc075773762be6f487"}, + {file = "viztracer-0.16.3-cp38-cp38-win_amd64.whl", hash = "sha256:8663707e5527f614cc038b6fa189697f0809e5593fa9522c0152261b79b493f0"}, + {file = "viztracer-0.16.3-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:38d330348f6be0f17628657a2e454b760de40383f3065ad48fe6e2ae57478d7a"}, + {file = "viztracer-0.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bbb88d7cbfe3d12021f97d7f58b7a21eae20d799ea7092f55d2a60eff1dbe2"}, + {file = "viztracer-0.16.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf1378cb93f62e58976ee31d394ad9b667eca5d0edfe8371609e2e68c6427e03"}, + {file = "viztracer-0.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c5bad11791c952b107f88b3a6a2a39848b0d0d6b8499222575854789722936"}, + {file = "viztracer-0.16.3-cp39-cp39-win32.whl", hash = "sha256:65d072348f96e402dd28d21e36e6d3f513f26dc79bf89cbde0efe594f239f8f5"}, + {file = "viztracer-0.16.3-cp39-cp39-win_amd64.whl", hash = "sha256:b8e35a109e9b1d5515ffeb0806e834274c0133a73d0a2a2b2fd24f031a1f43b3"}, + {file = "viztracer-0.16.3.tar.gz", hash = "sha256:943cb874cf92cdc786cd87938ac64ea081e3ae06ef73f577deac5b4a2a9621d5"}, +] + +[package.dependencies] +objprint = ">0.1.3" + +[package.extras] +full = ["orjson"] + [[package]] name = "websockets" version = "13.0.1" @@ -2399,4 +2459,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "fb06fc05f7c288a68c9a0df8700f03a6efdc6e44cf6b5f8f797fb44573c17e86" +content-hash = "c163191f3c7a8976f056ff27de47b70d3b5676aa81b784653c7ebf0358005b4e" diff --git a/pyproject.toml b/pyproject.toml index fdcbd5a..0890406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,6 +76,10 @@ fake-s3 = "1.0.2" types-protobuf = "^4.24.0.4" pytest-httpx = "^0.30.0" + +[tool.poetry.group.profiling.dependencies] +viztracer = "^0.16.3" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/api/test_applications.py b/tests/api/test_applications.py index 9478c60..4051082 100644 --- a/tests/api/test_applications.py +++ b/tests/api/test_applications.py @@ -7,8 +7,8 @@ from yepcord.rest_api.main import app from yepcord.yepcord.snowflake import Snowflake from .utils import TestClientType, create_users, create_application -from ..yep_image import YEP_IMAGE from ..utils import register_app_error_handler +from ..yep_image import YEP_IMAGE register_app_error_handler(app) diff --git a/tests/cdn/test_storage_and_cdn.py b/tests/cdn/test_storage_and_cdn.py index 5d9082e..3be3c98 100644 --- a/tests/cdn/test_storage_and_cdn.py +++ b/tests/cdn/test_storage_and_cdn.py @@ -39,7 +39,7 @@ register_app_error_handler(app) TestClientType = app.test_client_class -core = Core(b64decode(Config.KEY)) +core = Core() @pt.fixture diff --git a/tests/test_core.py b/tests/test_core.py index 063bb06..29e749b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -41,7 +41,7 @@ "user_id": Snowflake.makeId() } -core = Core(b64decode(Config.KEY)) +core = Core() @pt.fixture @@ -177,20 +177,18 @@ async def test_checkRelationShipAvailable_fail(): @pt.mark.asyncio -async def test_getRelationships_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_getRelationships_success(): user = await User.get(id=VARS["user_id_100000"]) - rels = await testCore.getRelationships(user) + rels = await user.get_relationships() assert len(rels) == 1 - assert rels[0]["type"] == 3 - assert rels[0]["user_id"] == str(VARS["user_id_200000"]) + assert rels[0].type == RelationshipType.PENDING + assert rels[0].other_user(user).id == VARS["user_id_200000"] @pt.mark.asyncio -async def test_getRelationships_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_getRelationships_fail(): user = await User.get(id=VARS["user_id"] + 100001) - rels = await testCore.getRelationships(user) + rels = await user.get_relationships() assert len(rels) == 0 @@ -209,12 +207,11 @@ async def test_getRelationship_fail(): @pt.mark.asyncio -async def test_getRelatedUsers_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_getRelatedUsers_success(): user = await User.get(id=VARS["user_id_100000"]) - users = await testCore.getRelatedUsers(user, True) + users = await user.get_related_users() assert len(users) == 1 - assert users[0] == VARS["user_id_200000"] + assert users[0].id == VARS["user_id_200000"] @pt.mark.asyncio @@ -310,12 +307,11 @@ async def test_verifyUserMfaNonce(): @pt.mark.asyncio -async def test_getDMChannelOrCreate_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_getDMChannelOrCreate_success(): user1 = await User.y.get(VARS["user_id_100000"]) user2 = await User.y.get(VARS["user_id_200000"]) - channel = await testCore.getDMChannelOrCreate(user1, user2) - channel2 = await testCore.getDMChannelOrCreate(user1, user2) + channel = await Channel.Y.get_dm(user1, user2) + channel2 = await Channel.Y.get_dm(user1, user2) assert channel is not None assert channel.type == ChannelType.DM assert channel.id == channel2.id @@ -324,31 +320,27 @@ async def test_getDMChannelOrCreate_success(testCore: Coroutine[Any, Any, Core]) @pt.mark.asyncio -async def test_getChannel_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - channel = await testCore.getChannel(VARS["channel_id"]) +async def test_getChannel_success(): + channel = await Channel.Y.get(VARS["channel_id"]) assert channel is not None assert channel.type == ChannelType.DM @pt.mark.asyncio -async def test_getChannel_fail(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - channel = await testCore.getChannel(0) +async def test_getChannel_fail(): + channel = await Channel.Y.get(0) assert channel is None @pt.mark.asyncio -async def test_getLastMessageIdForChannel_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - channel = await testCore.getChannel(VARS["channel_id"]) +async def test_getLastMessageIdForChannel_success(): + channel = await Channel.Y.get(VARS["channel_id"]) assert await channel.get_last_message_id() is None or await channel.get_last_message_id() > 0 @pt.mark.asyncio -async def test_getChannelMessagesCount_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - channel = await testCore.getChannel(VARS["channel_id"]) +async def test_getChannelMessagesCount_success(): + channel = await Channel.Y.get(VARS["channel_id"]) assert await Message.filter(channel=channel).count() == 0 diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 30aa708..9d2b6ff 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -23,6 +23,7 @@ from typing import List, TYPE_CHECKING from ..yepcord.config import Config +from ..yepcord.ctx import getCore from ..yepcord.enums import GatewayOp from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount from ..yepcord.models.interaction import Interaction @@ -30,7 +31,6 @@ if TYPE_CHECKING: # pragma: no cover from ..yepcord.models import Channel, Invite, GuildMember, UserData, User, UserSettings - from ..yepcord.core import Core from .gateway import GatewayClient from .presences import Presence @@ -66,10 +66,9 @@ async def json(self) -> dict: class ReadyEvent(DispatchEvent): NAME = "READY" - def __init__(self, user: User, client: GatewayClient, core: Core): + def __init__(self, user: User, client: GatewayClient): self.user = user self.client = client - self.core = core async def json(self) -> dict: userdata = await self.user.userdata @@ -102,14 +101,20 @@ async def json(self) -> dict: "id": str(self.user.id), "flags": 0, }, - "users": await self.core.getRelatedUsers(self.user), + "users": [ + (await user.data).ds_json + for user in await self.user.get_related_users() + ], "guilds": [ await guild.ds_json(user_id=self.user.id, for_gateway=True, with_channels=True) for guild in await self.user.get_guilds() ], "session_id": self.client.sid, "presences": [], # TODO - "relationships": await self.core.getRelationships(self.user), + "relationships": [ + await relationship.ds_json(self.user) + for relationship in await self.user.get_relationships() + ], "connected_accounts": [ conn.ds_json() for conn in await ConnectedAccount.filter(user=self.user, verified=True) ], @@ -125,12 +130,14 @@ async def json(self) -> dict: "guild_experiments": [], # TODO "guild_join_requests": [], # TODO "merged_members": [], # TODO - "private_channels": [await channel.ds_json(user_id=self.user.id) - for channel in await self.core.getPrivateChannels(self.user)], + "private_channels": [ + await channel.ds_json(user_id=self.user.id) + for channel in await self.user.get_private_channels() + ], "read_state": { "version": 1, "partial": False, - "entries": await self.core.getReadStatesJ(self.user) if not self.user.is_bot else [] + "entries": await getCore().getReadStatesJ(self.user) if not self.user.is_bot else [] }, "resume_gateway_url": f"wss://{Config.GATEWAY_HOST}/", "session_type": "normal", diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index 0614b99..ecb3fc3 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -29,9 +29,8 @@ from .presences import Presences, Presence from .utils import require_auth, get_token_type, TokenType, init_redis_pool from ..yepcord.classes.fakeredis import FakeRedis -from ..yepcord.core import Core from ..yepcord.ctx import getCore -from ..yepcord.enums import GatewayOp +from ..yepcord.enums import GatewayOp, RelationshipType from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember from ..yepcord.mq_broker import getBroker @@ -91,7 +90,7 @@ async def handle_IDENTIFY(self, data: dict) -> None: self.cached_presence = Presence(self.user_id, settings.status, settings.custom_status, []) await self.gateway.authenticated(self, self.cached_presence) - await self.esend(ReadyEvent(session.user, self, getCore())) + await self.esend(ReadyEvent(session.user, self)) if not session.user.is_bot: guild_ids = [guild.id for guild in await session.user.get_guilds()] await self.esend(ReadySupplementalEvent(await self.gateway.getFriendsPresences(self.user_id), guild_ids)) @@ -182,24 +181,23 @@ class GatewayEvents: def __init__(self, gw: Gateway): self.gw = gw self.send = gw.send - self.core = gw.core async def presence_update(self, user_id: int, presence: Presence): user = await User.get(id=user_id) userdata = await user.data - users = await self.core.getRelatedUsers(user, only_ids=True) + user_ids = [user.id for user in await user.get_related_users()] event = PresenceUpdateEvent(userdata, presence) await self.gw.broker.publish(channel="yepcord_events", message={ "data": await event.json(), "event": event.NAME, - "users": users, + "users": user_ids, "channel_id": None, "guild_id": None, "permissions": None, }) - await self.sendToUsers(RawDispatchEventWrapper(event), users, set()) + await self.sendToUsers(RawDispatchEventWrapper(event), user_ids, set()) async def _send(self, client: GatewayClient, event: RawDispatchEvent) -> None: if client.is_bot and event.data.get("t") in self.BOTS_EVENTS_BLACKLIST: @@ -288,8 +286,7 @@ def unsubscribe(self, guild_id: int = None, role_id: int = None, *user_ids: int, class Gateway: - def __init__(self, core: Core): - self.core = core + def __init__(self): self.broker = getBroker() self.broker.subscriber("yepcord_events")(self.mcl_yepcordEventsCallback) self.broker.subscriber("yepcord_sys_events")(self.mcl_yepcordSysEventsCallback) @@ -411,8 +408,11 @@ async def disconnect(self, ws: Websocket): async def getFriendsPresences(self, uid: int) -> list[dict]: presences = [] user = await User.get(id=uid) - friends = await self.core.getRelationships(user) - friends = [int(u["user_id"]) for u in friends if u["type"] == 1] + friends = [ + relationship.other_user(user).id + for relationship in await user.get_relationships() + if relationship.type == RelationshipType.FRIEND + ] for friend in friends: if presence := await self.presences.get(friend): presences.append({ diff --git a/yepcord/gateway/main.py b/yepcord/gateway/main.py index 3050761..e309487 100644 --- a/yepcord/gateway/main.py +++ b/yepcord/gateway/main.py @@ -21,8 +21,6 @@ from ..yepcord.config import Config from ..yepcord.classes.other import ZlibCompressor -from ..yepcord.core import Core -from ..yepcord.utils import b64decode from json import loads as jloads from asyncio import CancelledError from .gateway import Gateway @@ -33,8 +31,7 @@ class YEPcord(Quart): app = YEPcord("YEPcord-Gateway") -core = Core(b64decode(Config.KEY)) -gw = Gateway(core) +gw = Gateway() @app.before_serving diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index 1477660..cec27bd 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -68,7 +68,7 @@ def depUser(allow_without_user: bool = False): async def depChannelO(channel_id: Optional[int] = None, user: User = Depends(depUser())) -> Optional[Channel]: - if (channel := await getCore().getChannel(channel_id)) is None: + if (channel := await Channel.Y.get(channel_id)) is None: return if not await getCore().getUserByChannel(channel, user.id): raise Unauthorized diff --git a/yepcord/rest_api/main.py b/yepcord/rest_api/main.py index 9214fd1..ff02304 100644 --- a/yepcord/rest_api/main.py +++ b/yepcord/rest_api/main.py @@ -52,7 +52,6 @@ class YEPcord(Quart): app = YEPcord("YEPcord-api") QuartSchema(app) -core = Core(b64decode(Config.KEY)) storage = getStorage() gateway = GatewayDispatcher() app.gifs = Gifs(Config.TENOR_KEY) diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 03e926b..cbb69e2 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -83,7 +83,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha changes = data.to_json(channel.type) if "parent_id" in changes: - parent = await getCore().getChannel(changes["parent_id"]) + parent = await Channel.Y.get(changes["parent_id"]) if (changes["parent_id"] is None or (parent is not None and parent.guild == guild and parent.type == ChannelType.GUILD_CATEGORY)): changes["parent"] = parent @@ -273,10 +273,10 @@ async def add_recipient(target_user: int, user: User = DepUser, channel: Channel if channel.type not in (ChannelType.DM, ChannelType.GROUP_DM): raise MissingPermissions if channel.type == ChannelType.DM: - recipients = await channel.recipients.filter(~Q(id=user.id)).all() + recipients = await channel.recipients.filter(~Q(id=user.id)) recipients.append(target_user) - ch = await getCore().createDMGroupChannel(user, recipients) - await getGw().dispatch(DMChannelCreateEvent(ch), channel=channel) + new_channel = await Channel.Y.create_dm_group(user, recipients) + await getGw().dispatch(DMChannelCreateEvent(new_channel), channel=channel) elif channel.type == ChannelType.GROUP_DM: recipients = await channel.recipients.all() if target_user not in recipients and len(recipients) < 10: diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index 7427c7b..8433443 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -85,7 +85,7 @@ async def update_guild(data: GuildUpdate, user: User = DepUser, guild: Guild = D setattr(data, image_type, h) for ch in ("afk_channel", "system_channel"): if (channel_id := getattr(data, ch)) is not None: - if (channel := await getCore().getChannel(channel_id)) is None: + if (channel := await Channel.Y.get(channel_id)) is None: setattr(data, ch, None) elif channel.guild != guild: setattr(data, ch, None) @@ -577,10 +577,10 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs for role in roles: if guild_roles[role.id].position >= user_top_role.position and member.user != guild.owner: raise MissingPermissions - add, remove = await getCore().setMemberRolesFromList(target_member, roles) - for role_id in add: + added, removed = await target_member.set_roles_from_list(roles) + for role_id in added: await getGw().dispatchSub([target_user], role_id=role_id) - for role_id in remove: + for role_id in removed: await getGw().dispatchUnsub([target_user], role_id=role_id) data.roles = None if data.nick is not None: @@ -638,7 +638,7 @@ async def update_vanity_url(data: SetVanityUrl, user: User = DepUser, guild: Gui await invite.delete() guild.vanity_url_code = data.code await guild.save(update_fields=["vanity_url_code"]) - channel = await getCore().getChannel(guild.system_channel) if guild.system_channel is not None else None + channel = await Channel.Y.get(guild.system_channel) if guild.system_channel is not None else None if channel is None: channel = (await guild.get_channels())[0] await Invite.create(id=Snowflake.makeId(), channel=channel, inviter=guild.owner, vanity_code=data.code) @@ -803,7 +803,7 @@ async def create_scheduled_event(data: CreateEvent, user: User = DepUser, guild: data_dict = data.model_dump() if data.entity_type in (ScheduledEventEntityType.STAGE_INSTANCE, ScheduledEventEntityType.VOICE): - if ((channel := await getCore().getChannel(data.channel_id)) is None or channel.guild != guild + if ((channel := await Channel.Y.get(data.channel_id)) is None or channel.guild != guild or channel.type not in (ChannelType.GUILD_VOICE, ChannelType.GUILD_STAGE_VOICE)): raise InvalidDataErr(400, Errors.make(50035, {"channel_id": { "code": "CHANNEL_INVALID", "message": "Invalid channel" diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index 49f958f..28d2520 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -29,7 +29,7 @@ ApplicationCommandOptionType, MessageType, ApplicationCommandType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, UnknownMessage, \ UnknownUser, MissingAccess, CannotSendEmptyMessage, InteractionAlreadyAck, Unauthorized -from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild +from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild, Channel from ...yepcord.models.interaction import Interaction from ...yepcord.snowflake import Snowflake from ...yepcord.utils import execute_after @@ -55,7 +55,7 @@ async def resolve_options(interaction_options: list[InteractionOption], guild: G result["members"] = {} result["members"][option.value] = await member.ds_json(False) elif option.type == T.CHANNEL: - if guild is None or (channel := await getCore().getChannel(option.value)) is None: + if guild is None or (channel := await Channel.Y.get(option.value)) is None: continue if channel.guild != guild: continue @@ -125,7 +125,7 @@ async def create_interaction(user: User = DepUser): if (application := await Application.get_or_none(id=data.application_id, deleted=False)) is None: raise UnknownApplication guild = None - channel = await getCore().getChannel(data.channel_id) + channel = await Channel.Y.get(data.channel_id) if data.guild_id: if (guild := await getCore().getGuild(data.guild_id)) is None: raise UnknownGuild diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index 4777b58..63944e6 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -24,7 +24,7 @@ from ...yepcord.ctx import getCore, getGw from ...yepcord.enums import ChannelType, GuildPermissions, MessageType from ...yepcord.errors import UnknownInvite, UserBanned, MissingAccess -from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan +from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan, Channel from ...yepcord.snowflake import Snowflake # Base path is /api/vX/invites @@ -79,7 +79,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): await GuildMember.create(id=Snowflake.makeId(), user=user, guild=guild) await getGw().dispatch(GuildCreateEvent(await guild.ds_json(user_id=user.id)), user_ids=[user.id]) if guild.system_channel: - sys_channel = await getCore().getChannel(guild.system_channel) + sys_channel = await Channel.Y.get(guild.system_channel) message = await Message.create( id=Snowflake.makeId(), author=user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index 07bea2c..31975b3 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -31,7 +31,7 @@ from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownGuild, MissingAccess from ...yepcord.models import User, Guild, GuildMember, Message, Role, AuditLogEntry, Application, Bot, Authorization, \ - Integration, GuildBan + Integration, GuildBan, Channel from ...yepcord.snowflake import Snowflake from ...yepcord.utils import b64decode @@ -134,7 +134,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori permissions=GuildPermissions.VIEW_AUDIT_LOG) await getGw().dispatch(GuildCreateEvent(await guild.ds_json(user_id=bot.user.id)), user_ids=[bot.user.id]) - if (sys_channel := await getCore().getChannel(guild.system_channel)) is not None: + if (sys_channel := await Channel.Y.get(guild.system_channel)) is not None: message = await Message.create( id=Snowflake.makeId(), author=bot.user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index c3ccbd6..25d0fab 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -249,7 +249,10 @@ async def new_relationship(data: RelationshipRequest, user: User = DepUser): @users_me.get("/relationships", oauth_scopes=["relationships.read"]) async def get_relationships(user: User = DepUser): - return await getCore().getRelationships(user, with_data=True) + return [ + await relationship.ds_json(user, True) + for relationship in await user.get_relationships() + ] @users_me.get("/notes/", allow_bots=True) @@ -355,7 +358,7 @@ async def accept_relationship_or_block(data: RelationshipPut, user_id: int, user else: await getGw().dispatch(RelationshipAddEvent(user.id, await user.data, 1), [user_id]) await getGw().dispatch(RelationshipAddEvent(user_id, target_user_data, 1), [user.id]) - channel = await getCore().getDMChannelOrCreate(user, target_user_data.user) + channel = await Channel.Y.get_dm(user, target_user_data.user) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": user_id}), [user_id]) await getGw().dispatch(DMChannelCreateEvent(channel, channel_json_kwargs={"user_id": user.id}), [user.id]) elif data.type == 2: @@ -400,7 +403,10 @@ async def leave_guild(user: User = DepUser, guild: Guild = DepGuild, member: Gui @users_me.get("/channels") async def get_dm_channels(user: User = DepUser): - return [await channel.ds_json(user_id=user.id) for channel in await getCore().getPrivateChannels(user)] + return [ + await channel.ds_json(user_id=user.id) + for channel in await user.get_private_channels() + ] @users_me.post("/channels", body_cls=DmChannelCreate) @@ -412,11 +418,11 @@ async def new_dm_channel(data: DmChannelCreate, user: User = DepUser): if None in recipients_users: raise InvalidRecipient if len(recipients) == 1: - channel = await getCore().getDMChannelOrCreate(user, recipients_users[0]) + channel = await Channel.Y.get_dm(user, recipients_users[0]) elif len(recipients) == 0: - channel = await getCore().createDMGroupChannel(user, [], data.name) + channel = await Channel.Y.create_dm_group(user, [], data.name) else: - channel = await getCore().createDMGroupChannel(user, recipients_users, data.name) + channel = await Channel.Y.create_dm_group(user, recipients_users, data.name) await getGw().dispatch(DMChannelCreateEvent(channel), channel=channel) return await channel.ds_json(with_ids=False, user_id=user.id) diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index 25649e9..bd486c0 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -21,11 +21,10 @@ from json import loads as jloads, dumps as jdumps from os import urandom from time import time -from typing import Optional, Union +from typing import Optional import maxminddb -from tortoise.expressions import Q, Subquery -from tortoise.functions import Count +from tortoise.expressions import Q from . import ctx from .classes.other import EmailMsg, JWT, MFA @@ -33,53 +32,18 @@ from .config import Config from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors, InvalidKey -from .models import User, Relationship, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, \ - Reaction, Sticker, Webhook, Role, GuildEvent, ThreadMember -from .snowflake import Snowflake +from .models import User, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, Sticker, \ + Webhook, Role, GuildEvent, ThreadMember from .storage import getStorage from .utils import b64encode, b64decode -from ..gateway.events import DMChannelCreateEvent # noinspection PyMethodMayBeStatic class Core(Singleton): - def __init__(self, key=None): - self.key = key if key and len(key) >= 16 and isinstance(key, bytes) else urandom(32) + def __init__(self): + self.key = b64decode(Config.KEY) self.ipdb = None - async def getRelationships(self, user: User, with_data=False) -> list[dict]: - rels = [] - rel: Relationship - for rel in await Relationship.filter( - Q(from_user=user) | Q(to_user=user) - ).select_related("from_user", "to_user"): - if (rel_json := await rel.ds_json(user, with_data)) is not None: - rels.append(rel_json) - return rels - - async def getRelatedUsers(self, user: User, only_ids=False) -> list: - users = [] - for r in await Relationship.filter(Q(from_user=user) | Q(to_user=user)).select_related("from_user", "to_user"): - other_user = r.other_user(user) - if only_ids: - users.append(other_user.id) - continue - data = await other_user.data - users.append(data.ds_json) - for channel in await self.getPrivateChannels(user, with_hidden=True): - channel = await Channel.get(id=channel.id) - for recipient in await channel.recipients.all(): - if recipient.id == user.id: continue - if only_ids: - if recipient.id in users: continue - users.append(recipient.id) - continue - if [rec for rec in users if rec["id"] == recipient.id]: - continue - data = await recipient.data - users.append(data.ds_json) - return users - def generateMfaTicketSignature(self, user: User, session_id: int) -> str: payload = { "user_id": user.id, @@ -124,44 +88,6 @@ async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> if nonce_type != payload["type"]: raise InvalidKey - async def getChannel(self, channel_id: Optional[int]) -> Optional[Channel]: - if channel_id is None: - return - return await Channel.get_or_none(id=channel_id).select_related("guild", "owner", "parent") - - async def getDChannel(self, user1: User, user2: User) -> Optional[Channel]: - return await Channel.get_or_none( - id__in=Subquery( - Channel - .filter(recipients__id__in=[user1.id, user2.id]) - .annotate(user_count=Count('recipients__id', distinct=True)) - .filter(user_count=2) - .group_by("id") - .values_list('id', flat=True) - ) - ) - - async def getDMChannelOrCreate(self, user1: User, user2: User) -> Channel: - channel = await self.getDChannel(user1, user2) - if channel is None: - channel = await Channel.create(id=Snowflake.makeId(), type=ChannelType.DM) - await channel.recipients.add(user1) - await channel.recipients.add(user2) - return await Channel.get(id=channel.id) - - if await channel.dm_is_hidden(user1): - await channel.dm_unhide(user1) - await ctx.getGw().dispatch(DMChannelCreateEvent(channel), user_ids=[user1.id]) - - return channel - - async def getPrivateChannels(self, user: User, with_hidden: bool = False) -> list[Channel]: - return [ - channel - for channel in await Channel.filter(recipients__id=user.id).select_related("owner") - if not with_hidden and not await channel.dm_is_hidden(user) - ] - #async def sendMessage(self, message: Message) -> Message: # async def _addToReadStates(): # TODO: recalculate read states when requested by user # users = await self.getRelatedUsersToChannel(message.channel) @@ -199,11 +125,6 @@ async def getReadStatesJ(self, user: User) -> list: states.append(await st.ds_json()) return states - async def getUserByChannelId(self, channel_id: int, user_id: int) -> Optional[User]: - if not (channel := await self.getChannel(channel_id)): - return None - return await self.getUserByChannel(channel, user_id) - async def getUserByChannel(self, channel: Channel, user_id: int) -> Optional[User]: if channel.type in (ChannelType.DM, ChannelType.GROUP_DM): if await Channel.exists(id=channel.id, recipients__id__in=[user_id]): @@ -247,33 +168,6 @@ async def mfaNonceToCode(self, nonce: str) -> Optional[str]: signature = token.split(".")[2] return signature.replace("-", "").replace("_", "")[:8].upper() - async def createDMGroupChannel(self, user: User, recipients: list[User], name: Optional[str] = None) -> Channel: - if user not in recipients: - recipients.append(user) - channel = await Channel.create(id=Snowflake.makeId(), type=ChannelType.GROUP_DM, name=name, owner=user) - for recipient in recipients: - await channel.recipients.add(recipient) - return channel - - async def getMessageReactionsJ(self, message: Message, user: Union[User, int]) -> list: - if isinstance(user, User): - user = user.id - reactions = [] - result = await (Reaction.filter(message=message) - .group_by("emoji_name", "emoji__id") - .annotate(count=Count("id")) - .values("id", "emoji_name", "emoji__id", "count")) - - me_results = set(await Reaction.filter(message=message, user=user).values_list("id", flat=True)) - - for r in result: - reactions.append({ - "emoji": {"id": str(r["emoji__id"]) if r["emoji__id"] else None, "name": r["emoji_name"]}, - "count": r["count"], - "me": r["id"] in me_results - }) - return reactions - async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[list[Message], int]: filter_args = {"channel": channel} if "author_id" in search_filter: @@ -366,21 +260,6 @@ async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[st return mutual_guilds_json - async def setMemberRolesFromList(self, member: GuildMember, roles: list[Role]) -> tuple[list, list]: - current_roles = await member.roles.all() - add = [] - remove = [] - for role in roles: - if role not in current_roles and not role.managed: - add.append(role.id) - await member.roles.add(role) - for role in current_roles: - if role not in roles and not role.managed: - remove.append(role.id) - await member.roles.remove(role) - - return add, remove - # noinspection PyUnusedLocal async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids: list[int]) -> list[GuildMember]: # noinspection PyUnresolvedReferences diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index f4b55f1..311a5c7 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -21,16 +21,69 @@ from typing import Optional, Union from tortoise import fields -from tortoise.expressions import Q +from tortoise.expressions import Q, Subquery from tortoise.fields import SET_NULL +from tortoise.functions import Count -from ..ctx import getCore +from ..ctx import getCore, getGw from ..enums import ChannelType import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model +from ..snowflake import Snowflake + + +class ChannelUtils: + @staticmethod + async def get(channel_id: int) -> Optional[models.Channel]: + if channel_id is None: + return + return await Channel.get_or_none(id=channel_id).select_related("guild", "owner", "parent") + + @staticmethod + async def get_dm(user1: models.User, user2: models.User) -> Optional[models.Channel]: + channel = await Channel.get_or_none( + id__in=Subquery( + Channel + .filter(recipients__id__in=[user1.id, user2.id]) + .annotate(user_count=Count("recipients__id", distinct=True)) + .filter(user_count=2) + .group_by("id") + .values_list("id", flat=True) + ) + ) + + if channel is None: + channel = await Channel.create(id=Snowflake.makeId(), type=ChannelType.DM) + await channel.recipients.add(user1) + await channel.recipients.add(user2) + + return channel + + if await channel.dm_is_hidden(user1): + from ...gateway.events import DMChannelCreateEvent + + await channel.dm_unhide(user1) + await getGw().dispatch(DMChannelCreateEvent(channel), user_ids=[user1.id]) + + return channel + + @staticmethod + async def create_dm_group( + owner: models.User, recipients: list[models.User], name: Optional[str] = None + ) -> models.Channel: + recipients = set(recipients) + recipients.add(owner) + + channel = await Channel.create(id=Snowflake.makeId(), type=ChannelType.GROUP_DM, name=name, owner=owner) + for recipient in recipients: + await channel.recipients.add(recipient) + + return channel class Channel(Model): + Y = ChannelUtils + id: int = SnowflakeField(pk=True) type: int = fields.IntField() guild: Optional[models.Guild] = fields.ForeignKeyField("models.Guild", null=True, default=None) diff --git a/yepcord/yepcord/models/guild_member.py b/yepcord/yepcord/models/guild_member.py index be3df2d..8a72cca 100644 --- a/yepcord/yepcord/models/guild_member.py +++ b/yepcord/yepcord/models/guild_member.py @@ -151,3 +151,18 @@ async def permissions(self) -> int: for role in await self.get_roles(): permissions |= role.permissions return permissions + + async def set_roles_from_list(self, roles: list[models.Role]) -> tuple[list[int], list[int]]: + current_roles = await self.roles.all() + added = [] + removed = [] + for role in roles: + if role not in current_roles and not role.managed: + added.append(role.id) + await self.roles.add(role) + for role in current_roles: + if role not in roles and not role.managed: + removed.append(role.id) + await self.roles.remove(role) + + return added, removed diff --git a/yepcord/yepcord/models/message.py b/yepcord/yepcord/models/message.py index 243b84e..de971c1 100644 --- a/yepcord/yepcord/models/message.py +++ b/yepcord/yepcord/models/message.py @@ -21,6 +21,7 @@ from typing import Optional from tortoise import fields +from tortoise.functions import Count import yepcord.yepcord.models as models from ..ctx import getCore @@ -115,7 +116,7 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: if ping.startswith("&"): data["mention_roles"].append(ping[1:]) continue - if not (member := await getCore().getUserByChannelId(self.channel.id, int(ping))): + if not (member := await getCore().getUserByChannel(self.channel, int(ping))): continue if isinstance(member, (models.GuildMember, models.ThreadMember)): member = member.user @@ -133,7 +134,7 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: if "guild_id" in self.message_reference: data["message_reference"]["guild_id"] = str(self.message_reference["guild_id"]) if self.type in (MessageType.REPLY, MessageType.THREAD_STARTER_MESSAGE): - ref_channel = await getCore().getChannel(int(self.message_reference["channel_id"])) + ref_channel = await models.Channel.Y.get(int(self.message_reference["channel_id"])) ref_message = None if ref_channel: ref_message = await ref_channel.get_message(int(self.message_reference["message_id"])) @@ -141,7 +142,7 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: data["referenced_message"] = await ref_message.ds_json() if ref_message else None if self.nonce is not None: data["nonce"] = self.nonce - if not search and (reactions := await getCore().getMessageReactionsJ(self, user_id)): + if not search and (reactions := await self._get_reactions_json(user_id)): data["reactions"] = reactions if self.interaction: @@ -153,3 +154,23 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: "user": userdata, } return data + + async def _get_reactions_json(self, user_id: int) -> list: + result = await (models.Reaction.filter(message=self) + .group_by("emoji_name", "emoji__id") + .annotate(count=Count("id")) + .values("id", "emoji_name", "emoji__id", "count")) + + me_results = set(await models.Reaction.filter(message=self, user__id=user_id).values_list("id", flat=True)) + + return [ + { + "emoji": { + "id": str(reaction["emoji__id"]) if reaction["emoji__id"] else None, + "name": reaction["emoji_name"] + }, + "count": reaction["count"], + "me": reaction["id"] in me_results, + } + for reaction in result + ] diff --git a/yepcord/yepcord/models/relationship.py b/yepcord/yepcord/models/relationship.py index c20bed8..34f6154 100644 --- a/yepcord/yepcord/models/relationship.py +++ b/yepcord/yepcord/models/relationship.py @@ -143,7 +143,7 @@ def discord_rel_type(self, current_user: models.User) -> Optional[int]: elif self.to_user == current_user: return RelTypeDiscord.REQUEST_RECV - async def ds_json(self, current_user: models.User, with_data=False) -> Optional[dict]: + async def ds_json(self, current_user: models.User, with_data: bool = False) -> Optional[dict]: other_user = self.other_user(current_user) if (rel_type := self.discord_rel_type(current_user)) is None: return diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 793ebe1..5104a9c 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -33,6 +33,7 @@ from ..classes.other import MFA from ..config import Config from ..ctx import getCore +from ..enums import RelationshipType from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser from ..snowflake import Snowflake from ..utils import int_size, b64encode @@ -286,3 +287,31 @@ async def get_guilds(self) -> list[models.Guild]: member.guild for member in await models.GuildMember.filter(user=self).select_related("guild", "guild__owner") ] + + async def get_private_channels(self) -> list[models.Channel]: + return [ + channel + for channel in await models.Channel.filter(recipients__id=self.id).select_related("owner") + if not await channel.dm_is_hidden(self) + ] + + async def get_relationships(self) -> list[models.Relationship]: + return [ + relationship + for relationship in + await models.Relationship.filter(Q(from_user=self) | Q(to_user=self)).select_related("from_user", "to_user") + if not (relationship.type == RelationshipType.BLOCK and relationship.from_user.id != self.id) + ] + + async def get_related_users(self) -> list[models.User]: + users = { + relationship.other_user(self).id: relationship.other_user(self) + for relationship in await self.get_relationships() + } + for channel in await models.Channel.filter(recipients__id=self.id): + for recipient in await channel.recipients.all(): + if recipient.id in users or recipient == self: + continue + users[recipient.id] = recipient + + return list(users.values()) From a2c45e0641d53d445168b1fe96040f5236c36d20 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 6 Sep 2024 17:17:59 +0300 Subject: [PATCH 09/26] move more guild methods --- yepcord/gateway/gateway.py | 6 ++- yepcord/rest_api/dependencies.py | 7 +-- yepcord/rest_api/routes/channels.py | 54 +++++++++++----------- yepcord/rest_api/routes/guilds.py | 34 +++++++------- yepcord/rest_api/routes/interactions.py | 12 +++-- yepcord/rest_api/routes/invites.py | 4 +- yepcord/rest_api/routes/oauth2.py | 4 +- yepcord/rest_api/routes/users_me.py | 2 +- yepcord/rest_api/routes/webhooks.py | 57 ++++++++++++----------- yepcord/rest_api/utils.py | 4 +- yepcord/yepcord/core.py | 60 +------------------------ yepcord/yepcord/gateway_dispatcher.py | 2 +- yepcord/yepcord/models/channel.py | 19 +++++++- yepcord/yepcord/models/emoji.py | 18 +++++++- yepcord/yepcord/models/guild.py | 21 ++++++++- yepcord/yepcord/models/interaction.py | 4 +- yepcord/yepcord/models/message.py | 8 ++-- yepcord/yepcord/models/user.py | 2 +- yepcord/yepcord/models/webhook.py | 10 ++++- 19 files changed, 168 insertions(+), 160 deletions(-) diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index ecb3fc3..d126b1f 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -144,7 +144,8 @@ async def handle_LAZY_REQUEST(self, data: dict) -> None: if not (guild_id := int(data.get("guild_id"))): return if not data.get("members", True): return guild = await getCore().getGuild(guild_id) - if not await getCore().getGuildMember(guild, self.user_id): return + if not await GuildMember.exists(guild=guild, user__id=self.user_id): + return members = await getCore().getGuildMembers(guild) statuses = {} @@ -164,7 +165,8 @@ async def handle_LAZY_REQUEST(self, data: dict) -> None: async def handle_GUILD_MEMBERS(self, data: dict) -> None: if not (guild_id := int(data.get("guild_id")[0])): return guild = await getCore().getGuild(guild_id) - if not await getCore().getGuildMember(guild, self.user_id): return + if not await GuildMember.exists(guild=guild, user__id=self.user_id): + return query = data.get("query", "") limit = data.get("limit", 100) diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index cec27bd..2478d07 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -70,7 +70,7 @@ def depUser(allow_without_user: bool = False): async def depChannelO(channel_id: Optional[int] = None, user: User = Depends(depUser())) -> Optional[Channel]: if (channel := await Channel.Y.get(channel_id)) is None: return - if not await getCore().getUserByChannel(channel, user.id): + if not await channel.user_can_access(user.id): raise Unauthorized return channel @@ -153,7 +153,7 @@ async def depGuildMember(guild: Guild = Depends(depGuild), user: User = Depends( async def depRole(role: int, guild: Guild = Depends(depGuild)) -> Role: - if not role or not (role := await getCore().getRole(role, guild)): + if not role or (role := await guild.get_role(role)) is None: raise UnknownRole return role @@ -161,7 +161,8 @@ async def depRole(role: int, guild: Guild = Depends(depGuild)) -> Role: async def depGuildTemplate(template: str, guild: Guild = Depends(depGuild)) -> GuildTemplate: try: template_id = int.from_bytes(b64decode(template), "big") - if not (template := await getCore().getGuildTemplateById(template_id, guild)): + template = await GuildTemplate.get_or_none(id=template_id, guild=guild).select_related("guild", "creator") + if template is None: raise ValueError except ValueError: raise UnknownGuildTemplate diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index cbb69e2..265a9d5 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -78,7 +78,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha await getGw().dispatch(DMChannelUpdateEvent(channel), channel=channel) elif channel.type in GUILD_CHANNELS: guild = channel.guild - member = await getCore().getGuildMember(guild, user.id) + member = await guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, channel=channel) changes = data.to_json(channel.type) @@ -135,7 +135,7 @@ async def delete_channel(user: User = DepUser, channel: Channel = DepChannel): await channel.save() await getGw().dispatch(DMChannelUpdateEvent(channel), channel=channel) elif channel.type in GUILD_CHANNELS: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, channel=channel) entry = await AuditLogEntry.utils.channel_delete(user, channel) @@ -160,7 +160,7 @@ async def delete_channel(user: User = DepUser, channel: Channel = DepChannel): @channels.get("//messages", qs_cls=GetMessagesQuery, allow_bots=True) async def get_messages(query_args: GetMessagesQuery, user: User = DepUser, channel: Channel = DepChannel): if channel.guild is not None: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) messages = await channel.get_messages(**query_args.model_dump()) messages = [await message.ds_json(user_id=user.id) for message in messages] @@ -174,7 +174,7 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): if await Relationship.utils.is_blocked(oth, user): raise CannotSendToThisUser elif channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.SEND_MESSAGES, GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) @@ -199,7 +199,7 @@ async def delete_message(user: User = DepUser, channel: Channel = DepChannel, me if message.author != user: if channel.type not in GUILD_CHANNELS: raise CannotExecuteOnDM - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_MESSAGES, GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) guild_id = channel.guild.id if channel.guild else None @@ -215,7 +215,7 @@ async def edit_message(data: MessageUpdate, user: User = DepUser, channel: Chann if message.author != user: raise CannotEditAnotherUserMessage if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.SEND_MESSAGES, GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) await message.update(**data.to_json(), edit_timestamp=datetime.now()) @@ -227,7 +227,7 @@ async def edit_message(data: MessageUpdate, user: User = DepUser, channel: Chann @channels.get("//messages/", allow_bots=True) async def get_message(user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild is not None: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) if message.ephemeral and message.author != user: raise UnknownMessage @@ -260,7 +260,7 @@ async def delete_message_ack(user: User = DepUser, channel: Channel = DepChannel @channels.post("//typing", allow_bots=True) async def send_typing_event(user: User = DepUser, channel: Channel = DepChannel): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.VIEW_CHANNEL, channel=channel) await getGw().dispatch(TypingEvent(user.id, channel.id), channel=channel, permissions=GuildPermissions.VIEW_CHANNEL) return "", 204 @@ -317,7 +317,7 @@ async def delete_recipient(target_user: int, user: User = DepUser, channel: Chan @channels.put("//pins/", allow_bots=True) async def pin_message(user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.VIEW_CHANNEL, channel=channel) if not message.pinned: if await Message.filter(pinned_timestamp__not_isnull=True, channel=message.channel).count() >= 50: @@ -342,7 +342,7 @@ async def pin_message(user: User = DepUser, channel: Channel = DepChannel, messa @channels.delete("//pins/", allow_bots=True) async def unpin_message(user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild: - member = await getCore().getGuildMember(message.guild, user.id) + member = await message.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.VIEW_CHANNEL, channel=channel) if message.pinned: message.pinned_timestamp = None @@ -354,7 +354,7 @@ async def unpin_message(user: User = DepUser, channel: Channel = DepChannel, mes @channels.get("//pins", allow_bots=True) async def get_pinned_messages(user: User = DepUser, channel: Channel = DepChannel): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission( GuildPermissions.VIEW_CHANNEL, GuildPermissions.READ_MESSAGE_HISTORY, channel=channel, @@ -371,10 +371,10 @@ async def get_pinned_messages(user: User = DepUser, channel: Channel = DepChanne async def add_message_reaction(reaction: str, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) - if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): + if not is_emoji(reaction) and not (reaction := await Emoji.Y.get_by_reaction(reaction)): raise UnknownEmoji emoji = { "emoji": None if not isinstance(reaction, Emoji) else reaction, @@ -390,10 +390,10 @@ async def add_message_reaction(reaction: str, user: User = DepUser, channel: Cha async def remove_message_reaction(reaction: str, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) - if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): + if not is_emoji(reaction) and not (reaction := await Emoji.Y.get_by_reaction(reaction)): raise UnknownEmoji emoji = { "emoji": None if not isinstance(reaction, Emoji) else reaction, @@ -409,10 +409,10 @@ async def remove_message_reaction(reaction: str, user: User = DepUser, channel: async def get_message_reactions(query_args: GetReactionsQuery, reaction: str, user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.ADD_REACTIONS, GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) - if not is_emoji(reaction) and not (reaction := await getCore().getEmojiByReaction(reaction)): + if not is_emoji(reaction) and not (reaction := await Emoji.Y.get_by_reaction(reaction)): raise UnknownEmoji emoji = None if not isinstance(reaction, Emoji) else reaction emoji_name = reaction if isinstance(reaction, str) else reaction.name @@ -430,7 +430,7 @@ async def get_message_reactions(query_args: GetReactionsQuery, reaction: str, us @channels.get("//messages/search", qs_cls=SearchQuery) async def search_messages(query_args: SearchQuery, user: User = DepUser, channel: Channel = DepChannel): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) messages, total = await getCore().searchMessages(channel, query_args.model_dump(exclude_defaults=True)) @@ -443,7 +443,7 @@ async def search_messages(query_args: SearchQuery, user: User = DepUser, channel @channels.post("//invites", body_cls=InviteCreate, allow_bots=True) async def create_invite(data: InviteCreate, user: User = DepUser, channel: Channel = DepChannel): if channel.guild: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.CREATE_INSTANT_INVITE) invite = await Invite.create( id=Snowflake.makeId(), channel=channel, inviter=user, **data.model_dump(include={"max_age", "max_uses"}), @@ -460,9 +460,9 @@ async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, channel: Channel = DepChannel): if not channel.guild: raise CannotExecuteOnDM - if not (member := await getCore().getGuildMember(channel.guild, user.id)): + if not (member := await channel.guild.get_member(user.id)): raise MissingAccess - target = await getCore().getRole(target_id) if data.type == 0 else await User.get_or_none(id=target_id) + target = await channel.guild.get_role(target_id) if data.type == 0 else await User.get_or_none(id=target_id) if target is None or (isinstance(target, Role) and target.guild != channel.guild): return "", 204 await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.MANAGE_ROLES, channel=channel) @@ -491,7 +491,7 @@ async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, async def delete_permission_overwrite(target_id: int, user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: raise CannotExecuteOnDM - if not (member := await getCore().getGuildMember(channel.guild, user.id)): + if not (member := await channel.guild.get_member(user.id)): raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_CHANNELS, GuildPermissions.MANAGE_ROLES, channel=channel) overwrite = await channel.get_permission_overwrite(target_id) @@ -515,7 +515,7 @@ async def delete_permission_overwrite(target_id: int, user: User = DepUser, chan async def get_channel_invites(user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: raise CannotExecuteOnDM - if not (member := await getCore().getGuildMember(channel.guild, user.id)): + if not (member := await channel.guild.get_member(user.id)): raise MissingAccess await member.checkPermission(GuildPermissions.VIEW_CHANNEL, channel=channel) return [ @@ -529,7 +529,7 @@ async def get_channel_invites(user: User = DepUser, channel: Channel = DepChanne async def create_webhook(data: WebhookCreate, user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: raise CannotExecuteOnDM - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) webhook = await Webhook.create(id=Snowflake.makeId(), type=WebhookType.INCOMING, name=data.name, @@ -544,7 +544,7 @@ async def create_webhook(data: WebhookCreate, user: User = DepUser, channel: Cha async def get_channel_webhooks(user: User = DepUser, channel: Channel = DepChannel): if not channel.guild: raise CannotExecuteOnDM - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) return [ @@ -559,7 +559,7 @@ async def create_thread( ): if not channel.guild: raise CannotExecuteOnDM - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.CREATE_PUBLIC_THREADS, channel=channel) thread = await Channel.create(id=message.id, type=ChannelType.GUILD_PUBLIC_THREAD, guild=channel.guild, @@ -654,7 +654,7 @@ async def search_application_commands( @channels.get("//messages//interaction-data", allow_bots=True) async def get_message_interaction(user: User = DepUser, channel: Channel = DepChannel, message: Message = DepMessage): if channel.guild is not None: - member = await getCore().getGuildMember(channel.guild, user.id) + member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, channel=channel) if message.ephemeral and message.author != user: raise UnknownMessage diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index 8433443..b5be128 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -112,7 +112,7 @@ async def update_guild(data: GuildUpdate, user: User = DepUser, guild: Guild = D async def get_guild_templates(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) templates = [] - if template := await getCore().getGuildTemplate(guild): + if template := await guild.get_template(): templates.append(await template.ds_json()) return templates @@ -121,7 +121,7 @@ async def get_guild_templates(guild: Guild = DepGuild, member: GuildMember = Dep async def create_guild_template(data: TemplateCreate, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) - if await getCore().getGuildTemplate(guild): + if await GuildTemplate.exists(guild=guild): raise CanHaveOneTemplate template: GuildTemplate = await GuildTemplate.create( @@ -188,7 +188,7 @@ async def create_guild_emoji(data: EmojiCreate, user: User = DepUser, guild: Gui async def update_guild_emoji(data: EmojiUpdate, emoji: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) - if (emoji := await getCore().getEmoji(emoji)) is None or emoji.guild != guild: + if (emoji := await Emoji.get_or_none(id=emoji, guild=guild)) is None: raise UnknownEmoji await emoji.update(**data.model_dump(exclude_defaults=True)) @@ -202,7 +202,7 @@ async def delete_guild_emoji(emoji: int, user: User = DepUser, guild: Guild = De member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) - if not (emoji := await getCore().getEmoji(emoji)) or emoji.guild != guild: + if (emoji := await Emoji.get_or_none(id=emoji, guild=guild).select_related("guild")) is None: return "", 204 await emoji.delete() @@ -245,7 +245,7 @@ async def create_channel(data: ChannelCreate, user: User = DepUser, guild: Guild del data_json["parent_id"] channel = await Channel.create(id=Snowflake.makeId(), guild=guild, **data_json) for overwrite in data.permission_overwrites: - target = await getCore().getRole(overwrite.id) if data.type == 0 else await User.get_or_none(id=overwrite.id) + target = await guild.get_role(overwrite.id) if data.type == 0 else await User.get_or_none(id=overwrite.id) if target is None: raise (UnknownRole if data.type == 0 else UnknownUser) kw = {"target_role": target} if isinstance(target, Role) else {"target_user": target} @@ -304,7 +304,7 @@ async def process_bot_kick(user: User = DepUser, bot_member: GuildMember = DepGu async def kick_member(user_id: int, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.KICK_MEMBERS) - if not (target_member := await getCore().getGuildMember(guild, user_id)): + if not (target_member := await guild.get_member(user_id)): return "", 204 if not await member.perm_checker.canKickOrBan(target_member): raise MissingPermissions @@ -328,7 +328,7 @@ async def kick_member(user_id: int, user: User = DepUser, guild: Guild = DepGuil async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.BAN_MEMBERS) - target_member = await getCore().getGuildMember(guild, user_id) + target_member = await guild.get_member(user_id) if target_member is not None and not await member.perm_checker.canKickOrBan(target_member): raise MissingPermissions if await GuildBan.exists(guild=guild, user__id=user_id): @@ -550,7 +550,7 @@ async def add_role_members(data: AddRoleMembers, user: User = DepUser, guild: Gu raise MissingPermissions members = {} for member_id in data.member_ids: - target_member = await getCore().getGuildMember(guild, member_id) + target_member = await guild.get_member(member_id) if not await target_member.roles.filter(id=role.id).exists(): await target_member.roles.add(role) target_member_json = await target_member.ds_json() @@ -567,7 +567,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs if target_user == "@me": target_user = user.id target_user = int(target_user) - target_member = await getCore().getGuildMember(guild, target_user) + target_member = await guild.get_member(target_user) if data.roles is not None: await member.checkPermission(GuildPermissions.MANAGE_ROLES) roles = [int(role) for role in data.roles] @@ -675,7 +675,7 @@ async def get_audit_logs(query_args: GetAuditLogsQuery, guild: Guild = DepGuild, async def create_from_template(data: GuildCreateFromTemplate, template: str, user: User = DepUser): try: template_id = int.from_bytes(b64decode(template), "big") - if not (template := await getCore().getGuildTemplateById(template_id)): + if not (template := await GuildTemplate.get_or_none(id=template_id)): raise ValueError except ValueError: raise UnknownGuildTemplate @@ -767,7 +767,7 @@ async def upload_guild_stickers(user: User = DepUser, guild: Guild = DepGuild, m async def update_guild_sticker(data: UpdateSticker, sticker_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) - if not (sticker := await getCore().getSticker(sticker_id)) or sticker.guild != guild: + if (sticker := await guild.get_sticker(sticker_id)) is None: raise UnknownSticker await sticker.update(**data.model_dump(exclude_defaults=True)) await getGw().sendStickersUpdateEvent(guild) @@ -777,7 +777,7 @@ async def update_guild_sticker(data: UpdateSticker, sticker_id: int, guild: Guil @guilds.delete("//stickers/", allow_bots=True) async def delete_guild_sticker(sticker_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) - if not (sticker := await getCore().getSticker(sticker_id)) or sticker.guild != guild: + if (sticker := await guild.get_sticker(sticker_id)) is None: raise UnknownSticker await sticker.delete() @@ -823,7 +823,7 @@ async def create_scheduled_event(data: CreateEvent, user: User = DepUser, guild: @guilds.get("//scheduled-events/", qs_cls=GetScheduledEvent, allow_bots=True) async def get_scheduled_event(query_args: GetScheduledEvent, event_id: int, guild: Guild = DepGuild): - if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: + if (event := await guild.get_scheduled_event(event_id)) is None: raise UnknownGuildEvent return await event.ds_json(with_user_count=query_args.with_user_count) @@ -841,7 +841,7 @@ async def get_scheduled_events(query_args: GetScheduledEvent, guild: Guild = Dep async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EVENTS) - if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: + if (event := await guild.get_scheduled_event(event_id)) is None: raise UnknownGuildEvent if (img := data.image) or img is None: @@ -882,7 +882,7 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil @guilds.put("//scheduled-events//users/@me", allow_bots=True) async def subscribe_to_scheduled_event(event_id: int, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): - if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: + if (event := await guild.get_scheduled_event(event_id)) is None: raise UnknownGuildEvent await event.subscribers.add(member) @@ -898,7 +898,7 @@ async def subscribe_to_scheduled_event(event_id: int, user: User = DepUser, guil @guilds.delete("//scheduled-events//users/@me", allow_bots=True) async def unsubscribe_from_scheduled_event(event_id: int, user: User = DepUser, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): - if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: + if (event := await guild.get_scheduled_event(event_id)) is None: raise UnknownGuildEvent if await event.subscribers.filter(user__id=user.id).get_or_none() is not None: @@ -912,7 +912,7 @@ async def unsubscribe_from_scheduled_event(event_id: int, user: User = DepUser, @guilds.delete("//scheduled-events/", allow_bots=True) async def delete_scheduled_event(event_id: int, guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_EVENTS) - if not (event := await getCore().getGuildEvent(event_id)) or event.guild != guild: + if (event := await guild.get_scheduled_event(event_id)) is None: raise UnknownGuildEvent await event.delete() diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index 28d2520..f873b85 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -49,7 +49,7 @@ async def resolve_options(interaction_options: list[InteractionOption], guild: G if "users" not in result: result["users"] = {} result["users"][option.value] = (await user.userdata).ds_json - if guild is None or not (member := await getCore().getGuildMember(guild, user.id)): + if guild is None or not (member := await guild.get_member(user.id)): continue if "members" not in result: result["members"] = {} @@ -63,9 +63,7 @@ async def resolve_options(interaction_options: list[InteractionOption], guild: G result["channels"] = {} result["channels"][option.value] = await channel.ds_json() elif option.type == T.ROLE: - if guild is None or (role := await getCore().getRole(option.value)) is None: - continue - if role.guild != guild: + if guild is None or (role := await guild.get_role(option.value)) is None: continue if "roles" not in result: result["roles"] = {} @@ -129,13 +127,13 @@ async def create_interaction(user: User = DepUser): if data.guild_id: if (guild := await getCore().getGuild(data.guild_id)) is None: raise UnknownGuild - if (member := await getCore().getGuildMember(guild, user.id)) is None: + if (member := await guild.get_member(user.id)) is None: raise MissingAccess P = GuildPermissions await member.checkPermission(P.VIEW_CHANNEL, P.USE_APPLICATION_COMMANDS, channel=channel) if channel.guild != guild: raise UnknownChannel - if not await getCore().getUserByChannel(channel, user.id): + if not await channel.user_can_access(user.id): raise Unauthorized if guild is not None: if not await Integration.exists(guild=guild, application=application): @@ -149,7 +147,7 @@ async def create_interaction(user: User = DepUser): (message := await channel.get_message(data.data.target_id)) is None: raise UnknownMessage if command.type == ApplicationCommandType.USER and \ - (target_member := await getCore().getGuildMember(guild, data.data.target_id)) is None: + (target_member := await guild.get_member(data.data.target_id)) is None: raise UnknownUser if data.data.version != command.version or data.data.type != command.type or data.data.name != command.name: diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index 63944e6..c1d9f96 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -71,7 +71,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): for excl in ["max_age", "max_uses", "created_at"]: # Remove excluded fields if excl in inv: del inv[excl] - if not await getCore().getGuildMember(channel.guild, user.id): + if not await GuildMember.exists(guild=channel.guild, user=user): guild = channel.guild if await GuildBan.exists(guild=guild, user=user): raise UserBanned @@ -94,7 +94,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): @invites.delete("/", allow_bots=True) async def delete_invite(user: User = DepUser, invite: Invite = DepInvite): if invite.channel.guild: - if (member := await getCore().getGuildMember(invite.channel.guild, user.id)) is None: + if (member := await invite.channel.guild.get_member(user.id)) is None: raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_GUILD) await invite.delete() diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index 31975b3..32b7900 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -68,7 +68,7 @@ async def get_application_authorization_info(query_args: AppAuthorizeGetQs, user if "bot" in scopes: async def guild_ds_json(g: Guild) -> dict: - member = await getCore().getGuildMember(g, user.id) + member = await g.get_member(user.id) return {"id": str(g.id), "name": g.name, "icon": g.icon, "mfa_level": g.mfa_level, "permissions": str(await member.permissions)} @@ -103,7 +103,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori if (guild := await getCore().getGuild(data.guild_id)) is None: raise UnknownGuild - if not (member := await getCore().getGuildMember(guild, user.id)): + if not (member := await guild.get_member(user.id)): raise MissingAccess await member.checkPermission(GuildPermissions.MANAGE_GUILD) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 25d0fab..94789a4 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -530,7 +530,7 @@ async def remote_auth_cancel(data: RemoteAuthCancel, user: User = DepUser): @users_me.get("/guilds", allow_bots=True, oauth_scopes=["guilds"]) async def get_guilds(user: User = DepUser): async def ds_json(guild: Guild) -> dict: - member = await getCore().getGuildMember(guild, user.id) + member = await guild.get_member(user.id) return { "approximate_member_count": await getCore().getGuildMemberCount(guild), "approximate_presence_count": 0, diff --git a/yepcord/rest_api/routes/webhooks.py b/yepcord/rest_api/routes/webhooks.py index c492693..d41a429 100644 --- a/yepcord/rest_api/routes/webhooks.py +++ b/yepcord/rest_api/routes/webhooks.py @@ -36,31 +36,38 @@ webhooks = YBlueprint('webhooks', __name__) -@webhooks.delete("/") -@webhooks.delete("//", allow_bots=True) -async def delete_webhook(webhook: int, user: Optional[User] = DepUserO, token: Optional[str]=None): - if webhook := await getCore().getWebhook(webhook): - if webhook.token != token: - guild = webhook.channel.guild - if user is None or not (member := await getCore().getGuildMember(guild, user.id)): - raise MissingPermissions - await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) - await webhook.delete() - await getGw().dispatch(WebhooksUpdateEvent(webhook.channel.guild.id, webhook.channel.id), - guild_id=webhook.channel.guild.id, permissions=GuildPermissions.MANAGE_WEBHOOKS) +@webhooks.delete("/") +@webhooks.delete("//", allow_bots=True) +async def delete_webhook(webhook_id: int, user: Optional[User] = DepUserO, token: Optional[str]=None): + if (webhook := await Webhook.Y.get(webhook_id)) is None: + return "", 204 + + if webhook.token != token: + guild = webhook.channel.guild + if user is None or not (member := await guild.get_member(user.id)): + raise MissingPermissions + await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) + await webhook.delete() + await getGw().dispatch( + WebhooksUpdateEvent(webhook.channel.guild.id, webhook.channel.id), + guild_id=webhook.channel.guild.id, + permissions=GuildPermissions.MANAGE_WEBHOOKS + ) return "", 204 -@webhooks.patch("/") -@webhooks.patch("//", body_cls=WebhookUpdate, allow_bots=True) -async def edit_webhook(data: WebhookUpdate, webhook: int, user: Optional[User] = DepUserO, token: Optional[str]=None): - if not (webhook := await getCore().getWebhook(webhook)): +@webhooks.patch("/") +@webhooks.patch("//", body_cls=WebhookUpdate, allow_bots=True) +async def edit_webhook( + data: WebhookUpdate, webhook_id: int, user: Optional[User] = DepUserO, token: Optional[str] = None +): + if (webhook := await Webhook.Y.get(webhook_id)) is None: raise UnknownWebhook channel = webhook.channel guild = channel.guild if webhook.token != token: - if user is None or not (member := await getCore().getGuildMember(guild, user.id)): + if user is None or not (member := await guild.get_member(user.id)): raise MissingPermissions await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) if data.channel_id: @@ -88,23 +95,23 @@ async def edit_webhook(data: WebhookUpdate, webhook: int, user: Optional[User] = return await webhook.ds_json() -@webhooks.get("/") -@webhooks.get("//", allow_bots=True) -async def get_webhook(webhook: int, user: Optional[User] = DepUserO, token: Optional[str]=None): - if not (webhook := await getCore().getWebhook(webhook)): +@webhooks.get("/") +@webhooks.get("//", allow_bots=True) +async def get_webhook(webhook_id: int, user: Optional[User] = DepUserO, token: Optional[str]=None): + if (webhook := await Webhook.Y.get(webhook_id)) is None: raise UnknownWebhook if webhook.token != token: guild = webhook.channel.guild - if user is None or not (member := await getCore().getGuildMember(guild, user.id)): + if user is None or not (member := await guild.get_member(user.id)): raise MissingPermissions await member.checkPermission(GuildPermissions.MANAGE_WEBHOOKS) return await webhook.ds_json() -@webhooks.post("//", qs_cls=WebhookMessageCreateQuery) -async def post_webhook_message(query_args: WebhookMessageCreateQuery, webhook: int, token: str): - if not (webhook := await getCore().getWebhook(webhook)): +@webhooks.post("//", qs_cls=WebhookMessageCreateQuery) +async def post_webhook_message(query_args: WebhookMessageCreateQuery, webhook_id: int, token: str): + if (webhook := await Webhook.Y.get(webhook_id)) is None: raise UnknownWebhook if webhook.token != token: raise MissingPermissions diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index d3917a0..c4d29d7 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -33,7 +33,7 @@ from ..yepcord.enums import MessageType from ..yepcord.errors import Errors, InvalidDataErr, UnknownChannel, UnknownMessage, InvalidFormBody, \ FileExceedsMaxSize, CannotSendEmptyMessage -from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message +from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message, Sticker from ..yepcord.snowflake import Snowflake if TYPE_CHECKING: # pragma: no cover @@ -188,7 +188,7 @@ def insertError(error): async def process_stickers(sticker_ids: list[int]): - stickers = [await getCore().getSticker(sticker_id) for sticker_id in sticker_ids] + stickers = await Sticker.filter(id__in=sticker_ids).select_related("guild") stickers_data = {"sticker_items": [], "stickers": []} for sticker in stickers: if sticker is None: diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index bd486c0..c28ea0b 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -32,8 +32,7 @@ from .config import Config from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors, InvalidKey -from .models import User, Channel, Message, ReadState, Emoji, Invite, Guild, GuildMember, GuildTemplate, Sticker, \ - Webhook, Role, GuildEvent, ThreadMember +from .models import User, Channel, Message, ReadState, Invite, Guild, GuildMember, ThreadMember from .storage import getStorage from .utils import b64encode, b64decode @@ -125,15 +124,6 @@ async def getReadStatesJ(self, user: User) -> list: states.append(await st.ds_json()) return states - async def getUserByChannel(self, channel: Channel, user_id: int) -> Optional[User]: - if channel.type in (ChannelType.DM, ChannelType.GROUP_DM): - if await Channel.exists(id=channel.id, recipients__id__in=[user_id]): - return await User.y.get(user_id) - elif channel.type in GUILD_CHANNELS: - return await self.getGuildMember(channel.guild, user_id) - elif channel.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): - return await self.getThreadMember(channel, user_id) - async def sendVerificationEmail(self, user: User) -> None: key = new(self.key, str(user.id).encode('utf-8'), sha256).digest() t = int(time()) @@ -193,16 +183,6 @@ async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[l count = await query.count() return messages, count - async def getRole(self, role_id: int, guild: Optional[Guild] = None) -> Role: - q = {"id": role_id} - if guild is not None: - q["guild"] = guild - return await Role.get_or_none(**q).select_related("guild") - - async def getGuildMember(self, guild: Guild, user_id: int) -> Optional[GuildMember]: - return await GuildMember.get_or_none(guild=guild, user__id=user_id).select_related("user", "guild", - "guild__owner") - async def getGuildMembers(self, guild: Guild) -> list[GuildMember]: return await GuildMember.filter(guild=guild).select_related("user") @@ -212,21 +192,6 @@ async def getGuildMemberCount(self, guild: Guild) -> int: async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") - async def getEmoji(self, emoji_id: int) -> Optional[Emoji]: - return await Emoji.get_or_none(id=emoji_id).select_related("guild") - - async def getEmojiByReaction(self, reaction: str) -> Optional[Emoji]: - try: - name, emoji_id = reaction.split(":") - emoji_id = int(emoji_id) - if "~" in name: - name = name.split("~")[0] - except ValueError: - return - if not (emoji := await self.getEmoji(emoji_id)): - return - return emoji if emoji.name == name else None - async def bulkDeleteGuildMessagesFromBanned( self, guild: Guild, user_id: int, after_id: int ) -> dict[Channel, list[int]]: @@ -273,33 +238,12 @@ async def getVanityCodeInvite(self, code: str) -> Optional[Invite]: if code is None: return return await Invite.get_or_none(vanity_code=code) - async def getGuildTemplate(self, guild: Guild) -> Optional[GuildTemplate]: - return await GuildTemplate.get_or_none(guild=guild).select_related("creator", "guild") - - async def getGuildTemplateById(self, template_id: int, guild: Optional[Guild] = None) -> Optional[GuildTemplate]: - q = {"id": template_id} - if guild is not None: - q["guild"] = guild - return await GuildTemplate.get_or_none(**q).select_related("guild", "creator") - async def setTemplateDirty(self, guild: Guild) -> None: - if not (template := await self.getGuildTemplate(guild)): + if (template := await guild.get_template()) is None: return template.is_dirty = True await template.save(update_fields=["is_dirty"]) - async def getWebhook(self, webhook_id: int) -> Optional[Webhook]: - return await Webhook.get_or_none(id=webhook_id).select_related("channel", "channel__guild", "user") - - async def getSticker(self, sticker_id: int) -> Optional[Sticker]: - return await Sticker.get_or_none(id=sticker_id).select_related("guild", "user") - - async def getGuildEvent(self, event_id: int) -> Optional[GuildEvent]: - return await GuildEvent.get_or_none(id=event_id).select_related("channel", "guild", "creator") - - async def getThreadMember(self, thread: Channel, user_id: int) -> Optional[ThreadMember]: - return await ThreadMember.get_or_none(channel=thread, user__id=user_id) - def getLanguageCode(self, ip: str, default: str = "en-US") -> str: if self.ipdb is None and not os.path.exists("other/ip_database.mmdb"): return default diff --git a/yepcord/yepcord/gateway_dispatcher.py b/yepcord/yepcord/gateway_dispatcher.py index fa7a13e..7e08041 100644 --- a/yepcord/yepcord/gateway_dispatcher.py +++ b/yepcord/yepcord/gateway_dispatcher.py @@ -144,7 +144,7 @@ async def getChannelFilter(self, channel: Channel, permissions: int = 0) -> dict roles[role_id] &= ~overwrite.deny roles[role_id] |= overwrite.allow else: - if not (member := await ctx.getCore().getGuildMember(channel.guild, overwrite.target_user.id)): + if not (member := await channel.guild.get_member(overwrite.target_user.id)): continue try: await member.checkPermission(permissions, channel=channel) diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 311a5c7..4b8c397 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -26,7 +26,7 @@ from tortoise.functions import Count from ..ctx import getCore, getGw -from ..enums import ChannelType +from ..enums import ChannelType, GUILD_CHANNELS import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model from ..snowflake import Snowflake @@ -222,7 +222,7 @@ async def ds_json(self, user_id: int=None, with_ids: bool=True) -> dict: for member in await self.get_thread_members() ], } - if user_id and (member := await getCore().getThreadMember(self, user_id)) is not None: + if user_id and (member := await self.get_thread_member(user_id)) is not None: data["member"] = { "muted": False, "mute_config": None, @@ -296,3 +296,18 @@ async def get_permission_overwrites( query = query.filter(Q(target_role__id__in=role_ids) | Q(target_user__id=target.user.id)).order_by("type") return await query + + async def user_can_access(self, user_id: int) -> bool: + if self.type in (ChannelType.DM, ChannelType.GROUP_DM): + return await self.recipients.filter(id=user_id).exists() + if self.type in GUILD_CHANNELS: + return await models.GuildMember.exists(guild=self.guild, user__id=user_id) + if self.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): + return await models.ThreadMember.exists(channel=self, user__id=user_id) + + return False + + async def get_thread_member(self, user_id: int) -> Optional[models.ThreadMember]: + if self.type not in {ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD}: + return + return await models.ThreadMember.get_or_none(channel=self, user__id=user_id) diff --git a/yepcord/yepcord/models/emoji.py b/yepcord/yepcord/models/emoji.py index 8d4be79..1944f3e 100644 --- a/yepcord/yepcord/models/emoji.py +++ b/yepcord/yepcord/models/emoji.py @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ - +from __future__ import annotations from typing import Optional from tortoise import fields @@ -24,7 +24,23 @@ from ._utils import SnowflakeField, Model +class EmojiUtils: + @staticmethod + async def get_by_reaction(reaction: str) -> Optional[models.Emoji]: + try: + name, emoji_id = reaction.split(":") + emoji_id = int(emoji_id) + if "~" in name: + name = name.split("~")[0] + except ValueError: + return + + return await Emoji.get_or_none(id=emoji_id, name=name).select_related("guild") + + class Emoji(Model): + Y = EmojiUtils + id: int = SnowflakeField(pk=True) name: str = fields.CharField(max_length=64) user: Optional[models.User] = fields.ForeignKeyField("models.User", on_delete=fields.SET_NULL, null=True) diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index 48f9f03..fb8a30e 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -231,7 +231,7 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo data.update(props) if for_gateway or user_id: - member = await getCore().getGuildMember(self, user_id) + member = await self.get_member(user_id) data["joined_at"] = member.joined_at.strftime("%Y-%m-%dT%H:%M:%S.000000+00:00") data["threads"] = [ thread.ds_json() @@ -269,3 +269,22 @@ async def get_stickers(self) -> list[models.Sticker]: async def get_events(self) -> list[models.GuildEvent]: return await models.GuildEvent.filter(guild=self).select_related("channel", "guild", "creator") + + async def get_member(self, user_id: int) -> Optional[models.GuildMember]: + return await models.GuildMember.get_or_none( + guild=self, user__id=user_id, + ).select_related("user", "guild", "guild__owner") + + async def get_template(self) -> Optional[models.GuildTemplate]: + return await models.GuildTemplate.get_or_none(guild=self).select_related("creator", "guild") + + async def get_role(self, role_id: int) -> Optional[models.Role]: + return await models.Role.get_or_none(id=role_id, guild=self).select_related("guild") + + async def get_sticker(self, sticker_id: int) -> Optional[models.Sticker]: + return await models.Sticker.get_or_none(id=sticker_id, guild=self).select_related("guild", "user") + + async def get_scheduled_event(self, event_id: int) -> Optional[models.GuildEvent]: + return await models.GuildEvent.get_or_none( + id=event_id, guild=self + ).select_related("channel", "guild", "creator") diff --git a/yepcord/yepcord/models/interaction.py b/yepcord/yepcord/models/interaction.py index d74a75a..0b76e28 100644 --- a/yepcord/yepcord/models/interaction.py +++ b/yepcord/yepcord/models/interaction.py @@ -68,10 +68,10 @@ async def ds_json(self, with_user=False, with_token=False, resolved: dict = None if self.guild is not None: data["guild_id"] = str(self.guild.id) - member = await getCore().getGuildMember(self.guild, self.user.id) + member = await self.guild.get_member(self.user.id) data["member"] = await member.ds_json() - if (bot_member := await getCore().getGuildMember(self.guild, self.application.id)) is not None: + if (bot_member := await self.guild.get_member(self.application.id)) is not None: data["app_permissions"] = str(await bot_member.permissions) if self.channel is not None: diff --git a/yepcord/yepcord/models/message.py b/yepcord/yepcord/models/message.py index de971c1..f1b6a4f 100644 --- a/yepcord/yepcord/models/message.py +++ b/yepcord/yepcord/models/message.py @@ -116,12 +116,10 @@ async def ds_json(self, user_id: int = None, search: bool = False) -> dict: if ping.startswith("&"): data["mention_roles"].append(ping[1:]) continue - if not (member := await getCore().getUserByChannel(self.channel, int(ping))): + if not await self.channel.user_can_access(int(ping)): continue - if isinstance(member, (models.GuildMember, models.ThreadMember)): - member = member.user - mdata = await member.data - data["mentions"].append(mdata.ds_json) + pinged_data = await models.UserData.get(user__id=int(ping)).select_related("user") + data["mentions"].append(pinged_data.ds_json) if self.type in (MessageType.RECIPIENT_ADD, MessageType.RECIPIENT_REMOVE): if (userid := self.extra_data.get("user")) \ and (udata := await models.UserData.get_or_none(id=userid).select_related("user")): diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 5104a9c..4292dd2 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -177,7 +177,7 @@ async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, } } if guild_id and (guild := await getCore().getGuild(guild_id)): - if member := await getCore().getGuildMember(guild, self.id): + if member := await guild.get_member(self.id): data["guild_member_profile"] = {"guild_id": str(guild_id)} data["guild_member"] = await member.ds_json() if mutual_friends_count: diff --git a/yepcord/yepcord/models/webhook.py b/yepcord/yepcord/models/webhook.py index ab6ebb5..9548551 100644 --- a/yepcord/yepcord/models/webhook.py +++ b/yepcord/yepcord/models/webhook.py @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ - +from __future__ import annotations from typing import Optional from tortoise import fields @@ -24,7 +24,15 @@ from ._utils import SnowflakeField, Model +class WebhookUtils: + @staticmethod + async def get(webhook_id: int) -> Optional[models.Webhook]: + return await Webhook.get_or_none(id=webhook_id).select_related("channel", "channel__guild", "user") + + class Webhook(Model): + Y = WebhookUtils + id: int = SnowflakeField(pk=True) type: int = fields.IntField() name: str = fields.CharField(max_length=128) From 832cbb0459e660614fd240a3c5a20dcedfed351b Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 7 Sep 2024 18:45:34 +0300 Subject: [PATCH 10/26] move more guild methods --- yepcord/gateway/gateway.py | 6 +-- yepcord/rest_api/dependencies.py | 2 +- yepcord/rest_api/routes/channels.py | 10 ++--- yepcord/rest_api/routes/guilds.py | 40 ++++++++++------- yepcord/rest_api/routes/users_me.py | 2 +- yepcord/yepcord/core.py | 69 +---------------------------- yepcord/yepcord/models/channel.py | 33 ++++++++++++++ yepcord/yepcord/models/guild.py | 36 ++++++++++++--- yepcord/yepcord/models/invite.py | 17 +++++-- 9 files changed, 112 insertions(+), 103 deletions(-) diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index d126b1f..f663420 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -140,14 +140,14 @@ async def handle_STATUS(self, data: dict) -> None: await self.gateway.ev.presence_update(self.user_id, presence) @require_auth - async def handle_LAZY_REQUEST(self, data: dict) -> None: + async def handle_LAZY_REQUEST(self, data: dict) -> None: # TODO: handle ranges if not (guild_id := int(data.get("guild_id"))): return if not data.get("members", True): return guild = await getCore().getGuild(guild_id) if not await GuildMember.exists(guild=guild, user__id=self.user_id): return - members = await getCore().getGuildMembers(guild) + members = await GuildMember.filter(guild=guild).select_related("user") statuses = {} for member in members: if presence := await self.gateway.presences.get(member.user.id): @@ -156,7 +156,7 @@ async def handle_LAZY_REQUEST(self, data: dict) -> None: statuses[member.user.id] = Presence(member.user.id, "offline", None) await self.esend(GuildMembersListUpdateEvent( members, - await getCore().getGuildMemberCount(guild), + await guild.get_member_count(), statuses, guild_id )) diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index 2478d07..89bfd82 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -130,7 +130,7 @@ async def depInvite(invite: Optional[str] = None) -> Invite: if not invite: raise ValueError except ValueError: - if not (invite := await getCore().getVanityCodeInvite(invite)): + if not (invite := await Invite.Y.get_from_vanity_code(invite)): raise UnknownInvite return invite diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 265a9d5..a055368 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -109,7 +109,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=channel.guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(channel.guild) + await channel.guild.set_template_dirty() return await channel.ds_json() @@ -151,7 +151,7 @@ async def delete_channel(user: User = DepUser, channel: Channel = DepChannel): await channel.delete() await getGw().dispatch(ChannelDeleteEvent(await channel.ds_json()), guild_id=channel.guild.id) - await getCore().setTemplateDirty(channel.guild) + await channel.guild.set_template_dirty() return await channel.ds_json() return "", 204 @@ -433,7 +433,7 @@ async def search_messages(query_args: SearchQuery, user: User = DepUser, channel member = await channel.guild.get_member(user.id) await member.checkPermission(GuildPermissions.READ_MESSAGE_HISTORY, GuildPermissions.VIEW_CHANNEL, channel=channel) - messages, total = await getCore().searchMessages(channel, query_args.model_dump(exclude_defaults=True)) + messages, total = await channel.search_messages(query_args.model_dump(exclude_defaults=True)) messages = [[await message.ds_json(search=True)] for message in messages] for message in messages: message[0]["hit"] = True @@ -482,7 +482,7 @@ async def create_or_update_permission_overwrite(data: PermissionOverwriteModel, await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=channel.guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(channel.guild) + await channel.guild.set_template_dirty() return "", 204 @@ -506,7 +506,7 @@ async def delete_permission_overwrite(target_id: int, user: User = DepUser, chan await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=channel.guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(channel.guild) + await channel.guild.set_template_dirty() return "", 204 diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index b5be128..df36bad 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -63,11 +63,15 @@ async def create_guild(data: GuildCreate, user: User = DepUser): await guild.save(update_fields=["icon"]) await getGw().dispatch(GuildCreateEvent( - await guild.ds_json(user_id=user.id, with_members=True, with_channels=True) + await guild.ds_json( + user_id=user.id, + with_channels=True, + with_member=await GuildMember.get(guild=guild, user=user).select_related("user"), + ) ), user_ids=[user.id]) await getGw().dispatchSub([user.id], guild.id) - return await guild.ds_json(user_id=user.id, with_members=False, with_channels=True) + return await guild.ds_json(user_id=user.id, with_channels=True) @guilds.patch("/", body_cls=GuildUpdate, allow_bots=True) @@ -103,7 +107,7 @@ async def update_guild(data: GuildUpdate, user: User = DepUser, guild: Guild = D await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return await guild.ds_json(user_id=user.id) @@ -231,7 +235,7 @@ async def update_channels_positions(guild: Guild = DepGuild, member: GuildMember change = change.model_dump(exclude_defaults=True, exclude={"id"}) await channel.update(**change) await getGw().dispatch(ChannelUpdateEvent(await channel.ds_json()), guild_id=channel.guild.id) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return "", 204 @@ -257,7 +261,7 @@ async def create_channel(data: ChannelCreate, user: User = DepUser, guild: Guild await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return await channel.ds_json() @@ -355,7 +359,7 @@ async def ban_member(user_id: int, data: BanMember, user: User = DepUser, guild: permissions=GuildPermissions.BAN_MEMBERS) if (delete_message_seconds := data.delete_message_seconds) > 0: after = Snowflake.fromTimestamp(int(time() - delete_message_seconds)) - deleted_messages = await getCore().bulkDeleteGuildMessagesFromBanned(guild, user_id, after) + deleted_messages = await guild.bulk_delete_messages_from_banned(user_id, after) for channel, messages in deleted_messages.items(): if len(messages) > 1: await getGw().dispatch(MessageBulkDeleteEvent(guild.id, channel.id, messages), channel=channel, @@ -429,7 +433,7 @@ async def create_role(data: RoleCreate, user: User = DepUser, guild: Guild = Dep await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return role.ds_json() @@ -455,7 +459,7 @@ async def update_role(data: RoleUpdate, user: User = DepUser, guild: Guild = Dep await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return role.ds_json() @@ -485,7 +489,7 @@ async def update_roles_positions(guild: Guild = DepGuild, member: GuildMember = await getGw().dispatch(GuildRoleUpdateEvent(guild.id, role.ds_json()), guild_id=guild.id, permissions=GuildPermissions.MANAGE_ROLES) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() return [ role.ds_json() @@ -507,7 +511,7 @@ async def delete_role(user: User = DepUser, guild: Guild = DepGuild, member: Gui await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, permissions=GuildPermissions.VIEW_AUDIT_LOG) - await getCore().setTemplateDirty(guild) + await guild.set_template_dirty() await getGw().dispatchUnsub([], role_id=role.id, delete=True) @@ -613,7 +617,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs async def get_vanity_url(guild: Guild = DepGuild, member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) code = {"code": guild.vanity_url_code} - if invite := await getCore().getVanityCodeInvite(guild.vanity_url_code): + if invite := await Invite.Y.get_from_vanity_code(guild.vanity_url_code): code["uses"] = invite.uses return code @@ -627,14 +631,14 @@ async def update_vanity_url(data: SetVanityUrl, user: User = DepUser, guild: Gui if data.code == guild.vanity_url_code: return {"code": guild.vanity_url_code} if not data.code: - if invite := await getCore().getVanityCodeInvite(guild.vanity_url_code): + if invite := await Invite.Y.get_from_vanity_code(guild.vanity_url_code): await invite.delete() guild.vanity_url_code = None await guild.save(update_fields=["vanity_url_code"]) else: - if await getCore().getVanityCodeInvite(data.code): + if await Invite.exists(vanity_code=data.code): return {"code": guild.vanity_url_code} - if guild.vanity_url_code and (invite := await getCore().getVanityCodeInvite(guild.vanity_url_code)): + if guild.vanity_url_code and (invite := await Invite.Y.get_from_vanity_code(guild.vanity_url_code)): await invite.delete() guild.vanity_url_code = data.code await guild.save(update_fields=["vanity_url_code"]) @@ -688,11 +692,15 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use await guild.save(update_fields=["icon"]) await getGw().dispatch(GuildCreateEvent( - await guild.ds_json(user_id=user.id, with_members=True, with_channels=True) + await guild.ds_json( + user_id=user.id, + with_channels=True, + with_member=await GuildMember.get(guild=guild, user=user).select_related("user"), + ) ), user_ids=[user.id]) await getGw().dispatchSub([user.id], guild.id) - return await guild.ds_json(user_id=user.id, with_members=False, with_channels=True) + return await guild.ds_json(user_id=user.id, with_channels=True) @guilds.post("//delete", body_cls=GuildDelete, allow_bots=True) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 94789a4..5c6d3ac 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -532,7 +532,7 @@ async def get_guilds(user: User = DepUser): async def ds_json(guild: Guild) -> dict: member = await guild.get_member(user.id) return { - "approximate_member_count": await getCore().getGuildMemberCount(guild), + "approximate_member_count": await guild.get_member_count(), "approximate_presence_count": 0, "features": ["ANIMATED_ICON", "BANNER", "INVITE_SPLASH", "VANITY_URL", "PREMIUM_TIER_3_OVERRIDE", "ROLE_ICONS", *guild.features], diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index c28ea0b..d8742b6 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -30,9 +30,8 @@ from .classes.other import EmailMsg, JWT, MFA from .classes.singleton import Singleton from .config import Config -from .enums import ChannelType, GUILD_CHANNELS from .errors import InvalidDataErr, Errors, InvalidKey -from .models import User, Channel, Message, ReadState, Invite, Guild, GuildMember, ThreadMember +from .models import User, Channel, ReadState, Guild, GuildMember from .storage import getStorage from .utils import b64encode, b64decode @@ -101,14 +100,6 @@ async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> # return message - async def getRelatedUsersToChannelCount(self, channel: Channel) -> int: - if channel.type in [ChannelType.DM, ChannelType.GROUP_DM]: - return await channel.recipients.filter().count() - elif channel.type in GUILD_CHANNELS: - return await GuildMember.filter(guild=channel.guild).count() - elif channel.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): - return await ThreadMember.filter(channel=channel).count() - async def setReadState(self, user: User, channel: Channel, count: int, last: int) -> None: read_state, _ = await ReadState.get_or_create( user=user, channel=channel, defaults={"last_read_id": last, "count": count} @@ -158,57 +149,9 @@ async def mfaNonceToCode(self, nonce: str) -> Optional[str]: signature = token.split(".")[2] return signature.replace("-", "").replace("_", "")[:8].upper() - async def searchMessages(self, channel: Channel, search_filter: dict) -> tuple[list[Message], int]: - filter_args = {"channel": channel} - if "author_id" in search_filter: - filter_args["author__id"] = search_filter["author_id"] - if "mentions" in search_filter: - filter_args["content__contains"] = f"<@{search_filter['mentions']}>" - if "has" in search_filter: - ... # TODO: add `has` filter - if "min_id" in search_filter: - filter_args["id__gt"] = search_filter["min_id"] - if "max_id" in search_filter: - filter_args["id__lt"] = search_filter["max_id"] - if "pinned" in search_filter: - filter_args["pinned"] = search_filter["pinned"].lower() == "true" - if "content" in search_filter: - filter_args["content__icontains"] = search_filter["content"] - query = (Message.filter(**filter_args).select_related("author", "channel", "thread", "guild") - .order_by("-id").limit(25)) - if "offset" in search_filter: - query = query.offset(search_filter["offset"]) - - messages = await query - count = await query.count() - return messages, count - - async def getGuildMembers(self, guild: Guild) -> list[GuildMember]: - return await GuildMember.filter(guild=guild).select_related("user") - - async def getGuildMemberCount(self, guild: Guild) -> int: - return await GuildMember.filter(guild=guild).count() - async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") - async def bulkDeleteGuildMessagesFromBanned( - self, guild: Guild, user_id: int, after_id: int - ) -> dict[Channel, list[int]]: - messages = await (Message.filter(guild=guild, author__id=user_id, id__gt=after_id).select_related("channel") - .limit(500)) - result = {} - messages_ids = [] - for message in messages: - if message.channel not in result: - result[message.channel] = [] - result[message.channel].append(message.id) - messages_ids.append(message.id) - - await Message.filter(id__in=messages_ids).delete() - - return result - async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[str, str]]: user_guilds_member = await GuildMember.filter(user=user).select_related("guild") user_guild_ids = [member.guild.id for member in user_guilds_member] @@ -234,16 +177,6 @@ async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) ).select_related("user").limit(limit) - async def getVanityCodeInvite(self, code: str) -> Optional[Invite]: - if code is None: return - return await Invite.get_or_none(vanity_code=code) - - async def setTemplateDirty(self, guild: Guild) -> None: - if (template := await guild.get_template()) is None: - return - template.is_dirty = True - await template.save(update_fields=["is_dirty"]) - def getLanguageCode(self, ip: str, default: str = "en-US") -> str: if self.ipdb is None and not os.path.exists("other/ip_database.mmdb"): return default diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 4b8c397..5139708 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -307,7 +307,40 @@ async def user_can_access(self, user_id: int) -> bool: return False + async def get_related_users_count(self) -> int: + if self.type in [ChannelType.DM, ChannelType.GROUP_DM]: + return await self.recipients.filter().count() + elif self.type in GUILD_CHANNELS: + return await models.GuildMember.filter(guild=self.guild).count() + elif self.type in (ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD): + return await models.ThreadMember.filter(channel=self).count() + async def get_thread_member(self, user_id: int) -> Optional[models.ThreadMember]: if self.type not in {ChannelType.GUILD_PUBLIC_THREAD, ChannelType.GUILD_PRIVATE_THREAD}: return return await models.ThreadMember.get_or_none(channel=self, user__id=user_id) + + async def search_messages(self, search_filter: dict) -> tuple[list[models.Message], int]: + filter_args = {"channel": self} + if "author_id" in search_filter: + filter_args["author__id"] = search_filter["author_id"] + if "mentions" in search_filter: + filter_args["content__contains"] = f"<@{search_filter['mentions']}>" + if "has" in search_filter: + ... # TODO: add `has` filter + if "min_id" in search_filter: + filter_args["id__gt"] = search_filter["min_id"] + if "max_id" in search_filter: + filter_args["id__lt"] = search_filter["max_id"] + if "pinned" in search_filter: + filter_args["pinned"] = search_filter["pinned"].lower() == "true" + if "content" in search_filter: + filter_args["content__icontains"] = search_filter["content"] + query = (models.Message.filter(**filter_args).select_related("author", "channel", "thread", "guild") + .order_by("-id").limit(25)) + if "offset" in search_filter: + query = query.offset(search_filter["offset"]) + + messages = await query + count = await query.count() + return messages, count diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index fb8a30e..6c8e083 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -151,8 +151,10 @@ class Guild(Model): nsfw: bool = fields.BooleanField(default=False) nsfw_level: int = fields.IntField(default=0) - async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: bool=False, - with_channels: bool=False) -> dict: + async def ds_json( + self, user_id: int, for_gateway: bool = False, with_member: Optional[models.GuildMember] = None, + with_channels: bool = False + ) -> dict: data = { "id": str(self.id), "version": int(time() * 1000), # What is this? @@ -210,7 +212,7 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo "widget_channel_id": None, }, "premium_subscription_count": 30, - "member_count": await getCore().getGuildMemberCount(self), + "member_count": await self.get_member_count(), "lazy": True, "large": False, "guild_scheduled_events": [ @@ -241,8 +243,8 @@ async def ds_json(self, user_id: int, for_gateway: bool=False, with_members: boo ] if for_gateway or with_channels: data["channels"] = [await channel.ds_json() for channel in await self.get_channels()] - if with_members: - data["members"] = [await member.ds_json() for member in await getCore().getGuildMembers(self)] + if with_member is not None: + data["members"] = [await with_member.ds_json()] return data @@ -288,3 +290,27 @@ async def get_scheduled_event(self, event_id: int) -> Optional[models.GuildEvent return await models.GuildEvent.get_or_none( id=event_id, guild=self ).select_related("channel", "guild", "creator") + + async def get_member_count(self) -> int: + return await models.GuildMember.filter(guild=self).count() + + async def bulk_delete_messages_from_banned( + self, user_id: int, after_message_id: int + ) -> dict[models.Channel, list[int]]: + messages = await models.Message.filter( + guild=self, author__id=user_id, id__gt=after_message_id + ).select_related("channel").limit(500) + result = {} + messages_ids = [] + for message in messages: + if message.channel not in result: + result[message.channel] = [] + result[message.channel].append(message.id) + messages_ids.append(message.id) + + await models.Message.filter(id__in=messages_ids).delete() + + return result + + async def set_template_dirty(self) -> None: + await models.GuildTemplate.filter(guild=self).update(is_dirty=True) diff --git a/yepcord/yepcord/models/invite.py b/yepcord/yepcord/models/invite.py index d38a0b8..cc202ab 100644 --- a/yepcord/yepcord/models/invite.py +++ b/yepcord/yepcord/models/invite.py @@ -16,20 +16,29 @@ along with this program. If not, see . """ +from __future__ import annotations + from datetime import datetime from typing import Optional from tortoise import fields -from ..ctx import getCore -from ..enums import ChannelType +import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model +from ..enums import ChannelType from ..snowflake import Snowflake from ..utils import b64encode, int_size -import yepcord.yepcord.models as models + + +class InviteUtils: + @staticmethod + async def get_from_vanity_code(code: str) -> Optional[Invite]: + return await Invite.get_or_none(vanity_code=code) if code else None class Invite(Model): + Y = InviteUtils + id: int = SnowflakeField(pk=True) type: int = fields.IntField(default=1) channel: models.Channel = fields.ForeignKeyField("models.Channel") @@ -67,7 +76,7 @@ async def ds_json(self, with_counts: bool=False) -> dict: } if with_counts: - data["approximate_member_count"] = await getCore().getRelatedUsersToChannelCount(self.channel) + data["approximate_member_count"] = await self.channel.get_related_users_count() if self.channel.type == ChannelType.GROUP_DM: data["channel"]["recipients"] = [ {"username": (await recipient.data).username} From 42f55e1ae5f2499b765e8b28df5681b1ff793df4 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Thu, 12 Sep 2024 16:57:36 +0300 Subject: [PATCH 11/26] move more guild methods; update mfa methods; add __slots__ to events.py --- tests/api/test_mfa.py | 2 +- tests/test_core.py | 10 +-- yepcord/gateway/events.py | 96 ++++++++++++++++++++++++++++- yepcord/gateway/gateway.py | 9 ++- yepcord/rest_api/routes/users_me.py | 4 +- yepcord/yepcord/core.py | 69 ++++++++------------- yepcord/yepcord/enums.py | 5 ++ yepcord/yepcord/models/guild.py | 1 - yepcord/yepcord/models/user.py | 34 +++++----- yepcord/yepcord/models/userdata.py | 10 +-- 10 files changed, 163 insertions(+), 77 deletions(-) diff --git a/tests/api/test_mfa.py b/tests/api/test_mfa.py index df7cb2e..fbef229 100644 --- a/tests/api/test_mfa.py +++ b/tests/api/test_mfa.py @@ -23,7 +23,7 @@ async def setup_db(): def generateMfaVerificationKey(nonce: str, mfa_key: str, key: bytes): if not (payload := JWT.decode(nonce, key + b64decode(mfa_key))): return - token = JWT.encode({"code": payload["code"]}, key) + token = JWT.encode({"code": payload["c"]}, key) signature = token.split(".")[2] return signature.replace("-", "").replace("_", "")[:8].upper() diff --git a/tests/test_core.py b/tests/test_core.py index 29e749b..b0461f5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -28,13 +28,13 @@ from yepcord.yepcord.config import Config, ConfigModel from yepcord.yepcord.core import Core -from yepcord.yepcord.enums import UserFlags as UserFlagsE, RelationshipType, ChannelType, GuildPermissions +from yepcord.yepcord.enums import UserFlags as UserFlagsE, RelationshipType, ChannelType, GuildPermissions, MfaNonceType from yepcord.yepcord.errors import InvalidDataErr, MfaRequiredErr from yepcord.yepcord.gateway_dispatcher import GatewayDispatcher from yepcord.yepcord.models import User, UserData, Session, Relationship, Guild, Channel, Role, PermissionOverwrite, \ GuildMember, Message from yepcord.yepcord.snowflake import Snowflake -from yepcord.yepcord.utils import b64decode, b64encode +from yepcord.yepcord.utils import b64encode EMAIL_ID = Snowflake.makeId() VARS = { @@ -298,9 +298,9 @@ async def test_generateUserMfaNonce(testCore: Coroutine[Any, Any, Core]): async def test_verifyUserMfaNonce(): user = await User.y.get(VARS["user_id"]) nonce, regenerate_nonce = VARS["mfa_nonce"] - await core.verifyUserMfaNonce(user, nonce, False) - await core.verifyUserMfaNonce(user, regenerate_nonce, True) - for args in ((nonce, True), (regenerate_nonce, False)): + await core.verifyUserMfaNonce(user, nonce, MfaNonceType.NORMAL) + await core.verifyUserMfaNonce(user, regenerate_nonce, MfaNonceType.REGENERATE) + for args in ((nonce, MfaNonceType.REGENERATE), (regenerate_nonce, MfaNonceType.NORMAL)): with pt.raises(InvalidDataErr): await core.verifyUserMfaNonce(user, *args) del VARS["mfa_nonce"] diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 9d2b6ff..c85c818 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -20,7 +20,7 @@ from base64 import b64encode from time import time -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Optional from ..yepcord.config import Config from ..yepcord.ctx import getCore @@ -47,6 +47,8 @@ async def json(self) -> dict: ... class RawDispatchEvent(DispatchEvent): + __slots__ = ("data",) + def __init__(self, data: dict): self.data = data @@ -66,6 +68,8 @@ async def json(self) -> dict: class ReadyEvent(DispatchEvent): NAME = "READY" + __slots__ = ("user", "client",) + def __init__(self, user: User, client: GatewayClient): self.user = user self.client = client @@ -97,7 +101,7 @@ async def json(self) -> dict: "purchased_flags": 0, "nsfw_allowed": userdata.nsfw_allowed, "mobile": True, # TODO: check - "mfa_enabled": settings.mfa, + "mfa_enabled": bool(settings.mfa), "id": str(self.user.id), "flags": 0, }, @@ -177,6 +181,8 @@ async def json(self) -> dict: class ReadySupplementalEvent(DispatchEvent): NAME = "READY_SUPPLEMENTAL" + __slots__ = ("friends_presences", "guild_ids",) + def __init__(self, friends_presences: list[dict], guilds_ids: list[int]): self.friends_presences = friends_presences self.guilds_ids = guilds_ids @@ -200,6 +206,8 @@ async def json(self) -> dict: class RelationshipAddEvent(DispatchEvent): NAME = "RELATIONSHIP_ADD" + __slots__ = ("user_id", "userdata", "type",) + def __init__(self, user_id: int, userdata: UserData, type_: int): self.user_id = user_id self.userdata = userdata @@ -222,6 +230,8 @@ async def json(self) -> dict: class DMChannelCreateEvent(DispatchEvent): NAME = "CHANNEL_CREATE" + __slots__ = ("channel", "_channel_kwargs",) + def __init__(self, channel: Channel, *, channel_json_kwargs: dict=None): self.channel = channel self._channel_kwargs = {} if channel_json_kwargs is None else channel_json_kwargs @@ -243,6 +253,8 @@ class DMChannelUpdateEvent(DMChannelCreateEvent): class RelationshipRemoveEvent(DispatchEvent): NAME = "RELATIONSHIP_REMOVE" + __slots__ = ("user_id", "type",) + def __init__(self, user_id: int, type_: int): self.user_id = user_id self.type = type_ @@ -261,6 +273,8 @@ async def json(self) -> dict: class UserUpdateEvent(DispatchEvent): NAME = "USER_UPDATE" + __slots__ = ("user", "userdata", "settings",) + def __init__(self, user: User, userdata: UserData, settings: UserSettings): self.user = user self.userdata = userdata @@ -295,6 +309,8 @@ async def json(self) -> dict: class PresenceUpdateEvent(DispatchEvent): NAME = "PRESENCE_UPDATE" + __slots__ = ("userdata", "presence",) + def __init__(self, userdata: UserData, presence: Presence): self.userdata = userdata self.presence = presence @@ -317,6 +333,8 @@ async def json(self) -> dict: class MessageCreateEvent(DispatchEvent): NAME = "MESSAGE_CREATE" + __slots__ = ("message_obj",) + def __init__(self, message_obj: dict): self.message_obj = message_obj @@ -331,6 +349,8 @@ async def json(self) -> dict: class TypingEvent(DispatchEvent): NAME = "TYPING_START" + __slots__ = ("user_id", "channel_id",) + def __init__(self, user_id: int, channel_id: int): self.user_id = user_id self.channel_id = channel_id @@ -354,7 +374,9 @@ class MessageUpdateEvent(MessageCreateEvent): class MessageDeleteEvent(DispatchEvent): NAME = "MESSAGE_DELETE" - def __init__(self, message_id: int, channel_id: int, guild_id: int): + __slots__ = ("message_id", "channel_id", "guild_id",) + + def __init__(self, message_id: int, channel_id: int, guild_id: Optional[int]): self.message_id = message_id self.channel_id = channel_id self.guild_id = guild_id @@ -376,6 +398,8 @@ async def json(self) -> dict: class MessageAckEvent(DispatchEvent): NAME = "MESSAGE_ACK" + __slots__ = ("ack_object",) + def __init__(self, ack_object: dict): self.ack_object = ack_object @@ -390,6 +414,8 @@ async def json(self) -> dict: class ChannelRecipientAddEvent(DispatchEvent): NAME = "CHANNEL_RECIPIENT_ADD" + __slots__ = ("channel_id", "user_obj",) + def __init__(self, channel_id: int, user_obj: dict): self.channel_id = channel_id self.user_obj = user_obj @@ -412,6 +438,8 @@ class ChannelRecipientRemoveEvent(ChannelRecipientAddEvent): class DMChannelDeleteEvent(DispatchEvent): NAME = "CHANNEL_DELETE" + __slots__ = ("channel_obj",) + def __init__(self, channel_obj: dict): self.channel_obj = channel_obj @@ -426,6 +454,8 @@ async def json(self) -> dict: class ChannelPinsUpdateEvent(DispatchEvent): NAME = "CHANNEL_PINS_UPDATE" + __slots__ = ("channel_id", "last_pin_timestamp",) + def __init__(self, channel_id: int, last_pin_timestamp: str): self.channel_id = channel_id self.last_pin_timestamp = last_pin_timestamp @@ -444,6 +474,8 @@ async def json(self) -> dict: class MessageReactionAddEvent(DispatchEvent): NAME = "MESSAGE_REACTION_ADD" + __slots__ = ("user_id", "message_id", "channel_id", "emoji",) + def __init__(self, user_id: int, message_id: int, channel_id: int, emoji: dict): self.user_id = user_id self.message_id = message_id @@ -475,6 +507,8 @@ class MessageReactionRemoveEvent(MessageReactionAddEvent): class GuildCreateEvent(DispatchEvent): NAME = "GUILD_CREATE" + __slots__ = ("guild_obj",) + def __init__(self, guild_obj: dict): self.guild_obj = guild_obj @@ -498,6 +532,8 @@ class GuildUpdateEvent(GuildCreateEvent): class GuildDeleteEvent(DispatchEvent): NAME = "GUILD_DELETE" + __slots__ = ("guild_id",) + def __init__(self, guild_id: int): self.guild_id = guild_id @@ -514,6 +550,8 @@ async def json(self) -> dict: class GuildMembersListUpdateEvent(DispatchEvent): NAME = "GUILD_MEMBER_LIST_UPDATE" + __slots__ = ("members", "total_members", "statuses", "groups", "guild_id",) + def __init__(self, members: List[GuildMember], total_members: int, statuses: dict, guild_id: int): self.members = members self.total_members = total_members @@ -573,6 +611,8 @@ async def json(self) -> dict: class UserNoteUpdateEvent(DispatchEvent): NAME = "USER_NOTE_UPDATE" + __slots__ = ("user_id", "note",) + def __init__(self, user_id: int, note: str): self.user_id = user_id self.note = note @@ -591,6 +631,8 @@ async def json(self) -> dict: class UserSettingsProtoUpdateEvent(DispatchEvent): NAME = "USER_SETTINGS_PROTO_UPDATE" + __slots__ = ("proto", "type",) + def __init__(self, proto: str, type_: int): self.proto = proto self.type = type_ @@ -612,6 +654,8 @@ async def json(self) -> dict: class GuildEmojisUpdate(DispatchEvent): NAME = "GUILD_EMOJIS_UPDATE" + __slots__ = ("guild_id", "emojis",) + def __init__(self, guild_id: int, emojis: list[dict]): self.guild_id = guild_id self.emojis = emojis @@ -630,6 +674,8 @@ async def json(self) -> dict: class ChannelUpdateEvent(DispatchEvent): NAME = "CHANNEL_UPDATE" + __slots__ = ("channel_obj",) + def __init__(self, channel_obj: dict): self.channel_obj = channel_obj @@ -652,6 +698,8 @@ class ChannelDeleteEvent(ChannelUpdateEvent): class InviteDeleteEvent(DispatchEvent): NAME = "INVITE_DELETE" + __slots__ = ("invite",) + def __init__(self, invite: Invite): self.invite = invite @@ -671,6 +719,8 @@ async def json(self) -> dict: class GuildMemberRemoveEvent(DispatchEvent): NAME = "GUILD_MEMBER_REMOVE" + __slots__ = ("guild_id", "user_obj",) + def __init__(self, guild_id: int, user_obj: dict): self.guild_id = guild_id self.user_obj = user_obj @@ -690,6 +740,8 @@ async def json(self) -> dict: class GuildBanAddEvent(DispatchEvent): NAME = "GUILD_BAN_ADD" + __slots__ = ("guild_id", "user_obj",) + def __init__(self, guild_id: int, user_obj: dict): self.guild_id = guild_id self.user_obj = user_obj @@ -713,6 +765,8 @@ class GuildBanRemoveEvent(GuildBanAddEvent): class MessageBulkDeleteEvent(DispatchEvent): NAME = "MESSAGE_DELETE_BULK" + __slots__ = ("guild_id", "channel_id", "message_ids",) + def __init__(self, guild_id: int, channel_id: int, message_ids: list[int]): self.guild_id = guild_id self.channel_id = channel_id @@ -734,6 +788,8 @@ async def json(self) -> dict: class GuildRoleCreateEvent(DispatchEvent): NAME = "GUILD_ROLE_CREATE" + __slots__ = ("guild_id", "role_obj",) + def __init__(self, guild_id: int, role_obj: dict): self.guild_id = guild_id self.role_obj = role_obj @@ -757,6 +813,8 @@ class GuildRoleUpdateEvent(GuildRoleCreateEvent): class GuildRoleDeleteEvent(DispatchEvent): NAME = "GUILD_ROLE_DELETE" + __slots__ = ("guild_id", "role_id",) + def __init__(self, guild_id: int, role_id: int): self.guild_id = guild_id self.role_id = role_id @@ -776,6 +834,8 @@ async def json(self) -> dict: class GuildMemberUpdateEvent(DispatchEvent): NAME = "GUILD_MEMBER_UPDATE" + __slots__ = ("guild_id", "member_obj",) + def __init__(self, guild_id: int, member_obj: dict): self.guild_id = guild_id self.member_obj = member_obj @@ -793,6 +853,8 @@ async def json(self) -> dict: class GuildMembersChunkEvent(DispatchEvent): NAME = "GUILD_MEMBERS_CHUNK" + __slots__ = ("members", "presences", "guild_id",) + def __init__(self, members: List[GuildMember], presences: list, guild_id: int): self.members = members self.presences = presences @@ -816,6 +878,8 @@ async def json(self) -> dict: class GuildAuditLogEntryCreateEvent(DispatchEvent): NAME = "GUILD_AUDIT_LOG_ENTRY_CREATE" + __slots__ = ("entry_obj",) + def __init__(self, entry_obj: dict): self.entry_obj = entry_obj @@ -831,6 +895,8 @@ async def json(self) -> dict: class WebhooksUpdateEvent(DispatchEvent): NAME = "WEBHOOKS_UPDATE" + __slots__ = ("guild_id", "channel_id",) + def __init__(self, guild_id: int, channel_id: int): self.guild_id = guild_id self.channel_id = channel_id @@ -850,6 +916,8 @@ async def json(self) -> dict: class StickersUpdateEvent(DispatchEvent): NAME = "GUILD_STICKERS_UPDATE" + __slots__ = ("guild_id", "stickers",) + def __init__(self, guild_id: int, stickers: list[dict]): self.guild_id = guild_id self.stickers = stickers @@ -869,6 +937,8 @@ async def json(self) -> dict: class UserDeleteEvent(DispatchEvent): NAME = "USER_DELETE" + __slots__ = ("user_id",) + def __init__(self, user_id: int): self.user_id = user_id @@ -885,6 +955,8 @@ async def json(self) -> dict: class GuildScheduledEventCreateEvent(DispatchEvent): NAME = "GUILD_SCHEDULED_EVENT_CREATE" + __slots__ = ("event_obj",) + def __init__(self, event_obj: dict): self.event_obj = event_obj @@ -903,6 +975,8 @@ class GuildScheduledEventUpdateEvent(GuildScheduledEventCreateEvent): class ScheduledEventUserAddEvent(DispatchEvent): NAME = "GUILD_SCHEDULED_EVENT_USER_ADD" + __slots__ = ("user_id", "event_id", "guild_id",) + def __init__(self, user_id: int, event_id: int, guild_id: int): self.user_id = user_id self.event_id = event_id @@ -931,6 +1005,8 @@ class GuildScheduledEventDeleteEvent(GuildScheduledEventCreateEvent): class ThreadCreateEvent(DispatchEvent): NAME = "THREAD_CREATE" + __slots__ = ("thread_obj",) + def __init__(self, thread_obj: dict): self.thread_obj = thread_obj @@ -945,6 +1021,8 @@ async def json(self) -> dict: class ThreadMemberUpdateEvent(DispatchEvent): NAME = "THREAD_MEMBER_UPDATE" + __slots__ = ("member_obj",) + def __init__(self, member_obj: dict): self.member_obj = member_obj @@ -959,6 +1037,8 @@ async def json(self) -> dict: class IntegrationCreateEvent(DispatchEvent): NAME = "INTEGRATION_CREATE" + __slots__ = ("integration",) + def __init__(self, integration: Integration): self.integration = integration @@ -973,6 +1053,8 @@ async def json(self) -> dict: class IntegrationDeleteEvent(DispatchEvent): NAME = "INTEGRATION_DELETE" + __slots__ = ("guild_id", "application_id",) + def __init__(self, guild_id: int, application_id: int): self.guild_id = guild_id self.application_id = application_id @@ -992,6 +1074,8 @@ async def json(self) -> dict: class GuildIntegrationsUpdateEvent(DispatchEvent): NAME = "GUILD_INTEGRATIONS_UPDATE" + __slots__ = ("guild_id",) + def __init__(self, guild_id: int): self.guild_id = guild_id @@ -1008,6 +1092,8 @@ async def json(self) -> dict: class InteractionCreateEvent(DispatchEvent): NAME = "INTERACTION_CREATE" + __slots__ = ("interaction", "full", "interaction_kwargs",) + def __init__(self, interaction: Interaction, full: bool, **interaction_kwargs): self.interaction = interaction self.full = full @@ -1030,6 +1116,8 @@ async def json(self) -> dict: class InteractionSuccessEvent(DispatchEvent): NAME = "INTERACTION_SUCCESS" + __slots__ = ("interaction",) + def __init__(self, interaction: Interaction): self.interaction = interaction @@ -1051,6 +1139,8 @@ class InteractionFailureEvent(InteractionSuccessEvent): class UserConnectionsUpdate(DispatchEvent): NAME = "USER_CONNECTIONS_UPDATE" + __slots__ = ("connection",) + def __init__(self, connection: ConnectedAccount): self.connection = connection diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index f663420..b678e70 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -24,6 +24,7 @@ from quart import Websocket from redis.asyncio import Redis +from tortoise.expressions import Q from .events import * from .presences import Presences, Presence @@ -170,9 +171,15 @@ async def handle_GUILD_MEMBERS(self, data: dict) -> None: query = data.get("query", "") limit = data.get("limit", 100) + user_ids = data.get("user_ids", []) if limit > 100 or limit < 1: limit = 100 - members = await getCore().getGuildMembersGw(guild, query, limit, data.get("user_ids", [])) + + q = Q(guild=guild) & (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) + if user_ids: + q &= Q(user__id__in=user_ids) + members = await GuildMember.filter(q).select_related("user").limit(limit) + presences = [] # TODO: add presences await self.esend(GuildMembersChunkEvent(members, presences, guild_id)) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 5c6d3ac..b576271 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -32,7 +32,7 @@ from ...yepcord.classes.other import MFA from ...yepcord.config import Config from ...yepcord.ctx import getCore, getCDNStorage, getGw -from ...yepcord.enums import RelationshipType, ChannelType +from ...yepcord.enums import RelationshipType, ChannelType, MfaNonceType from ...yepcord.errors import InvalidDataErr, Errors, UnknownToken, UnknownUser, UnknownConnection, UnknownDiscordTag, \ AlreadyFriends, MalformedUserSettings, Already2Fa, PasswordDoesNotMatch, Invalid2FaSecret, Invalid2FaCode, \ NotYet2Fa, InvalidKey, InvalidGuild, InvalidRecipient, MustTransferGuildsBeforeDelete, MissingAccess @@ -332,7 +332,7 @@ async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): "code": "BASE_TYPE_REQUIRED", "message": "This field is required" }})) regenerate = data.regenerate - await getCore().verifyUserMfaNonce(user, nonce, regenerate) + await getCore().verifyUserMfaNonce(user, nonce, MfaNonceType.REGENERATE if regenerate else MfaNonceType.NORMAL) if await getCore().mfaNonceToCode(nonce) != key: raise InvalidKey diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index d8742b6..4d1f2d4 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -15,76 +15,66 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ + import os.path from hashlib import sha256 from hmac import new from json import loads as jloads, dumps as jdumps from os import urandom from time import time -from typing import Optional +from typing import Optional, Union import maxminddb -from tortoise.expressions import Q from . import ctx from .classes.other import EmailMsg, JWT, MFA from .classes.singleton import Singleton from .config import Config +from .enums import MfaNonceType from .errors import InvalidDataErr, Errors, InvalidKey from .models import User, Channel, ReadState, Guild, GuildMember from .storage import getStorage from .utils import b64encode, b64decode +def _assert(value: ..., exc: Union[type[BaseException], BaseException] = ValueError) -> None: + if not value: + raise exc + + # noinspection PyMethodMayBeStatic class Core(Singleton): def __init__(self): self.key = b64decode(Config.KEY) self.ipdb = None - def generateMfaTicketSignature(self, user: User, session_id: int) -> str: - payload = { - "user_id": user.id, - "session_id": session_id - } - token = JWT.encode(payload, self.key, time() + 300) - return b64encode(token) - - def verifyMfaTicketSignature(self, user: User, session_id: int, token: str) -> bool: - if not (payload := JWT.decode(token, self.key)): - return False - if payload["user_id"] != user.id: return False - if payload["session_id"] != session_id: return False - return True - async def getMfaFromTicket(self, ticket: str) -> Optional[MFA]: try: - uid, sid, sig = ticket.split(".") - uid = jloads(b64decode(uid).decode("utf8"))[0] - sid = int.from_bytes(b64decode(sid), "big") + user_id, session_id, sig = ticket.split(".") + user_id = jloads(b64decode(user_id).decode("utf8"))[0] + session_id = int.from_bytes(b64decode(session_id), "big") sig = b64decode(sig).decode("utf8") + + _assert(user := await User.y.get(user_id)) + _assert(payload := JWT.decode(sig, self.key)) + _assert(payload["u"] == user.id) + _assert(payload["s"] == session_id) except (ValueError, IndexError): return - if not (user := await User.y.get(uid)): - return - if not self.verifyMfaTicketSignature(user, sid, sig): - return - settings = await user.settings - return MFA(settings.mfa, uid) + + return MFA(await user.get_mfa_key(), user_id) async def generateUserMfaNonce(self, user: User) -> tuple[str, str]: exp = time() + 600 code = b64encode(urandom(16)) - nonce = JWT.encode({"type": "normal", "code": code, "user_id": user.id}, self.key, exp) - rnonce = JWT.encode({"type": "regenerate", "code": code, "user_id": user.id}, self.key, exp) + nonce = JWT.encode({"t": MfaNonceType.NORMAL, "c": code, "u": user.id}, self.key, exp) + rnonce = JWT.encode({"t": MfaNonceType.REGENERATE, "c": code, "u": user.id}, self.key, exp) return nonce, rnonce - async def verifyUserMfaNonce(self, user: User, nonce: str, regenerate: bool) -> None: - if not (payload := JWT.decode(nonce, self.key)) or payload["user_id"] != user.id: - raise InvalidKey - nonce_type = "normal" if not regenerate else "regenerate" - if nonce_type != payload["type"]: - raise InvalidKey + async def verifyUserMfaNonce(self, user: User, nonce: str, nonce_type: MfaNonceType) -> None: + _assert(payload := JWT.decode(nonce, self.key), InvalidKey) + _assert(payload["u"] == user.id, InvalidKey) + _assert(nonce_type == payload["t"], InvalidKey) #async def sendMessage(self, message: Message) -> Message: # async def _addToReadStates(): # TODO: recalculate read states when requested by user @@ -145,7 +135,7 @@ async def verifyEmail(self, user: User, token: str) -> None: async def mfaNonceToCode(self, nonce: str) -> Optional[str]: if not (payload := JWT.decode(nonce, self.key)): return - token = JWT.encode({"code": payload["code"]}, self.key) + token = JWT.encode({"code": payload["c"]}, self.key) signature = token.split(".")[2] return signature.replace("-", "").replace("_", "")[:8].upper() @@ -168,15 +158,6 @@ async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[st return mutual_guilds_json - # noinspection PyUnusedLocal - async def getGuildMembersGw(self, guild: Guild, query: str, limit: int, user_ids: list[int]) -> list[GuildMember]: - # noinspection PyUnresolvedReferences - return await GuildMember.filter( - Q(guild=guild) & - (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) #& - #((GuildMember.user.id in user_ids) if user_ids else (GuildMember.user.id not in [0])) - ).select_related("user").limit(limit) - def getLanguageCode(self, ip: str, default: str = "en-US") -> str: if self.ipdb is None and not os.path.exists("other/ip_database.mmdb"): return default diff --git a/yepcord/yepcord/enums.py b/yepcord/yepcord/enums.py index 96c3f50..63c3248 100644 --- a/yepcord/yepcord/enums.py +++ b/yepcord/yepcord/enums.py @@ -419,3 +419,8 @@ class InteractionCallbackType: APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8 MODAL = 9 PREMIUM_REQUIRED = 10 + + +class MfaNonceType(E): + NORMAL = 1 + REGENERATE = 2 diff --git a/yepcord/yepcord/models/guild.py b/yepcord/yepcord/models/guild.py index 6c8e083..dffb4d1 100644 --- a/yepcord/yepcord/models/guild.py +++ b/yepcord/yepcord/models/guild.py @@ -23,7 +23,6 @@ from tortoise import fields from tortoise.transactions import atomic -from ..ctx import getCore import yepcord.yepcord.models as models from ..enums import Locales, ChannelType from ._utils import SnowflakeField, Model, ChoicesValidator diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 4292dd2..f147414 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -21,7 +21,8 @@ from datetime import datetime from os import urandom from random import randint, choice -from typing import Optional +from time import time +from typing import Optional, cast from bcrypt import checkpw, hashpw, gensalt from tortoise import fields @@ -30,7 +31,7 @@ import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model -from ..classes.other import MFA +from ..classes.other import MFA, JWT from ..config import Config from ..ctx import getCore from ..enums import RelationshipType @@ -98,15 +99,17 @@ async def login(email: str, password: str) -> models.Session: "message": "Invalid login or password."}, "password": {"code": "INVALID_LOGIN", "message": "Invalid login or password."}})) - settings = await user.settings - if settings.mfa: - sid = urandom(12) - raise MfaRequiredErr( - user.id, - b64encode(sid), - getCore().generateMfaTicketSignature(user, int.from_bytes(sid, "big")), - ) - return await models.Session.Y.create(user) + mfa_key = await user.get_mfa_key() + if not mfa_key: + return await models.Session.Y.create(user) + + sid = urandom(12) + sid_int = int.from_bytes(sid, "big") + raise MfaRequiredErr( + user.id, + b64encode(sid), + b64encode(JWT.encode({"u": user.id, "s": sid_int}, getCore().key, time() + 300)), + ) @staticmethod async def get_free_discriminator(login: str) -> Optional[int]: @@ -144,10 +147,8 @@ def created_at(self) -> datetime: @property async def mfa(self) -> Optional[MFA]: - settings = await self.settings - mfa = MFA(settings.mfa, self.id) - if mfa.valid: - return mfa + mfa = MFA(await self.get_mfa_key(), self.id) + return mfa if mfa.valid else None async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, mutual_friends_count: bool = False, guild_id: int = None) -> dict: @@ -315,3 +316,6 @@ async def get_related_users(self) -> list[models.User]: users[recipient.id] = recipient return list(users.values()) + + async def get_mfa_key(self) -> str | None: + return cast(str, await models.UserSettings.get(user=self).values_list("mfa", flat=True)) diff --git a/yepcord/yepcord/models/userdata.py b/yepcord/yepcord/models/userdata.py index 23a61d4..c18158f 100644 --- a/yepcord/yepcord/models/userdata.py +++ b/yepcord/yepcord/models/userdata.py @@ -52,8 +52,8 @@ def s_discriminator(self) -> str: @property def nsfw_allowed(self) -> bool: - dn = date.today() - return dn - self.birth > timedelta(days=18 * 365 + 4) + today = date.today() + return today - self.birth > timedelta(days=18 * 365 + 4) @property def ds_json(self) -> dict: @@ -71,7 +71,7 @@ def ds_json(self) -> dict: return data async def ds_json_full(self, without_email: bool = False) -> dict: - settings = await self.user.settings + locale, mfa = await models.UserSettings.get(user=self.user).values_list("locale", "mfa") data = { "id": str(self.id), "username": self.username, @@ -84,9 +84,9 @@ async def ds_json_full(self, without_email: bool = False) -> dict: "banner_color": self.banner_color if not self.user.is_bot else None, "accent_color": self.accent_color if not self.user.is_bot else None, "bio": self.bio, - "locale": settings.locale, + "locale": locale, "nsfw_allowed": self.nsfw_allowed, - "mfa_enabled": settings.mfa, + "mfa_enabled": bool(mfa), "email": self.user.email if not self.user.is_bot else None, "verified": self.user.verified, "phone": self.phone From 547d5e2ab55a2602765f067048f371cafc1113ce Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Fri, 13 Sep 2024 15:36:19 +0300 Subject: [PATCH 12/26] move email verification methods --- tests/api/test_auth.py | 14 +--- tests/test_core.py | 17 ++-- yepcord/rest_api/routes/auth.py | 31 +++++--- yepcord/rest_api/routes/channels.py | 6 +- yepcord/rest_api/routes/users_me.py | 6 +- yepcord/yepcord/classes/other.py | 53 ++++++++++++- yepcord/yepcord/core.py | 119 +++++----------------------- yepcord/yepcord/errors.py | 5 ++ yepcord/yepcord/models/user.py | 34 ++++++-- yepcord/yepcord/utils.py | 11 ++- 10 files changed, 149 insertions(+), 147 deletions(-) diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 94a126f..1c737cd 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -7,6 +7,7 @@ import pytest_asyncio from yepcord.rest_api.main import app +from yepcord.yepcord.classes.other import JWT from yepcord.yepcord.config import Config from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.utils import b64decode, b64encode @@ -25,15 +26,6 @@ async def setup_db(): await app.ensure_async(func)() -def generateEmailVerificationToken(user_id: int, email: str, key: bytes): - key = new(key, str(user_id).encode('utf-8'), sha256).digest() - t = int(time()) - sig = b64encode(new(key, f"{user_id}:{email}:{t}".encode('utf-8'), sha256).digest()) - token = b64encode(dumps({"id": user_id, "email": email, "time": t})) - token += f".{sig}" - return token - - @pt.mark.asyncio async def test_login_nonexistent_user(): client: TestClientType = app.test_client() @@ -109,7 +101,7 @@ async def test_verify_email(): client: TestClientType = app.test_client() user = (await create_users(client, 1))[0] - token = generateEmailVerificationToken(int(user["id"]), user["email"], b64decode(Config.KEY)) + token = JWT.encode({"id": int(user["id"]), "email": user["email"]}, b64decode(Config.KEY), expires_after=600) resp = await client.post("/api/v9/auth/verify", json={"token": ""}) assert resp.status_code == 400 @@ -117,7 +109,7 @@ async def test_verify_email(): assert resp.status_code == 400 resp = await client.post("/api/v9/auth/verify", json={'token': token}) - assert resp.status_code == 200 + assert resp.status_code == 200, await resp.get_json() json = await resp.get_json() assert json["token"] assert json["user_id"] == user["id"] diff --git a/tests/test_core.py b/tests/test_core.py index b0461f5..dc67090 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -26,6 +26,7 @@ import pytest_asyncio from tortoise import Tortoise +from yepcord.yepcord.classes.other import MFA from yepcord.yepcord.config import Config, ConfigModel from yepcord.yepcord.core import Core from yepcord.yepcord.enums import UserFlags as UserFlagsE, RelationshipType, ChannelType, GuildPermissions, MfaNonceType @@ -265,8 +266,7 @@ async def test_getMfa(): @pt.mark.asyncio -async def test_getMfaFromTicket_success(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_getMfaFromTicket_success(): user = await User.y.get(VARS["user_id"]) settings = await user.settings settings.mfa = "b" * 16 @@ -278,31 +278,30 @@ async def test_getMfaFromTicket_success(testCore: Coroutine[Any, Any, Core]): except MfaRequiredErr as e: ticket = b64encode(dumps([e.uid, "login"])) + f".{e.sid}.{e.sig}" - mfa = await testCore.getMfaFromTicket(ticket) + mfa = await MFA.get_from_ticket(ticket) assert mfa is not None assert mfa.key.lower() == "b" * 16 @pt.mark.asyncio -async def test_generateUserMfaNonce(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore +async def test_generateUserMfaNonce(): user = await User.y.get(VARS["user_id"]) settings = await user.settings settings.mfa = "c" * 16 await settings.save(update_fields=["mfa"]) user = await User.y.get(VARS["user_id"]) - VARS["mfa_nonce"] = await testCore.generateUserMfaNonce(user) + VARS["mfa_nonce"] = await user.generate_mfa_nonce() @pt.mark.asyncio async def test_verifyUserMfaNonce(): user = await User.y.get(VARS["user_id"]) nonce, regenerate_nonce = VARS["mfa_nonce"] - await core.verifyUserMfaNonce(user, nonce, MfaNonceType.NORMAL) - await core.verifyUserMfaNonce(user, regenerate_nonce, MfaNonceType.REGENERATE) + await user.verify_mfa_nonce(nonce, MfaNonceType.NORMAL) + await user.verify_mfa_nonce(regenerate_nonce, MfaNonceType.REGENERATE) for args in ((nonce, MfaNonceType.REGENERATE), (regenerate_nonce, MfaNonceType.NORMAL)): with pt.raises(InvalidDataErr): - await core.verifyUserMfaNonce(user, *args) + await user.verify_mfa_nonce(*args) del VARS["mfa_nonce"] diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index a0545dd..1b99e63 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -26,9 +26,11 @@ from ..utils import captcha from ..y_blueprint import YBlueprint from ...gateway.events import UserUpdateEvent -from ...yepcord.classes.other import EmailMsg +from ...yepcord.classes.other import EmailMsg, MFA, JWT +from ...yepcord.config import Config from ...yepcord.ctx import getCore, getGw -from ...yepcord.errors import InvalidDataErr, Errors, PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket +from ...yepcord.errors import InvalidDataErr, Errors, PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket, \ + InvalidToken from ...yepcord.models import Session, User from ...yepcord.utils import LOCALES, b64decode @@ -65,7 +67,7 @@ async def login_with_mfa(data: MfaLogin): raise Invalid2FaAuthTicket if not (code := data.code): raise Invalid2FaCode - if not (mfa := await getCore().getMfaFromTicket(ticket)): + if not (mfa := await MFA.get_from_ticket(ticket)): raise Invalid2FaAuthTicket code = code.replace("-", "").replace(" ", "") user = await User.y.get(mfa.uid) @@ -93,9 +95,9 @@ async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User raise PasswordDoesNotMatch if not user.check_password(password): raise PasswordDoesNotMatch - nonce = await getCore().generateUserMfaNonce(user) + nonce = await user.generate_mfa_nonce() - code = await getCore().mfaNonceToCode(nonce[0]) + code = await MFA.nonce_to_code(nonce[0]) await EmailMsg( user.email, f"Your one-time verification key is {code}", @@ -111,19 +113,22 @@ async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User @auth.post("/verify/resend") async def resend_verification_email(user: User = DepUser): if not user.verified: - await getCore().sendVerificationEmail(user) + await user.send_verification_email() return "", 204 @auth.post("/verify", body_cls=VerifyEmail) async def verify_email(data: VerifyEmail): if not data.token: - raise InvalidDataErr(400, Errors.make(50035, {"token": {"code": "TOKEN_INVALID", "message": "Invalid token."}})) - try: - email = jloads(b64decode(data.token.split(".")[0]).decode("utf8"))["email"] - user = await User.get(email=email, verified=False) - except (ValueError, DoesNotExist): - raise InvalidDataErr(400, Errors.make(50035, {"token": {"code": "TOKEN_INVALID", "message": "Invalid token."}})) - await getCore().verifyEmail(user, data.token) + raise InvalidToken + + if (data := JWT.decode(data.token, b64decode(Config.KEY))) is None: + raise InvalidToken + if (user := await User.get_or_none(id=data["id"], email=data["email"], verified=False)) is None: + raise InvalidToken + + user.verified = True + await user.save(update_fields=["verified"]) + await getGw().dispatch(UserUpdateEvent(user, await user.data, await user.settings), [user.id]) return {"token": (await Session.Y.create(user)).token, "user_id": str(user.id)} diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index a055368..8ed5a73 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -188,7 +188,7 @@ async def send_message(user: User = DepUser, channel: Channel = DepChannel): user_ids=[other_user.id]) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel, permissions=GuildPermissions.VIEW_CHANNEL) - await getCore().setReadState(user, channel, 0, message.id) + await user.update_read_state(channel, 0, message.id) await getGw().dispatch(MessageAckEvent({"version_id": 1, "message_id": str(message.id), "channel_id": str(message.channel.id)}), user_ids=[user.id]) return await message.ds_json() @@ -238,13 +238,13 @@ async def get_message(user: User = DepUser, channel: Channel = DepChannel, messa async def send_message_ack(data: MessageAck, message: int, user: User = DepUser, channel: Channel = DepChannel): message = await _getMessage(user, channel, message) if data.manual and (ct := data.mention_count): - await getCore().setReadState(user, channel, ct, message.id) + await user.update_read_state(channel, ct, message.id) await getGw().sendMessageAck(user.id, channel.id, message.id, ct, True) else: count = await Message.filter( channel=channel, ephemeral=False, id__gt=message.id, id__lt=await channel.get_last_message_id(), ).count() - await getCore().setReadState(user, channel, count, message.id) + await user.update_read_state(channel, count, message.id) await getGw().dispatch(MessageAckEvent({ "version_id": 1, "message_id": str(message.id), "channel_id": str(channel.id), }), user_ids=[user.id]) diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index b576271..496b223 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -76,7 +76,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): data.new_password = None if data.email is not None: await user.change_email(data.email) - await getCore().sendVerificationEmail(user) + await user.send_verification_email() data.email = None if data.avatar != "" and data.avatar is not None: if (img := getImage(data.avatar)) and validImage(img): @@ -332,8 +332,8 @@ async def get_backup_codes(data: MfaCodesVerification, user: User = DepUser): "code": "BASE_TYPE_REQUIRED", "message": "This field is required" }})) regenerate = data.regenerate - await getCore().verifyUserMfaNonce(user, nonce, MfaNonceType.REGENERATE if regenerate else MfaNonceType.NORMAL) - if await getCore().mfaNonceToCode(nonce) != key: + await user.verify_mfa_nonce(nonce, MfaNonceType.REGENERATE if regenerate else MfaNonceType.NORMAL) + if await MFA.nonce_to_code(nonce) != key: raise InvalidKey codes = await user.create_backup_codes() if regenerate else await user.get_backup_codes() diff --git a/yepcord/yepcord/classes/other.py b/yepcord/yepcord/classes/other.py index 5ec0a85..2946a6c 100644 --- a/yepcord/yepcord/classes/other.py +++ b/yepcord/yepcord/classes/other.py @@ -16,6 +16,7 @@ along with this program. If not, see . """ +from __future__ import annotations import re from base64 import b32decode from hashlib import sha512 @@ -30,7 +31,7 @@ from mailers.exceptions import DeliveryError from ..config import Config -from ..utils import b64decode, b64encode +from ..utils import b64decode, b64encode, assert_ class ZlibCompressor: @@ -54,6 +55,15 @@ async def send(self): except DeliveryError: pass + @classmethod + async def send_verification(cls, email: str, token: str) -> None: + await cls(email, "Confirm your e-mail in YEPCord", ( + f"Thank you for signing up for a YEPCord account!\n" + f"First you need to make sure that you are you!\n" + f"Click to verify your email address:\n" + f"https://{Config.PUBLIC_HOST}/verify#token={token}" + )).send() + class JWT: """ @@ -79,11 +89,19 @@ def decode(token: str, secret: Union[str, bytes]) -> Optional[dict]: return loads(payload) @staticmethod - def encode(payload: dict, secret: Union[str, bytes], expire_timestamp: Union[int, float] = 0) -> str: + def encode( + payload: dict, secret: Union[str, bytes], expires_at: Optional[Union[int, float]] = None, + expires_after: Optional[int] = None + ) -> str: + if expires_after is not None: + expires_at = int(time() + expires_after) + if expires_at is None: + expires_at = 0 + header = { "alg": "HS512", "typ": "JWT", - "exp": int(expire_timestamp) + "exp": int(expires_at) } header = b64encode(dumps(header, separators=(',', ':'))) payload = b64encode(dumps(payload, separators=(',', ':'))) @@ -147,3 +165,32 @@ def getCodes(self) -> tuple[str, str]: @property def valid(self) -> bool: return bool(self._re.match(self.key)) + + @staticmethod + async def get_from_ticket(ticket: str) -> Optional[MFA]: + from ..models import User + + try: + user_id, session_id, sig = ticket.split(".") + user_id = loads(b64decode(user_id).decode("utf8"))[0] + session_id = int.from_bytes(b64decode(session_id), "big") + sig = b64decode(sig).decode("utf8") + + assert_(user := await User.y.get(user_id)) + assert_(payload := JWT.decode(sig, b64decode(Config.KEY))) + assert_(payload["u"] == user.id) + assert_(payload["s"] == session_id) + except (ValueError, IndexError): + return + + return MFA(await user.get_mfa_key(), user_id) + + @staticmethod + async def nonce_to_code(nonce: str) -> Optional[str]: + key = b64decode(Config.KEY) + + if not (payload := JWT.decode(nonce, key)): + return + token = JWT.encode({"code": payload["c"]}, key) + signature = token.split(".")[2] + return signature.replace("-", "").replace("_", "")[:8].upper() diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index 4d1f2d4..e4639d0 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -17,64 +17,30 @@ """ import os.path -from hashlib import sha256 -from hmac import new -from json import loads as jloads, dumps as jdumps -from os import urandom -from time import time -from typing import Optional, Union +from typing import Optional import maxminddb from . import ctx -from .classes.other import EmailMsg, JWT, MFA from .classes.singleton import Singleton from .config import Config -from .enums import MfaNonceType -from .errors import InvalidDataErr, Errors, InvalidKey -from .models import User, Channel, ReadState, Guild, GuildMember +from .models import User, ReadState, Guild, GuildMember from .storage import getStorage -from .utils import b64encode, b64decode - - -def _assert(value: ..., exc: Union[type[BaseException], BaseException] = ValueError) -> None: - if not value: - raise exc +from .utils import b64decode # noinspection PyMethodMayBeStatic class Core(Singleton): + COUNTRY_TO_LANG = { + "UA": "uk", "US": "en-US", "BG": "bg", "CZ": "cs", "DK": "da", "DE": "de", "GR": "el", "GB": "en-GB", + "ES": "es-ES", "FI": "fi", "FR": "fr", "IN": "hi", "HR": "hr", "HU": "hu", "IT": "it", "JP": "ja", + "KR": "ko", "LT": "lt", "NL": "nl", "NO": "no", "PL": "pl", "BR": "pt-BR", "RO": "ro", "RU": "RU", + "SE": "sv-SE", "TH": "th", "TR": "tr", "VN": "vi", "CN": "zh-CN", "TW": "zh-TW", + } + IP_DATABASE: Optional[maxminddb.Reader] = None + def __init__(self): self.key = b64decode(Config.KEY) - self.ipdb = None - - async def getMfaFromTicket(self, ticket: str) -> Optional[MFA]: - try: - user_id, session_id, sig = ticket.split(".") - user_id = jloads(b64decode(user_id).decode("utf8"))[0] - session_id = int.from_bytes(b64decode(session_id), "big") - sig = b64decode(sig).decode("utf8") - - _assert(user := await User.y.get(user_id)) - _assert(payload := JWT.decode(sig, self.key)) - _assert(payload["u"] == user.id) - _assert(payload["s"] == session_id) - except (ValueError, IndexError): - return - - return MFA(await user.get_mfa_key(), user_id) - - async def generateUserMfaNonce(self, user: User) -> tuple[str, str]: - exp = time() + 600 - code = b64encode(urandom(16)) - nonce = JWT.encode({"t": MfaNonceType.NORMAL, "c": code, "u": user.id}, self.key, exp) - rnonce = JWT.encode({"t": MfaNonceType.REGENERATE, "c": code, "u": user.id}, self.key, exp) - return nonce, rnonce - - async def verifyUserMfaNonce(self, user: User, nonce: str, nonce_type: MfaNonceType) -> None: - _assert(payload := JWT.decode(nonce, self.key), InvalidKey) - _assert(payload["u"] == user.id, InvalidKey) - _assert(nonce_type == payload["t"], InvalidKey) #async def sendMessage(self, message: Message) -> Message: # async def _addToReadStates(): # TODO: recalculate read states when requested by user @@ -90,14 +56,6 @@ async def verifyUserMfaNonce(self, user: User, nonce: str, nonce_type: MfaNonceT # return message - async def setReadState(self, user: User, channel: Channel, count: int, last: int) -> None: - read_state, _ = await ReadState.get_or_create( - user=user, channel=channel, defaults={"last_read_id": last, "count": count} - ) - read_state.last_read_id = last - read_state.count = count - await read_state.save(update_fields=["last_read_id", "count"]) - async def getReadStatesJ(self, user: User) -> list: states = [] st: ReadState @@ -105,40 +63,6 @@ async def getReadStatesJ(self, user: User) -> list: states.append(await st.ds_json()) return states - async def sendVerificationEmail(self, user: User) -> None: - key = new(self.key, str(user.id).encode('utf-8'), sha256).digest() - t = int(time()) - sig = b64encode(new(key, f"{user.id}:{user.email}:{t}".encode('utf-8'), sha256).digest()) - token = b64encode(jdumps({"id": user.id, "email": user.email, "time": t})) - token += f".{sig}" - link = f"https://{Config.PUBLIC_HOST}/verify#token={token}" - await EmailMsg(user.email, "Confirm your e-mail in YEPCord", - f"Thank you for signing up for a YEPCord account!\nFirst you need to make sure that you are you!" - f" Click to verify your email address:\n{link}").send() - - async def verifyEmail(self, user: User, token: str) -> None: - try: - data, sig = token.split(".") - data = jloads(b64decode(data).decode("utf8")) - sig = b64decode(sig) - t = data["time"] - assert data["email"] == user.email and data["id"] == user.id and time() - t < 600 - key = new(self.key, str(user.id).encode('utf-8'), sha256).digest() - vsig = new(key, f"{user.id}:{user.email}:{t}".encode('utf-8'), sha256).digest() - assert sig == vsig - except: - raise InvalidDataErr(400, Errors.make(50035, {"token": {"code": "TOKEN_INVALID", - "message": "Invalid token."}})) - user.verified = True - await user.save(update_fields=["verified"]) - - async def mfaNonceToCode(self, nonce: str) -> Optional[str]: - if not (payload := JWT.decode(nonce, self.key)): - return - token = JWT.encode({"code": payload["c"]}, self.key) - signature = token.split(".")[2] - return signature.replace("-", "").replace("_", "")[:8].upper() - async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") @@ -159,23 +83,20 @@ async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[st return mutual_guilds_json def getLanguageCode(self, ip: str, default: str = "en-US") -> str: - if self.ipdb is None and not os.path.exists("other/ip_database.mmdb"): + cls = self.__class__ + + if cls.IP_DATABASE is None and not os.path.exists("other/ip_database.mmdb"): return default - if self.ipdb is None: - self.ipdb = maxminddb.open_database("other/ip_database.mmdb") + if cls.IP_DATABASE is None: + cls.IP_DATABASE = maxminddb.open_database("other/ip_database.mmdb") try: - country_code = (self.ipdb.get(ip) or {"country": {"iso_code": None}})["country"]["iso_code"] or default + country_code = (cls.IP_DATABASE.get(ip) or {"country": {"iso_code": None}})["country"]["iso_code"] \ + or default except (ValueError, KeyError): return default - country_to_language = { - "UA": "uk", "US": "en-US", "BG": "bg", "CZ": "cs", "DK": "da", "DE": "de", "GR": "el", "GB": "en-GB", - "ES": "es-ES", "FI": "fi", "FR": "fr", "IN": "hi", "HR": "hr", "HU": "hu", "IT": "it", "JP": "ja", - "KR": "ko", "LT": "lt", "NL": "nl", "NO": "no", "PL": "pl", "BR": "pt-BR", "RO": "ro", "RU": "RU", - "SE": "sv-SE", "TH": "th", "TR": "tr", "VN": "vi", "CN": "zh-CN", "TW": "zh-TW", - } - - return country_to_language.get(country_code, default) + + return cls.COUNTRY_TO_LANG.get(country_code, default) ctx._get_core = Core.getInstance diff --git a/yepcord/yepcord/errors.py b/yepcord/yepcord/errors.py index c9b28f2..6223c57 100644 --- a/yepcord/yepcord/errors.py +++ b/yepcord/yepcord/errors.py @@ -210,3 +210,8 @@ def __init__(self, error_code: int): Unauthorized = InvalidDataErr(401, Errors.make(0, message="401: Unauthorized")) Error0 = InvalidDataErr(400, Errors.make(0)) + +InvalidToken = InvalidDataErr(400, Errors.make(50035, {"token": { + "code": "TOKEN_INVALID", + "message": "Invalid token." +}})) diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index f147414..13a7a9e 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -31,13 +31,13 @@ import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model -from ..classes.other import MFA, JWT +from ..classes.other import MFA, JWT, EmailMsg from ..config import Config from ..ctx import getCore -from ..enums import RelationshipType -from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser +from ..enums import RelationshipType, MfaNonceType +from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser, InvalidKey from ..snowflake import Snowflake -from ..utils import int_size, b64encode +from ..utils import int_size, b64encode, b64decode, assert_ class UserUtils: @@ -87,7 +87,7 @@ async def register( await models.UserData.create(id=user_id, user=user, birth=birth, username=login, discriminator=discriminator) await models.UserSettings.create(id=user_id, user=user, locale=locale) - await getCore().sendVerificationEmail(user) + await user.send_verification_email() return user @staticmethod @@ -319,3 +319,27 @@ async def get_related_users(self) -> list[models.User]: async def get_mfa_key(self) -> str | None: return cast(str, await models.UserSettings.get(user=self).values_list("mfa", flat=True)) + + async def generate_mfa_nonce(self) -> tuple[str, str]: + key = b64decode(Config.KEY) + exp = time() + 600 + code = b64encode(urandom(16)) + nonce = JWT.encode({"t": MfaNonceType.NORMAL, "c": code, "u": self.id}, key, exp) + rnonce = JWT.encode({"t": MfaNonceType.REGENERATE, "c": code, "u": self.id}, key, exp) + return nonce, rnonce + + async def verify_mfa_nonce(self, nonce: str, nonce_type: MfaNonceType) -> None: + key = b64decode(Config.KEY) + assert_(payload := JWT.decode(nonce, key), InvalidKey) + assert_(payload["u"] == self.id, InvalidKey) + assert_(nonce_type == payload["t"], InvalidKey) + + async def update_read_state(self, channel: models.Channel, count: int, last: int) -> None: + await models.ReadState.update_or_create(user=self, channel=channel, defaults={ + "last_read_id": last, + "count": count, + }) + + async def send_verification_email(self) -> None: + token = JWT.encode({"id": self.id, "email": self.email}, b64decode(Config.KEY), expires_after=600) + await EmailMsg.send_verification(self.email, token) diff --git a/yepcord/yepcord/utils.py b/yepcord/yepcord/utils.py index 5255279..46c03d5 100644 --- a/yepcord/yepcord/utils.py +++ b/yepcord/yepcord/utils.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ +import json from asyncio import get_event_loop, sleep as asleep from base64 import b64encode as _b64encode, b64decode as _b64decode from io import BytesIO @@ -33,9 +34,12 @@ def b64decode(data: Union[str, bytes]) -> bytes: return _b64decode(data) -def b64encode(data: Union[str, bytes]) -> str: +def b64encode(data: Union[str, bytes, dict, list]) -> str: + if isinstance(data, (dict, list)): + data = json.dumps(data) if isinstance(data, str): data = data.encode("utf8") + data = _b64encode(data).decode("utf8") for search, replace in (('+', '-'), ('/', '_'), ('=', '')): data = data.replace(search, replace) @@ -118,3 +122,8 @@ def unfreeze(obj: Any) -> Any: if isinstance(obj, list): return [unfreeze(v) for v in obj] return obj + + +def assert_(value: ..., exc: Union[type[BaseException], BaseException] = ValueError) -> None: + if not value: + raise exc From 6fc2125a8137d3986f952a9af1b69c5a3cab121d Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 14 Sep 2024 19:33:08 +0300 Subject: [PATCH 13/26] rewrite getMutualGuildsJ using one query --- tests/api/test_user_profile.py | 33 ++++++++++++++++++++++++++++++++- yepcord/gateway/events.py | 5 ++++- yepcord/yepcord/core.py | 25 +------------------------ yepcord/yepcord/models/user.py | 31 +++++++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/tests/api/test_user_profile.py b/tests/api/test_user_profile.py index baa9791..8abba91 100644 --- a/tests/api/test_user_profile.py +++ b/tests/api/test_user_profile.py @@ -2,7 +2,8 @@ import pytest_asyncio from yepcord.rest_api.main import app -from .utils import TestClientType, create_users, create_guild +from yepcord.yepcord.enums import ChannelType +from .utils import TestClientType, create_users, create_guild, create_invite from ..yep_image import YEP_IMAGE from ..utils import register_app_error_handler @@ -67,6 +68,7 @@ async def test_get_my_profile(): assert resp.status_code == 200 json = await resp.get_json() assert json["user"]["id"] == user_id + assert len(json["mutual_guilds"]) > 0 @pt.mark.asyncio @@ -166,3 +168,32 @@ async def test_hypesquad_change_house(): resp = await client.post("/api/v9/hypesquad/online", headers=headers, json={'house_id': 4}) assert resp.status_code == 400 + + +@pt.mark.asyncio +async def test_get_other_profile(): + client: TestClientType = app.test_client() + user, user2 = await create_users(client, 2) + guild = await create_guild(client, user, "Test Guild") + headers = {"Authorization": user["token"]} + channel = [channel for channel in guild["channels"] if channel["type"] == ChannelType.GUILD_TEXT][0] + invite = await create_invite(client, user, channel["id"]) + + resp = await client.post(f"/api/v9/invites/{invite['code']}", headers={"Authorization": user2["token"]}) + assert resp.status_code == 200 + + resp = await client.get(f"/api/v9/users/{user2['id']}/profile?with_mutual_guilds=true", headers=headers) + assert resp.status_code == 200 + json = await resp.get_json() + assert json["user"]["id"] == user2["id"] + assert len(json["mutual_guilds"]) > 0 + assert json["mutual_guilds"] == [{"id": guild["id"], "nick": None}] + + resp = await client.patch(f"/api/v9/guilds/{guild['id']}/members/{user2['id']}", headers=headers, + json={"nick": "TEST"}) + assert resp.status_code == 200 + + resp = await client.get(f"/api/v9/users/{user2['id']}/profile?with_mutual_guilds=true", headers=headers) + assert resp.status_code == 200 + json = await resp.get_json() + assert json["mutual_guilds"] == [{"id": guild["id"], "nick": "TEST"}] diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index c85c818..2ea2197 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -141,7 +141,10 @@ async def json(self) -> dict: "read_state": { "version": 1, "partial": False, - "entries": await getCore().getReadStatesJ(self.user) if not self.user.is_bot else [] + "entries": [ + await state.ds_json() + for state in await self.user.get_read_states() + ] }, "resume_gateway_url": f"wss://{Config.GATEWAY_HOST}/", "session_type": "normal", diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index e4639d0..c8ba340 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -24,7 +24,7 @@ from . import ctx from .classes.singleton import Singleton from .config import Config -from .models import User, ReadState, Guild, GuildMember +from .models import Guild from .storage import getStorage from .utils import b64decode @@ -56,32 +56,9 @@ def __init__(self): # return message - async def getReadStatesJ(self, user: User) -> list: - states = [] - st: ReadState - for st in await ReadState.filter(user=user).select_related("channel", "user"): - states.append(await st.ds_json()) - return states - async def getGuild(self, guild_id: int) -> Optional[Guild]: return await Guild.get_or_none(id=guild_id).select_related("owner") - async def getMutualGuildsJ(self, user: User, current_user: User) -> list[dict[str, str]]: - user_guilds_member = await GuildMember.filter(user=user).select_related("guild") - user_guild_ids = [member.guild.id for member in user_guilds_member] - user_guilds_member = {member.guild.id: member for member in user_guilds_member} - - current_user_guilds_member = await GuildMember.filter(user=current_user).select_related("guild") - current_user_guild_ids = [member.guild.id for member in current_user_guilds_member] - - mutual_guilds_ids = set(user_guild_ids) & set(current_user_guild_ids) - mutual_guilds_json = [] - for guild_id in mutual_guilds_ids: - member = user_guilds_member[guild_id] - mutual_guilds_json.append({"id": str(guild_id), "nick": member.nick}) - - return mutual_guilds_json - def getLanguageCode(self, ip: str, default: str = "en-US") -> str: cls = self.__class__ diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 13a7a9e..cd73a2a 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -26,7 +26,8 @@ from bcrypt import checkpw, hashpw, gensalt from tortoise import fields -from tortoise.expressions import Q +from tortoise.expressions import Q, Subquery +from tortoise.functions import Count from tortoise.transactions import atomic import yepcord.yepcord.models as models @@ -150,8 +151,10 @@ async def mfa(self) -> Optional[MFA]: mfa = MFA(await self.get_mfa_key(), self.id) return mfa if mfa.valid else None - async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, mutual_friends_count: bool = False, - guild_id: int = None) -> dict: + async def profile_json( + self, other_user: User, with_mutual_guilds: bool = False, mutual_friends_count: bool = False, + guild_id: int = None + ) -> dict: data = await self.data premium_since = self.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") connections = await models.ConnectedAccount.filter(user=self, verified=True, visibility=1) @@ -184,7 +187,21 @@ async def profile_json(self, other_user: User, with_mutual_guilds: bool = False, if mutual_friends_count: data["mutual_friends_count"] = 0 # TODO: add mutual friends count if with_mutual_guilds: - data["mutual_guilds"] = await getCore().getMutualGuildsJ(self, other_user) + query = Q(user=self) + if self != other_user: + query &= Q(guild__id__in=Subquery( + models.GuildMember + .filter(user__id__in=[self.id, other_user.id]) + .group_by("guild_id") + .annotate(user_count=Count("user__id", distinct=True)) + .filter(user_count=2) + .values_list("guild_id", flat=True) + )) + + data["mutual_guilds"] = [ + {"id": str(guild_id), "nick": nick} + for nick, guild_id in await models.GuildMember.filter(query).values_list("nick", "guild_id") + ] if self.is_bot: data["user"]["bot"] = True @@ -343,3 +360,9 @@ async def update_read_state(self, channel: models.Channel, count: int, last: int async def send_verification_email(self) -> None: token = JWT.encode({"id": self.id, "email": self.email}, b64decode(Config.KEY), expires_after=600) await EmailMsg.send_verification(self.email, token) + + async def get_read_states(self) -> list[models.ReadState]: + if self.is_bot: + return [] + + return await models.ReadState.filter(user=self).select_related("channel") From 4a7267b3b738cb77375b6d49892a55f475a9294a Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 18:15:13 +0300 Subject: [PATCH 14/26] add poethepoet; make s3 and ftp optional; update setup-python in gh action to v5 --- .coveragerc | 4 - .github/workflows/pytest.yml | 17 +- poetry.lock | 1646 ++++++++++++++++++---------------- pyproject.toml | 26 +- 4 files changed, 912 insertions(+), 781 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 119c611..0000000 --- a/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[run] -omit = - yepcord/yepcord/proto.py -data_file = coverage.coverage \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ae80fe1..f70cb11 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,13 +17,14 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 + - name: Install Poetry + run: pipx install poetry + - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Install Poetry - uses: snok/install-poetry@v1 + cache: "poetry" - name: Install dependencies run: poetry install --no-interaction @@ -56,7 +57,7 @@ jobs: - name: Setup database run: | - poetry run python app.py migrate + poe migrate env: YEPCORD_CONFIG: .github/settings_test.py DB_TYPE: ${{ matrix.database }} @@ -64,7 +65,7 @@ jobs: - name: Run test suite for core run: | - poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/yepcord --disable-warnings tests/test_core.py + poe test tests/test_core.py env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -72,7 +73,7 @@ jobs: - name: Run test suite for http api run: | - poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/rest_api --cov=yepcord/yepcord --cov=yepcord/remote_auth --disable-warnings tests/api/ + poe test --cov=yepcord/rest_api--cov=yepcord/remote_auth tests/api/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -82,7 +83,7 @@ jobs: - name: Run test suite for cdn/storage run: | mkdir -p tests/files/yepcord-test - poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/cdn --cov=yepcord/yepcord --cov-config=.coveragerc --disable-warnings tests/cdn/ + poe test --cov=yepcord/cdn tests/cdn/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} diff --git a/poetry.lock b/poetry.lock index ae340c2..0f83357 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aerich" @@ -52,7 +52,7 @@ files = [ name = "aioftp" version = "0.22.3" description = "ftp client/server for asyncio" -optional = false +optional = true python-versions = ">=3.8" files = [ {file = "aioftp-0.22.3-py3-none-any.whl", hash = "sha256:93d17d5d3b8033570f2dfee013172d49e52323437925120ec87190539113ebd0"}, @@ -117,13 +117,13 @@ zstd = ["cramjam"] [[package]] name = "aiormq" -version = "6.8.0" +version = "6.8.1" description = "Pure python AMQP asynchronous client library" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "aiormq-6.8.0-py3-none-any.whl", hash = "sha256:9a16174dcae4078c957a773d2f02d3dfd6c2fcf12c909dc244333a458f2aeab0"}, - {file = "aiormq-6.8.0.tar.gz", hash = "sha256:198f9c7430feb7bc491016099a06266dc45880b6b1de3925d410fde6541a66fb"}, + {file = "aiormq-6.8.1-py3-none-any.whl", hash = "sha256:5da896c8624193708f9409ffad0b20395010e2747f22aa4150593837f40aa017"}, + {file = "aiormq-6.8.1.tar.gz", hash = "sha256:a964ab09634be1da1f9298ce225b310859763d5cf83ef3a7eae1a6dc6bd1da1a"}, ] [package.dependencies] @@ -172,13 +172,13 @@ files = [ [[package]] name = "anyio" -version = "4.4.0" +version = "4.6.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, + {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, ] [package.dependencies] @@ -188,9 +188,9 @@ sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "async-timeout" @@ -333,78 +333,78 @@ files = [ [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -500,83 +500,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.2" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, + {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, + {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, + {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, + {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, + {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, + {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, + {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, + {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, + {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, + {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, + {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, + {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, + {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, + {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, ] [package.dependencies] @@ -587,38 +577,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.0" +version = "43.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, - {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, - {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, - {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, - {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, - {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, - {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, - {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, - {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, - {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, - {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, - {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, - {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, ] [package.dependencies] @@ -631,7 +621,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -653,18 +643,15 @@ tests = ["check-manifest (>=0.42)", "mock (>=1.3.0)", "pytest (==5.4.3)", "pytes [[package]] name = "emoji" -version = "2.12.1" +version = "2.14.0" description = "Emoji for Python" optional = false python-versions = ">=3.7" files = [ - {file = "emoji-2.12.1-py3-none-any.whl", hash = "sha256:a00d62173bdadc2510967a381810101624a2f0986145b8da0cffa42e29430235"}, - {file = "emoji-2.12.1.tar.gz", hash = "sha256:4aa0488817691aa58d83764b6c209f8a27c0b3ab3f89d1b8dceca1a62e4973eb"}, + {file = "emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799"}, + {file = "emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca"}, ] -[package.dependencies] -typing-extensions = ">=4.7.0" - [package.extras] dev = ["coverage", "pytest (>=7.4.4)"] @@ -717,39 +704,39 @@ pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" [[package]] name = "faststream" -version = "0.5.20" +version = "0.5.25" description = "FastStream: the simplest way to work with a messaging queues" optional = false python-versions = ">=3.8" files = [ - {file = "faststream-0.5.20-py3-none-any.whl", hash = "sha256:3fed6f407ea558b97f802ba00d6ab3f71db95c7cc64ddbe64576e4a5fff2f28d"}, - {file = "faststream-0.5.20.tar.gz", hash = "sha256:3f7de7f041058e2b5c82b29d70d82722de199e9f247a532cb93f669970f6eabb"}, + {file = "faststream-0.5.25-py3-none-any.whl", hash = "sha256:872d88a112d3d9c454dbc0fa4fe4211717e4422b3aa2f75f9d239bffc3c31075"}, + {file = "faststream-0.5.25.tar.gz", hash = "sha256:926a4a8e013140a37b1c06ad16eb9a35529c9f626def3d846ec4ac171e050bc6"}, ] [package.dependencies] aio-pika = {version = ">=9,<10", optional = true, markers = "extra == \"rabbit\""} aiokafka = {version = ">=0.9,<0.12", optional = true, markers = "extra == \"kafka\""} anyio = ">=3.7.1,<5" -fast-depends = ">=2.4.0b0,<2.5.0" +fast-depends = ">=2.4.0b0,<3.0.0" nats-py = {version = ">=2.7.0,<=3.0.0", optional = true, markers = "extra == \"nats\""} redis = {version = ">=5.0.0,<6.0.0", optional = true, markers = "extra == \"redis\""} -typer = ">=0.9,<0.12 || >0.12,<1" typing-extensions = ">=4.8.0" [package.extras] +cli = ["typer (>=0.9,!=0.12,<1)", "watchfiles (>=0.15.0,<0.25.0)"] confluent = ["confluent-kafka (>=2,<3)"] -dev = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "cairosvg", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "coverage[toml] (==7.6.1)", "detect-secrets (==1.5.0)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.112.2)", "griffe-typingdoc (==0.2.6)", "httpx (==0.27.2)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.7)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.0.5)", "mkdocs-material (==9.5.33)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.25.2)", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "pillow", "pre-commit (==3.5.0)", "pre-commit (==3.8.0)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.2)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "redis (>=5.0.0,<6.0.0)", "requests", "ruff (==0.6.2)", "semgrep (==1.85.0)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "typing-extensions (>=4.8.0,<4.12.1)", "watchfiles (==0.24.0)"] -devdocs = ["cairosvg", "griffe-typingdoc (==0.2.6)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.7)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.0.5)", "mkdocs-material (==9.5.33)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.25.2)", "pillow", "requests"] +dev = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "cairosvg", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "coverage[toml] (==7.6.1)", "detect-secrets (==1.5.0)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.0)", "httpx (==0.27.2)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.2.0)", "mkdocs-material (==9.5.36)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.1)", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "pillow", "pre-commit (==3.5.0)", "pre-commit (==3.8.0)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "redis (>=5.0.0,<6.0.0)", "requests", "ruff (==0.6.7)", "semgrep (==1.89.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "typing-extensions (>=4.8.0,<4.12.1)", "watchfiles (>=0.15.0,<0.25.0)"] +devdocs = ["cairosvg", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.2.0)", "mkdocs-material (==9.5.36)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.1)", "pillow", "requests"] kafka = ["aiokafka (>=0.9,<0.12)"] -lint = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "ruff (==0.6.2)", "semgrep (==1.85.0)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson"] +lint = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "ruff (==0.6.7)", "semgrep (==1.89.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] nats = ["nats-py (>=2.7.0,<=3.0.0)"] -optionals = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)"] +optionals = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "watchfiles (>=0.15.0,<0.25.0)"] otel = ["opentelemetry-sdk (>=1.24.0,<2.0.0)"] rabbit = ["aio-pika (>=9,<10)"] redis = ["redis (>=5.0.0,<6.0.0)"] -test-core = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "pytest (==8.3.2)", "pytest-asyncio (==0.24.0)", "typing-extensions (>=4.8.0,<4.12.1)"] -testing = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.112.2)", "httpx (==0.27.2)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.2)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "typing-extensions (>=4.8.0,<4.12.1)", "watchfiles (==0.24.0)"] -types = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson"] +test-core = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "typing-extensions (>=4.8.0,<4.12.1)"] +testing = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.0)", "httpx (==0.27.2)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "typing-extensions (>=4.8.0,<4.12.1)"] +types = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] [[package]] name = "flask" @@ -813,13 +800,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.5" +version = "1.0.6" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, + {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, + {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, ] [package.dependencies] @@ -830,7 +817,7 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" @@ -897,33 +884,40 @@ files = [ [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -977,13 +971,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "mailers" -version = "3.0.5" +version = "3.1.0" description = "Email delivery for asyncio." optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "mailers-3.0.5-py3-none-any.whl", hash = "sha256:564de4e4d8b5d56605b0c9a991c092d077fb85cfc4fdc2ed3960dcb51165ebf3"}, - {file = "mailers-3.0.5.tar.gz", hash = "sha256:9eb4d177fb85453869f779779249c5849c18bdb56fd5f6558c7b6a32ff90640f"}, + {file = "mailers-3.1.0-py3-none-any.whl", hash = "sha256:a66c2ddc4e02ae759332c649386bd2a62d12e04417e371187599e07cc0775504"}, + {file = "mailers-3.1.0.tar.gz", hash = "sha256:efc7715207e36cdb8bcd56fd97f1e6ee5e9547cd9b52463814e760f889478cf3"}, ] [package.dependencies] @@ -995,97 +989,74 @@ dkim = ["dkimpy (>=1.0,<2.0)"] jinja2 = ["jinja2 (>=3.0,<4.0)"] smtp = ["aiosmtplib (>=3.0,<4.0)"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, + {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, ] [[package]] @@ -1166,116 +1137,110 @@ files = [ {file = "maxminddb-2.6.2.tar.gz", hash = "sha256:7d842d32e2620abc894b7d79a5a1007a69df2c6cf279a06b94c9c3913f66f264"}, ] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "multidict" -version = "6.0.5" +version = "6.1.0" description = "multidict implementation" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "nats-py" version = "2.9.0" @@ -1394,6 +1359,17 @@ files = [ codegen = ["lxml", "requests", "yapf"] testing = ["coverage", "flake8", "flake8-comprehensions", "flake8-deprecated", "flake8-import-order", "flake8-print", "flake8-quotes", "flake8-rst-docstrings", "flake8-tuple", "yapf"] +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + [[package]] name = "pillow" version = "10.4.0" @@ -1506,6 +1482,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "poethepoet" +version = "0.29.0" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.29.0-py3-none-any.whl", hash = "sha256:f8dfe55006dcfb5cf31bcb1904e1262e1c642a4502fee3688cbf1bddfe5c7601"}, + {file = "poethepoet-0.29.0.tar.gz", hash = "sha256:676842302f2304a86b31ac56398dd672fae8471128d2086896393384dbafc095"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +pyyaml = ">=6.0.2,<7.0.0" +tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + [[package]] name = "priority" version = "2.0.0" @@ -1517,6 +1512,113 @@ files = [ {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] +[[package]] +name = "propcache" +version = "0.2.0" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.8" +files = [ + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, + {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, + {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, + {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, + {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, + {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, + {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, + {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, + {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, + {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, + {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, + {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, + {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, + {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, + {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, + {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, + {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, + {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, + {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, + {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, + {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, + {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, + {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, + {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, + {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, + {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, + {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, + {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, + {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, + {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, + {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, + {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, + {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, +] + [[package]] name = "protobuf" version = "4.25.3" @@ -1550,18 +1652,18 @@ files = [ [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, @@ -1569,103 +1671,104 @@ typing-extensions = [ [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -1684,20 +1787,6 @@ files = [ [package.extras] ssl = ["PyOpenSSL"] -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - [[package]] name = "pyhumps" version = "3.8.0" @@ -1722,13 +1811,13 @@ files = [ [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -1823,13 +1912,75 @@ files = [ [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -1882,45 +2033,27 @@ pydantic = ["pydantic (>=2)"] [[package]] name = "redis" -version = "5.0.8" +version = "5.1.1" description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, - {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, + {file = "redis-5.1.1-py3-none-any.whl", hash = "sha256:f8ea06b7482a668c6475ae202ed8d9bcaa409f6e87fb77ed1043d912afd62e24"}, + {file = "redis-5.1.1.tar.gz", hash = "sha256:f6c997521fedbae53387307c5d0bf784d9acc28d9f1d058abeac566ec4dbed72"}, ] [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] - -[[package]] -name = "rich" -version = "13.8.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, - {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] [[package]] name = "s3lite" version = "0.1.8" description = "Minimal async s3 implementation." -optional = false +optional = true python-versions = "<4.0,>=3.9" files = [ {file = "s3lite-0.1.8-py3-none-any.whl", hash = "sha256:a4994fdeea4544330b5a1ddcdb6b91242233da99c5dc6d806c50a71119a6faa8"}, @@ -1931,17 +2064,6 @@ files = [ httpx = ">=0.27.0,<0.28.0" python-dateutil = ">=2.8.2,<3.0.0" -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - [[package]] name = "six" version = "1.16.0" @@ -1980,13 +2102,13 @@ exceptiongroup = "*" [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -2030,23 +2152,6 @@ asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] asyncpg = ["asyncpg"] psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] -[[package]] -name = "typer" -version = "0.12.5" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, - {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - [[package]] name = "types-protobuf" version = "4.25.0.20240417" @@ -2071,13 +2176,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.30.6" +version = "0.31.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, - {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, + {file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"}, + {file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"}, ] [package.dependencies] @@ -2183,97 +2288,97 @@ full = ["orjson"] [[package]] name = "websockets" -version = "13.0.1" +version = "13.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.8" files = [ - {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f"}, - {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c"}, - {file = "websockets-13.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f"}, - {file = "websockets-13.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543"}, - {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d"}, - {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f"}, - {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8"}, - {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b"}, - {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448"}, - {file = "websockets-13.0.1-cp310-cp310-win32.whl", hash = "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3"}, - {file = "websockets-13.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0"}, - {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7"}, - {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4"}, - {file = "websockets-13.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2"}, - {file = "websockets-13.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0"}, - {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e"}, - {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462"}, - {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501"}, - {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418"}, - {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df"}, - {file = "websockets-13.0.1-cp311-cp311-win32.whl", hash = "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f"}, - {file = "websockets-13.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075"}, - {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a"}, - {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956"}, - {file = "websockets-13.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af"}, - {file = "websockets-13.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf"}, - {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c"}, - {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4"}, - {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab"}, - {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d"}, - {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237"}, - {file = "websockets-13.0.1-cp312-cp312-win32.whl", hash = "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185"}, - {file = "websockets-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99"}, - {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa"}, - {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231"}, - {file = "websockets-13.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9"}, - {file = "websockets-13.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75"}, - {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553"}, - {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920"}, - {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329"}, - {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7"}, - {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2"}, - {file = "websockets-13.0.1-cp313-cp313-win32.whl", hash = "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb"}, - {file = "websockets-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b"}, - {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e"}, - {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2"}, - {file = "websockets-13.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b"}, - {file = "websockets-13.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae"}, - {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f"}, - {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603"}, - {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36"}, - {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a"}, - {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb"}, - {file = "websockets-13.0.1-cp38-cp38-win32.whl", hash = "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4"}, - {file = "websockets-13.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9"}, - {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc"}, - {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63"}, - {file = "websockets-13.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37"}, - {file = "websockets-13.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9"}, - {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97"}, - {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f"}, - {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4"}, - {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0"}, - {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d"}, - {file = "websockets-13.0.1-cp39-cp39-win32.whl", hash = "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58"}, - {file = "websockets-13.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333"}, - {file = "websockets-13.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026"}, - {file = "websockets-13.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5"}, - {file = "websockets-13.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980"}, - {file = "websockets-13.0.1-py3-none-any.whl", hash = "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817"}, - {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, + {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, + {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, + {file = "websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa"}, + {file = "websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0"}, + {file = "websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f"}, + {file = "websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe"}, + {file = "websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, + {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, + {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, + {file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, + {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, + {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, + {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc"}, + {file = "websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49"}, + {file = "websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6"}, + {file = "websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14"}, + {file = "websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf"}, + {file = "websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c"}, + {file = "websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6"}, + {file = "websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708"}, + {file = "websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f"}, + {file = "websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2"}, + {file = "websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6"}, + {file = "websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d"}, + {file = "websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d"}, + {file = "websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23"}, + {file = "websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7"}, + {file = "websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295"}, + {file = "websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96"}, + {file = "websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf"}, + {file = "websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d"}, + {file = "websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7"}, + {file = "websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa"}, + {file = "websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6"}, + {file = "websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5"}, + {file = "websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c"}, + {file = "websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238"}, + {file = "websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6"}, + {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a"}, + {file = "websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b"}, + {file = "websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d"}, + {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027"}, + {file = "websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e"}, + {file = "websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb"}, + {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20"}, + {file = "websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678"}, + {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, + {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, ] [[package]] @@ -2319,108 +2424,115 @@ h11 = ">=0.9.0,<1" [[package]] name = "yarl" -version = "1.9.7" +version = "1.15.0" description = "Yet another URL library" optional = false python-versions = ">=3.8" files = [ - {file = "yarl-1.9.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:60c04415b31a1611ef5989a6084dd6f6b95652c6a18378b58985667b65b2ecb6"}, - {file = "yarl-1.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1787dcfdbe730207acb454548a6e19f80ae75e6d2d1f531c5a777bc1ab6f7952"}, - {file = "yarl-1.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5ddad20363f9f1bbedc95789c897da62f939e6bc855793c3060ef8b9f9407bf"}, - {file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdb156a06208fc9645ae7cc0fca45c40dd40d7a8c4db626e542525489ca81a9"}, - {file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:522fa3d300d898402ae4e0fa7c2c21311248ca43827dc362a667de87fdb4f1be"}, - {file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7f9cabfb8b980791b97a3ae3eab2e38b2ba5eab1af9b7495bdc44e1ce7c89e3"}, - {file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc728857df4087da6544fc68f62d7017fa68d74201d5b878e18ed4822c31fb3"}, - {file = "yarl-1.9.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dba2ebac677184d56374fa3e452b461f5d6a03aa132745e648ae8859361eb6b"}, - {file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a95167ae34667c5cc7d9206c024f793e8ffbadfb307d5c059de470345de58a21"}, - {file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9d319ac113ca47352319cbea92d1925a37cb7bd61a8c2f3e3cd2e96eb33cccae"}, - {file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d71a5d818d82586ac46265ae01466e0bda0638760f18b21f1174e0dd58a9d2f"}, - {file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ff03f1c1ac474c66d474929ae7e4dd195592c1c7cc8c36418528ed81b1ca0a79"}, - {file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78250f635f221dde97d02c57aade3313310469bc291888dfe32acd1012594441"}, - {file = "yarl-1.9.7-cp310-cp310-win32.whl", hash = "sha256:f3aaf9fa960d55bd7876d55d7ea3cc046f3660df1ff73fc1b8c520a741ed1f21"}, - {file = "yarl-1.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:e8362c941e07fbcde851597672a5e41b21dc292b7d5a1dc439b7a93c9a1af5d9"}, - {file = "yarl-1.9.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:596069ddeaf72b5eb36cd714dcd2b5751d0090d05a8d65113b582ed9e1c801fb"}, - {file = "yarl-1.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cb870907e8b86b2f32541403da9455afc1e535ce483e579bea0e6e79a0cc751c"}, - {file = "yarl-1.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca5e86be84492fa403c4dcd4dcaf8e1b1c4ffc747b5176f7c3d09878c45719b0"}, - {file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99cecfb51c84d00132db909e83ae388793ca86e48df7ae57f1be0beab0dcce5"}, - {file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25508739e9b44d251172145f54c084b71747b09e4d237dc2abb045f46c36a66e"}, - {file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60f3b5aec3146b6992640592856414870f5b20eb688c1f1d5f7ac010a7f86561"}, - {file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1557456afce5db3d655b5f8a31cdcaae1f47e57958760525c44b76e812b4987"}, - {file = "yarl-1.9.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71bb1435a84688ed831220c5305d96161beb65cac4a966374475348aa3de4575"}, - {file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f87d8645a7a806ec8f66aac5e3b1dcb5014849ff53ffe2a1f0b86ca813f534c7"}, - {file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:58e3f01673873b8573da3abe138debc63e4e68541b2104a55df4c10c129513a4"}, - {file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8af0bbd4d84f8abdd9b11be9488e32c76b1501889b73c9e2292a15fb925b378b"}, - {file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7fc441408ed0d9c6d2d627a02e281c21f5de43eb5209c16636a17fc704f7d0f8"}, - {file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a9552367dc440870556da47bb289a806f08ad06fbc4054072d193d9e5dd619ba"}, - {file = "yarl-1.9.7-cp311-cp311-win32.whl", hash = "sha256:628619008680a11d07243391271b46f07f13b75deb9fe92ef342305058c70722"}, - {file = "yarl-1.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:bc23d870864971c8455cfba17498ccefa53a5719ea9f5fce5e7e9c1606b5755f"}, - {file = "yarl-1.9.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d8cf3d0b67996edc11957aece3fbce4c224d0451c7c3d6154ec3a35d0e55f6b"}, - {file = "yarl-1.9.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a7748cd66fef49c877e59503e0cc76179caf1158d1080228e67e1db14554f08"}, - {file = "yarl-1.9.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a6fa3aeca8efabb0fbbb3b15e0956b0cb77f7d9db67c107503c30af07cd9e00"}, - {file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf37dd0008e5ac5c3880198976063c491b6a15b288d150d12833248cf2003acb"}, - {file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87aa5308482f248f8c3bd9311cd6c7dfd98ea1a8e57e35fb11e4adcac3066003"}, - {file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:867b13c1b361f9ba5d2f84dc5408082f5d744c83f66de45edc2b96793a9c5e48"}, - {file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ce93947554c2c85fe97fc4866646ec90840bc1162e4db349b37d692a811755"}, - {file = "yarl-1.9.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcd3d94b848cba132f39a5b40d80b0847d001a91a6f35a2204505cdd46afe1b2"}, - {file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d06d6a8f98dd87646d98f0c468be14b201e47ec6092ad569adf835810ad0dffb"}, - {file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:91567ff4fce73d2e7ac67ed5983ad26ba2343bc28cb22e1e1184a9677df98d7c"}, - {file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d5594512541e63188fea640b7f066c218d2176203d6e6f82abf702ae3dca3b2"}, - {file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c2743e43183e4afbb07d5605693299b8756baff0b086c25236c761feb0e3c56"}, - {file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daa69a3a2204355af39f4cfe7f3870d87c53d77a597b5100b97e3faa9460428b"}, - {file = "yarl-1.9.7-cp312-cp312-win32.whl", hash = "sha256:36b16884336c15adf79a4bf1d592e0c1ffdb036a760e36a1361565b66785ec6c"}, - {file = "yarl-1.9.7-cp312-cp312-win_amd64.whl", hash = "sha256:2ead2f87a1174963cc406d18ac93d731fbb190633d3995fa052d10cefae69ed8"}, - {file = "yarl-1.9.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:808eddabcb6f7b2cdb6929b3e021ac824a2c07dc7bc83f7618e18438b1b65781"}, - {file = "yarl-1.9.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:395ab0d8ce6d104a988da429bcbfd445e03fb4c911148dfd523f69d13f772e47"}, - {file = "yarl-1.9.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49827dfccbd59c4499605c13805e947349295466e490860a855b7c7e82ec9c75"}, - {file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b8bbdd425d0978311520ea99fb6c0e9e04e64aee84fac05f3157ace9f81b05"}, - {file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71d33fd1c219b5b28ee98cd76da0c9398a4ed4792fd75c94135237db05ba5ca8"}, - {file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62440431741d0b7d410e5cbad800885e3289048140a43390ecab4f0b96dde3bb"}, - {file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db97210433366dfba55590e48285b89ad0146c52bf248dd0da492dd9f0f72cf"}, - {file = "yarl-1.9.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:653597b615809f2e5f4dba6cd805608b6fd3597128361a22cc612cf7c7a4d1bf"}, - {file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df47612129e66f7ce7c9994d4cd4e6852f6e3bf97699375d86991481796eeec8"}, - {file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5e338b6febbae6c9fe86924bac3ea9c1944e33255c249543cd82a4af6df6047b"}, - {file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e649d37d04665dddb90994bbf0034331b6c14144cc6f3fbce400dc5f28dc05b7"}, - {file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0a1b8fd849567be56342e988e72c9d28bd3c77b9296c38b9b42d2fe4813c9d3f"}, - {file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9d715b2175dff9a49c6dafdc2ab3f04850ba2f3d4a77f69a5a1786b057a9d45"}, - {file = "yarl-1.9.7-cp313-cp313-win32.whl", hash = "sha256:bc9233638b07c2e4a3a14bef70f53983389bffa9e8cb90a2da3f67ac9c5e1842"}, - {file = "yarl-1.9.7-cp313-cp313-win_amd64.whl", hash = "sha256:62e110772330d7116f91e79cd83fef92545cb2f36414c95881477aa01971f75f"}, - {file = "yarl-1.9.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a564155cc2194ecd9c0d8f8dc57059b822a507de5f08120063675eb9540576aa"}, - {file = "yarl-1.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03e917cc44a01e1be60a83ee1a17550b929490aaa5df2a109adc02137bddf06b"}, - {file = "yarl-1.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eefda67ba0ba44ab781e34843c266a76f718772b348f7c5d798d8ea55b95517f"}, - {file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316c82b499b6df41444db5dea26ee23ece9356e38cea43a8b2af9e6d8a3558e4"}, - {file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10452727843bc847596b75e30a7fe92d91829f60747301d1bd60363366776b0b"}, - {file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:050f3e4d886be55728fef268587d061c5ce6f79a82baba71840801b63441c301"}, - {file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0aabe557446aa615693a82b4d3803c102fd0e7a6a503bf93d744d182a510184"}, - {file = "yarl-1.9.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23404842228e6fa8ace235024519df37f3f8e173620407644d40ddca571ff0f4"}, - {file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:34736fcc9d6d7080ebbeb0998ecb91e4f14ad8f18648cf0b3099e2420a225d86"}, - {file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:48f7a158f3ca67509d21cb02a96964e4798b6f133691cc0c86cf36e26e26ec8f"}, - {file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6639444d161c693cdabb073baaed1945c717d3982ecedf23a219bc55a242e728"}, - {file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:1cd450e10cb53d63962757c3f6f7870be49a3e448c46621d6bd46f8088d532de"}, - {file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74d3ef5e81f81507cea04bf5ae22f18ef538607a7c754aac2b6e3029956a2842"}, - {file = "yarl-1.9.7-cp38-cp38-win32.whl", hash = "sha256:4052dbd0c900bece330e3071c636f99dff06e4628461a29b38c6e222a427cf98"}, - {file = "yarl-1.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:dd08da4f2d171e19bd02083c921f1bef89f8f5f87000d0ffc49aa257bc5a9802"}, - {file = "yarl-1.9.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ab906a956d2109c6ea11e24c66592b06336e2743509290117f0f7f47d2c1dd3"}, - {file = "yarl-1.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8ad761493d5aaa7ab2a09736e62b8a220cb0b10ff8ccf6968c861cd8718b915"}, - {file = "yarl-1.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d35f9cdab0ec5e20cf6d2bd46456cf599052cf49a1698ef06b9592238d1cf1b1"}, - {file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a48d2b9f0ae29a456fb766ae461691378ecc6cf159dd9f938507d925607591c3"}, - {file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf85599c9336b89b92c313519bcaa223d92fa5d98feb4935a47cce2e8722b4b8"}, - {file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e8916b1ff7680b1f2b1608c82dc15c569b9f2cb2da100c747c291f1acf18a14"}, - {file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c80890e0a64fb0e5f71350d48da330995073881f8b8e623154aef631febfb0"}, - {file = "yarl-1.9.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9163d21aa40ff8528db2aee2b0b6752efe098055b41ab8e5422b2098457199fe"}, - {file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:65e3098969baf221bb45e3b2f60735fc2b154fc95902131ebc604bae4c629ea6"}, - {file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cddebd096effe4be90fd378e4224cd575ac99e1c521598a6900e94959006e02e"}, - {file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8525f955a2dcc281573b6aadeb8ab9c37e2d3428b64ca6a2feec2a794a69c1da"}, - {file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d585c7d834c13f24c7e3e0efaf1a4b7678866940802e11bd6c4d1f99c935e6b"}, - {file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78805148e780a9ca66f3123e04741e344b66cf06b4fb13223e3a209f39a6da55"}, - {file = "yarl-1.9.7-cp39-cp39-win32.whl", hash = "sha256:3f53df493ec80b76969d6e1ae6e4411a55ab1360e02b80c84bd4b33d61a567ba"}, - {file = "yarl-1.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:c81c28221a85add23a0922a6aeb2cdda7f9723e03e2dfae06fee5c57fe684262"}, - {file = "yarl-1.9.7-py3-none-any.whl", hash = "sha256:49935cc51d272264358962d050d726c3e5603a616f53e52ea88e9df1728aa2ee"}, - {file = "yarl-1.9.7.tar.gz", hash = "sha256:f28e602edeeec01fc96daf7728e8052bc2e12a672e2a138561a1ebaf30fd9df7"}, + {file = "yarl-1.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e61b2019ebb5345510b833c4dd1f4afb1f0c07753f86f184c63836ffc3fb08ba"}, + {file = "yarl-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:25a4e29ee758596b2a0daffa4814714e9b464077ca862baf78ed0e8698e46b61"}, + {file = "yarl-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:928f7a61c4311f3dd003af19bb779f99683f97a0559b765c80fdb8846dab0452"}, + {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b25de7e85ba90b2ff230153123b6b000a7f69c41d84a3a0dc3f878334c8509"}, + {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0127bc2ea72c1eaae6808ace661f0edf222f32ffa987d37f2dbb4798288f2656"}, + {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2508ee2bad8381b5254eadc35d32fe800d12eb2c63b744183341f3a66e435a7"}, + {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748dcacc19c69957f7063ea4fb359fa2180735b1a638c81a4a96b86a382a6f29"}, + {file = "yarl-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4d9c221cc8e32b14196498679bf2b324bec1d1127c4ba934d98e19298faa661"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:676d7356bb30825b7dbdad4fdd7a9feac379d074e5d4a36299767d72857ded42"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5eef9804e65eb292e9c5587e88fe6a27a11f121d358312ac47211e8f42876751"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2bece7fdc13e23db005879b67190db0d397f6ba89c81dc7e3c77e9f5819aff7f"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e1ddf05eeb422810b1aa919095db0691493442eebbf9cfb0f1e478a7b2fbdf3d"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:85e273e59b8b1a5f60a89df82cddeaf918181abd7ae7a2f2f899b68b0c774ff1"}, + {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d772ae3c12d3b8629db656050c86ee66924eaa98f7125a889175a59cfaafdb19"}, + {file = "yarl-1.15.0-cp310-cp310-win32.whl", hash = "sha256:4b1ab96a1ac91bd1233706d638ade35f663684deaa4e5e5f190858cba044afb9"}, + {file = "yarl-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2fe45c1143eefb680a4589c55e671fabd482a7f8c7791f311ea3bcc20139246"}, + {file = "yarl-1.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c8b034b60e74fb29064f765851e77e5910055e1c4a3cb75c32eccf2b470fc00f"}, + {file = "yarl-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70ac7893e67a81ed1346ee3e71203ca4b0c3550c005b1d1cf87bc1e61eecd04b"}, + {file = "yarl-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6237637b496bc04819190e724a4e61ff2f251abf432f70cf491b3bc4a3f2f253"}, + {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cc25cbd9ae01d49ac7b504ef5f3cbdcc8d139f9750dcfa0b80d405b4645cc2"}, + {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4f882e42c6cea89488b9a16919edde8c0b1a98f307c05abdd3dd3bc4368af40"}, + {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7711d83dafe52cda16ff2dd205cd83c05e4c06d5aaac596ae2cf7d50d094a530"}, + {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3b08d9e98d1a15338fcfbd52c02003704322c2d460c9b9be7df08f2952bdce6"}, + {file = "yarl-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06040266b5e6512a37b4703684d1798124764b43328254799e9678c588882a6"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97fcaf530318369da3cfd6ff52f5ab38daf8cb10ecee9a76efebf8031de09eef"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:994d27b24b61b1870f3571395c840433faabec5dcd239bd11ff6af7e34234bb6"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:18614630533ac37ec373bd8035aec8fa4dd9aedac641209c06de7e082622ff77"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c9c405ca78c70c3599d8956e53be0c9def9c51ad949964a49ad96c79729a5b1a"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4c5ff3e7609c214667c7d7e00d5f4f3576fefde47ebcb7e492c015117dafebbf"}, + {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fac5416c44e8e1d8ea9440096f88e1a7273257f3157184c5c715060e0c448a1"}, + {file = "yarl-1.15.0-cp311-cp311-win32.whl", hash = "sha256:a94c9058c5703c172904103d7b479f7e23dd4e5f8e67b49f6cd256d35ff169cb"}, + {file = "yarl-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:229f222bb47cd7ab225648efd1ae47fe6943f18e4c91bce66471faf09fe33128"}, + {file = "yarl-1.15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9084d99933824ed8d665f10f4ce62d08fed714e7678d5ff11a8c2c98b2dc18f9"}, + {file = "yarl-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df57f3c3ef760489f2e82192e6c93286c2bc80d6d854ef940e5345ae7153cd4b"}, + {file = "yarl-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ca160b4c649f0d56daef04751eef4571de46ed4b80f9051a87d090fef32f08e"}, + {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27c323b28723faed046f906c70466144c4dd12046a0128a301b29a65cfeff758"}, + {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2eafb4e92f72a3b6c27f1d5e921d046e2728850af8887f86857c3fe868a5b5c0"}, + {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06306c74f0775621a70fa5acd292119bbb6961d1f9a5f3d657a4c8c15b86f7b9"}, + {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f713d8f3c4e2eac0d91b741e8ef2e1082022de244685601ec83e899b445d86a"}, + {file = "yarl-1.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d816969b55a970b3accc7f9e4ea8f60043e3f7de96f21c06063d747ffc2f18ba"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e24a778470f3a9e9c11250d09daf5dea93369bc51aefca6605dbc963737a117"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d3f5e201bd170fb97c643e84df58e221372cd053fbb291ebbd878b165ea5057e"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4424082edff76fe46ff08851e91865097c0ad780fa79b87063dc5d5b80efc9d6"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:350b468a217d433cbb4482e9414a14dfd360a3d5ab92013175925abb234364cc"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c7f2deac59dc3e0528bdded248e637e789e5111ba1723a8d7a262eb93e133e15"}, + {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2429a651a2191c3fb8c9de21546c6046da539034d51dcb8df52302748004593d"}, + {file = "yarl-1.15.0-cp312-cp312-win32.whl", hash = "sha256:e4f7efb38331e8327c1cc7fb2a2905a7db03d1a7fdb04706bf6465d0e44d41d4"}, + {file = "yarl-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:9ae454916aa3abe28d0ef1c21ca1e8e36a14ccf52183d465dfaccffaa7ed462c"}, + {file = "yarl-1.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3f8be3e785009ffa148e66474fea5c787ccb203b3d0bd1f22e1e22f7da0f3b3"}, + {file = "yarl-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b7f902f13a230686f01bcff17cd9ba045653069811c8fd5027f0f414b417e2f"}, + {file = "yarl-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:627bb5bc4ed3d3ebceb9fb55717cec6cd58bb47fdb5669169ebbc248e9bf156c"}, + {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1208f2e081d34832f509cbe311237a0543effe23d60b2fa14c0d3f86e6d1d07"}, + {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c791a2d42da20ac568e5c0cc9b8af313188becd203a936ad959b578dafbcebb"}, + {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd042e6c3bf36448e3e3ed302b12ce79762480f4aff8e7a167cdf8c35dc93297"}, + {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae041f535fe2e57681954f7cccb81854d777ce4c2a87749428ebe6c71c02ec0"}, + {file = "yarl-1.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:454707fb16f180984da6338d1f51897f0b8d8c4c2e0592d9d1e9fa02a5bb8218"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6960b0d2e713e726cb2914e3051e080b12412f70dcb8731cf7a8fb52c37931bb"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c9b9159eeeb7cd1c7131dc7f5878454f97a4dc20cd157e6474174ccac448b844"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fee9acd5e39c8611957074dfba06552e430020eea831caf5eb2cea30f10e06bd"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ddea4abc4606c10dddb70651b210b7ab5b663148d6d7bc85d76963c923629891"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2add8ed2acf42398dfaa7dffd32e4d18ffbae341d62c8d4765bd9929336379b5"}, + {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:32840ff92c713053591ff0e66845d4e9f4bea8fd5fba3da00f8d92e77722f24e"}, + {file = "yarl-1.15.0-cp313-cp313-win32.whl", hash = "sha256:eb964d18c01b7a1263a6f07b88d63711fcd564fc429d934279cf12f4b467bf53"}, + {file = "yarl-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ceb200918c9bd163bd390cc169b254b23b4be121026b003be93a4f2f5b554b4b"}, + {file = "yarl-1.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:83363a5789f128618041b9a737c7b146f1965abddf4294b0444591406b437c1e"}, + {file = "yarl-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2124c642b8cc9b68e5981e429842dadc32bb850b010cccec9d24236253a19f60"}, + {file = "yarl-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5107d89c9edec6ee077970a95fb9eeb4776ea8c2337b6a39c0ade9a58f50f3e4"}, + {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33896afca6fb4e1988c099534c52823870dfc8730bc6f96a3831f24c1e0ab814"}, + {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4224bbbc8a2e9b9a3828d36c1bab7458441d7fb9fb3af321eb735732ba8ee89d"}, + {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1edaf4171fc1582352ac5d9b2783966fa0f4ff86187279ef2a491613d23b894a"}, + {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73c4af08e9bb9a9aa7df6c789b05b924b9a0c6a368bb0e418d0b85181b64b631"}, + {file = "yarl-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4aa7cca009817789fd5b8e52e8122f9e85dc580c88b816a93321c00a8acbced"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c0a86dd3e85c6aa3fc73236eb5cf7ce69dd8ad7abcd23f8ae1126831c8e40c2f"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:db903458a457a53ee0f764ed11c5b5368398e216b442c42dca9d90fbd2bbf31c"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8f074a24aa9a6a3d406474ec889ebb5d661f329349068e05e8dfcb3c4be67752"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:dc63bb79e896d6ce6aaf672ac304b54969280e949c45727867fc154a17ec7ab2"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ef780f9d480ffb423380abeb4cfcad66ecb8f93526dfa367d322fdad9ec7c25f"}, + {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e7e38bf6e52797084c5c396db5bb519615727e491e9003e2449631457bf77738"}, + {file = "yarl-1.15.0-cp38-cp38-win32.whl", hash = "sha256:d885dcdca7bae42bc9a2f6cbf766abcb2a6cc043b1905fc3782c6ea1f74a2b95"}, + {file = "yarl-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:e4f64c8c52dde564bf3251b41d7a6746564b0fc0516cebe9c9e6695224440d22"}, + {file = "yarl-1.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5156c12a97405339ec93facbc7860566db381af2de1bec338195563fb64f37ef"}, + {file = "yarl-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e5fa4c4e55cdacef1844f609bc9a02c8cd29c324a71ca1d3ee454701d4bb496"}, + {file = "yarl-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e1cc7823f43781390965c4762b54262cfcf76b6f152e489d00a5a1ac63063e4"}, + {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38cab8f91b1085f1fd0765d40c46c8f43282f109018d5fcd017c46ac3eaba0cf"}, + {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe72c41cdd55c88b238a8925849fde4069c0cdcdef83f8d967f8f3982659326"}, + {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0358b697abdf1f2d68038bd02ef8ddcc4813835744f79c755f8743aa485585e7"}, + {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b93a666cd8cfd43f605d1b81a32b9e290bf45c74c2bfd51ba705449c78448c7"}, + {file = "yarl-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81edbd9bf9f25cd995e6d51c307e1d279587d40b7473e258fef6d5e548560cd2"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad2e487824ba4cda87851a371139e255410e45d3bf2e334194789278d709cec"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:553a1e3537aeeb29c0eb29ef28b80e0e801697fa71d96ac60675b284ff8e582a"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:06b5b462cadf59c1df28ffbb0a3971fa16b60cf0c9d59a38bf5679a986d18685"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:097094a979af7b31520517c59179f6817b8426724343cecbec0eb3af1f8fb6cf"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:75d9762f65205a86381298eb9079f27c60b84de0c262e402dcf45c6cbc385234"}, + {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e2e3cb74684ff357e6b3c82dd71031d3c1fd7ee9f9b0a5205e5568c963e074f9"}, + {file = "yarl-1.15.0-cp39-cp39-win32.whl", hash = "sha256:a616c2e4b60cb8cdd9eb3b0c6fda4ab5f3e26244b427aaade560dcf63c5754fb"}, + {file = "yarl-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:7aa9f9af452c3e8486a0b88fddd58352e6cea17b691b18861d26e46cf65ffff0"}, + {file = "yarl-1.15.0-py3-none-any.whl", hash = "sha256:1656a8b531a96427f26f498b7d0f19931166ff30e4344eca99bdb27faca14fc5"}, + {file = "yarl-1.15.0.tar.gz", hash = "sha256:efc0430b80ed834c80c99c32946cfc6ee29dfcd7c62ad3c8f15657322ade7942"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" +propcache = ">=0.2.0" [[package]] name = "yc-protobuf3-to-dict" @@ -2439,13 +2551,13 @@ python-dateutil = ">=2.8.2,<3.0.0" [[package]] name = "zipp" -version = "3.20.1" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] @@ -2456,7 +2568,11 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] +[extras] +ftp = ["aioftp"] +s3 = ["s3lite"] + [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c163191f3c7a8976f056ff27de47b70d3b5676aa81b784653c7ebf0358005b4e" +content-hash = "7de0fd930cfbf57ee8f25cdc51bc607178952ae559a7b28b226388bfd22793a6" diff --git a/pyproject.toml b/pyproject.toml index 0890406..2297dae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,8 @@ yepcord = "yepcord.cli:main" python = "^3.9" quart = "0.19.6" aiofiles = "^24.1.0" -websockets = "13.0.1" -uvicorn = "^0.30.6" +websockets = "^13.1" +uvicorn = "^0.31.0" python-magic = "^0.4.27" pillow = "^10.4.0" protobuf = "4.25.3" @@ -51,7 +51,7 @@ bcrypt = "^4.2.0" quart-schema = "0.20.0" pydantic = "^2.8.2" werkzeug = "3.0.4" -aioftp = "^0.22.3" +aioftp = { version = "^0.22.3", optional = true } orjson = "^3.10.7" mailers = {version = "^3.0.5", extras = ["smtp"]} redis = "^5.0.8" @@ -63,7 +63,7 @@ uvloop = "^0.20.0" async-timeout = "^4.0.3" aerich = "^0.7.2" yc-protobuf3-to-dict = "^0.3.0" -s3lite = "^0.1.8" +s3lite = { version = "^0.1.8", optional = true } fast-depends = "^2.4.11" faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.20"} @@ -75,7 +75,11 @@ pyftpdlib = "1.5.8" fake-s3 = "1.0.2" types-protobuf = "^4.24.0.4" pytest-httpx = "^0.30.0" +poethepoet = "^0.29.0" +[tool.poetry.extras] +s3 = ["s3lite"] +ftp = ["aioftp"] [tool.poetry.group.profiling.dependencies] viztracer = "^0.16.3" @@ -83,3 +87,17 @@ viztracer = "^0.16.3" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.coverage.run] +omit = ["yepcord/yepcord/proto.py"] +data_file = "coverage.coverage" + +[tool.poe.tasks] +migrate = "yepcord.cli migrate" +run = "yepcord.cli:main" +test = "pytest -x --cov-report=xml --cov-append --disable-warnings --cov=yepcord/yepcord" +i = "poetry install" +ie = "poetry install --all-extras" +u = "poetry update" +a = "poetry add" +ad = "poetry add --group dev" From aa7e28fb46bb66924830e211d58728fd564a9ac7 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 18:49:34 +0300 Subject: [PATCH 15/26] move geoip code to GeoIp class; remove all references to "Core" class; remove ctx.getStorage and ctx.getCore --- tests/cdn/test_storage_and_cdn.py | 4 +- tests/test_core.py | 31 ++++-------- yepcord/gateway/events.py | 1 - yepcord/gateway/gateway.py | 12 ++--- yepcord/rest_api/dependencies.py | 3 +- yepcord/rest_api/main.py | 3 +- yepcord/rest_api/routes/applications.py | 8 ++-- yepcord/rest_api/routes/auth.py | 12 ++--- yepcord/rest_api/routes/channels.py | 13 +++-- yepcord/rest_api/routes/guilds.py | 27 ++++++----- yepcord/rest_api/routes/interactions.py | 4 +- yepcord/rest_api/routes/invites.py | 2 +- yepcord/rest_api/routes/oauth2.py | 10 ++-- yepcord/rest_api/routes/users_me.py | 7 +-- yepcord/rest_api/routes/webhooks.py | 5 +- yepcord/rest_api/utils.py | 4 +- yepcord/yepcord/core.py | 47 +------------------ yepcord/yepcord/ctx.py | 12 ----- yepcord/yepcord/models/channel.py | 2 +- yepcord/yepcord/models/interaction.py | 1 - yepcord/yepcord/models/message.py | 1 - yepcord/yepcord/models/user.py | 13 +++-- yepcord/yepcord/storage.py | 13 ++++- .../yepcord/{utils.py => utils/__init__.py} | 2 + yepcord/yepcord/utils/geoip.py | 30 ++++++++++++ 25 files changed, 117 insertions(+), 150 deletions(-) rename yepcord/yepcord/{utils.py => utils/__init__.py} (99%) create mode 100644 yepcord/yepcord/utils/geoip.py diff --git a/tests/cdn/test_storage_and_cdn.py b/tests/cdn/test_storage_and_cdn.py index 3be3c98..70bb105 100644 --- a/tests/cdn/test_storage_and_cdn.py +++ b/tests/cdn/test_storage_and_cdn.py @@ -25,12 +25,11 @@ from yepcord.cdn.main import app from yepcord.yepcord.config import Config -from yepcord.yepcord.core import Core from yepcord.yepcord.enums import StickerFormat, StickerType, ChannelType from yepcord.yepcord.models import User, Sticker, Emoji, Channel, Message, Attachment, Guild from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.storage import getStorage, _Storage -from yepcord.yepcord.utils import getImage, b64decode +from yepcord.yepcord.utils import getImage from .ftp_server import ftp_server from .local_server import local_server from .s3_server import s3_server @@ -39,7 +38,6 @@ register_app_error_handler(app) TestClientType = app.test_client_class -core = Core() @pt.fixture diff --git a/tests/test_core.py b/tests/test_core.py index dc67090..bb2c089 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -20,7 +20,6 @@ from datetime import date from json import dumps from random import randint -from typing import Coroutine, Any import pytest as pt import pytest_asyncio @@ -28,22 +27,19 @@ from yepcord.yepcord.classes.other import MFA from yepcord.yepcord.config import Config, ConfigModel -from yepcord.yepcord.core import Core from yepcord.yepcord.enums import UserFlags as UserFlagsE, RelationshipType, ChannelType, GuildPermissions, MfaNonceType from yepcord.yepcord.errors import InvalidDataErr, MfaRequiredErr from yepcord.yepcord.gateway_dispatcher import GatewayDispatcher from yepcord.yepcord.models import User, UserData, Session, Relationship, Guild, Channel, Role, PermissionOverwrite, \ GuildMember, Message from yepcord.yepcord.snowflake import Snowflake -from yepcord.yepcord.utils import b64encode +from yepcord.yepcord.utils import b64encode, GeoIp EMAIL_ID = Snowflake.makeId() VARS = { "user_id": Snowflake.makeId() } -core = Core() - @pt.fixture def event_loop(): @@ -59,11 +55,6 @@ async def setup_db(): await Tortoise.close_connections() -@pt.fixture(name='testCore') -async def _setup_db(): - return core - - @pt.mark.asyncio async def test_register_success(): user = await User.y.register("Test Login", f"{EMAIL_ID}_test@yepcord.ml", "test_passw0rd", "2000-01-01") @@ -344,17 +335,15 @@ async def test_getChannelMessagesCount_success(): @pt.mark.asyncio -async def test_geoip(testCore: Coroutine[Any, Any, Core]): - testCore = await testCore - - assert testCore.getLanguageCode("1.1.1.1") == "en-US" - assert testCore.getLanguageCode("134.249.127.127") == "uk" - assert testCore.getLanguageCode("103.21.236.200") == "de" - assert testCore.getLanguageCode("109.241.127.127") == "pl" - assert testCore.getLanguageCode("5.65.127.127") == "en-GB" - - assert testCore.getLanguageCode("255.255.255.255") == "en-US" - assert testCore.getLanguageCode("255.255.255.255", "uk") == "uk" +async def test_geoip(): + assert GeoIp.get_language_code("1.1.1.1") == "en-US" + assert GeoIp.get_language_code("134.249.127.127") == "uk" + assert GeoIp.get_language_code("103.21.236.200") == "de" + assert GeoIp.get_language_code("109.241.127.127") == "pl" + assert GeoIp.get_language_code("5.65.127.127") == "en-GB" + + assert GeoIp.get_language_code("255.255.255.255") == "en-US" + assert GeoIp.get_language_code("255.255.255.255", "uk") == "uk" def test_config(): diff --git a/yepcord/gateway/events.py b/yepcord/gateway/events.py index 2ea2197..86533d0 100644 --- a/yepcord/gateway/events.py +++ b/yepcord/gateway/events.py @@ -23,7 +23,6 @@ from typing import List, TYPE_CHECKING, Optional from ..yepcord.config import Config -from ..yepcord.ctx import getCore from ..yepcord.enums import GatewayOp from ..yepcord.models import Emoji, Application, Integration, ConnectedAccount from ..yepcord.models.interaction import Interaction diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index b678e70..8bd236a 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -30,9 +30,8 @@ from .presences import Presences, Presence from .utils import require_auth, get_token_type, TokenType, init_redis_pool from ..yepcord.classes.fakeredis import FakeRedis -from ..yepcord.ctx import getCore from ..yepcord.enums import GatewayOp, RelationshipType -from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember +from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember, Guild from ..yepcord.mq_broker import getBroker @@ -144,10 +143,10 @@ async def handle_STATUS(self, data: dict) -> None: async def handle_LAZY_REQUEST(self, data: dict) -> None: # TODO: handle ranges if not (guild_id := int(data.get("guild_id"))): return if not data.get("members", True): return - guild = await getCore().getGuild(guild_id) - if not await GuildMember.exists(guild=guild, user__id=self.user_id): + if not await GuildMember.exists(guild__id=guild_id, user__id=self.user_id): return + guild = await Guild.get_or_none(id=guild_id) members = await GuildMember.filter(guild=guild).select_related("user") statuses = {} for member in members: @@ -165,8 +164,7 @@ async def handle_LAZY_REQUEST(self, data: dict) -> None: # TODO: handle ranges @require_auth async def handle_GUILD_MEMBERS(self, data: dict) -> None: if not (guild_id := int(data.get("guild_id")[0])): return - guild = await getCore().getGuild(guild_id) - if not await GuildMember.exists(guild=guild, user__id=self.user_id): + if not await GuildMember.exists(guild__id=guild_id, user__id=self.user_id): return query = data.get("query", "") @@ -175,7 +173,7 @@ async def handle_GUILD_MEMBERS(self, data: dict) -> None: if limit > 100 or limit < 1: limit = 100 - q = Q(guild=guild) & (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) + q = Q(guild__id=guild_id) & (Q(nick__startswith=query) | Q(user__userdatas__username__istartswith=query)) if user_ids: q &= Q(user__id__in=user_ids) members = await GuildMember.filter(q).select_related("user").limit(limit) diff --git a/yepcord/rest_api/dependencies.py b/yepcord/rest_api/dependencies.py index 89bfd82..0d1716b 100644 --- a/yepcord/rest_api/dependencies.py +++ b/yepcord/rest_api/dependencies.py @@ -24,7 +24,6 @@ from typing_extensions import ParamSpec from yepcord.rest_api.utils import getSessionFromToken -from yepcord.yepcord.ctx import getCore from yepcord.yepcord.errors import InvalidDataErr, Errors, UnknownApplication, UnknownInvite, UnknownMessage, \ UnknownRole, UnknownGuildTemplate, MissingAccess, Unauthorized from yepcord.yepcord.models import Session, Authorization, Bot, User, Channel, Message, Webhook, Invite, Guild, \ @@ -137,7 +136,7 @@ async def depInvite(invite: Optional[str] = None) -> Invite: async def depGuildO(guild: Optional[int] = None, user: User = Depends(depUser())) -> Optional[Guild]: - if (guild := await getCore().getGuild(guild)) is None: + if (guild := await Guild.get_or_none(id=guild).select_related("owner")) is None: return if not await GuildMember.filter(guild=guild, user=user).exists(): raise MissingAccess diff --git a/yepcord/rest_api/main.py b/yepcord/rest_api/main.py index ff02304..b071faf 100644 --- a/yepcord/rest_api/main.py +++ b/yepcord/rest_api/main.py @@ -39,11 +39,10 @@ from .routes.webhooks import webhooks from ..yepcord.classes.gifs import Gifs from ..yepcord.config import Config -from ..yepcord.core import Core from ..yepcord.errors import InvalidDataErr, MfaRequiredErr, YDataError, EmbedErr, Errors from ..yepcord.gateway_dispatcher import GatewayDispatcher from ..yepcord.storage import getStorage -from ..yepcord.utils import b64decode, b64encode +from ..yepcord.utils import b64encode class YEPcord(Quart): diff --git a/yepcord/rest_api/routes/applications.py b/yepcord/rest_api/routes/applications.py index 1b48d1d..b1a4a76 100644 --- a/yepcord/rest_api/routes/applications.py +++ b/yepcord/rest_api/routes/applications.py @@ -22,11 +22,11 @@ from ..models.applications import CreateApplication, UpdateApplication, UpdateApplicationBot, GetCommandsQS, \ CreateCommand from ..y_blueprint import YBlueprint -from ...yepcord.ctx import getCDNStorage from ...yepcord.enums import ApplicationCommandType from ...yepcord.models import User, UserData, UserSettings, Application, Bot, gen_secret_key, gen_token_secret, \ ApplicationCommand, Guild from ...yepcord.snowflake import Snowflake +from ...yepcord.storage import getStorage from ...yepcord.utils import getImage # Base path is /api/vX/applications @@ -71,10 +71,10 @@ async def edit_application(data: UpdateApplication, application: Application = D changes = data.model_dump(exclude_defaults=True) if "icon" in changes and changes["icon"] is not None: img = getImage(changes["icon"]) - image = await getCDNStorage().setAppIconFromBytesIO(application.id, img) + image = await getStorage().setAppIconFromBytesIO(application.id, img) changes["icon"] = image if bot_data.avatar == application.icon: - await bot_data.update(avatar=await getCDNStorage().setAvatarFromBytesIO(application.id, img)) + await bot_data.update(avatar=await getStorage().setAvatarFromBytesIO(application.id, img)) bot_changes = {} if bot is not None and "bot_public" in changes: @@ -98,7 +98,7 @@ async def edit_application_bot(data: UpdateApplicationBot, application: Applicat changes = data.model_dump(exclude_defaults=True) if "avatar" in changes and changes["avatar"] is not None: img = getImage(changes["avatar"]) - avatar_hash = await getCDNStorage().setAvatarFromBytesIO(application.id, img) + avatar_hash = await getStorage().setAvatarFromBytesIO(application.id, img) changes["avatar"] = avatar_hash if changes: diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index 1b99e63..93935bb 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -16,10 +16,7 @@ along with this program. If not, see . """ -from json import loads as jloads - from quart import request -from tortoise.exceptions import DoesNotExist from ..dependencies import DepSession, DepUser from ..models.auth import Register, Login, MfaLogin, ViewBackupCodes, VerifyEmail @@ -28,11 +25,10 @@ from ...gateway.events import UserUpdateEvent from ...yepcord.classes.other import EmailMsg, MFA, JWT from ...yepcord.config import Config -from ...yepcord.ctx import getCore, getGw -from ...yepcord.errors import InvalidDataErr, Errors, PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket, \ - InvalidToken +from ...yepcord.ctx import getGw +from ...yepcord.errors import PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket, InvalidToken from ...yepcord.models import Session, User -from ...yepcord.utils import LOCALES, b64decode +from ...yepcord.utils import LOCALES, b64decode, GeoIp # Base path is /api/vX/auth auth = YBlueprint('auth', __name__) @@ -41,7 +37,7 @@ @auth.post("/register", body_cls=Register) @captcha async def register(data: Register): - locale = getCore().getLanguageCode(request.remote_addr, request.accept_languages.best_match(LOCALES, "en-US")) + locale = GeoIp.get_language_code(request.remote_addr, request.accept_languages.best_match(LOCALES, "en-US")) user = await User.y.register(data.username, data.email, data.password, data.date_of_birth, locale) session = await Session.Y.create(user) diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 8ed5a73..f727b6d 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -27,23 +27,22 @@ from ..dependencies import DepUser, DepChannel, DepMessage from ..models.channels import ChannelUpdate, MessageCreate, MessageUpdate, InviteCreate, PermissionOverwriteModel, \ - WebhookCreate, GetReactionsQuery, MessageAck, CreateThread, CommandsSearchQS, SearchQuery, \ - GetMessagesQuery + WebhookCreate, GetReactionsQuery, MessageAck, CreateThread, CommandsSearchQS, SearchQuery, GetMessagesQuery from ..utils import _getMessage, processMessage from ..y_blueprint import YBlueprint from ...gateway.events import MessageCreateEvent, TypingEvent, MessageDeleteEvent, MessageUpdateEvent, \ DMChannelCreateEvent, DMChannelUpdateEvent, ChannelRecipientAddEvent, ChannelRecipientRemoveEvent, \ DMChannelDeleteEvent, MessageReactionAddEvent, MessageReactionRemoveEvent, ChannelUpdateEvent, ChannelDeleteEvent, \ WebhooksUpdateEvent, ThreadCreateEvent, ThreadMemberUpdateEvent, MessageAckEvent, GuildAuditLogEntryCreateEvent -from ...yepcord.ctx import getCore, getCDNStorage, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import GuildPermissions, MessageType, ChannelType, WebhookType, GUILD_CHANNELS, MessageFlags -from ...yepcord.errors import UnknownMessage, UnknownUser, UnknownEmoji, UnknownInteraction, \ - MaxPinsReached, MissingPermissions, CannotSendToThisUser, CannotExecuteOnDM, CannotEditAnotherUserMessage, \ - MissingAccess +from ...yepcord.errors import UnknownMessage, UnknownUser, UnknownEmoji, UnknownInteraction, MaxPinsReached, \ + MissingPermissions, CannotSendToThisUser, CannotExecuteOnDM, CannotEditAnotherUserMessage, MissingAccess from ...yepcord.models import User, Channel, Message, ReadState, Emoji, PermissionOverwrite, Webhook, ThreadMember, \ ThreadMetadata, AuditLogEntry, Relationship, ApplicationCommand, Integration, Bot, Role, HiddenDmChannel, Invite, \ Reaction from ...yepcord.snowflake import Snowflake +from ...yepcord.storage import getStorage from ...yepcord.utils import getImage, b64encode, b64decode # Base path is /api/vX/channels @@ -70,7 +69,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha del changes["owner_id"] if "icon" in changes and changes["icon"] is not None: img = getImage(changes["icon"]) - image = await getCDNStorage().setChannelIconFromBytesIO(channel.id, img) + image = await getStorage().setChannelIconFromBytesIO(channel.id, img) changes["icon"] = image if "icon" in changes and changes["icon"] != channel.icon: changed.append("icon") if "name" in changes and changes["name"] != channel.name: changed.append("name") diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index df36bad..b125660 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -38,7 +38,7 @@ GuildScheduledEventCreateEvent, GuildScheduledEventUpdateEvent, GuildScheduledEventDeleteEvent, \ ScheduledEventUserAddEvent, ScheduledEventUserRemoveEvent, GuildCreateEvent, GuildAuditLogEntryCreateEvent, \ IntegrationDeleteEvent, GuildIntegrationsUpdateEvent -from ...yepcord.ctx import getCore, getCDNStorage, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import GuildPermissions, StickerType, StickerFormat, ScheduledEventStatus, ChannelType, \ ScheduledEventEntityType from ...yepcord.errors import InvalidDataErr, Errors, UnknownRole, UnknownUser, UnknownGuildTemplate, UnknownSticker, \ @@ -47,6 +47,7 @@ from ...yepcord.models import User, Guild, GuildMember, GuildTemplate, Emoji, Channel, PermissionOverwrite, UserData, \ Role, Invite, Sticker, GuildEvent, AuditLogEntry, Integration, ApplicationCommand, Webhook, GuildBan from ...yepcord.snowflake import Snowflake +from ...yepcord.storage import getStorage from ...yepcord.utils import getImage, b64decode, validImage, imageType # Base path is /api/vX/guilds @@ -58,7 +59,7 @@ async def create_guild(data: GuildCreate, user: User = DepUser): guild = await Guild.Y.create(user, data.name) if data.icon: img = getImage(data.icon) - if icon := await getCDNStorage().setGuildIconFromBytesIO(guild.id, img): + if icon := await getStorage().setGuildIconFromBytesIO(guild.id, img): guild.icon = icon await guild.save(update_fields=["icon"]) @@ -79,9 +80,9 @@ async def update_guild(data: GuildUpdate, user: User = DepUser, guild: Guild = D member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) data.owner_id = None # TODO: make guild ownership transfer - for image_type, func in (("icon", getCDNStorage().setGuildIconFromBytesIO), - ("banner", getCDNStorage().setBannerFromBytesIO), - ("splash", getCDNStorage().setGuildSplashFromBytesIO)): + for image_type, func in (("icon", getStorage().setGuildIconFromBytesIO), + ("banner", getStorage().setBannerFromBytesIO), + ("splash", getStorage().setGuildSplashFromBytesIO)): if img := getattr(data, image_type): setattr(data, image_type, "") img = getImage(img) @@ -177,7 +178,7 @@ async def create_guild_emoji(data: EmojiCreate, user: User = DepUser, guild: Gui await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) img = getImage(data.image) emoji_id = Snowflake.makeId() - result = await getCDNStorage().setEmojiFromBytesIO(emoji_id, img) + result = await getStorage().setEmojiFromBytesIO(emoji_id, img) emoji = await Emoji.create(id=emoji_id, name=data.name, user=user, guild=guild, animated=result["animated"]) await getGw().sendGuildEmojisUpdateEvent(guild) @@ -423,7 +424,7 @@ async def create_role(data: RoleCreate, user: User = DepUser, guild: Guild = Dep role_id = Snowflake.makeId() if data.icon: img = getImage(data.icon) - if h := await getCDNStorage().setRoleIconFromBytesIO(role_id, img): + if h := await getStorage().setRoleIconFromBytesIO(role_id, img): data.icon = h role = await Role.create(id=role_id, guild=guild, **data.model_dump()) await getGw().dispatch(GuildRoleCreateEvent(guild.id, role.ds_json()), guild_id=guild.id, @@ -445,7 +446,7 @@ async def update_role(data: RoleUpdate, user: User = DepUser, guild: Guild = Dep if role.id != guild.id and data.icon != "" and (img := data.icon) is not None: data.icon = "" img = getImage(img) - if h := await getCDNStorage().setRoleIconFromBytesIO(role.id, img): + if h := await getStorage().setRoleIconFromBytesIO(role.id, img): data.icon = h if role.id == guild.id: # Only allow permissions editing for @everyone role changes = {"permissions": data.permissions} if data.permissions is not None else {} @@ -600,7 +601,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs if img is not None: img = getImage(img) data.avatar = "" - if av := await getCDNStorage().setGuildAvatarFromBytesIO(user.id, guild.id, img): + if av := await getStorage().setGuildAvatarFromBytesIO(user.id, guild.id, img): data.avatar = av changes = data.model_dump(exclude_defaults=True) await target_member.update(**changes) @@ -687,7 +688,7 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use guild = await Guild.Y.create_from_template(user, template, data.name) if data.icon and (img := getImage(data.icon)) is not None: - if icon := await getCDNStorage().setGuildIconFromBytesIO(guild.id, img): + if icon := await getStorage().setGuildIconFromBytesIO(guild.id, img): guild.icon = icon await guild.save(update_fields=["icon"]) @@ -759,7 +760,7 @@ async def upload_guild_stickers(user: User = DepUser, guild: Guild = DepGuild, m sticker_type = getattr(StickerFormat, str(imageType(img)).upper(), StickerFormat.PNG) sticker_id = Snowflake.makeId() - await getCDNStorage().setStickerFromBytesIO(sticker_id, img) + await getStorage().setStickerFromBytesIO(sticker_id, img) sticker = await Sticker.create( id=sticker_id, guild=guild, user=user, name=data.name, tags=data.tags, type=StickerType.GUILD, @@ -806,7 +807,7 @@ async def create_scheduled_event(data: CreateEvent, user: User = DepUser, guild: "code": "IMAGE_INVALID", "message": "Invalid image" }})) - img = await getCDNStorage().setGuildEventFromBytesIO(event_id, img) + img = await getStorage().setGuildEventFromBytesIO(event_id, img) data.image = img data_dict = data.model_dump() @@ -859,7 +860,7 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil raise InvalidDataErr(400, Errors.make(50035, {"image": { "code": "IMAGE_INVALID", "message": "Invalid image" }})) - if h := await getCDNStorage().setGuildEventFromBytesIO(event.id, img): + if h := await getStorage().setGuildEventFromBytesIO(event.id, img): img = h data.image = img diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index f873b85..92c6ee9 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -24,7 +24,7 @@ from ..y_blueprint import YBlueprint from ...gateway.events import InteractionCreateEvent, InteractionFailureEvent, MessageCreateEvent, \ InteractionSuccessEvent -from ...yepcord.ctx import getCore, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import GuildPermissions, InteractionStatus, MessageFlags, InteractionCallbackType, \ ApplicationCommandOptionType, MessageType, ApplicationCommandType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, UnknownMessage, \ @@ -125,7 +125,7 @@ async def create_interaction(user: User = DepUser): guild = None channel = await Channel.Y.get(data.channel_id) if data.guild_id: - if (guild := await getCore().getGuild(data.guild_id)) is None: + if (guild := await Guild.get_or_none(id=data.guild_id)) is None: raise UnknownGuild if (member := await guild.get_member(user.id)) is None: raise MissingAccess diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index c1d9f96..54a7ecc 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -21,7 +21,7 @@ from ..y_blueprint import YBlueprint from ...gateway.events import MessageCreateEvent, DMChannelCreateEvent, ChannelRecipientAddEvent, GuildCreateEvent, \ InviteDeleteEvent -from ...yepcord.ctx import getCore, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import ChannelType, GuildPermissions, MessageType from ...yepcord.errors import UnknownInvite, UserBanned, MissingAccess from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan, Channel diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index 32b7900..0083f32 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -27,7 +27,7 @@ from ...gateway.events import GuildCreateEvent, MessageCreateEvent, GuildAuditLogEntryCreateEvent, \ GuildRoleCreateEvent, IntegrationCreateEvent from ...yepcord.config import Config -from ...yepcord.ctx import getCore, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownGuild, MissingAccess from ...yepcord.models import User, Guild, GuildMember, Message, Role, AuditLogEntry, Application, Bot, Authorization, \ @@ -100,7 +100,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori return {"location": f"https://{Config.PUBLIC_HOST}/oauth2/error?error=invalid_request&error_description=" f"Guild+not+specified."} - if (guild := await getCore().getGuild(data.guild_id)) is None: + if (guild := await Guild.get_or_none(id=data.guild_id).select_related("owner")) is None: raise UnknownGuild if not (member := await guild.get_member(user.id)): @@ -130,8 +130,10 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori await AuditLogEntry.utils.integration_create(user, guild, bot.user), ] for entry in entries: - await getGw().dispatch(GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, - permissions=GuildPermissions.VIEW_AUDIT_LOG) + await getGw().dispatch( + GuildAuditLogEntryCreateEvent(entry.ds_json()), guild_id=guild.id, + permissions=GuildPermissions.VIEW_AUDIT_LOG + ) await getGw().dispatch(GuildCreateEvent(await guild.ds_json(user_id=bot.user.id)), user_ids=[bot.user.id]) if (sys_channel := await Channel.Y.get(guild.system_channel)) is not None: diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 496b223..737f9b6 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -31,7 +31,7 @@ UserConnectionsUpdate from ...yepcord.classes.other import MFA from ...yepcord.config import Config -from ...yepcord.ctx import getCore, getCDNStorage, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import RelationshipType, ChannelType, MfaNonceType from ...yepcord.errors import InvalidDataErr, Errors, UnknownToken, UnknownUser, UnknownConnection, UnknownDiscordTag, \ AlreadyFriends, MalformedUserSettings, Already2Fa, PasswordDoesNotMatch, Invalid2FaSecret, Invalid2FaCode, \ @@ -40,6 +40,7 @@ GuildMember, RemoteAuthSession, Relationship, Authorization, Bot, ConnectedAccount, GuildEvent, Channel from ...yepcord.models.remote_auth_session import time_plus_150s from ...yepcord.proto import FrecencyUserSettings, PreloadedUserSettings +from ...yepcord.storage import getStorage from ...yepcord.utils import execute_after, validImage, getImage # Base path is /api/vX/users/@me @@ -80,7 +81,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): data.email = None if data.avatar != "" and data.avatar is not None: if (img := getImage(data.avatar)) and validImage(img): - if avatar := await getCDNStorage().setAvatarFromBytesIO(user.id, img): + if avatar := await getStorage().setAvatarFromBytesIO(user.id, img): data.avatar = avatar await userdata.refresh_from_db() @@ -96,7 +97,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): async def get_my_profile(data: UserProfileUpdate, user: User = DepUser): if data.banner != "" and data.banner is not None: if (img := getImage(data.banner)) and validImage(img): - if banner := await getCDNStorage().setBannerFromBytesIO(user.id, img): + if banner := await getStorage().setBannerFromBytesIO(user.id, img): data.banner = banner userdata = await user.data diff --git a/yepcord/rest_api/routes/webhooks.py b/yepcord/rest_api/routes/webhooks.py index d41a429..969bae6 100644 --- a/yepcord/rest_api/routes/webhooks.py +++ b/yepcord/rest_api/routes/webhooks.py @@ -26,10 +26,11 @@ from ..utils import processMessageData, process_stickers, validate_reply, processMessage from ..y_blueprint import YBlueprint from ...gateway.events import MessageCreateEvent, WebhooksUpdateEvent, MessageUpdateEvent -from ...yepcord.ctx import getCore, getCDNStorage, getGw +from ...yepcord.ctx import getGw from ...yepcord.enums import GuildPermissions, MessageFlags from ...yepcord.errors import UnknownWebhook, MissingPermissions, CannotSendEmptyMessage from ...yepcord.models import User, Channel, Message, Webhook +from ...yepcord.storage import getStorage from ...yepcord.utils import getImage # Base path is /api/vX/webhooks @@ -79,7 +80,7 @@ async def edit_webhook( if (img := data.avatar) or img is None: if img is not None: img = getImage(img) - if h := await getCDNStorage().setAvatarFromBytesIO(webhook.id, img): + if h := await getStorage().setAvatarFromBytesIO(webhook.id, img): img = h data.avatar = img diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index c4d29d7..399b279 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -29,12 +29,12 @@ import yepcord.yepcord.models as models from ..yepcord.classes.captcha import Captcha from ..yepcord.config import Config -from ..yepcord.ctx import getCore, getCDNStorage from ..yepcord.enums import MessageType from ..yepcord.errors import Errors, InvalidDataErr, UnknownChannel, UnknownMessage, InvalidFormBody, \ FileExceedsMaxSize, CannotSendEmptyMessage from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message, Sticker from ..yepcord.snowflake import Snowflake +from ..yepcord.storage import getStorage if TYPE_CHECKING: # pragma: no cover from .models.channels import MessageCreate @@ -134,7 +134,7 @@ async def processMessageData(data: Optional[dict], channel: Channel) -> tuple[di id=Snowflake.makeId(), channel=channel, message=None, filename=name, size=len(content), content_type=content_type, metadata=metadata ) - await getCDNStorage().uploadAttachment(content, att) + await getStorage().uploadAttachment(content, att) attachments.append(att) if not data.get("content") and \ not data.get("embeds") and \ diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index c8ba340..fdcd093 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -16,33 +16,13 @@ along with this program. If not, see . """ -import os.path -from typing import Optional - -import maxminddb - -from . import ctx from .classes.singleton import Singleton -from .config import Config -from .models import Guild -from .storage import getStorage -from .utils import b64decode # noinspection PyMethodMayBeStatic class Core(Singleton): - COUNTRY_TO_LANG = { - "UA": "uk", "US": "en-US", "BG": "bg", "CZ": "cs", "DK": "da", "DE": "de", "GR": "el", "GB": "en-GB", - "ES": "es-ES", "FI": "fi", "FR": "fr", "IN": "hi", "HR": "hr", "HU": "hu", "IT": "it", "JP": "ja", - "KR": "ko", "LT": "lt", "NL": "nl", "NO": "no", "PL": "pl", "BR": "pt-BR", "RO": "ro", "RU": "RU", - "SE": "sv-SE", "TH": "th", "TR": "tr", "VN": "vi", "CN": "zh-CN", "TW": "zh-TW", - } - IP_DATABASE: Optional[maxminddb.Reader] = None - - def __init__(self): - self.key = b64decode(Config.KEY) - - #async def sendMessage(self, message: Message) -> Message: + ... + # async def sendMessage(self, message: Message) -> Message: # async def _addToReadStates(): # TODO: recalculate read states when requested by user # users = await self.getRelatedUsersToChannel(message.channel) # if message.author in users: @@ -53,28 +33,5 @@ def __init__(self): # ) # read_state.count += 1 # await read_state.save(update_fields=["count"]) - # return message - async def getGuild(self, guild_id: int) -> Optional[Guild]: - return await Guild.get_or_none(id=guild_id).select_related("owner") - - def getLanguageCode(self, ip: str, default: str = "en-US") -> str: - cls = self.__class__ - - if cls.IP_DATABASE is None and not os.path.exists("other/ip_database.mmdb"): - return default - if cls.IP_DATABASE is None: - cls.IP_DATABASE = maxminddb.open_database("other/ip_database.mmdb") - - try: - country_code = (cls.IP_DATABASE.get(ip) or {"country": {"iso_code": None}})["country"]["iso_code"] \ - or default - except (ValueError, KeyError): - return default - - return cls.COUNTRY_TO_LANG.get(country_code, default) - - -ctx._get_core = Core.getInstance -ctx._get_storage = getStorage diff --git a/yepcord/yepcord/ctx.py b/yepcord/yepcord/ctx.py index 4fd8106..7be1354 100644 --- a/yepcord/yepcord/ctx.py +++ b/yepcord/yepcord/ctx.py @@ -21,23 +21,11 @@ from typing import TYPE_CHECKING, Optional, Callable if TYPE_CHECKING: # pragma: no cover - from .core import Core - from .storage import _Storage from .gateway_dispatcher import GatewayDispatcher -_get_core: Callable[[], Optional[Core]] = lambda: None -_get_storage: Callable[[], Optional[_Storage]] = lambda: None _get_gw: Callable[[], Optional[GatewayDispatcher]] = lambda: None -def getCore() -> Core: - return _get_core() - - -def getCDNStorage() -> _Storage: - return _get_storage() - - def getGw() -> GatewayDispatcher: return _get_gw() diff --git a/yepcord/yepcord/models/channel.py b/yepcord/yepcord/models/channel.py index 5139708..68234cb 100644 --- a/yepcord/yepcord/models/channel.py +++ b/yepcord/yepcord/models/channel.py @@ -25,7 +25,7 @@ from tortoise.fields import SET_NULL from tortoise.functions import Count -from ..ctx import getCore, getGw +from ..ctx import getGw from ..enums import ChannelType, GUILD_CHANNELS import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model diff --git a/yepcord/yepcord/models/interaction.py b/yepcord/yepcord/models/interaction.py index 0b76e28..829a951 100644 --- a/yepcord/yepcord/models/interaction.py +++ b/yepcord/yepcord/models/interaction.py @@ -23,7 +23,6 @@ import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model -from ..ctx import getCore from ..enums import InteractionType, ChannelType, InteractionStatus from ..utils import b64encode, b64decode diff --git a/yepcord/yepcord/models/message.py b/yepcord/yepcord/models/message.py index f1b6a4f..6f76066 100644 --- a/yepcord/yepcord/models/message.py +++ b/yepcord/yepcord/models/message.py @@ -24,7 +24,6 @@ from tortoise.functions import Count import yepcord.yepcord.models as models -from ..ctx import getCore from ..enums import MessageType from ..models._utils import SnowflakeField, Model from ..snowflake import Snowflake diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index cd73a2a..36e9081 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -34,7 +34,6 @@ from ._utils import SnowflakeField, Model from ..classes.other import MFA, JWT, EmailMsg from ..config import Config -from ..ctx import getCore from ..enums import RelationshipType, MfaNonceType from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser, InvalidKey from ..snowflake import Snowflake @@ -96,10 +95,10 @@ async def login(email: str, password: str) -> models.Session: email = email.strip().lower() user = await User.get_or_none(email=email) if not user or not user.check_password(password): - raise InvalidDataErr(400, Errors.make(50035, {"login": {"code": "INVALID_LOGIN", - "message": "Invalid login or password."}, - "password": {"code": "INVALID_LOGIN", - "message": "Invalid login or password."}})) + raise InvalidDataErr(400, Errors.make(50035, { + "login": {"code": "INVALID_LOGIN", "message": "Invalid login or password."}, + "password": {"code": "INVALID_LOGIN", "message": "Invalid login or password."}, + })) mfa_key = await user.get_mfa_key() if not mfa_key: return await models.Session.Y.create(user) @@ -109,7 +108,7 @@ async def login(email: str, password: str) -> models.Session: raise MfaRequiredErr( user.id, b64encode(sid), - b64encode(JWT.encode({"u": user.id, "s": sid_int}, getCore().key, time() + 300)), + b64encode(JWT.encode({"u": user.id, "s": sid_int}, b64decode(Config.KEY), time() + 300)), ) @staticmethod @@ -180,7 +179,7 @@ async def profile_json( "accent_color": data.accent_color } } - if guild_id and (guild := await getCore().getGuild(guild_id)): + if guild_id and (guild := await models.Guild.get_or_none(id=guild_id)): if member := await guild.get_member(self.id): data["guild_member_profile"] = {"guild_id": str(guild_id)} data["guild_member"] = await member.ds_json() diff --git a/yepcord/yepcord/storage.py b/yepcord/yepcord/storage.py index dcd98b8..28a4590 100644 --- a/yepcord/yepcord/storage.py +++ b/yepcord/yepcord/storage.py @@ -375,7 +375,10 @@ async def getAttachment(self, channel_id: int, attachment_id: int, name: str) -> return await super().getAttachment(channel_id, attachment_id, name) -def getStorage() -> _Storage: +_STORAGE_CACHE: dict[str, _Storage] = {} + + +def getStorageNoCache() -> _Storage: storage_type = Config.STORAGE["type"] assert storage_type in {"local", "s3", "ftp"}, "STORAGE.type must be one of ('local', 's3', 'ftp')" storage = Config.STORAGE[storage_type] @@ -391,3 +394,11 @@ def getStorage() -> _Storage: raise Exception("You must set 'host', 'port', 'user', 'password' variables to use ftp storage type.") return FTPStorage(**storage) return FileStorage(**storage) + + +def getStorage() -> _Storage: + storage_type = Config.STORAGE["type"] + if storage_type not in _STORAGE_CACHE: + _STORAGE_CACHE[storage_type] = getStorageNoCache() + + return _STORAGE_CACHE[storage_type] diff --git a/yepcord/yepcord/utils.py b/yepcord/yepcord/utils/__init__.py similarity index 99% rename from yepcord/yepcord/utils.py rename to yepcord/yepcord/utils/__init__.py index 46c03d5..eec966b 100644 --- a/yepcord/yepcord/utils.py +++ b/yepcord/yepcord/utils/__init__.py @@ -24,6 +24,8 @@ from magic import from_buffer +from .geoip import GeoIp + def b64decode(data: Union[str, bytes]) -> bytes: if isinstance(data, str): diff --git a/yepcord/yepcord/utils/geoip.py b/yepcord/yepcord/utils/geoip.py new file mode 100644 index 0000000..852dc7a --- /dev/null +++ b/yepcord/yepcord/utils/geoip.py @@ -0,0 +1,30 @@ +import os +from typing import Optional + +import maxminddb + + +class GeoIp: + COUNTRY_TO_LANG = { + "UA": "uk", "US": "en-US", "BG": "bg", "CZ": "cs", "DK": "da", "DE": "de", "GR": "el", "GB": "en-GB", + "ES": "es-ES", "FI": "fi", "FR": "fr", "IN": "hi", "HR": "hr", "HU": "hu", "IT": "it", "JP": "ja", + "KR": "ko", "LT": "lt", "NL": "nl", "NO": "no", "PL": "pl", "BR": "pt-BR", "RO": "ro", "RU": "RU", + "SE": "sv-SE", "TH": "th", "TR": "tr", "VN": "vi", "CN": "zh-CN", "TW": "zh-TW", + } + IP_DATABASE: Optional[maxminddb.Reader] = None + + @classmethod + def get_language_code(cls, ip: str, default: str = "en-US") -> str: + if cls.IP_DATABASE is None and not os.path.exists("other/ip_database.mmdb"): + return default + + if cls.IP_DATABASE is None: + cls.IP_DATABASE = maxminddb.open_database("other/ip_database.mmdb") + + try: + country_code = cls.IP_DATABASE.get(ip) + country_code = country_code["country"]["iso_code"] if country_code is not None else default + except (ValueError, KeyError): + return default + + return cls.COUNTRY_TO_LANG.get(country_code, default) From 17be32d5cd81087a33a7569b5d1c0b97472f1670 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 18:53:48 +0300 Subject: [PATCH 16/26] move captcha to utils submodule --- yepcord/rest_api/utils.py | 2 +- yepcord/yepcord/{classes => utils}/captcha.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename yepcord/yepcord/{classes => utils}/captcha.py (100%) diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 399b279..61a075f 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -27,7 +27,7 @@ from quart import request, current_app, g import yepcord.yepcord.models as models -from ..yepcord.classes.captcha import Captcha +from ..yepcord.utils.captcha import Captcha from ..yepcord.config import Config from ..yepcord.enums import MessageType from ..yepcord.errors import Errors, InvalidDataErr, UnknownChannel, UnknownMessage, InvalidFormBody, \ diff --git a/yepcord/yepcord/classes/captcha.py b/yepcord/yepcord/utils/captcha.py similarity index 100% rename from yepcord/yepcord/classes/captcha.py rename to yepcord/yepcord/utils/captcha.py From 96891ccb2773e276613e9159c41a392e2752e6e1 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 19:03:31 +0300 Subject: [PATCH 17/26] move gifs to utils submodule --- yepcord/rest_api/main.py | 7 +- yepcord/rest_api/routes/gifs.py | 17 +++-- yepcord/yepcord/{classes => utils}/gifs.py | 80 ++++++++++++---------- 3 files changed, 56 insertions(+), 48 deletions(-) rename yepcord/yepcord/{classes => utils}/gifs.py (74%) diff --git a/yepcord/rest_api/main.py b/yepcord/rest_api/main.py index b071faf..4560577 100644 --- a/yepcord/rest_api/main.py +++ b/yepcord/rest_api/main.py @@ -26,7 +26,6 @@ from .routes.auth import auth from .routes.channels import channels from .routes.connections import connections -from .routes.gifs import gifs from .routes.guilds import guilds from .routes.hypesquad import hypesquad from .routes.interactions import interactions @@ -46,16 +45,16 @@ class YEPcord(Quart): - gifs: Gifs + ... app = YEPcord("YEPcord-api") QuartSchema(app) storage = getStorage() gateway = GatewayDispatcher() -app.gifs = Gifs(Config.TENOR_KEY) +gifs = Gifs(Config.TENOR_KEY) -app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 +app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024 @app.before_serving diff --git a/yepcord/rest_api/routes/gifs.py b/yepcord/rest_api/routes/gifs.py index 0b7a6f3..a9fcdeb 100644 --- a/yepcord/rest_api/routes/gifs.py +++ b/yepcord/rest_api/routes/gifs.py @@ -16,19 +16,20 @@ along with this program. If not, see . """ -from quart import Blueprint, request, current_app +from quart import Blueprint, request + +from yepcord.yepcord.utils.gifs import Gifs # Base path is -gifs = Blueprint('gifs', __name__) +gifs = Blueprint("gifs", __name__) @gifs.get("/trending") async def api_gifs_trending_get(): result = {"gifs": [], "categories": []} - # noinspection PyUnresolvedReferences - for category in await current_app.gifs.categories: + for category in await Gifs.getInstance().get_categories(): result["categories"].append(category.json) - result["categories"][-1]["src"] = result["categories"][-1]["src"][:-4]+".mp4" + result["categories"][-1]["src"] = result["categories"][-1]["src"][:-4] + ".mp4" return result @@ -44,8 +45,7 @@ async def api_gifs_select_post(): @gifs.get("/search") async def api_gifs_search(): - # noinspection PyUnresolvedReferences - search = await current_app.gifs.search(**request.args) + search = await Gifs.getInstance().search(**request.args) return [gif.json for gif in search.gifs] @@ -53,5 +53,4 @@ async def api_gifs_search(): async def api_gifs_suggest(): args: dict = {**request.args} if "limit" in args: args["limit"] = int(args["limit"]) - # noinspection PyUnresolvedReferences - return await current_app.gifs.suggest(**args) + return await Gifs.getInstance().suggest(**args) diff --git a/yepcord/yepcord/classes/gifs.py b/yepcord/yepcord/utils/gifs.py similarity index 74% rename from yepcord/yepcord/classes/gifs.py rename to yepcord/yepcord/utils/gifs.py index 3f26f9d..b833bd2 100644 --- a/yepcord/yepcord/classes/gifs.py +++ b/yepcord/yepcord/utils/gifs.py @@ -16,56 +16,67 @@ along with this program. If not, see . """ -from dataclasses import dataclass, asdict from time import time -from typing import List from httpx import AsyncClient from .singleton import Singleton -@dataclass -class _J: - @property +class SlotsToDict: + __slots__ = () + def json(self) -> dict: - return asdict(self) + return { + slot: getattr(self, slot) + for slot in self.__slots__ + } + + +class GifCategory(SlotsToDict): + __slots__ = ("name", "src",) + def __init__(self, name: str, src: str): + self.name = name + self.src = src -@dataclass -class GifCategory(_J): - name: str - src: str +class Gif(SlotsToDict): + __slots__ = ("id", "title", "preview", "gif_src", "height", "width", "src", "url",) -@dataclass -class Gif(_J): - id: str - title: str - preview: str - gif_src: str - height: int - width: int - src: str - url: str + def __init__(self, id_: str, title: str, preview: str, gif_src: str, height: int, width: int, src: str, url: str): + self.id = id_ + self.title = title + self.preview = preview + self.gif_src = gif_src + self.height = height + self.width = width + self.src = src + self.url = url -@dataclass -class GifSearchResult(_J): - query: str - gifs: List[Gif] - time: int +class GifSearchResult(SlotsToDict): + __slots__ = ("query", "gifs", "time",) + def __init__(self, query: str, gifs: list[Gif], time_: int): + self.query = query + self.gifs = gifs + self.time = time_ -@dataclass -class GifSuggestion(_J): - query: str - tags: List[str] - time: int + +class GifSuggestion(SlotsToDict): + __slots__ = ("query", "tags", "time",) + + def __init__(self, query: str, tags: list[str], time_: int): + self.query = query + self.tags = tags + self.time = time_ # noinspection PyTestUnpassedFixture class Gifs(Singleton): + __slots__ = ("_key", "_categories", "_last_searches", "_last_suggestions", "_last_update", "_keep_searches",) + def __init__(self, key: str = None, keep_searches: int = 100): self._key = key self._categories = [] @@ -74,8 +85,7 @@ def __init__(self, key: str = None, keep_searches: int = 100): self._last_update = 0 self._keep_searches = keep_searches - @property - async def categories(self) -> List[GifCategory]: + async def get_categories(self) -> list[GifCategory]: if time() - self._last_update > 60: await self._update_categories() return self._categories @@ -92,7 +102,7 @@ async def _update_categories(self) -> None: self._categories = categories self._last_update = time() - async def search(self, q: str=None, **kwargs) -> GifSearchResult: + async def search(self, q: str = None, **kwargs) -> GifSearchResult: if self._key is None or not q: # pragma: no cover return GifSearchResult(q, [], 0) @@ -113,7 +123,7 @@ async def search(self, q: str=None, **kwargs) -> GifSearchResult: resp = await client.get(f"https://tenor.googleapis.com/v2/search", params=_params) for gif in resp.json()["results"]: gifs.append(Gif( - id=gif["id"], + id_=gif["id"], title=gif["title"], preview=gif["media_formats"]["gifpreview"]["url"], gif_src=gif["media_formats"]["gif"]["url"], @@ -132,7 +142,7 @@ async def search(self, q: str=None, **kwargs) -> GifSearchResult: return search - async def suggest(self, q: str=None, limit: int = 5, **kwargs) -> List[str]: + async def suggest(self, q: str = None, limit: int = 5, **kwargs) -> list[str]: if self._key is None or not q: # pragma: no cover return [] From 7c71d9230e348c2f521f70db1deab16592a8693b Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 19:40:44 +0300 Subject: [PATCH 18/26] move singleton to utils submodule --- yepcord/rest_api/main.py | 5 +++-- yepcord/rest_api/routes/gifs.py | 4 ++-- yepcord/yepcord/config.py | 2 +- yepcord/yepcord/core.py | 2 +- yepcord/yepcord/gateway_dispatcher.py | 2 +- yepcord/yepcord/snowflake.py | 4 +--- yepcord/yepcord/storage.py | 4 ++-- yepcord/yepcord/{classes => utils}/singleton.py | 0 8 files changed, 11 insertions(+), 12 deletions(-) rename yepcord/yepcord/{classes => utils}/singleton.py (100%) diff --git a/yepcord/rest_api/main.py b/yepcord/rest_api/main.py index 4560577..bb0eff8 100644 --- a/yepcord/rest_api/main.py +++ b/yepcord/rest_api/main.py @@ -26,6 +26,7 @@ from .routes.auth import auth from .routes.channels import channels from .routes.connections import connections +from .routes.gifs import gifs from .routes.guilds import guilds from .routes.hypesquad import hypesquad from .routes.interactions import interactions @@ -36,7 +37,7 @@ from .routes.users import users from .routes.users_me import users_me from .routes.webhooks import webhooks -from ..yepcord.classes.gifs import Gifs +from ..yepcord.utils.gifs import Gifs from ..yepcord.config import Config from ..yepcord.errors import InvalidDataErr, MfaRequiredErr, YDataError, EmbedErr, Errors from ..yepcord.gateway_dispatcher import GatewayDispatcher @@ -52,7 +53,7 @@ class YEPcord(Quart): QuartSchema(app) storage = getStorage() gateway = GatewayDispatcher() -gifs = Gifs(Config.TENOR_KEY) +gifs_ = Gifs(Config.TENOR_KEY) app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024 diff --git a/yepcord/rest_api/routes/gifs.py b/yepcord/rest_api/routes/gifs.py index a9fcdeb..e5dbfb7 100644 --- a/yepcord/rest_api/routes/gifs.py +++ b/yepcord/rest_api/routes/gifs.py @@ -28,7 +28,7 @@ async def api_gifs_trending_get(): result = {"gifs": [], "categories": []} for category in await Gifs.getInstance().get_categories(): - result["categories"].append(category.json) + result["categories"].append(category.json()) result["categories"][-1]["src"] = result["categories"][-1]["src"][:-4] + ".mp4" return result @@ -46,7 +46,7 @@ async def api_gifs_select_post(): @gifs.get("/search") async def api_gifs_search(): search = await Gifs.getInstance().search(**request.args) - return [gif.json for gif in search.gifs] + return [gif.json() for gif in search.gifs] @gifs.get("/suggest") diff --git a/yepcord/yepcord/config.py b/yepcord/yepcord/config.py index c0350f0..7daf233 100644 --- a/yepcord/yepcord/config.py +++ b/yepcord/yepcord/config.py @@ -25,7 +25,7 @@ from pydantic import BaseModel, Field, field_validator -from .classes.singleton import Singleton +from .utils.singleton import Singleton def _load_config() -> dict: diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py index fdcd093..da076a8 100644 --- a/yepcord/yepcord/core.py +++ b/yepcord/yepcord/core.py @@ -16,7 +16,7 @@ along with this program. If not, see . """ -from .classes.singleton import Singleton +from .utils.singleton import Singleton # noinspection PyMethodMayBeStatic diff --git a/yepcord/yepcord/gateway_dispatcher.py b/yepcord/yepcord/gateway_dispatcher.py index 7e08041..35d1de8 100644 --- a/yepcord/yepcord/gateway_dispatcher.py +++ b/yepcord/yepcord/gateway_dispatcher.py @@ -24,7 +24,7 @@ from tortoise.expressions import RawSQL from . import ctx -from .classes.singleton import Singleton +from .utils.singleton import Singleton from .enums import ChannelType from .errors import InvalidDataErr from .models import Channel, Guild, Role diff --git a/yepcord/yepcord/snowflake.py b/yepcord/yepcord/snowflake.py index 07a4423..4fa6e86 100644 --- a/yepcord/yepcord/snowflake.py +++ b/yepcord/yepcord/snowflake.py @@ -22,10 +22,8 @@ from time import time from typing import Union -from .classes.singleton import Singleton - -class Snowflake(Singleton): +class Snowflake: EPOCH = 1640995200_000 MAX_TIMESTAMP = 1 << 42 _INCREMENT = 0 diff --git a/yepcord/yepcord/storage.py b/yepcord/yepcord/storage.py index 28a4590..ed6469b 100644 --- a/yepcord/yepcord/storage.py +++ b/yepcord/yepcord/storage.py @@ -31,7 +31,7 @@ from PIL import Image, ImageSequence from aiofiles import open as aopen -from .classes.singleton import SingletonMeta +from .utils.singleton import SingletonMeta from .config import Config from .models import Attachment @@ -92,7 +92,7 @@ def _resize(form_: str): b = BytesIO() save_all = True if form_.lower() == "jpg": - img = img.convert('RGB') + img = img.convert("RGB") form_ = "JPEG" save_all = False img.save(b, format=form_, save_all=save_all) diff --git a/yepcord/yepcord/classes/singleton.py b/yepcord/yepcord/utils/singleton.py similarity index 100% rename from yepcord/yepcord/classes/singleton.py rename to yepcord/yepcord/utils/singleton.py From fb457390faf2c2684362d6f43cd329aec7be16f7 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 19:58:46 +0300 Subject: [PATCH 19/26] move the rest of classes to utils submodule --- tests/api/test_auth.py | 2 +- tests/api/test_guild.py | 4 +- tests/api/test_mfa.py | 17 +- tests/api/utils.py | 4 +- tests/test_core.py | 2 +- tests/utils.py | 2 +- yepcord/gateway/gateway.py | 4 +- yepcord/gateway/main.py | 2 +- yepcord/gateway/utils.py | 11 + yepcord/rest_api/models/guilds.py | 2 +- yepcord/rest_api/routes/auth.py | 15 +- yepcord/rest_api/routes/connections.py | 12 +- yepcord/rest_api/routes/guilds.py | 2 +- yepcord/rest_api/routes/hypesquad.py | 2 +- yepcord/rest_api/routes/users_me.py | 6 +- yepcord/yepcord/classes/__init__.py | 0 yepcord/yepcord/classes/other.py | 196 ------------------ yepcord/yepcord/models/user.py | 4 +- yepcord/yepcord/utils/bit_flags.py | 48 +++++ .../yepcord/{classes => utils}/connections.py | 0 yepcord/yepcord/utils/email_msg.py | 54 +++++ .../yepcord/{classes => utils}/fakeredis.py | 0 yepcord/yepcord/utils/jwt.py | 75 +++++++ yepcord/yepcord/utils/mfa.py | 85 ++++++++ 24 files changed, 313 insertions(+), 236 deletions(-) delete mode 100644 yepcord/yepcord/classes/__init__.py delete mode 100644 yepcord/yepcord/classes/other.py create mode 100644 yepcord/yepcord/utils/bit_flags.py rename yepcord/yepcord/{classes => utils}/connections.py (100%) create mode 100644 yepcord/yepcord/utils/email_msg.py rename yepcord/yepcord/{classes => utils}/fakeredis.py (100%) create mode 100644 yepcord/yepcord/utils/jwt.py create mode 100644 yepcord/yepcord/utils/mfa.py diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 1c737cd..671339c 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -7,7 +7,7 @@ import pytest_asyncio from yepcord.rest_api.main import app -from yepcord.yepcord.classes.other import JWT +from yepcord.yepcord.utils.jwt import JWT from yepcord.yepcord.config import Config from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.utils import b64decode, b64encode diff --git a/tests/api/test_guild.py b/tests/api/test_guild.py index 05d4b78..f951f2c 100644 --- a/tests/api/test_guild.py +++ b/tests/api/test_guild.py @@ -2,7 +2,7 @@ import pytest_asyncio from yepcord.rest_api.main import app -from yepcord.yepcord.classes.other import MFA +from yepcord.yepcord.utils.mfa import MFA from yepcord.yepcord.enums import ChannelType from yepcord.yepcord.snowflake import Snowflake from tests.api.utils import TestClientType, create_users, create_guild, create_invite, enable_mfa, create_guild_channel, \ @@ -306,7 +306,7 @@ async def test_delete_guild(): resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": "wrong"}) assert resp.status_code == 400 - resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": mfa.getCode()}) + resp = await client.post(f"/api/v9/guilds/{guild['id']}/delete", headers=headers1, json={"code": mfa.get_code()}) assert resp.status_code == 204 diff --git a/tests/api/test_mfa.py b/tests/api/test_mfa.py index fbef229..e980f87 100644 --- a/tests/api/test_mfa.py +++ b/tests/api/test_mfa.py @@ -2,9 +2,10 @@ import pytest_asyncio from yepcord.rest_api.main import app -from yepcord.yepcord.classes.other import MFA, JWT +from yepcord.yepcord.utils.jwt import JWT from yepcord.yepcord.config import Config from yepcord.yepcord.utils import b64decode +from yepcord.yepcord.utils.mfa import MFA from .utils import TestClientType, create_users, enable_mfa from ..utils import register_app_error_handler @@ -58,21 +59,21 @@ async def test_mfa_enable(): secret = "a" * 16 mfa = MFA(secret, 0) - code = mfa.getCode() + code = mfa.get_code() invalid_code = (code + str((int(code[-1]) + 1) % 10))[1:] resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers, json={'code': invalid_code, 'secret': secret, 'password': user["password"]}) assert resp.status_code == 400 resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers, - json={'code': mfa.getCode(), 'secret': secret, 'password': user["password"]}) + json={'code': mfa.get_code(), 'secret': secret, 'password': user["password"]}) assert resp.status_code == 200 json = await resp.get_json() assert json["token"] user["token"] = json["token"] headers = {"Authorization": user["token"]} resp = await client.post("/api/v9/users/@me/mfa/totp/enable", headers=headers, - json={'code': mfa.getCode(), 'secret': secret, 'password': user["password"]}) + json={'code': mfa.get_code(), 'secret': secret, 'password': user["password"]}) assert resp.status_code == 404 check_codes(json["backup_codes"], user_id) @@ -146,7 +147,7 @@ async def test_login_with_mfa(): assert json["mfa"] assert (ticket := json["ticket"]) - code = mfa.getCode() + code = mfa.get_code() invalid_code = (code + str((int(code[-1]) + 1) % 10))[1:] resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": "", "code": ""}) # No ticket @@ -158,7 +159,7 @@ async def test_login_with_mfa(): resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": invalid_code}) # Invalid code assert resp.status_code == 400 - resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": mfa.getCode()}) + resp = await client.post('/api/v9/auth/mfa/totp', json={"ticket": ticket, "code": mfa.get_code()}) assert resp.status_code == 200 json = await resp.get_json() assert json["token"] @@ -178,12 +179,12 @@ async def test_disable_mfa(): resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': ""}) assert resp.status_code == 400 - resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.getCode()}) + resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.get_code()}) assert resp.status_code == 200 json = await resp.get_json() assert json["token"] user["token"] = json["token"] headers = {"Authorization": user["token"]} - resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.getCode()}) + resp = await client.post("/api/v9/users/@me/mfa/totp/disable", headers=headers, json={'code': mfa.get_code()}) assert resp.status_code == 404 diff --git a/tests/api/utils.py b/tests/api/utils.py index 4ed79e1..2977c45 100644 --- a/tests/api/utils.py +++ b/tests/api/utils.py @@ -8,7 +8,7 @@ from quart.typing import TestWebsocketConnectionProtocol from yepcord.rest_api.main import app as _app -from yepcord.yepcord.classes.other import MFA +from yepcord.yepcord.utils.mfa import MFA from yepcord.yepcord.enums import ChannelType, GatewayOp from yepcord.yepcord.snowflake import Snowflake from yepcord.yepcord.utils import getImage @@ -58,7 +58,7 @@ async def create_users(app: TestClientType, count: int = 1) -> list[dict]: async def enable_mfa(app: TestClientType, user: dict, mfa: MFA) -> None: resp = await app.post("/api/v9/users/@me/mfa/totp/enable", headers={"Authorization": user["token"]}, - json={"code": mfa.getCode(), "secret": mfa.key, "password": user["password"]}) + json={"code": mfa.get_code(), "secret": mfa.key, "password": user["password"]}) assert resp.status_code == 200, (resp.status_code, await resp.get_json()) json = await resp.get_json() assert json["token"] diff --git a/tests/test_core.py b/tests/test_core.py index bb2c089..cd84ff7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -25,7 +25,7 @@ import pytest_asyncio from tortoise import Tortoise -from yepcord.yepcord.classes.other import MFA +from yepcord.yepcord.utils.mfa import MFA from yepcord.yepcord.config import Config, ConfigModel from yepcord.yepcord.enums import UserFlags as UserFlagsE, RelationshipType, ChannelType, GuildPermissions, MfaNonceType from yepcord.yepcord.errors import InvalidDataErr, MfaRequiredErr diff --git a/tests/utils.py b/tests/utils.py index 19b1745..65b268a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -21,7 +21,7 @@ from json import dumps from time import time -from yepcord.yepcord.classes.other import JWT +from yepcord.yepcord.utils.jwt import JWT from yepcord.yepcord.utils import b64encode, b64decode diff --git a/yepcord/gateway/gateway.py b/yepcord/gateway/gateway.py index 8bd236a..3f11070 100644 --- a/yepcord/gateway/gateway.py +++ b/yepcord/gateway/gateway.py @@ -20,7 +20,7 @@ import warnings from json import dumps as jdumps -from typing import Optional, Union +from typing import Union from quart import Websocket from redis.asyncio import Redis @@ -29,7 +29,7 @@ from .events import * from .presences import Presences, Presence from .utils import require_auth, get_token_type, TokenType, init_redis_pool -from ..yepcord.classes.fakeredis import FakeRedis +from ..yepcord.utils.fakeredis import FakeRedis from ..yepcord.enums import GatewayOp, RelationshipType from ..yepcord.models import Session, User, UserSettings, Bot, GuildMember, Guild from ..yepcord.mq_broker import getBroker diff --git a/yepcord/gateway/main.py b/yepcord/gateway/main.py index e309487..4a8edda 100644 --- a/yepcord/gateway/main.py +++ b/yepcord/gateway/main.py @@ -20,7 +20,7 @@ from tortoise.contrib.quart import register_tortoise from ..yepcord.config import Config -from ..yepcord.classes.other import ZlibCompressor +from .utils import ZlibCompressor from json import loads as jloads from asyncio import CancelledError from .gateway import Gateway diff --git a/yepcord/gateway/utils.py b/yepcord/gateway/utils.py index f4092e6..d51803d 100644 --- a/yepcord/gateway/utils.py +++ b/yepcord/gateway/utils.py @@ -17,6 +17,7 @@ """ from enum import Enum, auto from typing import Optional +from zlib import compressobj, Z_FULL_FLUSH from redis.asyncio import Redis @@ -63,3 +64,13 @@ async def init_redis_pool() -> Optional[Redis]: encoding="utf-8", decode_responses=True, ) + + +class ZlibCompressor: + __slots__ = ("_obj",) + + def __init__(self): + self._obj = compressobj() + + def __call__(self, data): + return self._obj.compress(data) + self._obj.flush(Z_FULL_FLUSH) diff --git a/yepcord/rest_api/models/guilds.py b/yepcord/rest_api/models/guilds.py index a59a9a2..f393a45 100644 --- a/yepcord/rest_api/models/guilds.py +++ b/yepcord/rest_api/models/guilds.py @@ -25,7 +25,7 @@ from pydantic_core.core_schema import ValidationInfo from .channels import PermissionOverwriteModel -from ...yepcord.classes.other import BitFlags +from ...yepcord.utils.bit_flags import BitFlags from ...yepcord.enums import SystemChannelFlags, ChannelType, ScheduledEventEntityType, GUILD_CHANNELS from ...yepcord.errors import InvalidDataErr, Errors from ...yepcord.utils import getImage, validImage, LOCALES diff --git a/yepcord/rest_api/routes/auth.py b/yepcord/rest_api/routes/auth.py index 93935bb..7be2c97 100644 --- a/yepcord/rest_api/routes/auth.py +++ b/yepcord/rest_api/routes/auth.py @@ -23,7 +23,9 @@ from ..utils import captcha from ..y_blueprint import YBlueprint from ...gateway.events import UserUpdateEvent -from ...yepcord.classes.other import EmailMsg, MFA, JWT +from ...yepcord.utils.mfa import MFA +from ...yepcord.utils.jwt import JWT +from ...yepcord.utils.email_msg import EmailMsg from ...yepcord.config import Config from ...yepcord.ctx import getGw from ...yepcord.errors import PasswordDoesNotMatch, Invalid2FaCode, Invalid2FaAuthTicket, InvalidToken @@ -67,7 +69,7 @@ async def login_with_mfa(data: MfaLogin): raise Invalid2FaAuthTicket code = code.replace("-", "").replace(" ", "") user = await User.y.get(mfa.uid) - if code not in mfa.getCodes(): + if code not in mfa.get_codes(): if not (len(code) == 8 and await user.use_backup_code(code)): raise Invalid2FaCode sess = await Session.Y.create(user) @@ -94,14 +96,7 @@ async def request_challenge_to_view_mfa_codes(data: ViewBackupCodes, user: User nonce = await user.generate_mfa_nonce() code = await MFA.nonce_to_code(nonce[0]) - await EmailMsg( - user.email, - f"Your one-time verification key is {code}", - f"It looks like you're trying to view your account's backup codes.\n" - f"This verification key expires in 10 minutes. This key is extremely sensitive, treat it like a " - f"password and do not share it with anyone.\n" - f"Enter it in the app to unlock your backup codes:\n{code}" - ).send() + await EmailMsg.send_mfa_code(user.email, code) return {"nonce": nonce[0], "regenerate_nonce": nonce[1]} diff --git a/yepcord/rest_api/routes/connections.py b/yepcord/rest_api/routes/connections.py index 2bda83e..6101f9b 100644 --- a/yepcord/rest_api/routes/connections.py +++ b/yepcord/rest_api/routes/connections.py @@ -20,7 +20,7 @@ from ..models.connections import ConnectionCallback from ..y_blueprint import YBlueprint from ...gateway.events import UserConnectionsUpdate -from ...yepcord.classes.connections import ConnectionGithub, ConnectionReddit, ConnectionTwitch, BaseConnection, \ +from ...yepcord.utils.connections import ConnectionGithub, ConnectionReddit, ConnectionTwitch, BaseConnection, \ ConnectionSpotify from ...yepcord.ctx import getGw from ...yepcord.models import User, ConnectedAccount @@ -29,8 +29,9 @@ connections = YBlueprint("connections", __name__) -async def unified_callback(connection_cls: type[BaseConnection], data: ConnectionCallback, - user_login_field: str = "login"): +async def unified_callback( + connection_cls: type[BaseConnection], data: ConnectionCallback, user_login_field: str = "login" +): if (conn := await connection_cls.get_connection_from_state(data.state)) is None: return "", 204 @@ -40,8 +41,9 @@ async def unified_callback(connection_cls: type[BaseConnection], data: Connectio if await ConnectedAccount.filter(type=connection_cls.SERVICE_NAME, service_id=user_info["id"]).exists(): return "", 204 - await conn.update(service_id=user_info["id"], name=user_info[user_login_field], access_token=access_token, - verified=True) + await conn.update( + service_id=user_info["id"], name=user_info[user_login_field], access_token=access_token, verified=True + ) await getGw().dispatch(UserConnectionsUpdate(conn), user_ids=[int(data.state.split(".")[0])]) return "", 204 diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index b125660..cae3297 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -712,7 +712,7 @@ async def delete_guild(data: GuildDelete, user: User = DepUser, guild: Guild = D if mfa := await user.mfa: if not data.code: raise Invalid2FaCode - if data.code not in mfa.getCodes(): + if data.code not in mfa.get_codes(): if not (len(data.code) == 8 and await user.use_backup_code(data.code)): raise Invalid2FaCode diff --git a/yepcord/rest_api/routes/hypesquad.py b/yepcord/rest_api/routes/hypesquad.py index a3fd090..df555a5 100644 --- a/yepcord/rest_api/routes/hypesquad.py +++ b/yepcord/rest_api/routes/hypesquad.py @@ -20,7 +20,7 @@ from ..models.hypesquad import HypesquadHouseChange from ..y_blueprint import YBlueprint from ...gateway.events import UserUpdateEvent -from ...yepcord.classes.other import BitFlags +from ...yepcord.utils.bit_flags import BitFlags from ...yepcord.ctx import getGw from ...yepcord.enums import UserFlags from ...yepcord.models import User diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index 737f9b6..c3b3d29 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -29,7 +29,7 @@ from ...gateway.events import RelationshipAddEvent, DMChannelCreateEvent, RelationshipRemoveEvent, UserUpdateEvent, \ UserNoteUpdateEvent, UserSettingsProtoUpdateEvent, GuildDeleteEvent, GuildMemberRemoveEvent, UserDeleteEvent, \ UserConnectionsUpdate -from ...yepcord.classes.other import MFA +from ...yepcord.utils.mfa import MFA from ...yepcord.config import Config from ...yepcord.ctx import getGw from ...yepcord.enums import RelationshipType, ChannelType, MfaNonceType @@ -288,7 +288,7 @@ async def enable_mfa(data: MfaEnable, session: Session = DepSession): mfa = MFA(secret, user.id) if not mfa.valid: raise Invalid2FaSecret - if not (code := data.code) or code not in mfa.getCodes(): + if not (code := data.code) or code not in mfa.get_codes(): raise Invalid2FaCode settings.mfa = secret await settings.save(update_fields=["mfa"]) @@ -312,7 +312,7 @@ async def disable_mfa(data: MfaDisable, session: Session = DepSession): raise NotYet2Fa mfa = await user.mfa code = code.replace("-", "").replace(" ", "") - if code not in mfa.getCodes(): + if code not in mfa.get_codes(): if not (len(code) == 8 and await user.use_backup_code(code)): raise Invalid2FaCode settings.mfa = None diff --git a/yepcord/yepcord/classes/__init__.py b/yepcord/yepcord/classes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/yepcord/yepcord/classes/other.py b/yepcord/yepcord/classes/other.py deleted file mode 100644 index 2946a6c..0000000 --- a/yepcord/yepcord/classes/other.py +++ /dev/null @@ -1,196 +0,0 @@ -""" - YEPCord: Free open source selfhostable fully discord-compatible chat - Copyright (C) 2022-2024 RuslanUC - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -""" - -from __future__ import annotations -import re -from base64 import b32decode -from hashlib import sha512 -from hmac import new -from json import loads, dumps -from struct import pack, unpack -from time import time -from typing import Union, Optional -from zlib import compressobj, Z_FULL_FLUSH - -from mailers import Mailer -from mailers.exceptions import DeliveryError - -from ..config import Config -from ..utils import b64decode, b64encode, assert_ - - -class ZlibCompressor: - def __init__(self): - self.cObj = compressobj() - - def __call__(self, data): - return self.cObj.compress(data) + self.cObj.flush(Z_FULL_FLUSH) - - -class EmailMsg: - def __init__(self, to: str, subject: str, text: str): - self.to = to - self.subject = subject - self.text = text - - async def send(self): - mailer = Mailer(Config.MAIL_CONNECT_STRING) - try: - await mailer.send_message(self.to, self.subject, self.text, from_address="no-reply@yepcord.ml") - except DeliveryError: - pass - - @classmethod - async def send_verification(cls, email: str, token: str) -> None: - await cls(email, "Confirm your e-mail in YEPCord", ( - f"Thank you for signing up for a YEPCord account!\n" - f"First you need to make sure that you are you!\n" - f"Click to verify your email address:\n" - f"https://{Config.PUBLIC_HOST}/verify#token={token}" - )).send() - - -class JWT: - """ - Json Web Token Hmac-sha512 implementation - """ - - @staticmethod - def decode(token: str, secret: Union[str, bytes]) -> Optional[dict]: - try: - header, payload, signature = token.split(".") - header_dict = loads(b64decode(header).decode("utf8")) - assert header_dict.get("alg") == "HS512" - assert header_dict.get("typ") == "JWT" - assert (exp := header_dict.get("exp", 0)) > time() or exp == 0 - signature = b64decode(signature) - except (IndexError, AssertionError, ValueError): - return - - sig = f"{header}.{payload}".encode("utf8") - sig = new(secret, sig, sha512).digest() - if sig == signature: - payload = b64decode(payload).decode("utf8") - return loads(payload) - - @staticmethod - def encode( - payload: dict, secret: Union[str, bytes], expires_at: Optional[Union[int, float]] = None, - expires_after: Optional[int] = None - ) -> str: - if expires_after is not None: - expires_at = int(time() + expires_after) - if expires_at is None: - expires_at = 0 - - header = { - "alg": "HS512", - "typ": "JWT", - "exp": int(expires_at) - } - header = b64encode(dumps(header, separators=(',', ':'))) - payload = b64encode(dumps(payload, separators=(',', ':'))) - - signature = f"{header}.{payload}".encode("utf8") - signature = new(secret, signature, sha512).digest() - signature = b64encode(signature) - - return f"{header}.{payload}.{signature}" - - -class BitFlags: - def __init__(self, value: int, cls): - self.cls = cls - self.value = value - self.parsedFlags = self.parseFlags() - - def parseFlags(self) -> list: - flags = [] - value = self.value - self.value = 0 - for val in self.cls.values().values(): - if (value & val) == val: - flags.append(val) - self.value |= val - return flags - - def add(self, val: int): - if val not in self.parsedFlags: - self.value |= val - self.parsedFlags.append(val) - return self - - def remove(self, val: int): - if val in self.parsedFlags: - self.value &= ~val - self.parsedFlags.remove(val) - return self - - -class MFA: - _re = re.compile(r'^[A-Z0-9]{16}$') - - def __init__(self, key: str, uid: int): - self.key = str(key).upper() - self.uid = self.id = uid - - def getCode(self, timestamp: Union[int, float] = None) -> str: - if timestamp is None: - timestamp = time() - key = b32decode(self.key.upper() + '=' * ((8 - len(self.key)) % 8)) - counter = pack('>Q', int(timestamp / 30)) - mac = new(key, counter, "sha1").digest() - offset = mac[-1] & 0x0f - binary = unpack('>L', mac[offset:offset + 4])[0] & 0x7fffffff - return str(binary)[-6:].zfill(6) - - def getCodes(self) -> tuple[str, str]: - return self.getCode(time() - 5), self.getCode(time() + 1) - - @property - def valid(self) -> bool: - return bool(self._re.match(self.key)) - - @staticmethod - async def get_from_ticket(ticket: str) -> Optional[MFA]: - from ..models import User - - try: - user_id, session_id, sig = ticket.split(".") - user_id = loads(b64decode(user_id).decode("utf8"))[0] - session_id = int.from_bytes(b64decode(session_id), "big") - sig = b64decode(sig).decode("utf8") - - assert_(user := await User.y.get(user_id)) - assert_(payload := JWT.decode(sig, b64decode(Config.KEY))) - assert_(payload["u"] == user.id) - assert_(payload["s"] == session_id) - except (ValueError, IndexError): - return - - return MFA(await user.get_mfa_key(), user_id) - - @staticmethod - async def nonce_to_code(nonce: str) -> Optional[str]: - key = b64decode(Config.KEY) - - if not (payload := JWT.decode(nonce, key)): - return - token = JWT.encode({"code": payload["c"]}, key) - signature = token.split(".")[2] - return signature.replace("-", "").replace("_", "")[:8].upper() diff --git a/yepcord/yepcord/models/user.py b/yepcord/yepcord/models/user.py index 36e9081..5c20c5e 100644 --- a/yepcord/yepcord/models/user.py +++ b/yepcord/yepcord/models/user.py @@ -32,7 +32,9 @@ import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model -from ..classes.other import MFA, JWT, EmailMsg +from ..utils.mfa import MFA +from ..utils.jwt import JWT +from ..utils.email_msg import EmailMsg from ..config import Config from ..enums import RelationshipType, MfaNonceType from ..errors import InvalidDataErr, Errors, MfaRequiredErr, UnknownUser, InvalidKey diff --git a/yepcord/yepcord/utils/bit_flags.py b/yepcord/yepcord/utils/bit_flags.py new file mode 100644 index 0000000..a4534c1 --- /dev/null +++ b/yepcord/yepcord/utils/bit_flags.py @@ -0,0 +1,48 @@ +""" + YEPCord: Free open source selfhostable fully discord-compatible chat + Copyright (C) 2022-2024 RuslanUC + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" + + +class BitFlags: + __slots__ = ("cls", "value", "parsed",) + + def __init__(self, value: int, cls): + self.cls = cls + self.value = value + self.parsed = self.parse_flags() + + def parse_flags(self) -> list: + flags = [] + value = self.value + self.value = 0 + for val in self.cls.values().values(): + if (value & val) == val: + flags.append(val) + self.value |= val + return flags + + def add(self, val: int): + if val not in self.parsed: + self.value |= val + self.parsed.append(val) + return self + + def remove(self, val: int): + if val in self.parsed: + self.value &= ~val + self.parsed.remove(val) + return self diff --git a/yepcord/yepcord/classes/connections.py b/yepcord/yepcord/utils/connections.py similarity index 100% rename from yepcord/yepcord/classes/connections.py rename to yepcord/yepcord/utils/connections.py diff --git a/yepcord/yepcord/utils/email_msg.py b/yepcord/yepcord/utils/email_msg.py new file mode 100644 index 0000000..13b0856 --- /dev/null +++ b/yepcord/yepcord/utils/email_msg.py @@ -0,0 +1,54 @@ +""" + YEPCord: Free open source selfhostable fully discord-compatible chat + Copyright (C) 2022-2024 RuslanUC + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" + +from mailers import Mailer +from mailers.exceptions import DeliveryError + +from ..config import Config + + +class EmailMsg: + def __init__(self, to: str, subject: str, text: str): + self.to = to + self.subject = subject + self.text = text + + async def send(self): + mailer = Mailer(Config.MAIL_CONNECT_STRING) + try: + await mailer.send_message(self.to, self.subject, self.text, from_address="no-reply@yepcord.ml") + except DeliveryError: + pass + + @classmethod + async def send_verification(cls, email: str, token: str) -> None: + await cls(email, "Confirm your e-mail in YEPCord", ( + f"Thank you for signing up for a YEPCord account!\n" + f"First you need to make sure that you are you!\n" + f"Click to verify your email address:\n" + f"https://{Config.PUBLIC_HOST}/verify#token={token}" + )).send() + + @classmethod + async def send_mfa_code(cls, email: str, code: str) -> None: + await cls(email, f"Your one-time verification key is {code}", ( + f"It looks like you're trying to view your account's backup codes.\n" + f"This verification key expires in 10 minutes. This key is extremely sensitive, treat it like a " + f"password and do not share it with anyone.\n" + f"Enter it in the app to unlock your backup codes:\n{code}" + )).send() diff --git a/yepcord/yepcord/classes/fakeredis.py b/yepcord/yepcord/utils/fakeredis.py similarity index 100% rename from yepcord/yepcord/classes/fakeredis.py rename to yepcord/yepcord/utils/fakeredis.py diff --git a/yepcord/yepcord/utils/jwt.py b/yepcord/yepcord/utils/jwt.py new file mode 100644 index 0000000..6ffbed1 --- /dev/null +++ b/yepcord/yepcord/utils/jwt.py @@ -0,0 +1,75 @@ +""" + YEPCord: Free open source selfhostable fully discord-compatible chat + Copyright (C) 2022-2024 RuslanUC + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" + +from __future__ import annotations + +import hmac +import json +from hashlib import sha512 +from time import time +from typing import Union, Optional + +from ..utils import b64decode, b64encode + + +class JWT: + """ + Json Web Token Hmac-sha512 implementation + """ + + @staticmethod + def decode(token: str, secret: Union[str, bytes]) -> Optional[dict]: + try: + header, payload, signature = token.split(".") + header_dict = json.loads(b64decode(header).decode("utf8")) + assert header_dict.get("alg") == "HS512" + assert header_dict.get("typ") == "JWT" + assert (exp := header_dict.get("exp", 0)) > time() or exp == 0 + signature = b64decode(signature) + except (IndexError, AssertionError, ValueError): + return + + sig = f"{header}.{payload}".encode("utf8") + sig = hmac.new(secret, sig, sha512).digest() + if sig == signature: + payload = b64decode(payload).decode("utf8") + return json.loads(payload) + + @staticmethod + def encode( + payload: dict, secret: Union[str, bytes], expires_at: Optional[Union[int, float]] = None, + expires_after: Optional[int] = None + ) -> str: + if expires_after is not None: + expires_at = int(time() + expires_after) + if expires_at is None: + expires_at = 0 + + header = { + "alg": "HS512", + "typ": "JWT", + "exp": int(expires_at) + } + header = b64encode(json.dumps(header, separators=(',', ':'))) + payload = b64encode(json.dumps(payload, separators=(',', ':'))) + + signature = f"{header}.{payload}".encode("utf8") + signature = hmac.new(secret, signature, sha512).digest() + signature = b64encode(signature) + + return f"{header}.{payload}.{signature}" \ No newline at end of file diff --git a/yepcord/yepcord/utils/mfa.py b/yepcord/yepcord/utils/mfa.py new file mode 100644 index 0000000..6e7bfae --- /dev/null +++ b/yepcord/yepcord/utils/mfa.py @@ -0,0 +1,85 @@ +""" + YEPCord: Free open source selfhostable fully discord-compatible chat + Copyright (C) 2022-2024 RuslanUC + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +""" + +from __future__ import annotations + +import re +from base64 import b32decode +from hmac import new +from json import loads +from struct import pack, unpack +from time import time +from typing import Union, Optional + +from .jwt import JWT +from ..config import Config +from ..utils import b64decode, assert_ + + +class MFA: + _re = re.compile(r'^[A-Z0-9]{16}$') + + def __init__(self, key: str, uid: int): + self.key = str(key).upper() + self.uid = self.id = uid + + def get_code(self, timestamp: Union[int, float] = None) -> str: + if timestamp is None: + timestamp = time() + key = b32decode(self.key.upper() + '=' * ((8 - len(self.key)) % 8)) + counter = pack('>Q', int(timestamp / 30)) + mac = new(key, counter, "sha1").digest() + offset = mac[-1] & 0x0f + binary = unpack('>L', mac[offset:offset + 4])[0] & 0x7fffffff + return str(binary)[-6:].zfill(6) + + def get_codes(self) -> tuple[str, str]: + return self.get_code(time() - 5), self.get_code(time() + 1) + + @property + def valid(self) -> bool: + return bool(self._re.match(self.key)) + + @staticmethod + async def get_from_ticket(ticket: str) -> Optional[MFA]: + from ..models import User + + try: + user_id, session_id, sig = ticket.split(".") + user_id = loads(b64decode(user_id).decode("utf8"))[0] + session_id = int.from_bytes(b64decode(session_id), "big") + sig = b64decode(sig).decode("utf8") + + assert_(user := await User.y.get(user_id)) + assert_(payload := JWT.decode(sig, b64decode(Config.KEY))) + assert_(payload["u"] == user.id) + assert_(payload["s"] == session_id) + except (ValueError, IndexError): + return + + return MFA(await user.get_mfa_key(), user_id) + + @staticmethod + async def nonce_to_code(nonce: str) -> Optional[str]: + key = b64decode(Config.KEY) + + if not (payload := JWT.decode(nonce, key)): + return + token = JWT.encode({"code": payload["c"]}, key) + signature = token.split(".")[2] + return signature.replace("-", "").replace("_", "")[:8].upper() From 5f42177fd0411a623aa713a37136797fb4bffad4 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 20:07:40 +0300 Subject: [PATCH 20/26] some methods/arguments renaming in storage classes --- tests/cdn/test_storage_and_cdn.py | 26 +++---- yepcord/rest_api/routes/applications.py | 6 +- yepcord/rest_api/routes/channels.py | 2 +- yepcord/rest_api/routes/guilds.py | 24 +++---- yepcord/rest_api/routes/users_me.py | 4 +- yepcord/rest_api/routes/webhooks.py | 2 +- yepcord/yepcord/storage.py | 92 ++++++++++++------------- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/tests/cdn/test_storage_and_cdn.py b/tests/cdn/test_storage_and_cdn.py index 70bb105..c1d6a9e 100644 --- a/tests/cdn/test_storage_and_cdn.py +++ b/tests/cdn/test_storage_and_cdn.py @@ -73,7 +73,7 @@ async def test_avatar(storage: _Storage): client: TestClientType = app.test_client() user_id = Snowflake.makeId() - avatar_hash = await storage.setAvatarFromBytesIO(user_id, getImage(YEP_IMAGE)) + avatar_hash = await storage.setUserAvatar(user_id, getImage(YEP_IMAGE)) assert avatar_hash is not None and len(avatar_hash) == 32 response = await client.get(f"/avatars/{user_id}/{avatar_hash}.idk?size=240") @@ -91,7 +91,7 @@ async def test_avatar(storage: _Storage): img = BytesIO() Image.open(getImage(YEP_IMAGE)).convert("RGB").save(img, format="JPEG") - avatar_hash = await storage.setAvatarFromBytesIO(user_id, img) + avatar_hash = await storage.setUserAvatar(user_id, img) assert avatar_hash is not None and len(avatar_hash) == 32 response = await client.get(f"/avatars/{user_id}/{avatar_hash}.jpg?size=128") @@ -110,7 +110,7 @@ async def test_avatar_animated(storage: _Storage): img = BytesIO() im1.save(img, format="GIF", save_all=True, append_images=[im2, im3], duration=100, loop=0) - avatar_hash = await storage.setAvatarFromBytesIO(user_id, img) + avatar_hash = await storage.setUserAvatar(user_id, img) assert avatar_hash is not None and len(avatar_hash) == 34 and avatar_hash.startswith("a_") response = await client.get(f"/avatars/{user_id}/{avatar_hash}.gif?size=240") @@ -131,7 +131,7 @@ async def test_banner(storage: _Storage): client: TestClientType = app.test_client() user_id = Snowflake.makeId() - banner_hash = await storage.setBannerFromBytesIO(user_id, getImage(YEP_IMAGE)) + banner_hash = await storage.setGuildBanner(user_id, getImage(YEP_IMAGE)) assert banner_hash is not None and len(banner_hash) == 32 response = await client.get(f"/banners/{user_id}/{banner_hash}.idk?size=240") @@ -152,7 +152,7 @@ async def test_guild_splash(storage: _Storage): client: TestClientType = app.test_client() guild_id = Snowflake.makeId() - splash_hash = await storage.setGuildSplashFromBytesIO(guild_id, getImage(YEP_IMAGE)) + splash_hash = await storage.setGuildSplash(guild_id, getImage(YEP_IMAGE)) assert splash_hash is not None and len(splash_hash) == 32 response = await client.get(f"/splashes/{guild_id}/{splash_hash}.idk?size=240") @@ -173,7 +173,7 @@ async def test_channel_icon(storage: _Storage): client: TestClientType = app.test_client() channel_id = Snowflake.makeId() - channel_icon_hash = await storage.setChannelIconFromBytesIO(channel_id, getImage(YEP_IMAGE)) + channel_icon_hash = await storage.setChannelIcon(channel_id, getImage(YEP_IMAGE)) assert channel_icon_hash is not None and len(channel_icon_hash) == 32 response = await client.get(f"/channel-icons/{channel_id}/{channel_icon_hash}.idk?size=240") @@ -194,7 +194,7 @@ async def test_guild_icon(storage: _Storage): client: TestClientType = app.test_client() guild_id = Snowflake.makeId() - guild_icon_hash = await storage.setGuildIconFromBytesIO(guild_id, getImage(YEP_IMAGE)) + guild_icon_hash = await storage.setGuildIcon(guild_id, getImage(YEP_IMAGE)) assert guild_icon_hash is not None and len(guild_icon_hash) == 32 response = await client.get(f"/icons/{guild_id}/{guild_icon_hash}.idk?size=240") @@ -216,7 +216,7 @@ async def test_guild_avatar(storage: _Storage): user_id = Snowflake.makeId() guild_id = Snowflake.makeId() - avatar_hash = await storage.setGuildAvatarFromBytesIO(user_id, guild_id, getImage(YEP_IMAGE)) + avatar_hash = await storage.setUserGuildAvatar(user_id, guild_id, getImage(YEP_IMAGE)) assert avatar_hash is not None and len(avatar_hash) == 32 response = await client.get(f"/guilds/{guild_id}/users/{user_id}/avatars/{avatar_hash}.idk?size=240") @@ -245,7 +245,7 @@ async def test_sticker(storage: _Storage): sticker = await Sticker.create( id=Snowflake.makeId(), guild=guild, name="test", user=user, type=StickerType.GUILD, format=StickerFormat.PNG ) - sticker_hash = await storage.setStickerFromBytesIO(sticker.id, getImage(YEP_IMAGE)) + sticker_hash = await storage.setSticker(sticker.id, getImage(YEP_IMAGE)) assert sticker_hash == "sticker" response = await client.get(f"/stickers/{sticker.id}.idk?size=240") @@ -271,7 +271,7 @@ async def test_event_image(storage: _Storage): client: TestClientType = app.test_client() event_id = Snowflake.makeId() - event_hash = await storage.setGuildEventFromBytesIO(event_id, getImage(YEP_IMAGE)) + event_hash = await storage.setGuildEventIcon(event_id, getImage(YEP_IMAGE)) assert event_hash is not None and len(event_hash) == 32 response = await client.get(f"/guild-events/{event_id}/{event_hash}?size=241") @@ -291,7 +291,7 @@ async def test_emoji(storage: _Storage): user = await User.create(id=Snowflake.makeId(), email=f"test_{Snowflake.makeId()}@yepcord.ml", password="") guild = await Guild.Y.create(user, "test") emoji = await Emoji.create(id=Snowflake.makeId(), name="test", user=user, guild=guild) - emoji_info = await storage.setEmojiFromBytesIO(emoji.id, getImage(YEP_IMAGE)) + emoji_info = await storage.setEmoji(emoji.id, getImage(YEP_IMAGE)) assert not emoji_info["animated"] response = await client.get(f"/emojis/{emoji.id}.idk?size=240") @@ -317,7 +317,7 @@ async def test_role_icon(storage: _Storage): client: TestClientType = app.test_client() role_id = Snowflake.makeId() - role_icon_hash = await storage.setRoleIconFromBytesIO(role_id, getImage(YEP_IMAGE)) + role_icon_hash = await storage.setRoleIcon(role_id, getImage(YEP_IMAGE)) assert role_icon_hash is not None and len(role_icon_hash) == 32 response = await client.get(f"/role-icons/{role_id}/{role_icon_hash}.idk?size=240") @@ -365,7 +365,7 @@ async def test_app_icon(storage: _Storage): client: TestClientType = app.test_client() app_id = Snowflake.makeId() - avatar_hash = await storage.setAppIconFromBytesIO(app_id, getImage(YEP_IMAGE)) + avatar_hash = await storage.setAppIcon(app_id, getImage(YEP_IMAGE)) assert avatar_hash is not None and len(avatar_hash) == 32 response = await client.get(f"/app-icons/{app_id}/{avatar_hash}.idk?size=240") diff --git a/yepcord/rest_api/routes/applications.py b/yepcord/rest_api/routes/applications.py index b1a4a76..3e21b9b 100644 --- a/yepcord/rest_api/routes/applications.py +++ b/yepcord/rest_api/routes/applications.py @@ -71,10 +71,10 @@ async def edit_application(data: UpdateApplication, application: Application = D changes = data.model_dump(exclude_defaults=True) if "icon" in changes and changes["icon"] is not None: img = getImage(changes["icon"]) - image = await getStorage().setAppIconFromBytesIO(application.id, img) + image = await getStorage().setAppIcon(application.id, img) changes["icon"] = image if bot_data.avatar == application.icon: - await bot_data.update(avatar=await getStorage().setAvatarFromBytesIO(application.id, img)) + await bot_data.update(avatar=await getStorage().setUserAvatar(application.id, img)) bot_changes = {} if bot is not None and "bot_public" in changes: @@ -98,7 +98,7 @@ async def edit_application_bot(data: UpdateApplicationBot, application: Applicat changes = data.model_dump(exclude_defaults=True) if "avatar" in changes and changes["avatar"] is not None: img = getImage(changes["avatar"]) - avatar_hash = await getStorage().setAvatarFromBytesIO(application.id, img) + avatar_hash = await getStorage().setUserAvatar(application.id, img) changes["avatar"] = avatar_hash if changes: diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index f727b6d..5c3b6f0 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -69,7 +69,7 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha del changes["owner_id"] if "icon" in changes and changes["icon"] is not None: img = getImage(changes["icon"]) - image = await getStorage().setChannelIconFromBytesIO(channel.id, img) + image = await getStorage().setChannelIcon(channel.id, img) changes["icon"] = image if "icon" in changes and changes["icon"] != channel.icon: changed.append("icon") if "name" in changes and changes["name"] != channel.name: changed.append("name") diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index cae3297..44af6a6 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -59,7 +59,7 @@ async def create_guild(data: GuildCreate, user: User = DepUser): guild = await Guild.Y.create(user, data.name) if data.icon: img = getImage(data.icon) - if icon := await getStorage().setGuildIconFromBytesIO(guild.id, img): + if icon := await getStorage().setGuildIcon(guild.id, img): guild.icon = icon await guild.save(update_fields=["icon"]) @@ -80,9 +80,9 @@ async def update_guild(data: GuildUpdate, user: User = DepUser, guild: Guild = D member: GuildMember = DepGuildMember): await member.checkPermission(GuildPermissions.MANAGE_GUILD) data.owner_id = None # TODO: make guild ownership transfer - for image_type, func in (("icon", getStorage().setGuildIconFromBytesIO), - ("banner", getStorage().setBannerFromBytesIO), - ("splash", getStorage().setGuildSplashFromBytesIO)): + for image_type, func in (("icon", getStorage().setGuildIcon), + ("banner", getStorage().setGuildBanner), + ("splash", getStorage().setGuildSplash)): if img := getattr(data, image_type): setattr(data, image_type, "") img = getImage(img) @@ -178,7 +178,7 @@ async def create_guild_emoji(data: EmojiCreate, user: User = DepUser, guild: Gui await member.checkPermission(GuildPermissions.MANAGE_EMOJIS_AND_STICKERS) img = getImage(data.image) emoji_id = Snowflake.makeId() - result = await getStorage().setEmojiFromBytesIO(emoji_id, img) + result = await getStorage().setEmoji(emoji_id, img) emoji = await Emoji.create(id=emoji_id, name=data.name, user=user, guild=guild, animated=result["animated"]) await getGw().sendGuildEmojisUpdateEvent(guild) @@ -424,7 +424,7 @@ async def create_role(data: RoleCreate, user: User = DepUser, guild: Guild = Dep role_id = Snowflake.makeId() if data.icon: img = getImage(data.icon) - if h := await getStorage().setRoleIconFromBytesIO(role_id, img): + if h := await getStorage().setRoleIcon(role_id, img): data.icon = h role = await Role.create(id=role_id, guild=guild, **data.model_dump()) await getGw().dispatch(GuildRoleCreateEvent(guild.id, role.ds_json()), guild_id=guild.id, @@ -446,7 +446,7 @@ async def update_role(data: RoleUpdate, user: User = DepUser, guild: Guild = Dep if role.id != guild.id and data.icon != "" and (img := data.icon) is not None: data.icon = "" img = getImage(img) - if h := await getStorage().setRoleIconFromBytesIO(role.id, img): + if h := await getStorage().setRoleIcon(role.id, img): data.icon = h if role.id == guild.id: # Only allow permissions editing for @everyone role changes = {"permissions": data.permissions} if data.permissions is not None else {} @@ -601,7 +601,7 @@ async def update_member(data: MemberUpdate, target_user: str, user: User = DepUs if img is not None: img = getImage(img) data.avatar = "" - if av := await getStorage().setGuildAvatarFromBytesIO(user.id, guild.id, img): + if av := await getStorage().setUserGuildAvatar(user.id, guild.id, img): data.avatar = av changes = data.model_dump(exclude_defaults=True) await target_member.update(**changes) @@ -688,7 +688,7 @@ async def create_from_template(data: GuildCreateFromTemplate, template: str, use guild = await Guild.Y.create_from_template(user, template, data.name) if data.icon and (img := getImage(data.icon)) is not None: - if icon := await getStorage().setGuildIconFromBytesIO(guild.id, img): + if icon := await getStorage().setGuildIcon(guild.id, img): guild.icon = icon await guild.save(update_fields=["icon"]) @@ -760,7 +760,7 @@ async def upload_guild_stickers(user: User = DepUser, guild: Guild = DepGuild, m sticker_type = getattr(StickerFormat, str(imageType(img)).upper(), StickerFormat.PNG) sticker_id = Snowflake.makeId() - await getStorage().setStickerFromBytesIO(sticker_id, img) + await getStorage().setSticker(sticker_id, img) sticker = await Sticker.create( id=sticker_id, guild=guild, user=user, name=data.name, tags=data.tags, type=StickerType.GUILD, @@ -807,7 +807,7 @@ async def create_scheduled_event(data: CreateEvent, user: User = DepUser, guild: "code": "IMAGE_INVALID", "message": "Invalid image" }})) - img = await getStorage().setGuildEventFromBytesIO(event_id, img) + img = await getStorage().setGuildEventIcon(event_id, img) data.image = img data_dict = data.model_dump() @@ -860,7 +860,7 @@ async def update_scheduled_event(data: UpdateScheduledEvent, event_id: int, guil raise InvalidDataErr(400, Errors.make(50035, {"image": { "code": "IMAGE_INVALID", "message": "Invalid image" }})) - if h := await getStorage().setGuildEventFromBytesIO(event.id, img): + if h := await getStorage().setGuildEventIcon(event.id, img): img = h data.image = img diff --git a/yepcord/rest_api/routes/users_me.py b/yepcord/rest_api/routes/users_me.py index c3b3d29..a061e0e 100644 --- a/yepcord/rest_api/routes/users_me.py +++ b/yepcord/rest_api/routes/users_me.py @@ -81,7 +81,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): data.email = None if data.avatar != "" and data.avatar is not None: if (img := getImage(data.avatar)) and validImage(img): - if avatar := await getStorage().setAvatarFromBytesIO(user.id, img): + if avatar := await getStorage().setUserAvatar(user.id, img): data.avatar = avatar await userdata.refresh_from_db() @@ -97,7 +97,7 @@ async def update_me(data: UserUpdate, user: User = DepUser): async def get_my_profile(data: UserProfileUpdate, user: User = DepUser): if data.banner != "" and data.banner is not None: if (img := getImage(data.banner)) and validImage(img): - if banner := await getStorage().setBannerFromBytesIO(user.id, img): + if banner := await getStorage().setGuildBanner(user.id, img): data.banner = banner userdata = await user.data diff --git a/yepcord/rest_api/routes/webhooks.py b/yepcord/rest_api/routes/webhooks.py index 969bae6..ad360e9 100644 --- a/yepcord/rest_api/routes/webhooks.py +++ b/yepcord/rest_api/routes/webhooks.py @@ -80,7 +80,7 @@ async def edit_webhook( if (img := data.avatar) or img is None: if img is not None: img = getImage(img) - if h := await getStorage().setAvatarFromBytesIO(webhook.id, img): + if h := await getStorage().setUserAvatar(webhook.id, img): img = h data.avatar = img diff --git a/yepcord/yepcord/storage.py b/yepcord/yepcord/storage.py index ed6469b..2d20c73 100644 --- a/yepcord/yepcord/storage.py +++ b/yepcord/yepcord/storage.py @@ -18,7 +18,7 @@ from __future__ import annotations from abc import abstractmethod, ABCMeta -from asyncio import get_event_loop, gather +from asyncio import get_event_loop from concurrent.futures import ThreadPoolExecutor from contextvars import ContextVar from hashlib import md5 @@ -73,17 +73,17 @@ async def s_upload(self, path: str, data: Union[bytes, BytesIO]) -> None: async def resizeAnimImage(img: Image, size: Tuple[int, int], form: str) -> bytes: - def _resize() -> bytes: + def _resize(form_: str) -> bytes: frames = [] for frame in ImageSequence.Iterator(img): frames.append(frame.resize(size)) b = BytesIO() - frames[0].save(b, format=form, save_all=True, append_images=frames[1:], loop=0) + frames[0].save(b, format=form_, save_all=True, append_images=frames[1:], loop=0) return b.getvalue() with ThreadPoolExecutor() as pool: - res = await gather(get_event_loop().run_in_executor(pool, lambda: _resize())) - return res[0] + res = await get_event_loop().run_in_executor(pool, _resize, form) + return res async def resizeImage(image: Image, size: Tuple[int, int], form: str) -> bytes: @@ -99,8 +99,8 @@ def _resize(form_: str): return b.getvalue() with ThreadPoolExecutor() as pool: - res = await gather(get_event_loop().run_in_executor(pool, _resize, form)) - return res[0] + res = await get_event_loop().run_in_executor(pool, _resize, form) + return res def imageFrames(img: Image) -> int: @@ -132,15 +132,15 @@ async def _getResizeImage(self, paths: list[str], size: tuple[int, int], anim: b return data async def _getImage( - self, type: str, id: int, hash: str, size: int, fmt: str, def_size: int, size_f + self, type: str, obj_id: int, hash: str, size: int, fmt: str, def_size: int, size_f ) -> Optional[bytes]: anim = hash.startswith("a_") def_fmt = "gif" if anim else "png" paths = [f"{hash}_{size}.{fmt}", f"{hash}_{def_size}.{fmt}", f"{hash}_{def_size}.{def_fmt}"] - paths = [f"{type}s/{id}/{name}" for name in paths] + paths = [f"{type}s/{obj_id}/{name}" for name in paths] return await self._getResizeImage(paths, (size, size_f(size)), anim, fmt) - async def _setImage(self, type: str, id: int, size: int, size_f, image: BytesIO, def_hash: str = None) -> str: + async def _setImage(self, type: str, obj_id: int, size: int, size_f, image: BytesIO, def_hash: str = None) -> str: if def_hash is not None: hash = def_hash else: @@ -154,28 +154,28 @@ async def _setImage(self, type: str, id: int, size: int, size_f, image: BytesIO, size = (size, size_f(size)) coro = resizeImage(image, size, form) if not anim else resizeAnimImage(image, size, form) data = await coro - await self._write(f"{type}s/{id}/{hash}_{size[0]}.{form}", data) + await self._write(f"{type}s/{obj_id}/{hash}_{size[0]}.{form}", data) return hash - async def getAvatar(self, uid: int, avatar_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getAvatar(self, user_id: int, avatar_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = avatar_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage("avatar", uid, avatar_hash, size, fmt, def_size, lambda s: s) + return await self._getImage("avatar", user_id, avatar_hash, size, fmt, def_size, lambda s: s) - async def getChannelIcon(self, cid: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getChannelIcon(self, channel_id: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = icon_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage("channel_icon", cid, icon_hash, size, fmt, def_size, lambda s: s) + return await self._getImage("channel_icon", channel_id, icon_hash, size, fmt, def_size, lambda s: s) - async def getGuildIcon(self, gid: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getGuildIcon(self, guild_id: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = icon_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage("icon", gid, icon_hash, size, fmt, def_size, lambda s: s) + return await self._getImage("icon", guild_id, icon_hash, size, fmt, def_size, lambda s: s) - async def getGuildAvatar(self, uid: int, gid: int, avatar_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getGuildAvatar(self, user_id: int, gid: int, avatar_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = avatar_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage(f"guild/{gid}/avatar", uid, avatar_hash, size, fmt, def_size, lambda s: s) + return await self._getImage(f"guild/{gid}/avatar", user_id, avatar_hash, size, fmt, def_size, lambda s: s) async def getSticker(self, sticker_id: int, size: int, fmt: str, animated: bool) -> Optional[bytes]: sticker_hash = "a_sticker" if animated else "sticker" @@ -190,63 +190,63 @@ async def getEmoji(self, emoji_id: int, size: int, fmt: str, anim: bool) -> Opti paths = [f"emojis/{'/'.join(name)}" for name in paths] return await self._getResizeImage(paths, (size, size), anim, fmt) - async def getRoleIcon(self, rid: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getRoleIcon(self, role_id: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = icon_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage("role_icon", rid, icon_hash, size, fmt, def_size, lambda s: s) + return await self._getImage("role_icon", role_id, icon_hash, size, fmt, def_size, lambda s: s) - async def getBanner(self, uid: int, banner_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getBanner(self, user_id: int, banner_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = banner_hash.startswith("a_") def_size = 480 if anim else 600 - return await self._getImage("banner", uid, banner_hash, size, fmt, def_size, lambda s: int(9 * s / 16)) + return await self._getImage("banner", user_id, banner_hash, size, fmt, def_size, lambda s: int(9 * s / 16)) - async def getGuildSplash(self, gid: int, banner_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getGuildSplash(self, guild_id: int, banner_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = banner_hash.startswith("a_") def_size = 480 if anim else 600 - return await self._getImage("splash", gid, banner_hash, size, fmt, def_size, lambda s: int(9 * s / 16)) + return await self._getImage("splash", guild_id, banner_hash, size, fmt, def_size, lambda s: int(9 * s / 16)) - async def getAppIcon(self, aid: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: + async def getAppIcon(self, app_id: int, icon_hash: str, size: int, fmt: str) -> Optional[bytes]: anim = icon_hash.startswith("a_") def_size = 256 if anim else 1024 - return await self._getImage("app-icon", aid, icon_hash, size, fmt, def_size, lambda s: s) + return await self._getImage("app-icon", app_id, icon_hash, size, fmt, def_size, lambda s: s) - async def setAvatarFromBytesIO(self, uid: int, image: BytesIO) -> str: + async def setUserAvatar(self, user_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 - return await self._setImage("avatar", uid, size, lambda s: s, image) + return await self._setImage("avatar", user_id, size, lambda s: s, image) - async def setBannerFromBytesIO(self, uid: int, image: BytesIO) -> str: + async def setGuildBanner(self, guild_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 480 if a else 600 - return await self._setImage("banner", uid, size, lambda s: int(9 * s / 16), image) + return await self._setImage("banner", guild_id, size, lambda s: int(9 * s / 16), image) - async def setGuildSplashFromBytesIO(self, gid: int, image: BytesIO) -> str: + async def setGuildSplash(self, guild_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 480 if a else 600 - return await self._setImage("splash", gid, size, lambda s: int(9 * s / 16), image) + return await self._setImage("splash", guild_id, size, lambda s: int(9 * s / 16), image) - async def setChannelIconFromBytesIO(self, cid: int, image: BytesIO) -> str: + async def setChannelIcon(self, channel_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 - return await self._setImage("channel_icon", cid, size, lambda s: s, image) + return await self._setImage("channel_icon", channel_id, size, lambda s: s, image) - async def setGuildIconFromBytesIO(self, gid: int, image: BytesIO) -> str: + async def setGuildIcon(self, guild_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 - return await self._setImage("icon", gid, size, lambda s: s, image) + return await self._setImage("icon", guild_id, size, lambda s: s, image) - async def setGuildAvatarFromBytesIO(self, uid: int, gid: int, image: BytesIO) -> str: + async def setUserGuildAvatar(self, user_id: int, guild_id: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 - return await self._setImage(f"guild/{gid}/avatar", uid, size, lambda s: s, image) + return await self._setImage(f"guild/{guild_id}/avatar", user_id, size, lambda s: s, image) - async def setStickerFromBytesIO(self, sticker_id: int, image: BytesIO) -> str: + async def setSticker(self, sticker_id: int, image: BytesIO) -> str: return await self._setImage(f"sticker", sticker_id, 320, lambda s: s, image, def_hash="sticker") - async def setGuildEventFromBytesIO(self, event_id: int, image: BytesIO) -> str: + async def setGuildEventIcon(self, event_id: int, image: BytesIO) -> str: return await self._setImage(f"guild_event", event_id, 600, lambda s: int(9 * s / 16), image) - async def setEmojiFromBytesIO(self, emoji_id: int, image: BytesIO) -> dict: + async def setEmoji(self, emoji_id: int, image: BytesIO) -> dict: image = Image.open(image) anim = imageFrames(image) > 1 form = "gif" if anim else "png" @@ -255,12 +255,12 @@ async def setEmojiFromBytesIO(self, emoji_id: int, image: BytesIO) -> dict: await self._write(f"emojis/{emoji_id}/56.{form}", data) return {"animated": anim} - async def setRoleIconFromBytesIO(self, rid: int, image: BytesIO) -> str: + async def setRoleIcon(self, rid: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 return await self._setImage("role_icon", rid, size, lambda s: s, image) - async def setAppIconFromBytesIO(self, aid: int, image: BytesIO) -> str: + async def setAppIcon(self, aid: int, image: BytesIO) -> str: a = imageFrames(Image.open(image)) > 1 size = 256 if a else 1024 return await self._setImage("app-icon", aid, size, lambda s: s, image) @@ -359,10 +359,10 @@ async def getEmoji(self, emoji_id: int, size: int, fmt: str, anim: bool) -> Opti self.session.set(ftp) return await super().getEmoji(emoji_id, size, fmt, anim) - async def setEmojiFromBytesIO(self, emoji_id: int, image: BytesIO) -> dict: + async def setEmoji(self, emoji_id: int, image: BytesIO) -> dict: async with self._getClient() as ftp: self.session.set(ftp) - return await super().setEmojiFromBytesIO(emoji_id, image) + return await super().setEmoji(emoji_id, image) async def uploadAttachment(self, data: bytes, attachment: Attachment) -> int: async with self._getClient() as ftp: From 05208c1d39f961171a31f07fd3dd3e0566da2b25 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Sat, 12 Oct 2024 20:46:53 +0300 Subject: [PATCH 21/26] some warnings fix --- yepcord/rest_api/models/applications.py | 6 +++--- yepcord/rest_api/models/guilds.py | 6 +++--- yepcord/rest_api/routes/guilds.py | 1 + yepcord/rest_api/routes/interactions.py | 4 ++-- yepcord/rest_api/y_blueprint.py | 2 +- yepcord/yepcord/models/invite.py | 4 ++-- yepcord/yepcord/mq_broker.py | 15 +++++++++++---- yepcord/yepcord/utils/jwt.py | 2 +- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/yepcord/rest_api/models/applications.py b/yepcord/rest_api/models/applications.py index 50e127b..b63896d 100644 --- a/yepcord/rest_api/models/applications.py +++ b/yepcord/rest_api/models/applications.py @@ -23,8 +23,8 @@ class UpdateApplication(BaseModel): privacy_policy_url: Optional[str] = "" terms_of_service_url: Optional[str] = "" role_connections_verification_url: Optional[str] = "" - tags: list[str] = Field(default_factory=list, max_items=5) - redirect_uris: list[str] = Field(default_factory=list, max_items=9) + tags: list[str] = Field(default_factory=list, max_length=5) + redirect_uris: list[str] = Field(default_factory=list, max_length=9) max_participants: Optional[int] = None bot_public: bool = None bot_require_code_grant: bool = None @@ -111,7 +111,7 @@ def __init__(self, **kwargs): class CommandOption(CommandBase): type: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] required: bool = True - choices: ChoicesType = Field(default=None, max_items=25) + choices: ChoicesType = Field(default=None, max_length=25) channel_types: Optional[list[Literal[0, 1, 2, 3, 4, 5, 10, 11, 12, 13, 14]]] = None min_value: Optional[int] = None max_value: Optional[int] = None diff --git a/yepcord/rest_api/models/guilds.py b/yepcord/rest_api/models/guilds.py index f393a45..2c14e2b 100644 --- a/yepcord/rest_api/models/guilds.py +++ b/yepcord/rest_api/models/guilds.py @@ -16,7 +16,7 @@ along with this program. If not, see . """ -from datetime import datetime, timezone +from datetime import datetime, timezone, UTC from time import mktime from typing import Optional, List @@ -598,7 +598,7 @@ def validate_privacy_level(cls, value: int): @field_validator("start") def validate_start(cls, value: int): - if value < datetime.utcnow().timestamp(): + if value < datetime.now(UTC).timestamp(): raise InvalidDataErr(400, Errors.make(50035, {"scheduled_start_time": { "code": "BASE_TYPE_BAD_TIME", "message": "Time should be in future." }})) @@ -607,7 +607,7 @@ def validate_start(cls, value: int): @field_validator("end") def validate_end(cls, value: Optional[int], info: ValidationInfo): if value is not None: - if value < datetime.utcnow().timestamp() or value < info.data.get("start", value-1): + if value < datetime.now(UTC).timestamp() or value < info.data.get("start", value-1): raise InvalidDataErr(400, Errors.make(50035, {"scheduled_end_time": { "code": "BASE_TYPE_BAD_TIME", "message": "Time should be in future." }})) diff --git a/yepcord/rest_api/routes/guilds.py b/yepcord/rest_api/routes/guilds.py index 44af6a6..c3023a8 100644 --- a/yepcord/rest_api/routes/guilds.py +++ b/yepcord/rest_api/routes/guilds.py @@ -531,6 +531,7 @@ async def get_role_member_count(guild: Guild = DepGuild, member: GuildMember = D await member.checkPermission(GuildPermissions.MANAGE_ROLES) counts = {} for role in await Role.filter(guild=guild).select_related("guildmembers").annotate(m=Count("guildmembers")): + # noinspection PyUnresolvedReferences counts[role.id] = role.m return counts diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index 92c6ee9..291e200 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -27,8 +27,8 @@ from ...yepcord.ctx import getGw from ...yepcord.enums import GuildPermissions, InteractionStatus, MessageFlags, InteractionCallbackType, \ ApplicationCommandOptionType, MessageType, ApplicationCommandType -from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, UnknownMessage, \ - UnknownUser, MissingAccess, CannotSendEmptyMessage, InteractionAlreadyAck, Unauthorized +from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, \ + UnknownMessage, UnknownUser, MissingAccess, CannotSendEmptyMessage, InteractionAlreadyAck, Unauthorized from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild, Channel from ...yepcord.models.interaction import Interaction from ...yepcord.snowflake import Snowflake diff --git a/yepcord/rest_api/y_blueprint.py b/yepcord/rest_api/y_blueprint.py index 392e7a5..b44221d 100644 --- a/yepcord/rest_api/y_blueprint.py +++ b/yepcord/rest_api/y_blueprint.py @@ -17,7 +17,7 @@ """ from functools import wraps -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Awaitable from fast_depends import inject from flask.sansio.scaffold import T_route, setupmethod diff --git a/yepcord/yepcord/models/invite.py b/yepcord/yepcord/models/invite.py index cc202ab..5561865 100644 --- a/yepcord/yepcord/models/invite.py +++ b/yepcord/yepcord/models/invite.py @@ -18,7 +18,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, UTC from typing import Optional from tortoise import fields @@ -61,7 +61,7 @@ async def ds_json(self, with_counts: bool=False) -> dict: expires_at = None if self.max_age > 0: expires_timestamp = int(Snowflake.toTimestamp(self.id) / 1000) + self.max_age - expires_at = datetime.utcfromtimestamp(expires_timestamp).strftime("%Y-%m-%dT%H:%M:%S+00:00") + expires_at = datetime.fromtimestamp(expires_timestamp, UTC).strftime("%Y-%m-%dT%H:%M:%S+00:00") data = { "code": self.code, "inviter": userdata.ds_json, diff --git a/yepcord/yepcord/mq_broker.py b/yepcord/yepcord/mq_broker.py index 5757f46..58c00a6 100644 --- a/yepcord/yepcord/mq_broker.py +++ b/yepcord/yepcord/mq_broker.py @@ -21,6 +21,7 @@ from json import dumps, loads from typing import Union, Optional, Callable, Coroutine +import websockets.exceptions from async_timeout import timeout from faststream.rabbit import RabbitBroker from faststream.redis import RedisBroker @@ -114,6 +115,7 @@ async def _real_run_client(self) -> None: # pragma: no cover async def _run_client(self) -> None: for _ in range(5): + # noinspection PyBroadException,PyPep8 try: await self._real_run_client() break @@ -149,10 +151,15 @@ async def close(self) -> None: async def publish(self, message: dict, channel: str) -> None: if self._connection is None or self._connection.closed: # pragma: no cover await self.start() - await self._connection.send(dumps({ - "channel": channel, - "message": message, - })) + for _ in range(5): + try: + await self._connection.send(dumps({ + "channel": channel, + "message": message, + })) + return + except websockets.ConnectionClosed: + await self.start() def subscriber(self, channel: str) -> Callable: # pragma: no cover def _handle(func): diff --git a/yepcord/yepcord/utils/jwt.py b/yepcord/yepcord/utils/jwt.py index 6ffbed1..4258e06 100644 --- a/yepcord/yepcord/utils/jwt.py +++ b/yepcord/yepcord/utils/jwt.py @@ -72,4 +72,4 @@ def encode( signature = hmac.new(secret, signature, sha512).digest() signature = b64encode(signature) - return f"{header}.{payload}.{signature}" \ No newline at end of file + return f"{header}.{payload}.{signature}" From eb11ef434fa99e9a967e4e3abeeb4688f9b6e469 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 15 Oct 2024 19:46:22 +0300 Subject: [PATCH 22/26] add readstate updating --- yepcord/rest_api/models/guilds.py | 3 ++- yepcord/rest_api/routes/channels.py | 32 +++++++++++++++++-------- yepcord/rest_api/routes/interactions.py | 11 +++++---- yepcord/rest_api/routes/invites.py | 4 +++- yepcord/rest_api/routes/oauth2.py | 3 ++- yepcord/rest_api/utils.py | 7 ++++-- yepcord/yepcord/models/invite.py | 3 ++- yepcord/yepcord/models/readstate.py | 28 ++++++++++++++++++++++ 8 files changed, 71 insertions(+), 20 deletions(-) diff --git a/yepcord/rest_api/models/guilds.py b/yepcord/rest_api/models/guilds.py index 2c14e2b..f57143b 100644 --- a/yepcord/rest_api/models/guilds.py +++ b/yepcord/rest_api/models/guilds.py @@ -16,7 +16,8 @@ along with this program. If not, see . """ -from datetime import datetime, timezone, UTC +from datetime import datetime, timezone +from pytz import UTC from time import mktime from typing import Optional, List diff --git a/yepcord/rest_api/routes/channels.py b/yepcord/rest_api/routes/channels.py index 5c3b6f0..1e852e8 100644 --- a/yepcord/rest_api/routes/channels.py +++ b/yepcord/rest_api/routes/channels.py @@ -93,12 +93,18 @@ async def update_channel(data: ChannelUpdate, user: User = DepUser, channel: Cha if channel.type == ChannelType.GROUP_DM: if "name" in changed: - message = await Message.create(id=Snowflake.makeId(), channel=channel, author=user, - type=MessageType.CHANNEL_NAME_CHANGE, content=channel.name) + message = await Message.create( + id=Snowflake.makeId(), channel=channel, author=user, + type=MessageType.CHANNEL_NAME_CHANGE, content=channel.name, + ) + await ReadState.update_from_message(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) if "icon" in changed: - message = await Message.create(id=Snowflake.makeId(), channel=channel, author=user, - type=MessageType.CHANNEL_ICON_CHANGE, content="") + message = await Message.create( + id=Snowflake.makeId(), channel=channel, author=user, + type=MessageType.CHANNEL_ICON_CHANGE, content="", + ) + await ReadState.update_from_message(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) elif channel.type in GUILD_CHANNELS: if "parent" in changes: @@ -283,6 +289,7 @@ async def add_recipient(target_user: int, user: User = DepUser, channel: Channel id=Snowflake.makeId(), author=user, channel=channel, content="", type=MessageType.RECIPIENT_ADD, extra_data={"user": target_user.id} ) + await ReadState.update_from_message(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) await channel.recipients.add(target_user) target_user_data = await target_user.data @@ -302,9 +309,10 @@ async def delete_recipient(target_user: int, user: User = DepUser, channel: Chan target_user = await User.y.get(target_user, False) recipients = await channel.recipients.all() if target_user in recipients: - msg = await Message.create(id=Snowflake.makeId(), author=user, channel=channel, content="", + message = await Message.create(id=Snowflake.makeId(), author=user, channel=channel, content="", type=MessageType.RECIPIENT_REMOVE, extra_data={"user": target_user.id}) - await getGw().dispatch(MessageCreateEvent(await msg.ds_json()), channel=msg.channel) + await ReadState.update_from_message(message) + await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=message.channel) await channel.recipients.remove(target_user) target_user_data = await target_user.data await getGw().dispatch(ChannelRecipientRemoveEvent(channel.id, target_user_data.ds_json), @@ -332,9 +340,11 @@ async def pin_message(user: User = DepUser, channel: Channel = DepChannel, messa id=Snowflake.makeId(), author=user, channel=channel, type=MessageType.CHANNEL_PINNED_MESSAGE, content="", message_reference=message_ref, guild=channel.guild ) + await ReadState.update_from_message(msg) - await getGw().dispatch(MessageCreateEvent(await msg.ds_json()), channel=msg.channel, - permissions=GuildPermissions.VIEW_CHANNEL) + await getGw().dispatch( + MessageCreateEvent(await msg.ds_json()), channel=msg.channel, permissions=GuildPermissions.VIEW_CHANNEL + ) return "", 204 @@ -565,14 +575,16 @@ async def create_thread( name=data.name, owner=user, parent=channel, flags=0) thread_member = await ThreadMember.create(id=Snowflake.makeId(), user=user, channel=thread, guild=channel.guild) - await Message.create( + message_ = await Message.create( id=Snowflake.makeId(), channel=thread, author=user, content="", type=MessageType.THREAD_STARTER_MESSAGE, message_reference={"message_id": message.id, "channel_id": channel.id, "guild_id": channel.guild.id} ) - await Message.create( + await ReadState.update_from_message(message_) + message_ = await Message.create( id=Snowflake.makeId(), channel=channel, author=user, content=thread.name, type=MessageType.THREAD_CREATED, message_reference={"message_id": message.id, "channel_id": channel.id, "guild_id": channel.guild.id} ) + await ReadState.update_from_message(message_) await ThreadMetadata.create(id=thread.id, channel=thread, archive_timestamp=datetime(1970, 1, 1), auto_archive_duration=data.auto_archive_duration) diff --git a/yepcord/rest_api/routes/interactions.py b/yepcord/rest_api/routes/interactions.py index 291e200..0425d1b 100644 --- a/yepcord/rest_api/routes/interactions.py +++ b/yepcord/rest_api/routes/interactions.py @@ -29,7 +29,7 @@ ApplicationCommandOptionType, MessageType, ApplicationCommandType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownChannel, UnknownGuild, \ UnknownMessage, UnknownUser, MissingAccess, CannotSendEmptyMessage, InteractionAlreadyAck, Unauthorized -from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild, Channel +from ...yepcord.models import User, Application, ApplicationCommand, Integration, Message, Guild, Channel, ReadState from ...yepcord.models.interaction import Interaction from ...yepcord.snowflake import Snowflake from ...yepcord.utils import execute_after @@ -201,9 +201,12 @@ async def send_interaction_response(interaction: Interaction, flags: bool, conte is_loading = flags & MessageFlags.LOADING == MessageFlags.LOADING bot_user = await User.y.get(interaction.application.id) - message = await Message.create(id=Snowflake.makeId(), author=bot_user, content=content, flags=flags, - interaction=interaction, channel=interaction.channel, ephemeral=is_ephemeral, - webhook_id=interaction.id, type=MessageType.CHAT_INPUT_COMMAND) + message = await Message.create( + id=Snowflake.makeId(), author=bot_user, content=content, flags=flags, interaction=interaction, + channel=interaction.channel, ephemeral=is_ephemeral, webhook_id=interaction.id, + type=MessageType.CHAT_INPUT_COMMAND + ) + await ReadState.update_from_message(message) message_obj = await message.ds_json() | {"nonce": str(interaction.nonce)} kw = {"session_id": interaction.session_id} if is_ephemeral else {} diff --git a/yepcord/rest_api/routes/invites.py b/yepcord/rest_api/routes/invites.py index 54a7ecc..f18197b 100644 --- a/yepcord/rest_api/routes/invites.py +++ b/yepcord/rest_api/routes/invites.py @@ -24,7 +24,7 @@ from ...yepcord.ctx import getGw from ...yepcord.enums import ChannelType, GuildPermissions, MessageType from ...yepcord.errors import UnknownInvite, UserBanned, MissingAccess -from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan, Channel +from ...yepcord.models import Invite, User, Message, GuildMember, GuildBan, Channel, ReadState from ...yepcord.snowflake import Snowflake # Base path is /api/vX/invites @@ -58,6 +58,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): id=Snowflake.makeId(), author=channel.owner, channel=channel, content="", type=MessageType.RECIPIENT_ADD, extra_data={"user": user.id} ) + await ReadState.update_from_message(message) await channel.recipients.add(user) await getGw().dispatch(ChannelRecipientAddEvent(channel.id, (await user.data).ds_json), user_ids=[recipient.id for recipient in recipients]) @@ -84,6 +85,7 @@ async def use_invite(user: User = DepUser, invite: Invite = DepInvite): id=Snowflake.makeId(), author=user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild ) + await ReadState.update_from_message(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=sys_channel, permissions=GuildPermissions.VIEW_CHANNEL) await invite.use() diff --git a/yepcord/rest_api/routes/oauth2.py b/yepcord/rest_api/routes/oauth2.py index 0083f32..cbd74bc 100644 --- a/yepcord/rest_api/routes/oauth2.py +++ b/yepcord/rest_api/routes/oauth2.py @@ -31,7 +31,7 @@ from ...yepcord.enums import ApplicationScope, GuildPermissions, MessageType from ...yepcord.errors import Errors, InvalidDataErr, UnknownApplication, UnknownGuild, MissingAccess from ...yepcord.models import User, Guild, GuildMember, Message, Role, AuditLogEntry, Application, Bot, Authorization, \ - Integration, GuildBan, Channel + Integration, GuildBan, Channel, ReadState from ...yepcord.snowflake import Snowflake from ...yepcord.utils import b64decode @@ -141,6 +141,7 @@ async def authorize_application(query_args: AppAuthorizePostQs, data: AppAuthori id=Snowflake.makeId(), author=bot.user, channel=sys_channel, content="", type=MessageType.USER_JOIN, guild=guild ) + await ReadState.update_from_message(message) await getGw().dispatch(MessageCreateEvent(await message.ds_json()), channel=sys_channel, permissions=GuildPermissions.VIEW_CHANNEL) diff --git a/yepcord/rest_api/utils.py b/yepcord/rest_api/utils.py index 61a075f..53f3344 100644 --- a/yepcord/rest_api/utils.py +++ b/yepcord/rest_api/utils.py @@ -29,7 +29,7 @@ import yepcord.yepcord.models as models from ..yepcord.utils.captcha import Captcha from ..yepcord.config import Config -from ..yepcord.enums import MessageType +from ..yepcord.enums import MessageType, ChannelType, GUILD_CHANNELS from ..yepcord.errors import Errors, InvalidDataErr, UnknownChannel, UnknownMessage, InvalidFormBody, \ FileExceedsMaxSize, CannotSendEmptyMessage from ..yepcord.models import Session, User, Channel, Attachment, Authorization, Bot, Webhook, Message, Sticker @@ -236,7 +236,10 @@ async def processMessage(data: dict, channel: Channel, author: Optional[User], v data_json["webhook_id"] = webhook.id message = await models.Message.create( id=Snowflake.makeId(), channel=channel, author=author, **data_json, **stickers_data, type=message_type, - guild=channel.guild, webhook_author=w_author) + guild=channel.guild, webhook_author=w_author, + ) + await models.ReadState.update_from_message(message) + message.nonce = data_json.get("nonce") for attachment in attachments: diff --git a/yepcord/yepcord/models/invite.py b/yepcord/yepcord/models/invite.py index 5561865..cbde881 100644 --- a/yepcord/yepcord/models/invite.py +++ b/yepcord/yepcord/models/invite.py @@ -18,7 +18,8 @@ from __future__ import annotations -from datetime import datetime, UTC +from datetime import datetime +from pytz import UTC from typing import Optional from tortoise import fields diff --git a/yepcord/yepcord/models/readstate.py b/yepcord/yepcord/models/readstate.py index 9127d97..777b8cf 100644 --- a/yepcord/yepcord/models/readstate.py +++ b/yepcord/yepcord/models/readstate.py @@ -16,10 +16,15 @@ along with this program. If not, see . """ +from __future__ import annotations + +from typing import Optional + from tortoise import fields import yepcord.yepcord.models as models from ._utils import SnowflakeField, Model +from ..enums import ChannelType, GUILD_CHANNELS class ReadState(Model): @@ -43,3 +48,26 @@ async def ds_json(self) -> dict: "last_message_id": str(self.last_read_id), "id": str(self.channel.id), } + + @classmethod + async def create_or_add( + cls, user: models.User, channel: models.Channel, mentions: int = 1, last_read_id: Optional[int] = None, + ) -> ReadState: + state, created = await cls.get_or_create(user=user, channel=channel, defaults={ + "count": mentions, + "last_read_id": last_read_id or 0, + }) + if not created: + state.count += mentions + state.last_read_id = last_read_id or state.last_read_id + await state.save(update_fields=["count", "last_read_id"]) + + return state + + @classmethod + async def update_from_message(cls, message: models.Message) -> None: + if message.channel.type in (ChannelType.DM, ChannelType.GROUP_DM): + for user in await message.channel.recipients.filter(id__not=message.author.id): + await models.ReadState.create_or_add(user, message.channel) + elif message.channel.type in GUILD_CHANNELS: + ... # TODO: update read state of mentioned users From 023fe439e732b9f1b08ee468c84aad7c5d34f45d Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 22 Oct 2024 14:51:57 +0300 Subject: [PATCH 23/26] remove core.py; fix asgi.py --- poetry.lock | 1016 +++++++++++++++++++++------------------ pyproject.toml | 19 +- yepcord/asgi.py | 3 +- yepcord/yepcord/core.py | 37 -- 4 files changed, 553 insertions(+), 522 deletions(-) delete mode 100644 yepcord/yepcord/core.py diff --git a/poetry.lock b/poetry.lock index 0f83357..abf3440 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,17 +50,17 @@ files = [ [[package]] name = "aioftp" -version = "0.22.3" +version = "0.23.1" description = "ftp client/server for asyncio" optional = true -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "aioftp-0.22.3-py3-none-any.whl", hash = "sha256:93d17d5d3b8033570f2dfee013172d49e52323437925120ec87190539113ebd0"}, - {file = "aioftp-0.22.3.tar.gz", hash = "sha256:baa2b13186aa01622e4b82f27c2f48f4dafb48e457a6b18fcda99a925e0dc270"}, + {file = "aioftp-0.23.1-py3-none-any.whl", hash = "sha256:62d1e59804afc7e42c57e8b3fa44e03e39630323cc666018a4e870d79dc58ec0"}, + {file = "aioftp-0.23.1.tar.gz", hash = "sha256:b80eadd8ca95d2bbbcfabe7de15cbe01ac117b2e7467717374de467bad93570b"}, ] [package.extras] -dev = ["alabaster", "async-timeout (>=4.0.0)", "black", "docutils (<0.18.0)", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "ruff", "siosocks", "sphinx", "trustme"] +dev = ["alabaster", "async-timeout (>=4.0.0)", "docutils (<0.18.0)", "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", "ruff", "siosocks", "sphinx", "trustme"] socks = ["siosocks (>=0.2.0)"] [[package]] @@ -147,17 +147,21 @@ uvloop = ["uvloop (>=0.18)"] [[package]] name = "aiosqlite" -version = "0.17.0" +version = "0.20.0" description = "asyncio bridge to the standard sqlite3 module" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, - {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, ] [package.dependencies] -typing_extensions = ">=3.7.2" +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] [[package]] name = "annotated-types" @@ -172,13 +176,13 @@ files = [ [[package]] name = "anyio" -version = "4.6.0" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, - {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] @@ -189,7 +193,7 @@ typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -500,73 +504,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.2" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, - {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, - {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, - {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, - {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, - {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, - {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, - {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, - {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, - {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, - {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, - {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, - {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, - {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, - {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, - {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.dependencies] @@ -577,38 +581,38 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.1" +version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, - {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, - {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, - {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, - {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, - {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, - {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, - {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, - {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, - {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, - {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, - {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, - {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, + {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, + {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, + {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, + {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, + {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, + {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, + {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, + {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, + {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, + {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, + {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, + {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, + {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, ] [package.dependencies] @@ -621,7 +625,7 @@ nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] @@ -689,13 +693,13 @@ uvicorn = ">=0.27.1" [[package]] name = "fast-depends" -version = "2.4.11" +version = "2.4.12" description = "FastDepends - extracted and cleared from HTTP domain logic FastAPI Dependency Injection System. Async and sync are both supported." optional = false python-versions = ">=3.8" files = [ - {file = "fast_depends-2.4.11-py3-none-any.whl", hash = "sha256:f361d6b6375184a800e93ecaf874a5ebc9905fe4f5fafecc50de2c81acf850d8"}, - {file = "fast_depends-2.4.11.tar.gz", hash = "sha256:29c8022232d1f67408d41fc251d2756c74ba98298e0502454de9f42df99a2ab0"}, + {file = "fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2"}, + {file = "fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d"}, ] [package.dependencies] @@ -704,13 +708,13 @@ pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<3.0.0" [[package]] name = "faststream" -version = "0.5.25" +version = "0.5.28" description = "FastStream: the simplest way to work with a messaging queues" optional = false python-versions = ">=3.8" files = [ - {file = "faststream-0.5.25-py3-none-any.whl", hash = "sha256:872d88a112d3d9c454dbc0fa4fe4211717e4422b3aa2f75f9d239bffc3c31075"}, - {file = "faststream-0.5.25.tar.gz", hash = "sha256:926a4a8e013140a37b1c06ad16eb9a35529c9f626def3d846ec4ac171e050bc6"}, + {file = "faststream-0.5.28-py3-none-any.whl", hash = "sha256:620f3edd2b85ac3d329726907540027abebc939ea7f16d209ef18de7d6e82f49"}, + {file = "faststream-0.5.28.tar.gz", hash = "sha256:67794ffa8054169488f211832c50c3a992d897ebba415032a327372e019bda4c"}, ] [package.dependencies] @@ -724,19 +728,20 @@ typing-extensions = ">=4.8.0" [package.extras] cli = ["typer (>=0.9,!=0.12,<1)", "watchfiles (>=0.15.0,<0.25.0)"] -confluent = ["confluent-kafka (>=2,<3)"] -dev = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "cairosvg", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "coverage[toml] (==7.6.1)", "detect-secrets (==1.5.0)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.0)", "httpx (==0.27.2)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.2.0)", "mkdocs-material (==9.5.36)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.1)", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "pillow", "pre-commit (==3.5.0)", "pre-commit (==3.8.0)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "redis (>=5.0.0,<6.0.0)", "requests", "ruff (==0.6.7)", "semgrep (==1.89.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "typing-extensions (>=4.8.0,<4.12.1)", "watchfiles (>=0.15.0,<0.25.0)"] -devdocs = ["cairosvg", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.2.0)", "mkdocs-material (==9.5.36)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.1)", "pillow", "requests"] +confluent = ["confluent-kafka (>=2,<3)", "confluent-kafka (>=2.6,<3)"] +dev = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.10)", "cairosvg", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka (>=2.6,<3)", "confluent-kafka-stubs", "coverage[toml] (==7.6.1)", "coverage[toml] (==7.6.3)", "detect-secrets (==1.5.0)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.2)", "httpx (==0.27.2)", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.3.5)", "mkdocs-material (==9.5.40)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.2)", "mypy (==1.12.0)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "pillow", "pre-commit (==3.5.0)", "pre-commit (==4.0.1)", "prometheus-client (>=0.20.0,<0.30.0)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "redis (>=5.0.0,<6.0.0)", "requests", "ruff (==0.6.9)", "semgrep (==1.91.0)", "typer (>=0.9,!=0.12,<1)", "types-aiofiles", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "typing-extensions (>=4.8.0,<4.12.1)", "watchfiles (>=0.15.0,<0.25.0)"] +devdocs = ["cairosvg", "mdx-include (==1.4.2)", "mike (==2.1.3)", "mkdocs-git-revision-date-localized-plugin (==1.2.9)", "mkdocs-glightbox (==0.4.0)", "mkdocs-literate-nav (==0.6.1)", "mkdocs-macros-plugin (==1.3.5)", "mkdocs-material (==9.5.40)", "mkdocs-minify-plugin (==0.8.0)", "mkdocs-static-i18n (==1.2.3)", "mkdocstrings[python] (==0.26.2)", "pillow", "requests"] kafka = ["aiokafka (>=0.9,<0.12)"] -lint = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.9)", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "ruff (==0.6.7)", "semgrep (==1.89.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] +lint = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "bandit (==1.7.10)", "codespell (==2.3.0)", "confluent-kafka (>=2,<3)", "confluent-kafka (>=2.6,<3)", "confluent-kafka-stubs", "mypy (==1.12.0)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "prometheus-client (>=0.20.0,<0.30.0)", "redis (>=5.0.0,<6.0.0)", "ruff (==0.6.9)", "semgrep (==1.91.0)", "typer (>=0.9,!=0.12,<1)", "types-aiofiles", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] nats = ["nats-py (>=2.7.0,<=3.0.0)"] -optionals = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "watchfiles (>=0.15.0,<0.25.0)"] +optionals = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "confluent-kafka (>=2.6,<3)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "prometheus-client (>=0.20.0,<0.30.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "watchfiles (>=0.15.0,<0.25.0)"] otel = ["opentelemetry-sdk (>=1.24.0,<2.0.0)"] +prometheus = ["prometheus-client (>=0.20.0,<0.30.0)"] rabbit = ["aio-pika (>=9,<10)"] redis = ["redis (>=5.0.0,<6.0.0)"] -test-core = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "typing-extensions (>=4.8.0,<4.12.1)"] -testing = ["coverage[toml] (==7.6.1)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.0)", "httpx (==0.27.2)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "typing-extensions (>=4.8.0,<4.12.1)"] -types = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "confluent-kafka-stubs", "mypy (==1.11.2)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] +test-core = ["coverage[toml] (==7.6.1)", "coverage[toml] (==7.6.3)", "dirty-equals (==0.8.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "typing-extensions (>=4.8.0,<4.12.1)"] +testing = ["coverage[toml] (==7.6.1)", "coverage[toml] (==7.6.3)", "dirty-equals (==0.8.0)", "email-validator (==2.2.0)", "fastapi (==0.115.2)", "httpx (==0.27.2)", "pydantic-settings (>=2.0.0,<3.0.0)", "pytest (==8.3.3)", "pytest-asyncio (==0.24.0)", "pyyaml (==6.0.2)", "typing-extensions (>=4.8.0,<4.12.1)"] +types = ["aio-pika (>=9,<10)", "aiokafka (>=0.9,<0.12)", "confluent-kafka (>=2,<3)", "confluent-kafka (>=2.6,<3)", "confluent-kafka-stubs", "mypy (==1.12.0)", "nats-py (>=2.7.0,<=3.0.0)", "opentelemetry-sdk (>=1.24.0,<2.0.0)", "prometheus-client (>=0.20.0,<0.30.0)", "redis (>=5.0.0,<6.0.0)", "typer (>=0.9,!=0.12,<1)", "types-aiofiles", "types-deprecated", "types-docutils", "types-pygments", "types-pyyaml", "types-redis", "types-setuptools", "types-ujson", "watchfiles (>=0.15.0,<0.25.0)"] [[package]] name = "flask" @@ -932,13 +937,13 @@ files = [ [[package]] name = "iso8601" -version = "1.1.0" +version = "2.1.0" description = "Simple module to parse ISO 8601 dates" optional = false -python-versions = ">=3.6.2,<4.0" +python-versions = ">=3.7,<4.0" files = [ - {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, - {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, + {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, + {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, ] [[package]] @@ -991,72 +996,72 @@ smtp = ["aiosmtplib (>=3.0,<4.0)"] [[package]] name = "markupsafe" -version = "3.0.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, - {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, - {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, - {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, - {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, - {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, - {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, - {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1269,68 +1274,68 @@ files = [ [[package]] name = "orjson" -version = "3.10.7" +version = "3.10.9" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, - {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, - {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, - {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, - {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, - {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, - {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, - {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, - {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, - {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, - {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, - {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, - {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, - {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, - {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, - {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, - {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, - {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, - {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, - {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, - {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, - {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, - {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, - {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, - {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, - {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, - {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, - {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, - {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, - {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, - {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, - {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, - {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, - {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, - {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, - {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, - {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, + {file = "orjson-3.10.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a377186a11b48c55969e34f0aa414c2826a234f212d6f2b312ba512e3cdb2c6f"}, + {file = "orjson-3.10.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bf37bf0ca538065c34efe1803378b2dadd7e05b06610a086c2857f15ee59e12"}, + {file = "orjson-3.10.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7d9d83a91168aa48309acba804e393b7d9216b66f15e38f339b9fbb00db8986d"}, + {file = "orjson-3.10.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0014038a17a1fe273da0a5489787677ef5a64566ab383ad6d929e44ed5683f4"}, + {file = "orjson-3.10.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6ae1b1733e4528e45675ed09a732b6ac37d716bce2facaf467f84ce774adecd"}, + {file = "orjson-3.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe91c2259c4a859356b6db1c6e649b40577492f66d483da8b8af6da0f87c00e3"}, + {file = "orjson-3.10.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a04f912c32463386ba117591c99a3d9e40b3b69bed9c5123d89dff06f0f5a4b0"}, + {file = "orjson-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae82ca347829ca47431767b079f96bb977f592189250ccdede676339a80c8982"}, + {file = "orjson-3.10.9-cp310-none-win32.whl", hash = "sha256:fd5083906825d7f5d23089425ce5424d783d6294020bcabb8518a3e1f97833e5"}, + {file = "orjson-3.10.9-cp310-none-win_amd64.whl", hash = "sha256:e9ff9521b5be0340c8e686bcfe2619777fd7583f71e7b494601cc91ad3919d2e"}, + {file = "orjson-3.10.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f3bd9df47385b8fabb3b2ee1e83f9960b8accc1905be971a1c257f16c32b491e"}, + {file = "orjson-3.10.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4948961b6bce1e2086b2cf0b56cc454cdab589d40c7f85be71fb5a5556c51d3"}, + {file = "orjson-3.10.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a9fc7a6cf2b229ddc323e136df13b3fb4466c50d84ed600cd0898223dd2fea3"}, + {file = "orjson-3.10.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2314846e1029a2d2b899140f350eaaf3a73281df43ba84ac44d94ca861b5b269"}, + {file = "orjson-3.10.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f52d993504827503411df2d60e60acf52885561458d6273f99ecd172f31c4352"}, + {file = "orjson-3.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e29bbf08d907756c145a3a3a1f7ce2f11f15e3edbd3342842589d6030981b76f"}, + {file = "orjson-3.10.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ae82992c00b480c3cc7dac6739324554be8c5d8e858a90044928506a3333ef4"}, + {file = "orjson-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6fdf8d32b6d94019dc15163542d345e9ce4c4661f56b318608aa3088a1a3a23b"}, + {file = "orjson-3.10.9-cp311-none-win32.whl", hash = "sha256:01f5fef452b4d7615f2e94153479370a4b59e0c964efb32dd902978f807a45cd"}, + {file = "orjson-3.10.9-cp311-none-win_amd64.whl", hash = "sha256:95361c4197c7ce9afdf56255de6f4e2474c39d16a277cce31d1b99a2520486d8"}, + {file = "orjson-3.10.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:43ad5560db54331c007dc38be5ba7706cb72974a29ae8227019d89305d750a6f"}, + {file = "orjson-3.10.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1471c3274b1a4a9b8f4b9ed6effaea9ad885796373797515c44b365b375c256d"}, + {file = "orjson-3.10.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41d8cac575acd15918903d74cfaabb5dbe57b357b93341332f647d1013928dcc"}, + {file = "orjson-3.10.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2920c8754f1aedc98bd357ec172af18ce48f5f1017a92244c85fe41d16d3c6e0"}, + {file = "orjson-3.10.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7fa3ff6a0d9d15a0d0d2254cca16cd919156a18423654ce5574591392fe9914"}, + {file = "orjson-3.10.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e91b90c0c26bd79593967c1adef421bcff88c9e723d49c93bb7ad8af80bc6b"}, + {file = "orjson-3.10.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f11949024f785ace1a516db32fa6255f6227226b2c988abf66f5aee61d43d8f7"}, + {file = "orjson-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:060e020d85d0ec145bc1b536b1fd9c10a0519c91991ead9724d6f759ebe26b9a"}, + {file = "orjson-3.10.9-cp312-none-win32.whl", hash = "sha256:71f73439999fe662843da3607cdf6e75b1551c330f487e5801d463d969091c63"}, + {file = "orjson-3.10.9-cp312-none-win_amd64.whl", hash = "sha256:12e2efe81356b8448f1cd130f8d75d3718de583112d71f2e2f8baa81bd835bb9"}, + {file = "orjson-3.10.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:0ab6e3ad10e964392f0e838751bcce2ef9c8fa8be7deddffff83088e5791566d"}, + {file = "orjson-3.10.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68ef65223baab00f469c8698f771ab3e6ccf6af2a987e77de5b566b4ec651150"}, + {file = "orjson-3.10.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6f130848205fea90a2cb9fa2b11cafff9a9f31f4efad225800bc8b9e4a702f24"}, + {file = "orjson-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ea7a98f3295ed8adb6730a5788cc78dafea28300d19932a1d2143457f7db802"}, + {file = "orjson-3.10.9-cp313-none-win32.whl", hash = "sha256:bdce39f96149a74fddeb2674c54f1da5e57724d32952eb6df2ac719b66d453cc"}, + {file = "orjson-3.10.9-cp313-none-win_amd64.whl", hash = "sha256:d11383701d4b58e795039b662ada46987744293d57bfa2719e7379b8d67bc796"}, + {file = "orjson-3.10.9-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1c3a1e845916a3739ab4162bb48dee66e0e727a19faf397176a7db0d9826cc3c"}, + {file = "orjson-3.10.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:063ca59d93d93d1387f0c4bb766c6d4f5b0e423fe7c366d0bd4401a56d1669d1"}, + {file = "orjson-3.10.9-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:938b7fcd79cf06fe348fb24b6163fbaa2fdc9fbed8b1f06318f24467f1487e63"}, + {file = "orjson-3.10.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc32a9e43c7693011ccde6f8eff8cba75ca0d2a55de11092faa4a716101e67f5"}, + {file = "orjson-3.10.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b3069b7e2f57f3eef2282029b9c2ba21f08a55f1018e483663a3356f046af4c"}, + {file = "orjson-3.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4289b5d1f88fd05dcafdd7a1f3b17bb722e77712b7618f98e86bdda560e0a1a"}, + {file = "orjson-3.10.9-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:74f5a7a7f282d326be71b722b0c350da7af6f5f15b9378da177e0e4a09bd91a3"}, + {file = "orjson-3.10.9-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:80e0c013e50cf7198319d8137931684eb9f32daa067e8276d9dbdd4010bb4add"}, + {file = "orjson-3.10.9-cp38-none-win32.whl", hash = "sha256:9d989152df8f60a76867354e0e08d896292ab9fb96a7ef89a5b3838de174522c"}, + {file = "orjson-3.10.9-cp38-none-win_amd64.whl", hash = "sha256:485358fe9892d6bfd88e5885b66bf88496e1842c8f35f61682ff9928b12a6cf0"}, + {file = "orjson-3.10.9-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ca54e6f320e33c8a6e471c424ee16576361d905c15d69e134c2906d3fcb31795"}, + {file = "orjson-3.10.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9a9eb03a29c9b30b6c8bb35e5fa20d96589a76e0042005be59b7c3af10a7e43"}, + {file = "orjson-3.10.9-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:731e8859fc99b398c286320726906404091141e9223dd5e9e6917f7e32e1cc68"}, + {file = "orjson-3.10.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75b061c11f5aab979a95927a76394b4a85e3e4d63d0a2a16b56a4f7c6503afab"}, + {file = "orjson-3.10.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b61b08f6397f004570fd6a840f4a58946b63b4c7029408cdedb45fe85c7d17f7"}, + {file = "orjson-3.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4f5e0360b7f0aba91dafe12469108109a0e8973956d4a9865ca262a6881406"}, + {file = "orjson-3.10.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e403429e2947a059545e305d97e4b0eb90d3bb44b396d6f327d7ae2018391e13"}, + {file = "orjson-3.10.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e492b93e122264c2dc78700859122631a4715bda88fabf57d9226954cfe7ec5"}, + {file = "orjson-3.10.9-cp39-none-win32.whl", hash = "sha256:bfba9605e85bfd19b83a21c2c25c2bed2000d5f097f3fa3ad5b5f8a7263a3148"}, + {file = "orjson-3.10.9-cp39-none-win_amd64.whl", hash = "sha256:77d277fa138d4bf145e8b24042004891c188c52ac8492724a183f42b0031cf0c"}, + {file = "orjson-3.10.9.tar.gz", hash = "sha256:c378074e0c46035dc66e57006993233ec66bf8487d501bab41649b4b7289ed4d"}, ] [[package]] @@ -1372,95 +1377,90 @@ files = [ [[package]] name = "pillow" -version = "10.4.0" +version = "11.0.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] @@ -1798,15 +1798,92 @@ files = [ {file = "pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3"}, ] +[[package]] +name = "pyinstrument" +version = "5.0.0" +description = "Call stack profiler for Python. Shows you why your code is slow!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a83cf18f5594e1b1899b12b46df7aabca556eef895846ccdaaa3a46a37d1274"}, + {file = "pyinstrument-5.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cc236313272d0222261be8e2b2a08e42d7ccbe54db9059babf4d77040da1880"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd685d68a31f3715ca61f82c37c1c2f8b75f45646bd9840e04681d91862bd85"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cecd0f6558f13fba74a9f036b2b168956206e9525dcb84c6add2d73ab61dc22"}, + {file = "pyinstrument-5.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8485c2e41082a20822001a6651667bb5327f6f5f6759987198593e45bb376"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6294b7111348765ba4c311fc91821ed8b59c6690c4dab23aa7165a67da9e972"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a164f3dae5c7db2faa501639659d64034cde8db62a4d6744712593a369bc8629"}, + {file = "pyinstrument-5.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f6bac8a434407de6f2ebddbcdecdb19b324c9315cbb8b8c2352714f7ced8181"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win32.whl", hash = "sha256:7e8dc887e535f5c5e5a2a64a0729496f11ddcef0c23b0a555d5ab6fa19759445"}, + {file = "pyinstrument-5.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c337190a1818841732643ba93065411591df526bc9de44b97ba8f56b581d2ef"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c9052f548ec5ccecc50676fbf1a1d0b60bdbd3cd67630c5253099af049d1f0ad"}, + {file = "pyinstrument-5.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:197d25487f52da3f8ec26d46db7202bc5d703cc73c1503371166417eb7cea14e"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a072d928dc16a32e0f3d1e51726f4472a69d66d838ee1d1bf248737fd70b9415"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2c7ae2c984879a645fce583bf3053b7e57495f60c1e158bb71ad7dfced1fbf1"}, + {file = "pyinstrument-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8284bf8847629c9a5054702b9306eab3ab14c2474959e01e606369ffbcf938bc"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fd94cc725efb1dd41ae8e20a5f06a6a5363dec959e8a9dacbac3f4d12d28f03"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e0fdb9fe6f9c694940410dcc82e23a3fe2928114328efd35047fc0bb8a6c959f"}, + {file = "pyinstrument-5.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffe938e63173ceb8ce7b6b309ce26c9d44d16f53c0162d89d6e706eb9e69802"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win32.whl", hash = "sha256:80d2a248516f372a89e0fe9ddf4a9d6388a4c6481b6ebd3dfe01b3cd028c0275"}, + {file = "pyinstrument-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:7ccf4267aff62de0e1d976e8f5da25dcb69737ae86e38d3cfffa24877837e7d1"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dec3529a5351ea160baeef1ef2a6e28b1a7a7b3fb5e9863fae8de6da73d0f69a"}, + {file = "pyinstrument-5.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a39e3ef84c56183f8274dfd584b8c2fae4783c6204f880513e70ab2440b9137"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3938f063ee065e05826628dadf1fb32c7d26b22df4a945c22f7fe25ea1ba6a2"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18990cc16b2e23b54738aa2f222863e1d36daaaec8f67b1613ddfa41f5b24db"}, + {file = "pyinstrument-5.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3731412b5bfdcef8014518f145140c69384793e218863a33a39ccfe5fb42045"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02b2eaf38460b14eea646d6bb7f373eb5bb5691d13f788e80bdcb3a4eaa2519e"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e57db06590f13657b2bce8c4d9cf8e9e2bd90bb729bcbbe421c531ba67ad7add"}, + {file = "pyinstrument-5.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddaa3001c1b798ec9bf1266ef476bbc0834b74d547d531f5ed99e7d05ac5d81b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win32.whl", hash = "sha256:b69ff982acf5ef2f4e0f32ce9b4b598f256faf88438f233ea3a72f1042707e5b"}, + {file = "pyinstrument-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bf4ef061d60befe72366ce0ed4c75dee5be089644de38f9936d2df0bcf44af0"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79a54def2d4aa83a4ed37c6cffc5494ae5de140f0453169eb4f7c744cc249d3a"}, + {file = "pyinstrument-5.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9538f746f166a40c8802ebe5c3e905d50f3faa189869cd71c083b8a639e574bb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbab65cae1483ad8a18429511d1eac9e3efec9f7961f2fd1bf90e1e2d69ef15"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4351ad041d208c597e296a0e9c2e6e21cc96804608bcafa40cfa168f3c2b8f79"}, + {file = "pyinstrument-5.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceee5252f4580abec29bcc5c965453c217b0d387c412a5ffb8afdcda4e648feb"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3050a4e7033103a13cfff9802680e2070a9173e1a258fa3f15a80b4eb9ee278"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3b1f44a34da7810938df615fb7cbc43cd879b42ca6b5cd72e655aee92149d012"}, + {file = "pyinstrument-5.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fde075196c8a3b2be191b8da05b92ff909c78d308f82df56d01a8cfdd6da07b9"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win32.whl", hash = "sha256:1a9b62a8b54e05e7723eb8b9595fadc43559b73290c87b3b1cb2dc5944559790"}, + {file = "pyinstrument-5.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2478d2c55f77ad8e281e67b0dfe7c2176304bb824c307e86e11890f5e68d7feb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c2e3b4283f85232fd5818e2153e6798bceb39a8c3ccfaa22fae08faf554740b7"}, + {file = "pyinstrument-5.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fb1139d2822abff1cbf1c81c018341f573b7afa23a94ce74888a0f6f47828cbc"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c971566d86ba46a7233d3f5b0d85d7ee4c9863f541f5d8f796c3947ebe17f68"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429376235960179d6ab9b97e7871090059d39de160b4e3b2723672f30e8eea8e"}, + {file = "pyinstrument-5.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8599b4b0630c776b30fc3c4f7476d5e3814ee7fe42d99131644fe3c00b40fdf1"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a8bc688afa2a5368042a7cb56866d5a28fdff8f37a282f7be79b17cae042841b"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5d34c06e2276d1f549a540bccb063688ea3d876e6df7c391205f1c8b4b96d5c8"}, + {file = "pyinstrument-5.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d3b2ec6e028731dbb2ba8cf06f19030162789e6696bca990a09519881ad42fb"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win32.whl", hash = "sha256:5ed6f5873a7526ec5915e45d956d044334ef302653cf63649e48c41561aaa285"}, + {file = "pyinstrument-5.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:9e87d65bae7d0f5ef50908e35d67d43b7cc566909995cc99e91721bb49b4ea06"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bd953163616bc29c2ccb1e4c0e48ccdd11e0a97fc849da26bc362bba372019ba"}, + {file = "pyinstrument-5.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d2a7279ed9b6d7cdae247bc2e57095a32f35dfe32182c334ab0ac3eb02e0eac"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68001dfcb8a37b624a1c3de5d2ee7d634f63eac7a6dd1357b7370a5cdbdcf567"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c4c3cc6410ad5afe0e352a7fb09fb1ab85eb5676ec5ec8522123759d9cc68f"}, + {file = "pyinstrument-5.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d87ddab66b1b3525ad3abc49a88aaa51efcaf83578e9d2a702c03a1cea39f28"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03182ffaa9c91687cbaba80dc0c5a47015c5ea170fe642f632d88e885cf07356"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:39b60417c9c12eed04e1886644e92aa0b281d72e5d0b097b16253cade43110f7"}, + {file = "pyinstrument-5.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7bb389b6d1573361bd1367b296133c5c69184e35fc18db22e29e8cdf56f158f9"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win32.whl", hash = "sha256:ae69478815edb3c63e7ebf82e1e13e38c3fb2bab833b1c013643c3475b1b8cf5"}, + {file = "pyinstrument-5.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:83caeb4150c0334e9e290c0f9bb164ff6bdc199065ecb62016268e8a88589a51"}, + {file = "pyinstrument-5.0.0.tar.gz", hash = "sha256:144f98eb3086667ece461f66324bf1cc1ee0475b399ab3f9ded8449cc76b7c90"}, +] + +[package.extras] +bin = ["click", "nox"] +docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "litestar", "numpy"] +test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] +types = ["typing-extensions"] + [[package]] name = "pypika-tortoise" -version = "0.1.6" +version = "0.2.1" description = "Forked from pypika and streamline just for tortoise-orm" optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.7" files = [ - {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, - {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, + {file = "pypika_tortoise-0.2.1-py3-none-any.whl", hash = "sha256:e91a1c5a78c6753ead1a9ba1aa169a1f1282c5035170e5462f0073564bc18886"}, + {file = "pypika_tortoise-0.2.1.tar.gz", hash = "sha256:979bbb9d60fe9f6e4129a25c44ee008aab4a4e97b296350be9983dcaa2766354"}, ] [[package]] @@ -2124,23 +2201,23 @@ files = [ [[package]] name = "tortoise-orm" -version = "0.21.6" +version = "0.21.7" description = "Easy async ORM for python, built with relations in mind" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "tortoise_orm-0.21.6-py3-none-any.whl", hash = "sha256:98fcf07dce3396075eac36b0d2b14d2267ff875d32455e03ee15e38de2f138df"}, - {file = "tortoise_orm-0.21.6.tar.gz", hash = "sha256:0fbc718001647bf282c01eaaa360f94f1432c9281701244180703d48d58a88ec"}, + {file = "tortoise_orm-0.21.7-py3-none-any.whl", hash = "sha256:2229925885461f424673223ea1875bd5e6961384c766833af55a1ea11a9b25eb"}, + {file = "tortoise_orm-0.21.7.tar.gz", hash = "sha256:8a790a931828aa37ac364b344c561e603422aced2af5e403f6790575da0f19c5"}, ] [package.dependencies] -aiosqlite = ">=0.16.0,<0.18.0" +aiosqlite = ">=0.16.0,<0.21.0" asyncmy = {version = ">=0.2.8,<0.3.0", optional = true, markers = "extra == \"asyncmy\""} ciso8601 = {version = "*", optional = true, markers = "sys_platform != \"win32\" and implementation_name == \"cpython\" and extra == \"accel\""} -iso8601 = ">=1.0.2,<2.0.0" +iso8601 = ">=2.1.0,<3.0.0" orjson = {version = "*", optional = true, markers = "extra == \"accel\""} pydantic = ">=2.0,<2.7.0 || >2.7.0,<3.0" -pypika-tortoise = ">=0.1.6,<0.2.0" +pypika-tortoise = ">=0.2.1,<0.3.0" pytz = "*" uvloop = {version = "*", optional = true, markers = "sys_platform != \"win32\" and implementation_name == \"cpython\" and extra == \"accel\""} @@ -2176,13 +2253,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.31.1" +version = "0.32.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"}, - {file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"}, + {file = "uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82"}, + {file = "uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e"}, ] [package.dependencies] @@ -2195,47 +2272,54 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "uvloop" -version = "0.20.0" +version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" files = [ - {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, - {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, - {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, - {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, - {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, - {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, - {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, - {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, - {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, - {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, - {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, - {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, - {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, - {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, - {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, - {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, - {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, - {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, - {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, - {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, - {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, - {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, - {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, - {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, - {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, - {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, - {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, - {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, - {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, - {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, - {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, ] [package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] [[package]] name = "viztracer" @@ -2424,109 +2508,93 @@ h11 = ">=0.9.0,<1" [[package]] name = "yarl" -version = "1.15.0" +version = "1.16.0" description = "Yet another URL library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "yarl-1.15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e61b2019ebb5345510b833c4dd1f4afb1f0c07753f86f184c63836ffc3fb08ba"}, - {file = "yarl-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:25a4e29ee758596b2a0daffa4814714e9b464077ca862baf78ed0e8698e46b61"}, - {file = "yarl-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:928f7a61c4311f3dd003af19bb779f99683f97a0559b765c80fdb8846dab0452"}, - {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b25de7e85ba90b2ff230153123b6b000a7f69c41d84a3a0dc3f878334c8509"}, - {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0127bc2ea72c1eaae6808ace661f0edf222f32ffa987d37f2dbb4798288f2656"}, - {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2508ee2bad8381b5254eadc35d32fe800d12eb2c63b744183341f3a66e435a7"}, - {file = "yarl-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:748dcacc19c69957f7063ea4fb359fa2180735b1a638c81a4a96b86a382a6f29"}, - {file = "yarl-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4d9c221cc8e32b14196498679bf2b324bec1d1127c4ba934d98e19298faa661"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:676d7356bb30825b7dbdad4fdd7a9feac379d074e5d4a36299767d72857ded42"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:5eef9804e65eb292e9c5587e88fe6a27a11f121d358312ac47211e8f42876751"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2bece7fdc13e23db005879b67190db0d397f6ba89c81dc7e3c77e9f5819aff7f"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e1ddf05eeb422810b1aa919095db0691493442eebbf9cfb0f1e478a7b2fbdf3d"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:85e273e59b8b1a5f60a89df82cddeaf918181abd7ae7a2f2f899b68b0c774ff1"}, - {file = "yarl-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d772ae3c12d3b8629db656050c86ee66924eaa98f7125a889175a59cfaafdb19"}, - {file = "yarl-1.15.0-cp310-cp310-win32.whl", hash = "sha256:4b1ab96a1ac91bd1233706d638ade35f663684deaa4e5e5f190858cba044afb9"}, - {file = "yarl-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2fe45c1143eefb680a4589c55e671fabd482a7f8c7791f311ea3bcc20139246"}, - {file = "yarl-1.15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c8b034b60e74fb29064f765851e77e5910055e1c4a3cb75c32eccf2b470fc00f"}, - {file = "yarl-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70ac7893e67a81ed1346ee3e71203ca4b0c3550c005b1d1cf87bc1e61eecd04b"}, - {file = "yarl-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6237637b496bc04819190e724a4e61ff2f251abf432f70cf491b3bc4a3f2f253"}, - {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cc25cbd9ae01d49ac7b504ef5f3cbdcc8d139f9750dcfa0b80d405b4645cc2"}, - {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4f882e42c6cea89488b9a16919edde8c0b1a98f307c05abdd3dd3bc4368af40"}, - {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7711d83dafe52cda16ff2dd205cd83c05e4c06d5aaac596ae2cf7d50d094a530"}, - {file = "yarl-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3b08d9e98d1a15338fcfbd52c02003704322c2d460c9b9be7df08f2952bdce6"}, - {file = "yarl-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06040266b5e6512a37b4703684d1798124764b43328254799e9678c588882a6"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97fcaf530318369da3cfd6ff52f5ab38daf8cb10ecee9a76efebf8031de09eef"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:994d27b24b61b1870f3571395c840433faabec5dcd239bd11ff6af7e34234bb6"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:18614630533ac37ec373bd8035aec8fa4dd9aedac641209c06de7e082622ff77"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c9c405ca78c70c3599d8956e53be0c9def9c51ad949964a49ad96c79729a5b1a"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4c5ff3e7609c214667c7d7e00d5f4f3576fefde47ebcb7e492c015117dafebbf"}, - {file = "yarl-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fac5416c44e8e1d8ea9440096f88e1a7273257f3157184c5c715060e0c448a1"}, - {file = "yarl-1.15.0-cp311-cp311-win32.whl", hash = "sha256:a94c9058c5703c172904103d7b479f7e23dd4e5f8e67b49f6cd256d35ff169cb"}, - {file = "yarl-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:229f222bb47cd7ab225648efd1ae47fe6943f18e4c91bce66471faf09fe33128"}, - {file = "yarl-1.15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9084d99933824ed8d665f10f4ce62d08fed714e7678d5ff11a8c2c98b2dc18f9"}, - {file = "yarl-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df57f3c3ef760489f2e82192e6c93286c2bc80d6d854ef940e5345ae7153cd4b"}, - {file = "yarl-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ca160b4c649f0d56daef04751eef4571de46ed4b80f9051a87d090fef32f08e"}, - {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27c323b28723faed046f906c70466144c4dd12046a0128a301b29a65cfeff758"}, - {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2eafb4e92f72a3b6c27f1d5e921d046e2728850af8887f86857c3fe868a5b5c0"}, - {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06306c74f0775621a70fa5acd292119bbb6961d1f9a5f3d657a4c8c15b86f7b9"}, - {file = "yarl-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f713d8f3c4e2eac0d91b741e8ef2e1082022de244685601ec83e899b445d86a"}, - {file = "yarl-1.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d816969b55a970b3accc7f9e4ea8f60043e3f7de96f21c06063d747ffc2f18ba"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e24a778470f3a9e9c11250d09daf5dea93369bc51aefca6605dbc963737a117"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d3f5e201bd170fb97c643e84df58e221372cd053fbb291ebbd878b165ea5057e"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4424082edff76fe46ff08851e91865097c0ad780fa79b87063dc5d5b80efc9d6"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:350b468a217d433cbb4482e9414a14dfd360a3d5ab92013175925abb234364cc"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c7f2deac59dc3e0528bdded248e637e789e5111ba1723a8d7a262eb93e133e15"}, - {file = "yarl-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2429a651a2191c3fb8c9de21546c6046da539034d51dcb8df52302748004593d"}, - {file = "yarl-1.15.0-cp312-cp312-win32.whl", hash = "sha256:e4f7efb38331e8327c1cc7fb2a2905a7db03d1a7fdb04706bf6465d0e44d41d4"}, - {file = "yarl-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:9ae454916aa3abe28d0ef1c21ca1e8e36a14ccf52183d465dfaccffaa7ed462c"}, - {file = "yarl-1.15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3f8be3e785009ffa148e66474fea5c787ccb203b3d0bd1f22e1e22f7da0f3b3"}, - {file = "yarl-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b7f902f13a230686f01bcff17cd9ba045653069811c8fd5027f0f414b417e2f"}, - {file = "yarl-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:627bb5bc4ed3d3ebceb9fb55717cec6cd58bb47fdb5669169ebbc248e9bf156c"}, - {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1208f2e081d34832f509cbe311237a0543effe23d60b2fa14c0d3f86e6d1d07"}, - {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c791a2d42da20ac568e5c0cc9b8af313188becd203a936ad959b578dafbcebb"}, - {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd042e6c3bf36448e3e3ed302b12ce79762480f4aff8e7a167cdf8c35dc93297"}, - {file = "yarl-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae041f535fe2e57681954f7cccb81854d777ce4c2a87749428ebe6c71c02ec0"}, - {file = "yarl-1.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:454707fb16f180984da6338d1f51897f0b8d8c4c2e0592d9d1e9fa02a5bb8218"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6960b0d2e713e726cb2914e3051e080b12412f70dcb8731cf7a8fb52c37931bb"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c9b9159eeeb7cd1c7131dc7f5878454f97a4dc20cd157e6474174ccac448b844"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fee9acd5e39c8611957074dfba06552e430020eea831caf5eb2cea30f10e06bd"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ddea4abc4606c10dddb70651b210b7ab5b663148d6d7bc85d76963c923629891"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2add8ed2acf42398dfaa7dffd32e4d18ffbae341d62c8d4765bd9929336379b5"}, - {file = "yarl-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:32840ff92c713053591ff0e66845d4e9f4bea8fd5fba3da00f8d92e77722f24e"}, - {file = "yarl-1.15.0-cp313-cp313-win32.whl", hash = "sha256:eb964d18c01b7a1263a6f07b88d63711fcd564fc429d934279cf12f4b467bf53"}, - {file = "yarl-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ceb200918c9bd163bd390cc169b254b23b4be121026b003be93a4f2f5b554b4b"}, - {file = "yarl-1.15.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:83363a5789f128618041b9a737c7b146f1965abddf4294b0444591406b437c1e"}, - {file = "yarl-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2124c642b8cc9b68e5981e429842dadc32bb850b010cccec9d24236253a19f60"}, - {file = "yarl-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5107d89c9edec6ee077970a95fb9eeb4776ea8c2337b6a39c0ade9a58f50f3e4"}, - {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33896afca6fb4e1988c099534c52823870dfc8730bc6f96a3831f24c1e0ab814"}, - {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4224bbbc8a2e9b9a3828d36c1bab7458441d7fb9fb3af321eb735732ba8ee89d"}, - {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1edaf4171fc1582352ac5d9b2783966fa0f4ff86187279ef2a491613d23b894a"}, - {file = "yarl-1.15.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73c4af08e9bb9a9aa7df6c789b05b924b9a0c6a368bb0e418d0b85181b64b631"}, - {file = "yarl-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4aa7cca009817789fd5b8e52e8122f9e85dc580c88b816a93321c00a8acbced"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c0a86dd3e85c6aa3fc73236eb5cf7ce69dd8ad7abcd23f8ae1126831c8e40c2f"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:db903458a457a53ee0f764ed11c5b5368398e216b442c42dca9d90fbd2bbf31c"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8f074a24aa9a6a3d406474ec889ebb5d661f329349068e05e8dfcb3c4be67752"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:dc63bb79e896d6ce6aaf672ac304b54969280e949c45727867fc154a17ec7ab2"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ef780f9d480ffb423380abeb4cfcad66ecb8f93526dfa367d322fdad9ec7c25f"}, - {file = "yarl-1.15.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e7e38bf6e52797084c5c396db5bb519615727e491e9003e2449631457bf77738"}, - {file = "yarl-1.15.0-cp38-cp38-win32.whl", hash = "sha256:d885dcdca7bae42bc9a2f6cbf766abcb2a6cc043b1905fc3782c6ea1f74a2b95"}, - {file = "yarl-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:e4f64c8c52dde564bf3251b41d7a6746564b0fc0516cebe9c9e6695224440d22"}, - {file = "yarl-1.15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5156c12a97405339ec93facbc7860566db381af2de1bec338195563fb64f37ef"}, - {file = "yarl-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e5fa4c4e55cdacef1844f609bc9a02c8cd29c324a71ca1d3ee454701d4bb496"}, - {file = "yarl-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e1cc7823f43781390965c4762b54262cfcf76b6f152e489d00a5a1ac63063e4"}, - {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38cab8f91b1085f1fd0765d40c46c8f43282f109018d5fcd017c46ac3eaba0cf"}, - {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbe72c41cdd55c88b238a8925849fde4069c0cdcdef83f8d967f8f3982659326"}, - {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0358b697abdf1f2d68038bd02ef8ddcc4813835744f79c755f8743aa485585e7"}, - {file = "yarl-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b93a666cd8cfd43f605d1b81a32b9e290bf45c74c2bfd51ba705449c78448c7"}, - {file = "yarl-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81edbd9bf9f25cd995e6d51c307e1d279587d40b7473e258fef6d5e548560cd2"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad2e487824ba4cda87851a371139e255410e45d3bf2e334194789278d709cec"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:553a1e3537aeeb29c0eb29ef28b80e0e801697fa71d96ac60675b284ff8e582a"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:06b5b462cadf59c1df28ffbb0a3971fa16b60cf0c9d59a38bf5679a986d18685"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:097094a979af7b31520517c59179f6817b8426724343cecbec0eb3af1f8fb6cf"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:75d9762f65205a86381298eb9079f27c60b84de0c262e402dcf45c6cbc385234"}, - {file = "yarl-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e2e3cb74684ff357e6b3c82dd71031d3c1fd7ee9f9b0a5205e5568c963e074f9"}, - {file = "yarl-1.15.0-cp39-cp39-win32.whl", hash = "sha256:a616c2e4b60cb8cdd9eb3b0c6fda4ab5f3e26244b427aaade560dcf63c5754fb"}, - {file = "yarl-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:7aa9f9af452c3e8486a0b88fddd58352e6cea17b691b18861d26e46cf65ffff0"}, - {file = "yarl-1.15.0-py3-none-any.whl", hash = "sha256:1656a8b531a96427f26f498b7d0f19931166ff30e4344eca99bdb27faca14fc5"}, - {file = "yarl-1.15.0.tar.gz", hash = "sha256:efc0430b80ed834c80c99c32946cfc6ee29dfcd7c62ad3c8f15657322ade7942"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2"}, + {file = "yarl-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84"}, + {file = "yarl-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb"}, + {file = "yarl-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b"}, + {file = "yarl-1.16.0-cp310-cp310-win32.whl", hash = "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929"}, + {file = "yarl-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2"}, + {file = "yarl-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2"}, + {file = "yarl-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6"}, + {file = "yarl-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56"}, + {file = "yarl-1.16.0-cp311-cp311-win32.whl", hash = "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c"}, + {file = "yarl-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6"}, + {file = "yarl-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7"}, + {file = "yarl-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968"}, + {file = "yarl-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3"}, + {file = "yarl-1.16.0-cp312-cp312-win32.whl", hash = "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67"}, + {file = "yarl-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732"}, + {file = "yarl-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d"}, + {file = "yarl-1.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2"}, + {file = "yarl-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36"}, + {file = "yarl-1.16.0-cp313-cp313-win32.whl", hash = "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b"}, + {file = "yarl-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e"}, + {file = "yarl-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae"}, + {file = "yarl-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca"}, + {file = "yarl-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f"}, + {file = "yarl-1.16.0-cp39-cp39-win32.whl", hash = "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7"}, + {file = "yarl-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027"}, + {file = "yarl-1.16.0-py3-none-any.whl", hash = "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3"}, + {file = "yarl-1.16.0.tar.gz", hash = "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4"}, ] [package.dependencies] @@ -2575,4 +2643,4 @@ s3 = ["s3lite"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "7de0fd930cfbf57ee8f25cdc51bc607178952ae559a7b28b226388bfd22793a6" +content-hash = "a64839f6c062146319bb699657b2154dbc2bdb5e5e1d080d8ea4dab93b68df1e" diff --git a/pyproject.toml b/pyproject.toml index 2297dae..706ff6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,32 +40,32 @@ python = "^3.9" quart = "0.19.6" aiofiles = "^24.1.0" websockets = "^13.1" -uvicorn = "^0.31.0" +uvicorn = "^0.32.0" python-magic = "^0.4.27" -pillow = "^10.4.0" +pillow = "^11.0.0" protobuf = "4.25.3" python-dateutil = "^2.9.0.post0" -cryptography = "^43.0.0" +cryptography = "^43.0.3" emoji = "^2.12.1" bcrypt = "^4.2.0" quart-schema = "0.20.0" pydantic = "^2.8.2" werkzeug = "3.0.4" -aioftp = { version = "^0.22.3", optional = true } -orjson = "^3.10.7" +aioftp = { version = "^0.23.1", optional = true } +orjson = "^3.10.9" mailers = {version = "^3.0.5", extras = ["smtp"]} redis = "^5.0.8" click = "^8.1.7" maxminddb = "^2.6.2" wget = "3.2" -tortoise-orm = {extras = ["aiosqlite", "asyncmy", "accel"], version = "^0.21.6"} -uvloop = "^0.20.0" +tortoise-orm = {extras = ["aiosqlite", "asyncmy", "accel"], version = "^0.21.7"} +uvloop = "^0.21.0" async-timeout = "^4.0.3" aerich = "^0.7.2" yc-protobuf3-to-dict = "^0.3.0" s3lite = { version = "^0.1.8", optional = true } -fast-depends = "^2.4.11" -faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.20"} +fast-depends = "^2.4.12" +faststream = {extras = ["kafka", "nats", "rabbit", "redis"], version = "^0.5.28"} [tool.poetry.group.dev.dependencies] pytest = "^8.2.0" @@ -83,6 +83,7 @@ ftp = ["aioftp"] [tool.poetry.group.profiling.dependencies] viztracer = "^0.16.3" +pyinstrument = "^5.0.0" [build-system] requires = ["poetry-core"] diff --git a/yepcord/asgi.py b/yepcord/asgi.py index dc887e5..c4e59d4 100644 --- a/yepcord/asgi.py +++ b/yepcord/asgi.py @@ -41,8 +41,7 @@ from yepcord.yepcord.config import Config app = Quart("YEPCord server") -app.gifs = rest_api.app.gifs -app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 +app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024 QuartSchema(app) app.before_serving(rest_api.before_serving) diff --git a/yepcord/yepcord/core.py b/yepcord/yepcord/core.py deleted file mode 100644 index da076a8..0000000 --- a/yepcord/yepcord/core.py +++ /dev/null @@ -1,37 +0,0 @@ -""" - YEPCord: Free open source selfhostable fully discord-compatible chat - Copyright (C) 2022-2024 RuslanUC - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -""" - -from .utils.singleton import Singleton - - -# noinspection PyMethodMayBeStatic -class Core(Singleton): - ... - # async def sendMessage(self, message: Message) -> Message: - # async def _addToReadStates(): # TODO: recalculate read states when requested by user - # users = await self.getRelatedUsersToChannel(message.channel) - # if message.author in users: - # users.remove(message.author) - # for user in users: - # read_state, _ = await ReadState.get_or_create( - # user=user, channel=message.channel, defaults={"last_read_id": message.id, "count": 0} - # ) - # read_state.count += 1 - # await read_state.save(update_fields=["count"]) - # return message - From 515d02ef8297e1b9838ffa550c2c4b570f700901 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 22 Oct 2024 14:55:09 +0300 Subject: [PATCH 24/26] fix poe in github actions? --- .github/workflows/pytest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f70cb11..81d6631 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -57,7 +57,7 @@ jobs: - name: Setup database run: | - poe migrate + poetry run poe migrate env: YEPCORD_CONFIG: .github/settings_test.py DB_TYPE: ${{ matrix.database }} @@ -65,7 +65,7 @@ jobs: - name: Run test suite for core run: | - poe test tests/test_core.py + poetry run poe test tests/test_core.py env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -73,7 +73,7 @@ jobs: - name: Run test suite for http api run: | - poe test --cov=yepcord/rest_api--cov=yepcord/remote_auth tests/api/ + poetry run poe test --cov=yepcord/rest_api--cov=yepcord/remote_auth tests/api/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -83,7 +83,7 @@ jobs: - name: Run test suite for cdn/storage run: | mkdir -p tests/files/yepcord-test - poe test --cov=yepcord/cdn tests/cdn/ + poetry run poe test --cov=yepcord/cdn tests/cdn/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} From 27ea9852af0d65ce80ffdce1682f1b44f4f12ad2 Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 22 Oct 2024 14:57:34 +0300 Subject: [PATCH 25/26] dont use poe in gh actions yet --- .github/workflows/pytest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 81d6631..888edfc 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -57,7 +57,7 @@ jobs: - name: Setup database run: | - poetry run poe migrate + poetry run python app.py migrate env: YEPCORD_CONFIG: .github/settings_test.py DB_TYPE: ${{ matrix.database }} @@ -65,7 +65,7 @@ jobs: - name: Run test suite for core run: | - poetry run poe test tests/test_core.py + poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/yepcord --disable-warnings tests/test_core.py env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -73,7 +73,7 @@ jobs: - name: Run test suite for http api run: | - poetry run poe test --cov=yepcord/rest_api--cov=yepcord/remote_auth tests/api/ + poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/rest_api --cov=yepcord/yepcord --cov=yepcord/remote_auth --disable-warnings tests/api/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} @@ -83,7 +83,7 @@ jobs: - name: Run test suite for cdn/storage run: | mkdir -p tests/files/yepcord-test - poetry run poe test --cov=yepcord/cdn tests/cdn/ + poetry run pytest -x --cov-report=xml --cov-append --cov=yepcord/cdn --cov=yepcord/yepcord --cov-config=.coveragerc --disable-warnings tests/cdn/ env: DB_TYPE: ${{ matrix.database }} KEY: ${{ secrets.KEY }} From 0cf1deac4e312bf7bfb2728b5f0df1bf995250cf Mon Sep 17 00:00:00 2001 From: RuslanUC Date: Tue, 22 Oct 2024 14:59:42 +0300 Subject: [PATCH 26/26] fix s3/ftp not being installed in gh actions --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 888edfc..312e295 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -27,7 +27,7 @@ jobs: cache: "poetry" - name: Install dependencies - run: poetry install --no-interaction + run: poetry install --no-interaction --all-extras - name: Shutdown Ubuntu MySQL if: matrix.database == 'mariadb' || matrix.database == 'mysql'