From 19a65ec3b382b5e8fac00ab7d74683c46806d120 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:51:56 +0000 Subject: [PATCH 01/24] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.10.1 → v3.11.0](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.11.0) - [github.com/asottile/reorder-python-imports: v3.10.0 → v3.11.0](https://github.com/asottile/reorder-python-imports/compare/v3.10.0...v3.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 759d9b14..ce974309 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,12 +16,12 @@ repos: hooks: - id: black - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.11.0 hooks: - id: pyupgrade args: [--py37-plus, --keep-runtime-typing] - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 + rev: v3.11.0 hooks: - id: reorder-python-imports args: [--py37-plus, --add-import, 'from __future__ import annotations'] From d3c6c82dfc8b1c123dd39510d797c64c4d60061f Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 13:15:56 +0100 Subject: [PATCH 02/24] improve beatmap api resilience with tenacity & error handling --- app/objects/beatmap.py | 47 +++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 70b919f7..5b84599e 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -2,6 +2,7 @@ import functools import hashlib +import asyncio from collections import defaultdict from datetime import datetime from datetime import timedelta @@ -11,6 +12,11 @@ from typing import Any from typing import Mapping from typing import Optional +from typing import TypedDict + +import aiohttp +from tenacity import retry +from tenacity.stop import stop_after_attempt import app.settings import app.state @@ -32,8 +38,15 @@ IGNORED_BEATMAP_CHARS = dict.fromkeys(map(ord, r':\/*<>?"|'), None) +class BeatmapApiResponse(TypedDict): + data: Optional[list[dict[str, Any]]] + status_code: int -async def api_get_beatmaps(**params: Any) -> Optional[list[dict[str, Any]]]: +@retry( + reraise=True, + stop=stop_after_attempt() +) +async def api_get_beatmaps(**params: Any) -> BeatmapApiResponse: """\ Fetch data from the osu!api with a beatmap's md5. @@ -53,9 +66,9 @@ async def api_get_beatmaps(**params: Any) -> Optional[list[dict[str, Any]]]: async with app.state.services.http_client.get(url, params=params) as response: response_data = await response.json() if response.status == 200 and response_data: # (data may be []) - return response_data + return {"data": response_data, "status_code": response.status} - return None + return {"data": None, "status_code": response.status} async def ensure_local_osu_file( @@ -370,7 +383,7 @@ async def from_md5(cls, md5: str, set_id: int = -1) -> Optional[Beatmap]: # set not found in db, try api api_data = await api_get_beatmaps(h=md5) - if not api_data: + if api_data["data"] is None: return None set_id = int(api_data[0]["beatmapset_id"]) @@ -412,7 +425,7 @@ async def from_bid(cls, bid: int) -> Optional[Beatmap]: # set not found in db, try getting via api api_data = await api_get_beatmaps(b=bid) - if not api_data: + if api_data["data"] is None: return None set_id = int(api_data[0]["beatmapset_id"]) @@ -628,8 +641,22 @@ def _cache_expired(self) -> bool: async def _update_if_available(self) -> None: """Fetch the newest data from the api, check for differences and propogate any update into our cache & database.""" - api_data = await api_get_beatmaps(s=self.id) - if api_data: + + try: + api_data = await api_get_beatmaps(s=self.id) + except (asyncio.TimeoutError, aiohttp.ClientConnectorError, aiohttp.ContentTypeError): + # NOTE: ClientConnectorError & TimeoutError are directly caused by the API being unavailable + + # NOTE: ContentTypeError is caused by the API returning HTML and + # normally happens when CF protection is enabled while + # osu! recovers from a DDOS attack + + # we do not want to delete the beatmap in this case, so we simply return + # but do not set the last check, as we would like to retry these ASAP + + return + + if api_data["data"] is not None: old_maps = {bmap.id: bmap for bmap in self.maps} new_maps = {int(api_map["beatmap_id"]): api_map for api_map in api_data} @@ -714,7 +741,11 @@ async def _update_if_available(self) -> None: # update maps in sql await self._save_to_sql() - else: + elif api_data["status_code"] in (404, 200): + # NOTE: 200 can return an empty array of beatmaps, + # so we still delete in this case if the beatmap data is None + # TODO: is 404 and 200 the only cases where we should delete the beatmap? + # TODO: we have the map on disk but it's # been removed from the osu!api. map_md5s_to_delete = {bmap.md5 for bmap in self.maps} From 8234481a74df086a3023f50c328f0de7f2950b67 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 13:18:44 +0100 Subject: [PATCH 03/24] chore: lint --- app/objects/beatmap.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 5b84599e..cff0ea6b 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -1,8 +1,8 @@ from __future__ import annotations +import asyncio import functools import hashlib -import asyncio from collections import defaultdict from datetime import datetime from datetime import timedelta @@ -38,14 +38,13 @@ IGNORED_BEATMAP_CHARS = dict.fromkeys(map(ord, r':\/*<>?"|'), None) + class BeatmapApiResponse(TypedDict): data: Optional[list[dict[str, Any]]] status_code: int -@retry( - reraise=True, - stop=stop_after_attempt() -) + +@retry(reraise=True, stop=stop_after_attempt()) async def api_get_beatmaps(**params: Any) -> BeatmapApiResponse: """\ Fetch data from the osu!api with a beatmap's md5. @@ -644,11 +643,15 @@ async def _update_if_available(self) -> None: try: api_data = await api_get_beatmaps(s=self.id) - except (asyncio.TimeoutError, aiohttp.ClientConnectorError, aiohttp.ContentTypeError): + except ( + asyncio.TimeoutError, + aiohttp.ClientConnectorError, + aiohttp.ContentTypeError, + ): # NOTE: ClientConnectorError & TimeoutError are directly caused by the API being unavailable # NOTE: ContentTypeError is caused by the API returning HTML and - # normally happens when CF protection is enabled while + # normally happens when CF protection is enabled while # osu! recovers from a DDOS attack # we do not want to delete the beatmap in this case, so we simply return From cf0f22573586a35930ef4b9e510d8a361851faea Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 13:20:08 +0100 Subject: [PATCH 04/24] fix: add missing retry count to tenacity --- app/objects/beatmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index cff0ea6b..da7fefac 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -44,7 +44,7 @@ class BeatmapApiResponse(TypedDict): status_code: int -@retry(reraise=True, stop=stop_after_attempt()) +@retry(reraise=True, stop=stop_after_attempt(5)) async def api_get_beatmaps(**params: Any) -> BeatmapApiResponse: """\ Fetch data from the osu!api with a beatmap's md5. From 6153e1491f9ee6954552781cb9e81678a35b48df Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 13:21:39 +0100 Subject: [PATCH 05/24] fix: asyncio.TimeoutError is not possible --- app/objects/beatmap.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index da7fefac..5c1331ea 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import functools import hashlib from collections import defaultdict @@ -643,12 +642,8 @@ async def _update_if_available(self) -> None: try: api_data = await api_get_beatmaps(s=self.id) - except ( - asyncio.TimeoutError, - aiohttp.ClientConnectorError, - aiohttp.ContentTypeError, - ): - # NOTE: ClientConnectorError & TimeoutError are directly caused by the API being unavailable + except (aiohttp.ClientConnectorError, aiohttp.ContentTypeError): + # NOTE: ClientConnectorError are directly caused by the API being unavailable # NOTE: ContentTypeError is caused by the API returning HTML and # normally happens when CF protection is enabled while From 8f8279c2011fabf4351a5baafe4f6f3c1e0a0fde Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 13:23:43 +0100 Subject: [PATCH 06/24] style: grammar lol --- app/objects/beatmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 5c1331ea..fb655aac 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -643,7 +643,7 @@ async def _update_if_available(self) -> None: try: api_data = await api_get_beatmaps(s=self.id) except (aiohttp.ClientConnectorError, aiohttp.ContentTypeError): - # NOTE: ClientConnectorError are directly caused by the API being unavailable + # NOTE: ClientConnectorError is directly caused by the API being unavailable # NOTE: ContentTypeError is caused by the API returning HTML and # normally happens when CF protection is enabled while From 6ae80cbe337e7a0b75432a64b28f7121bdf86a15 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 21 Sep 2023 15:30:08 +0300 Subject: [PATCH 07/24] chore: update deps; add tenacity --- Pipfile | 3 +- Pipfile.lock | 636 +++++++++++++++++++++++++++------------------------ 2 files changed, 336 insertions(+), 303 deletions(-) diff --git a/Pipfile b/Pipfile index bf5ed92e..fe7ee273 100644 --- a/Pipfile +++ b/Pipfile @@ -20,9 +20,10 @@ uvloop = "*" py3rijndael = "*" pytimeparse = "*" sqlalchemy = "1.4.41" -databases = {extras = ["mysql"], version = "0.5.5"} akatsuki-pp-py = "*" cryptography = "*" +tenacity = "*" +databases = {extras = ["mysql"], version = "0.5.5"} [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c944ec41..dfa0cf94 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1917dae1e244d8da84dbb0f5e956c3c30dd5e009a8f10d9f50502918daf75a57" + "sha256": "a2fdaebdbd6cad7dab091bb1db15ed75a5f69dd5a4dc3a921d658b7f44e20e00" }, "pipfile-spec": 6, "requires": { @@ -187,11 +187,11 @@ }, "async-timeout": { "hashes": [ - "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", - "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" + "markers": "python_version >= '3.7'", + "version": "==4.0.3" }, "attrs": { "hashes": [ @@ -388,40 +388,40 @@ }, "click": { "hashes": [ - "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", - "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.6" + "version": "==8.1.7" }, "cryptography": { "hashes": [ - "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306", - "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84", - "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47", - "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d", - "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116", - "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207", - "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81", - "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087", - "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd", - "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507", - "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858", - "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae", - "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34", - "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906", - "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd", - "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922", - "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7", - "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4", - "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574", - "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1", - "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c", - "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e", - "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de" + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "index": "pypi", - "version": "==41.0.3" + "version": "==41.0.4" }, "databases": { "extras": [ @@ -436,27 +436,27 @@ }, "datadog": { "hashes": [ - "sha256:3d7bcda6177b43be4cdb52e16b4bdd4f9005716c0dd7cfea009e018c36bb7a3d", - "sha256:e4fbc92a85e2b0919a226896ae45fc5e4b356c0c57f1c2659659dfbe0789c674" + "sha256:47be3b2c3d709a7f5b709eb126ed4fe6cc7977d618fe5c158dd89c2a9f7d9916", + "sha256:a45ec997ab554208837e8c44d81d0e1456539dc14da5743687250e028bc809b7" ], "index": "pypi", - "version": "==0.46.0" + "version": "==0.47.0" }, "exceptiongroup": { "hashes": [ - "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", - "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" ], "markers": "python_version < '3.11'", - "version": "==1.1.2" + "version": "==1.1.3" }, "fastapi": { "hashes": [ - "sha256:494eb3494d89e8079c20859d7ca695f66eaccc40f46fe8c75ab6186d15f05ffd", - "sha256:ca2ae65fe42f6a34b5cf6c994337149154b1b400c39809d7b2dccdceb5ae77af" + "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d", + "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83" ], "index": "pypi", - "version": "==0.101.0" + "version": "==0.103.1" }, "frozenlist": { "hashes": [ @@ -529,6 +529,7 @@ "hashes": [ "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", + "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1", "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", @@ -554,6 +555,7 @@ "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", + "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417", "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", @@ -577,8 +579,10 @@ "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", + "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47", "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", + "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c", "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", @@ -588,7 +592,7 @@ "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" ], - "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "markers": "python_version >= '3' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", "version": "==2.0.2" }, "h11": { @@ -689,65 +693,69 @@ }, "orjson": { "hashes": [ - "sha256:009a0f79804c604998b068f5f942e40546913ed45ee2f0a3d0e75695bf7543fa", - "sha256:05c57100517b6dbfe34181ed2248bebfab03bd2a7aafb6fbf849c6fd3bb2fbda", - "sha256:082714b5554fcced092c45272f22a93400389733083c43f5043c4316e86f57a2", - "sha256:1440a404ce84f43e2f8e97d8b5fe6f271458e0ffd37290dc3a9f6aa067c69930", - "sha256:19e2502b4af2055050dcc74718f2647b65102087c6f5b3f939e2e1a3e3099602", - "sha256:1da8edaefb75f25b449ed4e22d00b9b49211b97dcefd44b742bdd8721d572788", - "sha256:27d69628f449c52a7a34836b15ec948804254f7954457f88de53f2f4de99512f", - "sha256:2f5ac6e30ee10af57f52e72f9c8b9bc4846a9343449d10ca2ae9760615da3042", - "sha256:303f1324f5ea516f8e874ea0f8d15c581caabdca59fc990705fc76f3bd9f3bdf", - "sha256:32aef33ae33901c327fd5679f91fa37199834d122dffd234416a6fe4193d1982", - "sha256:413d7cf731f1222373360128a3d5232d52630a7355f446bf2659fc3445ec0b76", - "sha256:42cb645780f732c829bc351346a54157d57f2bc409e671ee36b9fc1037bb77fe", - "sha256:43c3bbf4b6f94fad2fd73c81293da8b343fbd07ce48d7836c07d0d54b58c8e93", - "sha256:448feda092c681c0a5b8eec62dd4f625ad5d316dafd56c81fb3f05b5221827ff", - "sha256:47210746acda49febe3bb07253eb5d63d7c7511beec5fa702aad3ce64e15664f", - "sha256:47b237da3818c8e546df4d2162f0a5cfd50b7b58528907919a27244141e0e48e", - "sha256:526cb34e63faaad908c34597294507b7a4b999a436b4f206bc4e60ff4e911c20", - "sha256:5297463d8831c2327ed22bf92eb6d50347071ff1c73fb4702d50b8bc514aeac9", - "sha256:59c444e3931ea4fe7dec26d195486a681fedc0233230c9b84848f8e60affd4a4", - "sha256:5ae680163ab09f04683d35fbd63eee858019f0066640f7cbad4dba3e7422a4bc", - "sha256:5b1ff8e920518753b310034e5796f0116f7732b0b27531012d46f0b54f3c8c85", - "sha256:63333de96d83091023c9c99cc579973a2977b15feb5cdc8d9660104c886e9ab8", - "sha256:69a33486b5b6e5a99939fdb13c1c0d8bcc7c89fe6083e7b9ce3c70931ca9fb71", - "sha256:6fe77af2ff33c370fb06c9fdf004a66d85ea19c77f0273bbf70c70f98f832725", - "sha256:776659e18debe5de73c30b0957cd6454fcc61d87377fcb276441fca1b9f1305d", - "sha256:7b3177bd67756e53bdbd72c79fae3507796a67b67c32a16f4b55cad48ef25c13", - "sha256:7bce6ff507a83c6a4b6b00726f3a7d7aed0b1f0884aac0440e95b55cac0b113e", - "sha256:7e5abca1e0a9d110bab7346fab0acd3b7848d2ee13318bc24a31bbfbdad974b8", - "sha256:8323739e7905ae4ec4dbdebb31067d28be981f30c11b6ae88ddec2671c0b3194", - "sha256:85b1870d5420292419b34002659082d77f31b13d4d8cbd67bed9d717c775a0fb", - "sha256:893c62afd5b26f04e2814dffa4d9d4060583ac43dc3e79ed3eadf62a5ac37b2c", - "sha256:8ac43842f5ba26e6f21b4e63312bd1137111a9b9821d7f7dfe189a4015c6c6bc", - "sha256:8def4f6560c7b6dbc4b356dfd8e6624a018d920ce5a2864291a2bf1052cd6b68", - "sha256:97ddec69ca4fa1b66d512cf4f4a3fe6a57c4bf21209295ab2f4ada415996e08a", - "sha256:9dcea93630986209c690f27f32398956b04ccbba8f1fa7c3d1bb88a01d9ab87a", - "sha256:9f2b1007174c93dd838f52e623c972df33057e3cb7ad9341b7d9bbd66b8d8fb4", - "sha256:a5cc22ef6973992db18952f8b978781e19a0c62c098f475db936284df9311df7", - "sha256:aa6017140fe487ab8fae605a2890c94c6fbe7a8e763ff33bbdb00e27ce078cfd", - "sha256:ab7501722ec2172b1c6ea333bc47bba3bbb9b5fc0e3e891191e8447f43d3187d", - "sha256:ad43fd5b1ededb54fe01e67468710fcfec8a5830e4ce131f85e741ea151a18e9", - "sha256:b21908252c8a13b8f48d4cccdb7fabb592824cf39c9fa4e9076015dd65eabeba", - "sha256:b6c37ab097c062bdf535105c7156839c4e370065c476bb2393149ad31a2cdf6e", - "sha256:b84542669d1b0175dc2870025b73cbd4f4a3beb17796de6ec82683663e0400f3", - "sha256:bbc0dafd1de42c8dbfd6e5d1fe4deab15d2de474e11475921286bebefd109ec8", - "sha256:bd2761384ddb9de63b20795845d5cedadf052255a34c3ff1750cfc77b29d9926", - "sha256:c55f42a8b07cdb7d514cfaeb56f6e9029eef1cbc8e670ac31fc377c46b993cd1", - "sha256:cc3fe0c0ae7acf00d827efe2506131f1b19af3c87e3d76b0e081748984e51c26", - "sha256:cddc5b8bd7b0d1dfd36637eedbd83726b8b8a5969d3ecee70a9b54a94b8a0258", - "sha256:ce062844255cce4d6a8a150e8e78b9fcd6c5a3f1ff3f8792922de25827c25b9c", - "sha256:d3da4faf6398154c1e75d32778035fa7dc284814809f76e8f8d50c4f54859399", - "sha256:d6ece3f48f14a06c325181f2b9bd9a9827aac2ecdcad11eb12f561fb697eaaaa", - "sha256:e2fa8c385b27bab886caa098fa3ae114d56571ae6e7a5610cb624d7b0a66faed", - "sha256:ec4421f377cce51decd6ea3869a8b41e9f05c50bf6acef8284f8906e642992c4", - "sha256:f7b795c6ac344b0c49776b7e135a9bed0cd15b1ade2a4c7b3a19e3913247702e", - "sha256:f954115d8496d4ab5975438e3ce07780c1644ea0a66c78a943ef79f33769b61a", - "sha256:fa7c7a39eeb8dd171f59d96fd4610f908ac14b2f2eb268f4498e5f310bda8da7" + "sha256:01d647b2a9c45a23a84c3e70e19d120011cba5f56131d185c1b78685457320bb", + "sha256:0eb850a87e900a9c484150c414e21af53a6125a13f6e378cf4cc11ae86c8f9c5", + "sha256:11c10f31f2c2056585f89d8229a56013bc2fe5de51e095ebc71868d070a8dd81", + "sha256:14d3fb6cd1040a4a4a530b28e8085131ed94ebc90d72793c59a713de34b60838", + "sha256:154fd67216c2ca38a2edb4089584504fbb6c0694b518b9020ad35ecc97252bb9", + "sha256:1c3cee5c23979deb8d1b82dc4cc49be59cccc0547999dbe9adb434bb7af11cf7", + "sha256:1eb0b0b2476f357eb2975ff040ef23978137aa674cd86204cfd15d2d17318588", + "sha256:1f8b47650f90e298b78ecf4df003f66f54acdba6a0f763cc4df1eab048fe3738", + "sha256:21a3344163be3b2c7e22cef14fa5abe957a892b2ea0525ee86ad8186921b6cf0", + "sha256:23be6b22aab83f440b62a6f5975bcabeecb672bc627face6a83bc7aeb495dc7e", + "sha256:26ffb398de58247ff7bde895fe30817a036f967b0ad0e1cf2b54bda5f8dcfdd9", + "sha256:2f8fcf696bbbc584c0c7ed4adb92fd2ad7d153a50258842787bc1524e50d7081", + "sha256:355efdbbf0cecc3bd9b12589b8f8e9f03c813a115efa53f8dc2a523bfdb01334", + "sha256:36b1df2e4095368ee388190687cb1b8557c67bc38400a942a1a77713580b50ae", + "sha256:38e34c3a21ed41a7dbd5349e24c3725be5416641fdeedf8f56fcbab6d981c900", + "sha256:3aab72d2cef7f1dd6104c89b0b4d6b416b0db5ca87cc2fac5f79c5601f549cc2", + "sha256:410aa9d34ad1089898f3db461b7b744d0efcf9252a9415bbdf23540d4f67589f", + "sha256:45a47f41b6c3beeb31ac5cf0ff7524987cfcce0a10c43156eb3ee8d92d92bf22", + "sha256:4891d4c934f88b6c29b56395dfc7014ebf7e10b9e22ffd9877784e16c6b2064f", + "sha256:4c616b796358a70b1f675a24628e4823b67d9e376df2703e893da58247458956", + "sha256:5198633137780d78b86bb54dafaaa9baea698b4f059456cd4554ab7009619221", + "sha256:5a2937f528c84e64be20cb80e70cea76a6dfb74b628a04dab130679d4454395c", + "sha256:5da9032dac184b2ae2da4bce423edff7db34bfd936ebd7d4207ea45840f03905", + "sha256:5e736815b30f7e3c9044ec06a98ee59e217a833227e10eb157f44071faddd7c5", + "sha256:63ef3d371ea0b7239ace284cab9cd00d9c92b73119a7c274b437adb09bda35e6", + "sha256:70b9a20a03576c6b7022926f614ac5a6b0914486825eac89196adf3267c6489d", + "sha256:76a0fc023910d8a8ab64daed8d31d608446d2d77c6474b616b34537aa7b79c7f", + "sha256:7951af8f2998045c656ba8062e8edf5e83fd82b912534ab1de1345de08a41d2b", + "sha256:7a34a199d89d82d1897fd4a47820eb50947eec9cda5fd73f4578ff692a912f89", + "sha256:7bab596678d29ad969a524823c4e828929a90c09e91cc438e0ad79b37ce41166", + "sha256:7ea3e63e61b4b0beeb08508458bdff2daca7a321468d3c4b320a758a2f554d31", + "sha256:80acafe396ab689a326ab0d80f8cc61dec0dd2c5dca5b4b3825e7b1e0132c101", + "sha256:82720ab0cf5bb436bbd97a319ac529aee06077ff7e61cab57cee04a596c4f9b4", + "sha256:83cc275cf6dcb1a248e1876cdefd3f9b5f01063854acdfd687ec360cd3c9712a", + "sha256:85e39198f78e2f7e054d296395f6c96f5e02892337746ef5b6a1bf3ed5910142", + "sha256:8769806ea0b45d7bf75cad253fba9ac6700b7050ebb19337ff6b4e9060f963fa", + "sha256:8bdb6c911dae5fbf110fe4f5cba578437526334df381b3554b6ab7f626e5eeca", + "sha256:8f4b0042d8388ac85b8330b65406c84c3229420a05068445c13ca28cc222f1f7", + "sha256:90fe73a1f0321265126cbba13677dcceb367d926c7a65807bd80916af4c17047", + "sha256:915e22c93e7b7b636240c5a79da5f6e4e84988d699656c8e27f2ac4c95b8dcc0", + "sha256:9274ba499e7dfb8a651ee876d80386b481336d3868cba29af839370514e4dce0", + "sha256:9d62c583b5110e6a5cf5169ab616aa4ec71f2c0c30f833306f9e378cf51b6c86", + "sha256:9ef82157bbcecd75d6296d5d8b2d792242afcd064eb1ac573f8847b52e58f677", + "sha256:a19e4074bc98793458b4b3ba35a9a1d132179345e60e152a1bb48c538ab863c4", + "sha256:a347d7b43cb609e780ff8d7b3107d4bcb5b6fd09c2702aa7bdf52f15ed09fa09", + "sha256:b4fb306c96e04c5863d52ba8d65137917a3d999059c11e659eba7b75a69167bd", + "sha256:b6df858e37c321cefbf27fe7ece30a950bcc3a75618a804a0dcef7ed9dd9c92d", + "sha256:b8e59650292aa3a8ea78073fc84184538783966528e442a1b9ed653aa282edcf", + "sha256:bcb9a60ed2101af2af450318cd89c6b8313e9f8df4e8fb12b657b2e97227cf08", + "sha256:c3ba725cf5cf87d2d2d988d39c6a2a8b6fc983d78ff71bc728b0be54c869c884", + "sha256:ca1706e8b8b565e934c142db6a9592e6401dc430e4b067a97781a997070c5378", + "sha256:cd3e7aae977c723cc1dbb82f97babdb5e5fbce109630fbabb2ea5053523c89d3", + "sha256:cf334ce1d2fadd1bf3e5e9bf15e58e0c42b26eb6590875ce65bd877d917a58aa", + "sha256:d8692948cada6ee21f33db5e23460f71c8010d6dfcfe293c9b96737600a7df78", + "sha256:e5205ec0dfab1887dd383597012199f5175035e782cdb013c542187d280ca443", + "sha256:e7e7f44e091b93eb39db88bb0cb765db09b7a7f64aea2f35e7d86cbf47046c65", + "sha256:e94b7b31aa0d65f5b7c72dd8f8227dbd3e30354b99e7a9af096d967a77f2a580", + "sha256:f26fb3e8e3e2ee405c947ff44a3e384e8fa1843bc35830fe6f3d9a95a1147b6e", + "sha256:f738fee63eb263530efd4d2e9c76316c1f47b3bbf38c1bf45ae9625feed0395e", + "sha256:f9e01239abea2f52a429fe9d95c96df95f078f0172489d691b4a848ace54a476" ], "index": "pypi", - "version": "==3.9.3" + "version": "==3.9.7" }, "psutil": { "hashes": [ @@ -786,118 +794,123 @@ }, "pydantic": { "hashes": [ - "sha256:22d63db5ce4831afd16e7c58b3192d3faf8f79154980d9397d9867254310ba4b", - "sha256:43bdbf359d6304c57afda15c2b95797295b702948082d4c23851ce752f21da70" + "sha256:1607cc106602284cd4a00882986570472f193fde9cb1259bceeaedb26aa79a6d", + "sha256:45b5e446c6dfaad9444819a293b921a40e1db1aa61ea08aede0522529ce90e81" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.3.0" }, "pydantic-core": { "hashes": [ - "sha256:01947ad728f426fa07fcb26457ebf90ce29320259938414bc0edd1476e75addb", - "sha256:0455876d575a35defc4da7e0a199596d6c773e20d3d42fa1fc29f6aa640369ed", - "sha256:047580388644c473b934d27849f8ed8dbe45df0adb72104e78b543e13bf69762", - "sha256:04922fea7b13cd480586fa106345fe06e43220b8327358873c22d8dfa7a711c7", - "sha256:08f89697625e453421401c7f661b9d1eb4c9e4c0a12fd256eeb55b06994ac6af", - "sha256:0a507d7fa44688bbac76af6521e488b3da93de155b9cba6f2c9b7833ce243d59", - "sha256:0d726108c1c0380b88b6dd4db559f0280e0ceda9e077f46ff90bc85cd4d03e77", - "sha256:12ef6838245569fd60a179fade81ca4b90ae2fa0ef355d616f519f7bb27582db", - "sha256:153a61ac4030fa019b70b31fb7986461119230d3ba0ab661c757cfea652f4332", - "sha256:16468bd074fa4567592d3255bf25528ed41e6b616d69bf07096bdb5b66f947d1", - "sha256:17156abac20a9feed10feec867fddd91a80819a485b0107fe61f09f2117fe5f3", - "sha256:1927f0e15d190f11f0b8344373731e28fd774c6d676d8a6cfadc95c77214a48b", - "sha256:1e8a7c62d15a5c4b307271e4252d76ebb981d6251c6ecea4daf203ef0179ea4f", - "sha256:2ad538b7e07343001934417cdc8584623b4d8823c5b8b258e75ec8d327cec969", - "sha256:2ca4687dd996bde7f3c420def450797feeb20dcee2b9687023e3323c73fc14a2", - "sha256:2edef05b63d82568b877002dc4cb5cc18f8929b59077120192df1e03e0c633f8", - "sha256:2f9ea0355f90db2a76af530245fa42f04d98f752a1236ed7c6809ec484560d5b", - "sha256:30527d173e826f2f7651f91c821e337073df1555e3b5a0b7b1e2c39e26e50678", - "sha256:32a1e0352558cd7ccc014ffe818c7d87b15ec6145875e2cc5fa4bb7351a1033d", - "sha256:3534118289e33130ed3f1cc487002e8d09b9f359be48b02e9cd3de58ce58fba9", - "sha256:36ba9e728588588f0196deaf6751b9222492331b5552f865a8ff120869d372e0", - "sha256:382f0baa044d674ad59455a5eff83d7965572b745cc72df35c52c2ce8c731d37", - "sha256:394f12a2671ff8c4dfa2e85be6c08be0651ad85bc1e6aa9c77c21671baaf28cd", - "sha256:3ba2c9c94a9176f6321a879c8b864d7c5b12d34f549a4c216c72ce213d7d953c", - "sha256:3ded19dcaefe2f6706d81e0db787b59095f4ad0fbadce1edffdf092294c8a23f", - "sha256:3fcf529382b282a30b466bd7af05be28e22aa620e016135ac414f14e1ee6b9e1", - "sha256:43a405ce520b45941df9ff55d0cd09762017756a7b413bbad3a6e8178e64a2c2", - "sha256:453862ab268f6326b01f067ed89cb3a527d34dc46f6f4eeec46a15bbc706d0da", - "sha256:4665f7ed345012a8d2eddf4203ef145f5f56a291d010382d235b94e91813f88a", - "sha256:478f5f6d7e32bd4a04d102160efb2d389432ecf095fe87c555c0a6fc4adfc1a4", - "sha256:49db206eb8fdc4b4f30e6e3e410584146d813c151928f94ec0db06c4f2595538", - "sha256:4b262bbc13022f2097c48a21adcc360a81d83dc1d854c11b94953cd46d7d3c07", - "sha256:4cbe929efa77a806e8f1a97793f2dc3ea3475ae21a9ed0f37c21320fe93f6f50", - "sha256:4e562cc63b04636cde361fd47569162f1daa94c759220ff202a8129902229114", - "sha256:546064c55264156b973b5e65e5fafbe5e62390902ce3cf6b4005765505e8ff56", - "sha256:54df7df399b777c1fd144f541c95d351b3aa110535a6810a6a569905d106b6f3", - "sha256:56a85fa0dab1567bd0cac10f0c3837b03e8a0d939e6a8061a3a420acd97e9421", - "sha256:57a53a75010c635b3ad6499e7721eaa3b450e03f6862afe2dbef9c8f66e46ec8", - "sha256:584a7a818c84767af16ce8bda5d4f7fedb37d3d231fc89928a192f567e4ef685", - "sha256:5fd905a69ac74eaba5041e21a1e8b1a479dab2b41c93bdcc4c1cede3c12a8d86", - "sha256:61d4e713f467abcdd59b47665d488bb898ad3dd47ce7446522a50e0cbd8e8279", - "sha256:6213b471b68146af97b8551294e59e7392c2117e28ffad9c557c65087f4baee3", - "sha256:63797499a219d8e81eb4e0c42222d0a4c8ec896f5c76751d4258af95de41fdf1", - "sha256:64e8012ad60a5f0da09ed48725e6e923d1be25f2f091a640af6079f874663813", - "sha256:664402ef0c238a7f8a46efb101789d5f2275600fb18114446efec83cfadb5b66", - "sha256:68199ada7c310ddb8c76efbb606a0de656b40899388a7498954f423e03fc38be", - "sha256:69159afc2f2dc43285725f16143bc5df3c853bc1cb7df6021fce7ef1c69e8171", - "sha256:6f855bcc96ed3dd56da7373cfcc9dcbabbc2073cac7f65c185772d08884790ce", - "sha256:6feb4b64d11d5420e517910d60a907d08d846cacaf4e029668725cd21d16743c", - "sha256:72f1216ca8cef7b8adacd4c4c6b89c3b0c4f97503197f5284c80f36d6e4edd30", - "sha256:77dadc764cf7c5405e04866181c5bd94a447372a9763e473abb63d1dfe9b7387", - "sha256:782fced7d61469fd1231b184a80e4f2fa7ad54cd7173834651a453f96f29d673", - "sha256:79262be5a292d1df060f29b9a7cdd66934801f987a817632d7552534a172709a", - "sha256:7aa82d483d5fb867d4fb10a138ffd57b0f1644e99f2f4f336e48790ada9ada5e", - "sha256:853f103e2b9a58832fdd08a587a51de8b552ae90e1a5d167f316b7eabf8d7dde", - "sha256:867d3eea954bea807cabba83cfc939c889a18576d66d197c60025b15269d7cc0", - "sha256:878a5017d93e776c379af4e7b20f173c82594d94fa073059bcc546789ad50bf8", - "sha256:884235507549a6b2d3c4113fb1877ae263109e787d9e0eb25c35982ab28d0399", - "sha256:8c938c96294d983dcf419b54dba2d21056959c22911d41788efbf949a29ae30d", - "sha256:8efc1be43b036c2b6bcfb1451df24ee0ddcf69c31351003daf2699ed93f5687b", - "sha256:8fba0aff4c407d0274e43697e785bcac155ad962be57518d1c711f45e72da70f", - "sha256:90f3785146f701e053bb6b9e8f53acce2c919aca91df88bd4975be0cb926eb41", - "sha256:9137289de8fe845c246a8c3482dd0cb40338846ba683756d8f489a4bd8fddcae", - "sha256:9206c14a67c38de7b916e486ae280017cf394fa4b1aa95cfe88621a4e1d79725", - "sha256:94d2b36a74623caab262bf95f0e365c2c058396082bd9d6a9e825657d0c1e7fa", - "sha256:97c6349c81cee2e69ef59eba6e6c08c5936e6b01c2d50b9e4ac152217845ae09", - "sha256:a027f41c5008571314861744d83aff75a34cf3a07022e0be32b214a5bc93f7f1", - "sha256:a08fd490ba36d1fbb2cd5dcdcfb9f3892deb93bd53456724389135712b5fc735", - "sha256:a297c0d6c61963c5c3726840677b798ca5b7dfc71bc9c02b9a4af11d23236008", - "sha256:a4ea23b07f29487a7bef2a869f68c7ee0e05424d81375ce3d3de829314c6b5ec", - "sha256:a8b7acd04896e8f161e1500dc5f218017db05c1d322f054e89cbd089ce5d0071", - "sha256:ac2b680de398f293b68183317432b3d67ab3faeba216aec18de0c395cb5e3060", - "sha256:af24ad4fbaa5e4a2000beae0c3b7fd1c78d7819ab90f9370a1cfd8998e3f8a3c", - "sha256:af788b64e13d52fc3600a68b16d31fa8d8573e3ff2fc9a38f8a60b8d94d1f012", - "sha256:b013c7861a7c7bfcec48fd709513fea6f9f31727e7a0a93ca0dd12e056740717", - "sha256:b2799c2eaf182769889761d4fb4d78b82bc47dae833799fedbf69fc7de306faa", - "sha256:b27f3e67f6e031f6620655741b7d0d6bebea8b25d415924b3e8bfef2dd7bd841", - "sha256:b7206e41e04b443016e930e01685bab7a308113c0b251b3f906942c8d4b48fcb", - "sha256:b85778308bf945e9b33ac604e6793df9b07933108d20bdf53811bc7c2798a4af", - "sha256:bd7d1dde70ff3e09e4bc7a1cbb91a7a538add291bfd5b3e70ef1e7b45192440f", - "sha256:be86c2eb12fb0f846262ace9d8f032dc6978b8cb26a058920ecb723dbcb87d05", - "sha256:bf10963d8aed8bbe0165b41797c9463d4c5c8788ae6a77c68427569be6bead41", - "sha256:c1375025f0bfc9155286ebae8eecc65e33e494c90025cda69e247c3ccd2bab00", - "sha256:c5d8e764b5646623e57575f624f8ebb8f7a9f7fd1fae682ef87869ca5fec8dcf", - "sha256:cba5ad5eef02c86a1f3da00544cbc59a510d596b27566479a7cd4d91c6187a11", - "sha256:cc086ddb6dc654a15deeed1d1f2bcb1cb924ebd70df9dca738af19f64229b06c", - "sha256:d0c2b713464a8e263a243ae7980d81ce2de5ac59a9f798a282e44350b42dc516", - "sha256:d93aedbc4614cc21b9ab0d0c4ccd7143354c1f7cffbbe96ae5216ad21d1b21b5", - "sha256:d9610b47b5fe4aacbbba6a9cb5f12cbe864eec99dbfed5710bd32ef5dd8a5d5b", - "sha256:da055a1b0bfa8041bb2ff586b2cb0353ed03944a3472186a02cc44a557a0e661", - "sha256:dd2429f7635ad4857b5881503f9c310be7761dc681c467a9d27787b674d1250a", - "sha256:de39eb3bab93a99ddda1ac1b9aa331b944d8bcc4aa9141148f7fd8ee0299dafc", - "sha256:e40b1e97edd3dc127aa53d8a5e539a3d0c227d71574d3f9ac1af02d58218a122", - "sha256:e412607ca89a0ced10758dfb8f9adcc365ce4c1c377e637c01989a75e9a9ec8a", - "sha256:e953353180bec330c3b830891d260b6f8e576e2d18db3c78d314e56bb2276066", - "sha256:ec3473c9789cc00c7260d840c3db2c16dbfc816ca70ec87a00cddfa3e1a1cdd5", - "sha256:efff8b6761a1f6e45cebd1b7a6406eb2723d2d5710ff0d1b624fe11313693989", - "sha256:f773b39780323a0499b53ebd91a28ad11cde6705605d98d999dfa08624caf064", - "sha256:fa8e48001b39d54d97d7b380a0669fa99fc0feeb972e35a2d677ba59164a9a22", - "sha256:ff246c0111076c8022f9ba325c294f2cb5983403506989253e04dbae565e019b", - "sha256:ffe18407a4d000c568182ce5388bbbedeb099896904e43fc14eee76cfae6dec5" + "sha256:002d0ea50e17ed982c2d65b480bd975fc41086a5a2f9c924ef8fc54419d1dea3", + "sha256:02e1c385095efbd997311d85c6021d32369675c09bcbfff3b69d84e59dc103f6", + "sha256:046af9cfb5384f3684eeb3f58a48698ddab8dd870b4b3f67f825353a14441418", + "sha256:04fe5c0a43dec39aedba0ec9579001061d4653a9b53a1366b113aca4a3c05ca7", + "sha256:07a1aec07333bf5adebd8264047d3dc518563d92aca6f2f5b36f505132399efc", + "sha256:1480fa4682e8202b560dcdc9eeec1005f62a15742b813c88cdc01d44e85308e5", + "sha256:1508f37ba9e3ddc0189e6ff4e2228bd2d3c3a4641cbe8c07177162f76ed696c7", + "sha256:171a4718860790f66d6c2eda1d95dd1edf64f864d2e9f9115840840cf5b5713f", + "sha256:19e20f8baedd7d987bd3f8005c146e6bcbda7cdeefc36fad50c66adb2dd2da48", + "sha256:1a0ddaa723c48af27d19f27f1c73bdc615c73686d763388c8683fe34ae777bad", + "sha256:1aa712ba150d5105814e53cb141412217146fedc22621e9acff9236d77d2a5ef", + "sha256:1ac1750df1b4339b543531ce793b8fd5c16660a95d13aecaab26b44ce11775e9", + "sha256:1c721bfc575d57305dd922e6a40a8fe3f762905851d694245807a351ad255c58", + "sha256:1ce8c84051fa292a5dc54018a40e2a1926fd17980a9422c973e3ebea017aa8da", + "sha256:1fa1f6312fb84e8c281f32b39affe81984ccd484da6e9d65b3d18c202c666149", + "sha256:22134a4453bd59b7d1e895c455fe277af9d9d9fbbcb9dc3f4a97b8693e7e2c9b", + "sha256:23470a23614c701b37252618e7851e595060a96a23016f9a084f3f92f5ed5881", + "sha256:240a015102a0c0cc8114f1cba6444499a8a4d0333e178bc504a5c2196defd456", + "sha256:252851b38bad3bfda47b104ffd077d4f9604a10cb06fe09d020016a25107bf98", + "sha256:2a20c533cb80466c1d42a43a4521669ccad7cf2967830ac62c2c2f9cece63e7e", + "sha256:2dd50d6a1aef0426a1d0199190c6c43ec89812b1f409e7fe44cb0fbf6dfa733c", + "sha256:340e96c08de1069f3d022a85c2a8c63529fd88709468373b418f4cf2c949fb0e", + "sha256:3796a6152c545339d3b1652183e786df648ecdf7c4f9347e1d30e6750907f5bb", + "sha256:37a822f630712817b6ecc09ccc378192ef5ff12e2c9bae97eb5968a6cdf3b862", + "sha256:3a750a83b2728299ca12e003d73d1264ad0440f60f4fc9cee54acc489249b728", + "sha256:3c8945a105f1589ce8a693753b908815e0748f6279959a4530f6742e1994dcb6", + "sha256:3ccc13afee44b9006a73d2046068d4df96dc5b333bf3509d9a06d1b42db6d8bf", + "sha256:3f90e5e3afb11268628c89f378f7a1ea3f2fe502a28af4192e30a6cdea1e7d5e", + "sha256:4292ca56751aebbe63a84bbfc3b5717abb09b14d4b4442cc43fd7c49a1529efd", + "sha256:430ddd965ffd068dd70ef4e4d74f2c489c3a313adc28e829dd7262cc0d2dd1e8", + "sha256:439a0de139556745ae53f9cc9668c6c2053444af940d3ef3ecad95b079bc9987", + "sha256:44b4f937b992394a2e81a5c5ce716f3dcc1237281e81b80c748b2da6dd5cf29a", + "sha256:48c1ed8b02ffea4d5c9c220eda27af02b8149fe58526359b3c07eb391cb353a2", + "sha256:4ef724a059396751aef71e847178d66ad7fc3fc969a1a40c29f5aac1aa5f8784", + "sha256:50555ba3cb58f9861b7a48c493636b996a617db1a72c18da4d7f16d7b1b9952b", + "sha256:522a9c4a4d1924facce7270c84b5134c5cabcb01513213662a2e89cf28c1d309", + "sha256:5493a7027bfc6b108e17c3383959485087d5942e87eb62bbac69829eae9bc1f7", + "sha256:56ea80269077003eaa59723bac1d8bacd2cd15ae30456f2890811efc1e3d4413", + "sha256:5a2a3c9ef904dcdadb550eedf3291ec3f229431b0084666e2c2aa8ff99a103a2", + "sha256:5cfde4fab34dd1e3a3f7f3db38182ab6c95e4ea91cf322242ee0be5c2f7e3d2f", + "sha256:5e4a2cf8c4543f37f5dc881de6c190de08096c53986381daebb56a355be5dfe6", + "sha256:5e9c068f36b9f396399d43bfb6defd4cc99c36215f6ff33ac8b9c14ba15bdf6b", + "sha256:5ed7ceca6aba5331ece96c0e328cd52f0dcf942b8895a1ed2642de50800b79d3", + "sha256:5fa159b902d22b283b680ef52b532b29554ea2a7fc39bf354064751369e9dbd7", + "sha256:615a31b1629e12445c0e9fc8339b41aaa6cc60bd53bf802d5fe3d2c0cda2ae8d", + "sha256:621afe25cc2b3c4ba05fff53525156d5100eb35c6e5a7cf31d66cc9e1963e378", + "sha256:6656a0ae383d8cd7cc94e91de4e526407b3726049ce8d7939049cbfa426518c8", + "sha256:672174480a85386dd2e681cadd7d951471ad0bb028ed744c895f11f9d51b9ebe", + "sha256:692b4ff5c4e828a38716cfa92667661a39886e71136c97b7dac26edef18767f7", + "sha256:6bcc1ad776fffe25ea5c187a028991c031a00ff92d012ca1cc4714087e575973", + "sha256:6bf7d610ac8f0065a286002a23bcce241ea8248c71988bda538edcc90e0c39ad", + "sha256:75c0ebbebae71ed1e385f7dfd9b74c1cff09fed24a6df43d326dd7f12339ec34", + "sha256:788be9844a6e5c4612b74512a76b2153f1877cd845410d756841f6c3420230eb", + "sha256:7dc2ce039c7290b4ef64334ec7e6ca6494de6eecc81e21cb4f73b9b39991408c", + "sha256:813aab5bfb19c98ae370952b6f7190f1e28e565909bfc219a0909db168783465", + "sha256:8421cf496e746cf8d6b677502ed9a0d1e4e956586cd8b221e1312e0841c002d5", + "sha256:84e87c16f582f5c753b7f39a71bd6647255512191be2d2dbf49458c4ef024588", + "sha256:84f8bb34fe76c68c9d96b77c60cef093f5e660ef8e43a6cbfcd991017d375950", + "sha256:85cc4d105747d2aa3c5cf3e37dac50141bff779545ba59a095f4a96b0a460e70", + "sha256:883daa467865e5766931e07eb20f3e8152324f0adf52658f4d302242c12e2c32", + "sha256:8b2b1bfed698fa410ab81982f681f5b1996d3d994ae8073286515ac4d165c2e7", + "sha256:8ecbac050856eb6c3046dea655b39216597e373aa8e50e134c0e202f9c47efec", + "sha256:930bfe73e665ebce3f0da2c6d64455098aaa67e1a00323c74dc752627879fc67", + "sha256:9616567800bdc83ce136e5847d41008a1d602213d024207b0ff6cab6753fe645", + "sha256:9680dd23055dd874173a3a63a44e7f5a13885a4cfd7e84814be71be24fba83db", + "sha256:99faba727727b2e59129c59542284efebbddade4f0ae6a29c8b8d3e1f437beb7", + "sha256:9a718d56c4d55efcfc63f680f207c9f19c8376e5a8a67773535e6f7e80e93170", + "sha256:9b33bf9658cb29ac1a517c11e865112316d09687d767d7a0e4a63d5c640d1b17", + "sha256:9e8b374ef41ad5c461efb7a140ce4730661aadf85958b5c6a3e9cf4e040ff4bb", + "sha256:9e9b65a55bbabda7fccd3500192a79f6e474d8d36e78d1685496aad5f9dbd92c", + "sha256:a0b7486d85293f7f0bbc39b34e1d8aa26210b450bbd3d245ec3d732864009819", + "sha256:a53e3195f134bde03620d87a7e2b2f2046e0e5a8195e66d0f244d6d5b2f6d31b", + "sha256:a87c54e72aa2ef30189dc74427421e074ab4561cf2bf314589f6af5b37f45e6d", + "sha256:a892b5b1871b301ce20d40b037ffbe33d1407a39639c2b05356acfef5536d26a", + "sha256:a8acc9dedd304da161eb071cc7ff1326aa5b66aadec9622b2574ad3ffe225525", + "sha256:aaafc776e5edc72b3cad1ccedb5fd869cc5c9a591f1213aa9eba31a781be9ac1", + "sha256:acafc4368b289a9f291e204d2c4c75908557d4f36bd3ae937914d4529bf62a76", + "sha256:b0a5d7edb76c1c57b95df719af703e796fc8e796447a1da939f97bfa8a918d60", + "sha256:b25afe9d5c4f60dcbbe2b277a79be114e2e65a16598db8abee2a2dcde24f162b", + "sha256:b44c42edc07a50a081672e25dfe6022554b47f91e793066a7b601ca290f71e42", + "sha256:b594b64e8568cf09ee5c9501ede37066b9fc41d83d58f55b9952e32141256acd", + "sha256:b962700962f6e7a6bd77e5f37320cabac24b4c0f76afeac05e9f93cf0c620014", + "sha256:bb128c30cf1df0ab78166ded1ecf876620fb9aac84d2413e8ea1594b588c735d", + "sha256:bf9d42a71a4d7a7c1f14f629e5c30eac451a6fc81827d2beefd57d014c006c4a", + "sha256:c6595b0d8c8711e8e1dc389d52648b923b809f68ac1c6f0baa525c6440aa0daa", + "sha256:c8c6660089a25d45333cb9db56bb9e347241a6d7509838dbbd1931d0e19dbc7f", + "sha256:c9d469204abcca28926cbc28ce98f28e50e488767b084fb3fbdf21af11d3de26", + "sha256:d38bbcef58220f9c81e42c255ef0bf99735d8f11edef69ab0b499da77105158a", + "sha256:d4eb77df2964b64ba190eee00b2312a1fd7a862af8918ec70fc2d6308f76ac64", + "sha256:d63b7545d489422d417a0cae6f9898618669608750fc5e62156957e609e728a5", + "sha256:d7050899026e708fb185e174c63ebc2c4ee7a0c17b0a96ebc50e1f76a231c057", + "sha256:d79f1f2f7ebdb9b741296b69049ff44aedd95976bfee38eb4848820628a99b50", + "sha256:d85463560c67fc65cd86153a4975d0b720b6d7725cf7ee0b2d291288433fc21b", + "sha256:d9140ded382a5b04a1c030b593ed9bf3088243a0a8b7fa9f071a5736498c5483", + "sha256:d9b4916b21931b08096efed090327f8fe78e09ae8f5ad44e07f5c72a7eedb51b", + "sha256:df14f6332834444b4a37685810216cc8fe1fe91f447332cd56294c984ecbff1c", + "sha256:e49ce7dc9f925e1fb010fc3d555250139df61fa6e5a0a95ce356329602c11ea9", + "sha256:e61eae9b31799c32c5f9b7be906be3380e699e74b2db26c227c50a5fc7988698", + "sha256:ea053cefa008fda40f92aab937fb9f183cf8752e41dbc7bc68917884454c6362", + "sha256:f06e21ad0b504658a3a9edd3d8530e8cea5723f6ea5d280e8db8efc625b47e49", + "sha256:f14546403c2a1d11a130b537dda28f07eb6c1805a43dae4617448074fd49c282", + "sha256:f1a5d8f18877474c80b7711d870db0eeef9442691fcdb00adabfc97e183ee0b0", + "sha256:f2969e8f72c6236c51f91fbb79c33821d12a811e2a94b7aa59c65f8dbdfad34a", + "sha256:f468d520f47807d1eb5d27648393519655eadc578d5dd862d06873cce04c4d1b", + "sha256:f70dc00a91311a1aea124e5f64569ea44c011b58433981313202c46bccbec0e1", + "sha256:f93255b3e4d64785554e544c1c76cd32f4a354fa79e2eeca5d16ac2e7fdd57aa" ], "markers": "python_version >= '3.7'", - "version": "==2.4.0" + "version": "==2.6.3" }, "pymysql": { "hashes": [ @@ -1002,6 +1015,14 @@ "markers": "python_version >= '3.7'", "version": "==0.27.0" }, + "tenacity": { + "hashes": [ + "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a", + "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c" + ], + "index": "pypi", + "version": "==8.2.3" + }, "timeago": { "hashes": [ "sha256:9b8cb2e3102b329f35a04aa4531982d867b093b19481cfbb1dac7845fa2f79b0" @@ -1011,19 +1032,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", - "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], - "markers": "python_version >= '3.7'", - "version": "==4.7.1" + "markers": "python_version >= '3.8'", + "version": "==4.8.0" }, "urllib3": { "hashes": [ - "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", - "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" + "sha256:13abf37382ea2ce6fb744d4dad67838eec857c9f4f57009891805e0b5e123594", + "sha256:ef16afa8ba34a1f989db38e1dbbe0c302e4289a47856990d0682e374563ce35e" ], "markers": "python_version >= '3.7'", - "version": "==2.0.4" + "version": "==2.0.5" }, "uvicorn": { "hashes": [ @@ -1153,39 +1174,39 @@ "develop": { "black": { "hashes": [ - "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3", - "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb", - "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087", - "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320", - "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6", - "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3", - "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc", - "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f", - "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587", - "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91", - "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a", - "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad", - "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926", - "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9", - "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be", - "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd", - "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96", - "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491", - "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2", - "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a", - "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f", - "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995" + "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", + "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", + "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", + "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573", + "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d", + "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f", + "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9", + "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300", + "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948", + "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325", + "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9", + "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71", + "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186", + "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f", + "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe", + "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855", + "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80", + "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393", + "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c", + "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204", + "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377", + "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" ], "index": "pypi", - "version": "==23.7.0" + "version": "==23.9.1" }, "cfgv": { "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" + "markers": "python_version >= '3.8'", + "version": "==3.4.0" }, "classify-imports": { "hashes": [ @@ -1197,11 +1218,11 @@ }, "click": { "hashes": [ - "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", - "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.6" + "version": "==8.1.7" }, "distlib": { "hashes": [ @@ -1212,27 +1233,27 @@ }, "exceptiongroup": { "hashes": [ - "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", - "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" ], "markers": "python_version < '3.11'", - "version": "==1.1.2" + "version": "==1.1.3" }, "filelock": { "hashes": [ - "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81", - "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec" + "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", + "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd" ], - "markers": "python_version >= '3.7'", - "version": "==3.12.2" + "markers": "python_version >= '3.8'", + "version": "==3.12.4" }, "identify": { "hashes": [ - "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f", - "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54" + "sha256:24437fbf6f4d3fe6efd0eb9d67e24dd9106db99af5ceb27996a5f7895f24bf1b", + "sha256:d43d52b86b15918c137e3a74fff5224f60385cd0e9c38e99d07c257f02f151a5" ], "markers": "python_version >= '3.8'", - "version": "==2.5.26" + "version": "==2.5.29" }, "iniconfig": { "hashes": [ @@ -1244,35 +1265,36 @@ }, "mypy": { "hashes": [ - "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", - "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", - "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", - "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", - "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", - "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", - "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", - "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", - "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", - "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", - "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", - "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", - "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", - "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", - "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", - "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", - "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", - "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", - "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", - "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", - "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", - "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", - "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", - "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", - "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", - "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b" + "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315", + "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0", + "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373", + "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a", + "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161", + "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275", + "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693", + "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb", + "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65", + "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4", + "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb", + "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243", + "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14", + "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4", + "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1", + "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a", + "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160", + "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25", + "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12", + "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d", + "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92", + "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770", + "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2", + "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70", + "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb", + "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5", + "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f" ], "index": "pypi", - "version": "==1.4.1" + "version": "==1.5.1" }, "mypy-extensions": { "hashes": [ @@ -1316,31 +1338,33 @@ }, "pluggy": { "hashes": [ - "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", - "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" + "markers": "python_version >= '3.8'", + "version": "==1.3.0" }, "pre-commit": { "hashes": [ - "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb", - "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023" + "sha256:6bbd5129a64cad4c0dfaeeb12cd8f7ea7e15b77028d985341478c8af3c759522", + "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" ], "index": "pypi", - "version": "==3.3.3" + "version": "==3.4.0" }, "pytest": { "hashes": [ - "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", - "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", + "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" ], "index": "pypi", - "version": "==7.4.0" + "version": "==7.4.2" }, "pyyaml": { "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", @@ -1348,7 +1372,10 @@ "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", @@ -1356,9 +1383,12 @@ "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", @@ -1373,7 +1403,9 @@ "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", @@ -1386,19 +1418,19 @@ }, "reorder-python-imports": { "hashes": [ - "sha256:4dbfe21e27f569384b5e43539cb5696ef161f5c43827ee5d7de69febbfacc7c6", - "sha256:52bf76318bcfde5c6001f442c862ccf94dcdff42c0f9ec43a2ac6f23560c60bf" + "sha256:1317e154f35b57130d6f2acd3df0abbeaac3c8a5c767454ff5bd4989d07dfdbe", + "sha256:b39776d1f43083f6f537d14642a9b70ea6d7aa91a013330543d2ae7d12e2e7e2" ], "index": "pypi", - "version": "==3.10.0" + "version": "==3.11.0" }, "setuptools": { "hashes": [ - "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", - "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" ], - "markers": "python_version >= '3.7'", - "version": "==68.0.0" + "markers": "python_version >= '3.8'", + "version": "==68.2.2" }, "tomli": { "hashes": [ @@ -1410,19 +1442,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", - "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" ], - "markers": "python_version >= '3.7'", - "version": "==4.7.1" + "markers": "python_version >= '3.8'", + "version": "==4.8.0" }, "virtualenv": { "hashes": [ - "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff", - "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0" + "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b", + "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752" ], "markers": "python_version >= '3.7'", - "version": "==20.24.2" + "version": "==20.24.5" } } } From 3bc97b49c69827e16ae03b62dda3f070ab5bcf67 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 21 Sep 2023 15:33:25 +0300 Subject: [PATCH 08/24] chore: update requirements files; add comments to makefile update --- Makefile | 4 ++-- Pipfile.lock | 2 +- requirements-dev.txt | 53 ++++++++++++++++++++++---------------------- requirements.txt | 27 +++++++++++----------- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/Makefile b/Makefile index 15fd7c20..02c2794c 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,9 @@ install-dev: uninstall: pipenv --rm -update: +update: # THIS WILL NOT RUN ON WINDOWS DUE TO UVLOOP; USE WSL pipenv update --dev - make test + # make test ; disabled as it fails for now pipenv requirements > requirements.txt pipenv requirements --dev > requirements-dev.txt diff --git a/Pipfile.lock b/Pipfile.lock index dfa0cf94..e3a1d07f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -592,7 +592,7 @@ "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" ], - "markers": "python_version >= '3' and (platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))))", + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==2.0.2" }, "h11": { diff --git a/requirements-dev.txt b/requirements-dev.txt index b5e5f33a..4670a086 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,28 +1,28 @@ -i https://pypi.org/simple -black==23.7.0 -cfgv==3.3.1; python_full_version >= '3.6.1' +black==23.9.1 +cfgv==3.4.0; python_version >= '3.8' classify-imports==4.2.0; python_version >= '3.7' -click==8.1.6; python_version >= '3.7' +click==8.1.7; python_version >= '3.7' distlib==0.3.7 -exceptiongroup==1.1.2; python_version < '3.11' -filelock==3.12.2; python_version >= '3.7' -identify==2.5.25; python_version >= '3.8' +exceptiongroup==1.1.3; python_version < '3.11' +filelock==3.12.4; python_version >= '3.8' +identify==2.5.29; python_version >= '3.8' iniconfig==2.0.0; python_version >= '3.7' -mypy==1.4.1 +mypy==1.5.1 mypy-extensions==1.0.0; python_version >= '3.5' nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' packaging==23.1; python_version >= '3.7' -pathspec==0.11.1; python_version >= '3.7' -platformdirs==3.9.1; python_version >= '3.7' -pluggy==1.2.0; python_version >= '3.7' -pre-commit==3.3.3 -pytest==7.4.0 +pathspec==0.11.2; python_version >= '3.7' +platformdirs==3.10.0; python_version >= '3.7' +pluggy==1.3.0; python_version >= '3.8' +pre-commit==3.4.0 +pytest==7.4.2 pyyaml==6.0.1; python_version >= '3.6' -reorder-python-imports==3.10.0 -setuptools==68.0.0; python_version >= '3.7' +reorder-python-imports==3.11.0 +setuptools==68.2.2; python_version >= '3.8' tomli==2.0.1; python_version < '3.11' -typing-extensions==4.7.1; python_version >= '3.7' -virtualenv==20.24.0; python_version >= '3.7' +typing-extensions==4.8.0; python_version >= '3.8' +virtualenv==20.24.5; python_version >= '3.7' aiohttp==3.8.5 aiomysql==0.2.0 aioredis==2.0.1 @@ -30,27 +30,27 @@ aiosignal==1.3.1; python_version >= '3.7' akatsuki-pp-py==0.9.5 annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.2; python_version >= '3.6' +async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' bcrypt==4.0.1 -certifi==2023.5.7; python_version >= '3.6' +certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' -cryptography==41.0.3 +cryptography==41.0.4 databases[mysql]==0.5.5 -datadog==0.46.0 -fastapi==0.100.0 +datadog==0.47.0 +fastapi==0.103.1 frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.2 +orjson==3.9.7 psutil==5.9.5 py3rijndael==0.3.3 pycparser==2.21 -pydantic==2.0.3; python_version >= '3.7' -pydantic-core==2.3.0; python_version >= '3.7' +pydantic==2.3.0; python_version >= '3.7' +pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' python-dotenv==1.0.0 python-multipart==0.0.6 @@ -59,8 +59,9 @@ requests==2.31.0 sniffio==1.3.0; python_version >= '3.7' sqlalchemy==1.4.41 starlette==0.27.0; python_version >= '3.7' +tenacity==8.2.3 timeago==1.0.16 -urllib3==2.0.4; python_version >= '3.7' -uvicorn==0.23.1 +urllib3==2.0.5; python_version >= '3.7' +uvicorn==0.23.2 uvloop==0.17.0 yarl==1.9.2; python_version >= '3.7' diff --git a/requirements.txt b/requirements.txt index a5392de9..142b4640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,29 +6,29 @@ aiosignal==1.3.1; python_version >= '3.7' akatsuki-pp-py==0.9.5 annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.2; python_version >= '3.6' +async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' bcrypt==4.0.1 -certifi==2023.5.7; python_version >= '3.6' +certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' -click==8.1.6; python_version >= '3.7' -cryptography==41.0.3 +click==8.1.7; python_version >= '3.7' +cryptography==41.0.4 databases[mysql]==0.5.5 -datadog==0.46.0 -exceptiongroup==1.1.2; python_version < '3.11' -fastapi==0.100.0 +datadog==0.47.0 +exceptiongroup==1.1.3; python_version < '3.11' +fastapi==0.103.1 frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.2 +orjson==3.9.7 psutil==5.9.5 py3rijndael==0.3.3 pycparser==2.21 -pydantic==2.0.3; python_version >= '3.7' -pydantic-core==2.3.0; python_version >= '3.7' +pydantic==2.3.0; python_version >= '3.7' +pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' python-dotenv==1.0.0 python-multipart==0.0.6 @@ -37,9 +37,10 @@ requests==2.31.0 sniffio==1.3.0; python_version >= '3.7' sqlalchemy==1.4.41 starlette==0.27.0; python_version >= '3.7' +tenacity==8.2.3 timeago==1.0.16 -typing-extensions==4.7.1; python_version >= '3.7' -urllib3==2.0.4; python_version >= '3.7' -uvicorn==0.23.1 +typing-extensions==4.8.0; python_version >= '3.8' +urllib3==2.0.5; python_version >= '3.7' +uvicorn==0.23.2 uvloop==0.17.0 yarl==1.9.2; python_version >= '3.7' From 10659a7ee8c57c78db3af6274a2227513e5e6ce5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:35:44 +0000 Subject: [PATCH 09/24] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- app/objects/beatmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 081969fd..593c6688 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -39,7 +39,7 @@ class BeatmapApiResponse(TypedDict): - data: Optional[list[dict[str, Any]]] + data: list[dict[str, Any]] | None status_code: int From 7c10fb031f522b24c9d0943c60bfc87dcb3cabf5 Mon Sep 17 00:00:00 2001 From: 7mochi Date: Thu, 21 Sep 2023 09:38:46 -0500 Subject: [PATCH 10/24] chore: update dependencies --- Pipfile.lock | 72 +++----------------------------------------- requirements-dev.txt | 34 ++++++++++----------- requirements.txt | 22 +++++++------- 3 files changed, 32 insertions(+), 96 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 5159bc46..9e4b3f6e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,7 @@ { "_meta": { "hash": { -<<<<<<< upgrade-py-3.11 - "sha256": "b644f5b398947ac3348413fd3b0a6fcf9026262faa649330a743e13c0fa0fe1e" -======= - "sha256": "a2fdaebdbd6cad7dab091bb1db15ed75a5f69dd5a4dc3a921d658b7f44e20e00" ->>>>>>> master + "sha256": "da4c4fae9cf9bbb8504210ef0643fad79e0972030e2717f640562e37cee9ee06" }, "pipfile-spec": 6, "requires": { @@ -188,7 +184,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version <= '3.11.2'", "version": "==4.0.3" }, "attrs": { @@ -420,12 +416,8 @@ "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.7'", - "version": "==41.0.3" -======= "version": "==41.0.4" ->>>>>>> master }, "databases": { "extras": [ @@ -444,11 +436,8 @@ "sha256:a45ec997ab554208837e8c44d81d0e1456539dc14da5743687250e028bc809b7" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==0.47.0" -======= - "version": "==0.47.0" }, "exceptiongroup": { "hashes": [ @@ -457,7 +446,6 @@ ], "markers": "python_version < '3.11'", "version": "==1.1.3" ->>>>>>> master }, "fastapi": { "hashes": [ @@ -465,10 +453,7 @@ "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.7'", -======= ->>>>>>> master "version": "==0.103.1" }, "frozenlist": { @@ -768,10 +753,7 @@ "sha256:f9e01239abea2f52a429fe9d95c96df95f078f0172489d691b4a848ace54a476" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.7'", -======= ->>>>>>> master "version": "==3.9.7" }, "psutil": { @@ -1052,6 +1034,7 @@ "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==8.2.3" }, "timeago": { @@ -1207,14 +1190,6 @@ "develop": { "black": { "hashes": [ -<<<<<<< upgrade-py-3.11 - "sha256:3511c8a7e22ce653f89ae90dfddaf94f3bb7e2587a245246572d3b9c92adf066", - "sha256:9366c1f898981f09eb8da076716c02fd021f5a0e63581c66501d68a2e4eab844" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==23.9.0" -======= "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7", "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100", @@ -1239,8 +1214,8 @@ "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==23.9.1" ->>>>>>> master }, "cfgv": { "hashes": [ @@ -1273,23 +1248,6 @@ ], "version": "==0.3.7" }, -<<<<<<< upgrade-py-3.11 - "filelock": { - "hashes": [ - "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d", - "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb" - ], - "markers": "python_version >= '3.8'", - "version": "==3.12.3" - }, - "identify": { - "hashes": [ - "sha256:287b75b04a0e22d727bc9a41f0d4f3c1bcada97490fa6eabb5b28f0e9097e733", - "sha256:fdb527b2dfe24602809b2201e033c2a113d7bdf716db3ca8e3243f735dcecaba" - ], - "markers": "python_version >= '3.8'", - "version": "==2.5.27" -======= "exceptiongroup": { "hashes": [ "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", @@ -1313,7 +1271,6 @@ ], "markers": "python_version >= '3.8'", "version": "==2.5.29" ->>>>>>> master }, "iniconfig": { "hashes": [ @@ -1354,10 +1311,7 @@ "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.8'", -======= ->>>>>>> master "version": "==1.5.1" }, "mypy-extensions": { @@ -1414,10 +1368,7 @@ "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.8'", -======= ->>>>>>> master "version": "==3.4.0" }, "pytest": { @@ -1426,10 +1377,7 @@ "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 "markers": "python_version >= '3.7'", -======= ->>>>>>> master "version": "==7.4.2" }, "pyyaml": { @@ -1494,18 +1442,7 @@ "sha256:b39776d1f43083f6f537d14642a9b70ea6d7aa91a013330543d2ae7d12e2e7e2" ], "index": "pypi", -<<<<<<< upgrade-py-3.11 - "markers": "python_version >= '3.7'", - "version": "==3.10.0" - }, - "setuptools": { - "hashes": [ - "sha256:00478ca80aeebeecb2f288d3206b0de568df5cd2b8fada1209843cc9a8d88a48", - "sha256:af3d5949030c3f493f550876b2fd1dd5ec66689c4ee5d5344f009746f71fd5a8" - ], "markers": "python_version >= '3.8'", - "version": "==68.2.0" -======= "version": "==3.11.0" }, "setuptools": { @@ -1523,7 +1460,6 @@ ], "markers": "python_version < '3.11'", "version": "==2.0.1" ->>>>>>> master }, "typing-extensions": { "hashes": [ diff --git a/requirements-dev.txt b/requirements-dev.txt index 7a29a89b..6d36e223 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -i https://pypi.org/simple -black==23.9.1 +black==23.9.1; python_version >= '3.8' cfgv==3.4.0; python_version >= '3.8' classify-imports==4.2.0; python_version >= '3.7' click==8.1.7; python_version >= '3.7' @@ -8,44 +8,44 @@ exceptiongroup==1.1.3; python_version < '3.11' filelock==3.12.4; python_version >= '3.8' identify==2.5.29; python_version >= '3.8' iniconfig==2.0.0; python_version >= '3.7' -mypy==1.5.1 +mypy==1.5.1; python_version >= '3.8' mypy-extensions==1.0.0; python_version >= '3.5' nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' packaging==23.1; python_version >= '3.7' pathspec==0.11.2; python_version >= '3.7' platformdirs==3.10.0; python_version >= '3.7' pluggy==1.3.0; python_version >= '3.8' -pre-commit==3.4.0 -pytest==7.4.2 +pre-commit==3.4.0; python_version >= '3.8' +pytest==7.4.2; python_version >= '3.7' pyyaml==6.0.1; python_version >= '3.6' -reorder-python-imports==3.11.0 +reorder-python-imports==3.11.0; python_version >= '3.8' setuptools==68.2.2; python_version >= '3.8' tomli==2.0.1; python_version < '3.11' typing-extensions==4.8.0; python_version >= '3.8' virtualenv==20.24.5; python_version >= '3.7' -aiohttp==3.8.5 +aiohttp==3.8.5; python_version >= '3.6' aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' akatsuki-pp-py==0.9.5; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.3; python_version >= '3.7' +async-timeout==4.0.3; python_full_version <= '3.11.2' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1 +bcrypt==4.0.1; python_version >= '3.6' certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' -cryptography==41.0.4 -databases[mysql]==0.5.5 -datadog==0.47.0 -fastapi==0.103.1 +cryptography==41.0.4; python_version >= '3.7' +databases[mysql]==0.6.2; python_version >= '3.7' +datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' +fastapi==0.103.1; python_version >= '3.7' frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7 -psutil==5.9.5 +orjson==3.9.7; python_version >= '3.7' +psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' @@ -59,9 +59,9 @@ requests==2.31.0; python_version >= '3.7' sniffio==1.3.0; python_version >= '3.7' sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3 +tenacity==8.2.3; python_version >= '3.7' timeago==1.0.16 urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2 -uvloop==0.17.0 +uvicorn==0.23.2; python_version >= '3.8' +uvloop==0.17.0; python_version >= '3.7' yarl==1.9.2; python_version >= '3.7' diff --git a/requirements.txt b/requirements.txt index 256f62d3..8462b337 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,25 +5,25 @@ aiosignal==1.3.1; python_version >= '3.7' akatsuki-pp-py==0.9.5; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.3; python_version >= '3.7' +async-timeout==4.0.3; python_full_version <= '3.11.2' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1 +bcrypt==4.0.1; python_version >= '3.6' certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' click==8.1.7; python_version >= '3.7' -cryptography==41.0.4 -databases[mysql]==0.5.5 -datadog==0.47.0 +cryptography==41.0.4; python_version >= '3.7' +databases[mysql]==0.6.2; python_version >= '3.7' +datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' exceptiongroup==1.1.3; python_version < '3.11' -fastapi==0.103.1 +fastapi==0.103.1; python_version >= '3.7' frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7 -psutil==5.9.5 +orjson==3.9.7; python_version >= '3.7' +psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' @@ -37,10 +37,10 @@ requests==2.31.0; python_version >= '3.7' sniffio==1.3.0; python_version >= '3.7' sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3 +tenacity==8.2.3; python_version >= '3.7' timeago==1.0.16 typing-extensions==4.8.0; python_version >= '3.8' urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2 -uvloop==0.17.0 +uvicorn==0.23.2; python_version >= '3.8' +uvloop==0.17.0; python_version >= '3.7' yarl==1.9.2; python_version >= '3.7' From 710647acb7ed6d81a8ca0144352d1c2d19889c08 Mon Sep 17 00:00:00 2001 From: CI Date: Thu, 21 Sep 2023 14:40:02 +0000 Subject: [PATCH 11/24] [CI] Generated new Pipfile.lock --- Pipfile.lock | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 9e4b3f6e..aa0db8be 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -184,7 +184,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_full_version <= '3.11.2'", + "markers": "python_version >= '3.7'", "version": "==4.0.3" }, "attrs": { @@ -439,14 +439,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==0.47.0" }, - "exceptiongroup": { - "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.3" - }, "fastapi": { "hashes": [ "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d", @@ -1248,14 +1240,6 @@ ], "version": "==0.3.7" }, - "exceptiongroup": { - "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.3" - }, "filelock": { "hashes": [ "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", @@ -1453,14 +1437,6 @@ "markers": "python_version >= '3.8'", "version": "==68.2.2" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, "typing-extensions": { "hashes": [ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", From dd52f1968df5866ca967144e3ace3b71ee8a6555 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 21 Sep 2023 17:58:50 +0300 Subject: [PATCH 12/24] chore: relock dependencies --- Pipfile.lock | 47 +++++++++++++++++++++++--------------------- requirements-dev.txt | 46 +++++++++++++++++++++---------------------- requirements.txt | 36 ++++++++++++++++----------------- 3 files changed, 66 insertions(+), 63 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index aa0db8be..c8f00196 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -107,7 +107,6 @@ "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==3.8.5" }, "aiomysql": { @@ -160,7 +159,6 @@ "sha256:feddd245c858a566b48efcff6758964492ecd0a6fec342a6c302b1866e73ca9a" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.9.5" }, "annotated-types": { @@ -220,7 +218,6 @@ "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" ], "index": "pypi", - "markers": "python_version >= '3.6'", "version": "==4.0.1" }, "certifi": { @@ -416,7 +413,6 @@ "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==41.0.4" }, "databases": { @@ -427,7 +423,7 @@ "sha256:b09c370ad7c2f64c7f4316c096e265dc2e28304732639889272390decda2f893", "sha256:ff4010136ac2bb9da2322a2ffda4ef9185ae1c365e5891e52924dd9499d33dc4" ], - "markers": "python_version >= '3.7'", + "index": "pypi", "version": "==0.6.2" }, "datadog": { @@ -436,16 +432,22 @@ "sha256:a45ec997ab554208837e8c44d81d0e1456539dc14da5743687250e028bc809b7" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==0.47.0" }, + "exceptiongroup": { + "hashes": [ + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.3" + }, "fastapi": { "hashes": [ "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d", "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.103.1" }, "frozenlist": { @@ -745,7 +747,6 @@ "sha256:f9e01239abea2f52a429fe9d95c96df95f078f0172489d691b4a848ace54a476" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==3.9.7" }, "psutil": { @@ -766,7 +767,6 @@ "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==5.9.5" }, "py3rijndael": { @@ -918,7 +918,6 @@ "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.0.0" }, "python-multipart": { @@ -927,7 +926,6 @@ "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.0.6" }, "pytimeparse": { @@ -944,7 +942,6 @@ "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==5.0.0" }, "requests": { @@ -953,7 +950,6 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "sniffio": { @@ -1009,7 +1005,6 @@ "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898" ], "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.4.41" }, "starlette": { @@ -1026,7 +1021,6 @@ "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==8.2.3" }, "timeago": { @@ -1058,7 +1052,6 @@ "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==0.23.2" }, "uvloop": { @@ -1095,7 +1088,6 @@ "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==0.17.0" }, "yarl": { @@ -1206,7 +1198,6 @@ "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==23.9.1" }, "cfgv": { @@ -1240,6 +1231,14 @@ ], "version": "==0.3.7" }, + "exceptiongroup": { + "hashes": [ + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.3" + }, "filelock": { "hashes": [ "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", @@ -1295,7 +1294,6 @@ "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==1.5.1" }, "mypy-extensions": { @@ -1352,7 +1350,6 @@ "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==3.4.0" }, "pytest": { @@ -1361,7 +1358,6 @@ "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" ], "index": "pypi", - "markers": "python_version >= '3.7'", "version": "==7.4.2" }, "pyyaml": { @@ -1426,7 +1422,6 @@ "sha256:b39776d1f43083f6f537d14642a9b70ea6d7aa91a013330543d2ae7d12e2e7e2" ], "index": "pypi", - "markers": "python_version >= '3.8'", "version": "==3.11.0" }, "setuptools": { @@ -1437,6 +1432,14 @@ "markers": "python_version >= '3.8'", "version": "==68.2.2" }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, "typing-extensions": { "hashes": [ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d36e223..2908ca46 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -i https://pypi.org/simple -black==23.9.1; python_version >= '3.8' +black==23.9.1 cfgv==3.4.0; python_version >= '3.8' classify-imports==4.2.0; python_version >= '3.7' click==8.1.7; python_version >= '3.7' @@ -8,60 +8,60 @@ exceptiongroup==1.1.3; python_version < '3.11' filelock==3.12.4; python_version >= '3.8' identify==2.5.29; python_version >= '3.8' iniconfig==2.0.0; python_version >= '3.7' -mypy==1.5.1; python_version >= '3.8' +mypy==1.5.1 mypy-extensions==1.0.0; python_version >= '3.5' nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' packaging==23.1; python_version >= '3.7' pathspec==0.11.2; python_version >= '3.7' platformdirs==3.10.0; python_version >= '3.7' pluggy==1.3.0; python_version >= '3.8' -pre-commit==3.4.0; python_version >= '3.8' -pytest==7.4.2; python_version >= '3.7' +pre-commit==3.4.0 +pytest==7.4.2 pyyaml==6.0.1; python_version >= '3.6' -reorder-python-imports==3.11.0; python_version >= '3.8' +reorder-python-imports==3.11.0 setuptools==68.2.2; python_version >= '3.8' tomli==2.0.1; python_version < '3.11' typing-extensions==4.8.0; python_version >= '3.8' virtualenv==20.24.5; python_version >= '3.7' -aiohttp==3.8.5; python_version >= '3.6' +aiohttp==3.8.5 aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5; python_version >= '3.7' +akatsuki-pp-py==0.9.5 annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.3; python_full_version <= '3.11.2' +async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1; python_version >= '3.6' +bcrypt==4.0.1 certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' -cryptography==41.0.4; python_version >= '3.7' -databases[mysql]==0.6.2; python_version >= '3.7' -datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' -fastapi==0.103.1; python_version >= '3.7' +cryptography==41.0.4 +databases[mysql]==0.6.2 +datadog==0.47.0 +fastapi==0.103.1 frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7; python_version >= '3.7' -psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +orjson==3.9.7 +psutil==5.9.5 py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' -python-dotenv==1.0.0; python_version >= '3.8' -python-multipart==0.0.6; python_version >= '3.7' +python-dotenv==1.0.0 +python-multipart==0.0.6 pytimeparse==1.1.8 -redis==5.0.0; python_version >= '3.7' -requests==2.31.0; python_version >= '3.7' +redis==5.0.0 +requests==2.31.0 sniffio==1.3.0; python_version >= '3.7' -sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' +sqlalchemy==1.4.41 starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3; python_version >= '3.7' +tenacity==8.2.3 timeago==1.0.16 urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2; python_version >= '3.8' -uvloop==0.17.0; python_version >= '3.7' +uvicorn==0.23.2 +uvloop==0.17.0 yarl==1.9.2; python_version >= '3.7' diff --git a/requirements.txt b/requirements.txt index 8462b337..1ef233e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,46 @@ -i https://pypi.org/simple -aiohttp==3.8.5; python_version >= '3.6' +aiohttp==3.8.5 aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5; python_version >= '3.7' +akatsuki-pp-py==0.9.5 annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' -async-timeout==4.0.3; python_full_version <= '3.11.2' +async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1; python_version >= '3.6' +bcrypt==4.0.1 certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' click==8.1.7; python_version >= '3.7' -cryptography==41.0.4; python_version >= '3.7' -databases[mysql]==0.6.2; python_version >= '3.7' -datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' +cryptography==41.0.4 +databases[mysql]==0.6.2 +datadog==0.47.0 exceptiongroup==1.1.3; python_version < '3.11' -fastapi==0.103.1; python_version >= '3.7' +fastapi==0.103.1 frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7; python_version >= '3.7' -psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' +orjson==3.9.7 +psutil==5.9.5 py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' -python-dotenv==1.0.0; python_version >= '3.8' -python-multipart==0.0.6; python_version >= '3.7' +python-dotenv==1.0.0 +python-multipart==0.0.6 pytimeparse==1.1.8 -redis==5.0.0; python_version >= '3.7' -requests==2.31.0; python_version >= '3.7' +redis==5.0.0 +requests==2.31.0 sniffio==1.3.0; python_version >= '3.7' -sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' +sqlalchemy==1.4.41 starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3; python_version >= '3.7' +tenacity==8.2.3 timeago==1.0.16 typing-extensions==4.8.0; python_version >= '3.8' urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2; python_version >= '3.8' -uvloop==0.17.0; python_version >= '3.7' +uvicorn==0.23.2 +uvloop==0.17.0 yarl==1.9.2; python_version >= '3.7' From 4e33c3be8f8a0999e956c609148c0f14def0d31f Mon Sep 17 00:00:00 2001 From: CI Date: Thu, 21 Sep 2023 14:59:58 +0000 Subject: [PATCH 13/24] [CI] Generated new Pipfile.lock --- Pipfile.lock | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index c8f00196..aa0db8be 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -107,6 +107,7 @@ "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.8.5" }, "aiomysql": { @@ -159,6 +160,7 @@ "sha256:feddd245c858a566b48efcff6758964492ecd0a6fec342a6c302b1866e73ca9a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.9.5" }, "annotated-types": { @@ -218,6 +220,7 @@ "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==4.0.1" }, "certifi": { @@ -413,6 +416,7 @@ "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==41.0.4" }, "databases": { @@ -423,7 +427,7 @@ "sha256:b09c370ad7c2f64c7f4316c096e265dc2e28304732639889272390decda2f893", "sha256:ff4010136ac2bb9da2322a2ffda4ef9185ae1c365e5891e52924dd9499d33dc4" ], - "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.6.2" }, "datadog": { @@ -432,22 +436,16 @@ "sha256:a45ec997ab554208837e8c44d81d0e1456539dc14da5743687250e028bc809b7" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==0.47.0" }, - "exceptiongroup": { - "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.3" - }, "fastapi": { "hashes": [ "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d", "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.103.1" }, "frozenlist": { @@ -747,6 +745,7 @@ "sha256:f9e01239abea2f52a429fe9d95c96df95f078f0172489d691b4a848ace54a476" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.9.7" }, "psutil": { @@ -767,6 +766,7 @@ "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==5.9.5" }, "py3rijndael": { @@ -918,6 +918,7 @@ "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.0.0" }, "python-multipart": { @@ -926,6 +927,7 @@ "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.0.6" }, "pytimeparse": { @@ -942,6 +944,7 @@ "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==5.0.0" }, "requests": { @@ -950,6 +953,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "sniffio": { @@ -1005,6 +1009,7 @@ "sha256:f5fa526d027d804b1f85cdda1eb091f70bde6fb7d87892f6dd5a48925bc88898" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==1.4.41" }, "starlette": { @@ -1021,6 +1026,7 @@ "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==8.2.3" }, "timeago": { @@ -1052,6 +1058,7 @@ "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==0.23.2" }, "uvloop": { @@ -1088,6 +1095,7 @@ "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.17.0" }, "yarl": { @@ -1198,6 +1206,7 @@ "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==23.9.1" }, "cfgv": { @@ -1231,14 +1240,6 @@ ], "version": "==0.3.7" }, - "exceptiongroup": { - "hashes": [ - "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", - "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.3" - }, "filelock": { "hashes": [ "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4", @@ -1294,6 +1295,7 @@ "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.5.1" }, "mypy-extensions": { @@ -1350,6 +1352,7 @@ "sha256:96d529a951f8b677f730a7212442027e8ba53f9b04d217c4c67dc56c393ad945" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.4.0" }, "pytest": { @@ -1358,6 +1361,7 @@ "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==7.4.2" }, "pyyaml": { @@ -1422,6 +1426,7 @@ "sha256:b39776d1f43083f6f537d14642a9b70ea6d7aa91a013330543d2ae7d12e2e7e2" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.11.0" }, "setuptools": { @@ -1432,14 +1437,6 @@ "markers": "python_version >= '3.8'", "version": "==68.2.2" }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version < '3.11'", - "version": "==2.0.1" - }, "typing-extensions": { "hashes": [ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", From e673f63f26472322a89525828695a4e5fe07b05b Mon Sep 17 00:00:00 2001 From: 7mochi Date: Thu, 21 Sep 2023 10:10:26 -0500 Subject: [PATCH 14/24] chore: add async-timeout to dependencies --- Pipfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile b/Pipfile index 234ac048..b25541cd 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] aiohttp = "*" +async-timeout = "*" bcrypt = "*" datadog = "*" fastapi = "*" From c2c5150f8f6327486320a9341f74a48fe5847826 Mon Sep 17 00:00:00 2001 From: 7mochi Date: Thu, 21 Sep 2023 10:11:16 -0500 Subject: [PATCH 15/24] chore: Regenerate requirements --- Pipfile.lock | 3 ++- requirements-dev.txt | 46 +++++++++++++++++++++----------------------- requirements.txt | 35 ++++++++++++++++----------------- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index aa0db8be..0e1766ff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "da4c4fae9cf9bbb8504210ef0643fad79e0972030e2717f640562e37cee9ee06" + "sha256": "e53e4c3feb118befbf6f337a07168bfb8bbb6c2ad39b541106ff17b68f158eb3" }, "pipfile-spec": 6, "requires": { @@ -184,6 +184,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==4.0.3" }, diff --git a/requirements-dev.txt b/requirements-dev.txt index 2908ca46..5af6b117 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,67 +1,65 @@ -i https://pypi.org/simple -black==23.9.1 +black==23.9.1; python_version >= '3.8' cfgv==3.4.0; python_version >= '3.8' classify-imports==4.2.0; python_version >= '3.7' click==8.1.7; python_version >= '3.7' distlib==0.3.7 -exceptiongroup==1.1.3; python_version < '3.11' filelock==3.12.4; python_version >= '3.8' identify==2.5.29; python_version >= '3.8' iniconfig==2.0.0; python_version >= '3.7' -mypy==1.5.1 +mypy==1.5.1; python_version >= '3.8' mypy-extensions==1.0.0; python_version >= '3.5' nodeenv==1.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' packaging==23.1; python_version >= '3.7' pathspec==0.11.2; python_version >= '3.7' platformdirs==3.10.0; python_version >= '3.7' pluggy==1.3.0; python_version >= '3.8' -pre-commit==3.4.0 -pytest==7.4.2 +pre-commit==3.4.0; python_version >= '3.8' +pytest==7.4.2; python_version >= '3.7' pyyaml==6.0.1; python_version >= '3.6' -reorder-python-imports==3.11.0 +reorder-python-imports==3.11.0; python_version >= '3.8' setuptools==68.2.2; python_version >= '3.8' -tomli==2.0.1; python_version < '3.11' typing-extensions==4.8.0; python_version >= '3.8' virtualenv==20.24.5; python_version >= '3.7' -aiohttp==3.8.5 +aiohttp==3.8.5; python_version >= '3.6' aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5 +akatsuki-pp-py==0.9.5; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1 +bcrypt==4.0.1; python_version >= '3.6' certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' -cryptography==41.0.4 -databases[mysql]==0.6.2 -datadog==0.47.0 -fastapi==0.103.1 +cryptography==41.0.4; python_version >= '3.7' +databases[mysql]==0.6.2; python_version >= '3.7' +datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' +fastapi==0.103.1; python_version >= '3.7' frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7 -psutil==5.9.5 +orjson==3.9.7; python_version >= '3.7' +psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' -python-dotenv==1.0.0 -python-multipart==0.0.6 +python-dotenv==1.0.0; python_version >= '3.8' +python-multipart==0.0.6; python_version >= '3.7' pytimeparse==1.1.8 -redis==5.0.0 -requests==2.31.0 +redis==5.0.0; python_version >= '3.7' +requests==2.31.0; python_version >= '3.7' sniffio==1.3.0; python_version >= '3.7' -sqlalchemy==1.4.41 +sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3 +tenacity==8.2.3; python_version >= '3.7' timeago==1.0.16 urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2 -uvloop==0.17.0 +uvicorn==0.23.2; python_version >= '3.8' +uvloop==0.17.0; python_version >= '3.7' yarl==1.9.2; python_version >= '3.7' diff --git a/requirements.txt b/requirements.txt index 1ef233e9..a9b17ead 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,45 @@ -i https://pypi.org/simple -aiohttp==3.8.5 +aiohttp==3.8.5; python_version >= '3.6' aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5 +akatsuki-pp-py==0.9.5; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' async-timeout==4.0.3; python_version >= '3.7' attrs==23.1.0; python_version >= '3.7' -bcrypt==4.0.1 +bcrypt==4.0.1; python_version >= '3.6' certifi==2023.7.22; python_version >= '3.6' cffi==1.15.1 charset-normalizer==3.2.0; python_full_version >= '3.7.0' click==8.1.7; python_version >= '3.7' -cryptography==41.0.4 -databases[mysql]==0.6.2 -datadog==0.47.0 -exceptiongroup==1.1.3; python_version < '3.11' -fastapi==0.103.1 +cryptography==41.0.4; python_version >= '3.7' +databases[mysql]==0.6.2; python_version >= '3.7' +datadog==0.47.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' +fastapi==0.103.1; python_version >= '3.7' frozenlist==1.4.0; python_version >= '3.8' greenlet==2.0.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32'))))) h11==0.14.0; python_version >= '3.7' idna==3.4; python_version >= '3.5' multidict==6.0.4; python_version >= '3.7' -orjson==3.9.7 -psutil==5.9.5 +orjson==3.9.7; python_version >= '3.7' +psutil==5.9.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' py3rijndael==0.3.3 pycparser==2.21 pydantic==2.3.0; python_version >= '3.7' pydantic-core==2.6.3; python_version >= '3.7' pymysql==1.1.0; python_version >= '3.7' -python-dotenv==1.0.0 -python-multipart==0.0.6 +python-dotenv==1.0.0; python_version >= '3.8' +python-multipart==0.0.6; python_version >= '3.7' pytimeparse==1.1.8 -redis==5.0.0 -requests==2.31.0 +redis==5.0.0; python_version >= '3.7' +requests==2.31.0; python_version >= '3.7' sniffio==1.3.0; python_version >= '3.7' -sqlalchemy==1.4.41 +sqlalchemy==1.4.41; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' starlette==0.27.0; python_version >= '3.7' -tenacity==8.2.3 +tenacity==8.2.3; python_version >= '3.7' timeago==1.0.16 typing-extensions==4.8.0; python_version >= '3.8' urllib3==2.0.5; python_version >= '3.7' -uvicorn==0.23.2 -uvloop==0.17.0 +uvicorn==0.23.2; python_version >= '3.8' +uvloop==0.17.0; python_version >= '3.7' yarl==1.9.2; python_version >= '3.7' From 8eddc0db856980f7cd33e99bdab7dab38521e382 Mon Sep 17 00:00:00 2001 From: tsunyoku Date: Thu, 21 Sep 2023 16:35:29 +0100 Subject: [PATCH 16/24] fix: incorrect handling of BeatmapApiResponse response --- app/objects/beatmap.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index fb655aac..ec636447 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -384,7 +384,8 @@ async def from_md5(cls, md5: str, set_id: int = -1) -> Optional[Beatmap]: if api_data["data"] is None: return None - set_id = int(api_data[0]["beatmapset_id"]) + api_response = api_data["data"] + set_id = int(api_response[0]["beatmapset_id"]) # fetch (and cache) beatmap set beatmap_set = await BeatmapSet.from_bsid(set_id) @@ -426,7 +427,8 @@ async def from_bid(cls, bid: int) -> Optional[Beatmap]: if api_data["data"] is None: return None - set_id = int(api_data[0]["beatmapset_id"]) + api_response = api_data["data"] + set_id = int(api_response[0]["beatmapset_id"]) # fetch (and cache) beatmap set beatmap_set = await BeatmapSet.from_bsid(set_id) @@ -655,8 +657,10 @@ async def _update_if_available(self) -> None: return if api_data["data"] is not None: + api_response = api_data["data"] + old_maps = {bmap.id: bmap for bmap in self.maps} - new_maps = {int(api_map["beatmap_id"]): api_map for api_map in api_data} + new_maps = {int(api_map["beatmap_id"]): api_map for api_map in api_response} self.last_osuapi_check = datetime.now() @@ -886,7 +890,9 @@ async def _from_bsid_sql(cls, bsid: int) -> Optional[BeatmapSet]: async def _from_bsid_osuapi(cls, bsid: int) -> Optional[BeatmapSet]: """Fetch a mapset from the osu!api by set id.""" api_data = await api_get_beatmaps(s=bsid) - if api_data: + if api_data["data"] is not None: + api_response = api_data["data"] + self = cls(id=bsid, last_osuapi_check=datetime.now()) # XXX: pre-mapset bancho.py support @@ -899,7 +905,7 @@ async def _from_bsid_osuapi(cls, bsid: int) -> Optional[BeatmapSet]: current_maps = {row["id"]: row["status"] for row in res} - for api_bmap in api_data: + for api_bmap in api_response: # newer version available for this map bmap: Beatmap = Beatmap.__new__(Beatmap) bmap.id = int(api_bmap["beatmap_id"]) From 63cf6eda3c7120c5df614797cd10715274e1e52a Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 21 Sep 2023 19:10:27 +0300 Subject: [PATCH 17/24] docs: update documentation for python 3.11 upgrade --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 +- .github/docs/wiki/Setting-up-(Manual).md | 10 +- .github/docs/wiki/_Footer.md | 2 +- .../locale/de-DE/Setting-up-(Manual)-de-DE.md | 10 +- .github/docs/wiki/locale/de-DE/_Footer.md | 2 +- .../locale/zh-CN/Setting-up-(Manual)-zh-CN.md | 10 +- .github/docs/wiki/locale/zh-CN/_Footer.md | 2 +- README.md | 3 +- README_CN.md | 269 +----------------- README_DE.MD | 243 +--------------- app/utils.py | 4 +- 11 files changed, 29 insertions(+), 530 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3b9dca61..4b37fc3c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -46,9 +46,9 @@ body: label: Python Version description: What version of python are you using? options: - - 3.9.X (Default) + - 3.11.X (Default) - 3.10.X - - 3.11.X + - 3.9.X validations: required: true - type: textarea diff --git a/.github/docs/wiki/Setting-up-(Manual).md b/.github/docs/wiki/Setting-up-(Manual).md index 5380a4b3..e3b8d881 100644 --- a/.github/docs/wiki/Setting-up-(Manual).md +++ b/.github/docs/wiki/Setting-up-(Manual).md @@ -3,13 +3,13 @@ ## installing bancho.py's requirements ```sh -# python3.9 is often not available natively, +# python3.11 is often not available natively, # but we can rely on deadsnakes to provide it. -# https://github.com/deadsnakes/python3.9 +# https://github.com/deadsnakes/python3.11 sudo add-apt-repository -y ppa:deadsnakes # install required programs for running bancho.py -sudo apt install -y python3.9-dev python3.9-distutils \ +sudo apt install -y python3.11-dev python3.11-distutils \ build-essential \ mysql-server redis-server \ nginx certbot @@ -20,10 +20,10 @@ cd tools && ./enable_geoip_module.sh && cd .. # install python's package manager, pip # it's used to install python-specific dependencies wget https://bootstrap.pypa.io/get-pip.py -python3.9 get-pip.py && rm get-pip.py +python3.11 get-pip.py && rm get-pip.py # make sure pip and setuptools are up to date -python3.9 -m pip install -U pip setuptools pipenv +python3.11 -m pip install -U pip setuptools pipenv # install bancho.py's python-specific dependencies # (if you plan to work as a dev, you can use `make install-dev`) diff --git a/.github/docs/wiki/_Footer.md b/.github/docs/wiki/_Footer.md index 09740e82..45836f36 100644 --- a/.github/docs/wiki/_Footer.md +++ b/.github/docs/wiki/_Footer.md @@ -1,4 +1,4 @@ -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) +[![Python 11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) diff --git a/.github/docs/wiki/locale/de-DE/Setting-up-(Manual)-de-DE.md b/.github/docs/wiki/locale/de-DE/Setting-up-(Manual)-de-DE.md index b918652b..ffc082f3 100644 --- a/.github/docs/wiki/locale/de-DE/Setting-up-(Manual)-de-DE.md +++ b/.github/docs/wiki/locale/de-DE/Setting-up-(Manual)-de-DE.md @@ -5,13 +5,13 @@ ## installing bancho.py's requirements ```sh -# python3.9 is often not available natively, +# python3.11 is often not available natively, # but we can rely on deadsnakes to provide it. -# https://github.com/deadsnakes/python3.9 +# https://github.com/deadsnakes/python3.11 sudo add-apt-repository -y ppa:deadsnakes # install required programs for running bancho.py -sudo apt install -y python3.9-dev python3.9-distutils \ +sudo apt install -y python3.11-dev python3.11-distutils \ build-essential \ mysql-server redis-server \ nginx certbot @@ -22,10 +22,10 @@ cd tools && ./enable_geoip_module.sh && cd .. # install python's package manager, pip # it's used to install python-specific dependencies wget https://bootstrap.pypa.io/get-pip.py -python3.9 get-pip.py && rm get-pip.py +python3.11 get-pip.py && rm get-pip.py # make sure pip and setuptools are up to date -python3.9 -m pip install -U pip setuptools pipenv +python3.11 -m pip install -U pip setuptools pipenv # install bancho.py's python-specific dependencies # (if you plan to work as a dev, you can use `make install-dev`) diff --git a/.github/docs/wiki/locale/de-DE/_Footer.md b/.github/docs/wiki/locale/de-DE/_Footer.md index 9eea16a0..60e598dc 100644 --- a/.github/docs/wiki/locale/de-DE/_Footer.md +++ b/.github/docs/wiki/locale/de-DE/_Footer.md @@ -1,4 +1,4 @@ -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Codestil: black](https://img.shields.io/badge/code%20stil-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) diff --git a/.github/docs/wiki/locale/zh-CN/Setting-up-(Manual)-zh-CN.md b/.github/docs/wiki/locale/zh-CN/Setting-up-(Manual)-zh-CN.md index 5380a4b3..e3b8d881 100644 --- a/.github/docs/wiki/locale/zh-CN/Setting-up-(Manual)-zh-CN.md +++ b/.github/docs/wiki/locale/zh-CN/Setting-up-(Manual)-zh-CN.md @@ -3,13 +3,13 @@ ## installing bancho.py's requirements ```sh -# python3.9 is often not available natively, +# python3.11 is often not available natively, # but we can rely on deadsnakes to provide it. -# https://github.com/deadsnakes/python3.9 +# https://github.com/deadsnakes/python3.11 sudo add-apt-repository -y ppa:deadsnakes # install required programs for running bancho.py -sudo apt install -y python3.9-dev python3.9-distutils \ +sudo apt install -y python3.11-dev python3.11-distutils \ build-essential \ mysql-server redis-server \ nginx certbot @@ -20,10 +20,10 @@ cd tools && ./enable_geoip_module.sh && cd .. # install python's package manager, pip # it's used to install python-specific dependencies wget https://bootstrap.pypa.io/get-pip.py -python3.9 get-pip.py && rm get-pip.py +python3.11 get-pip.py && rm get-pip.py # make sure pip and setuptools are up to date -python3.9 -m pip install -U pip setuptools pipenv +python3.11 -m pip install -U pip setuptools pipenv # install bancho.py's python-specific dependencies # (if you plan to work as a dev, you can use `make install-dev`) diff --git a/.github/docs/wiki/locale/zh-CN/_Footer.md b/.github/docs/wiki/locale/zh-CN/_Footer.md index 8ae54f70..d5869e7f 100644 --- a/.github/docs/wiki/locale/zh-CN/_Footer.md +++ b/.github/docs/wiki/locale/zh-CN/_Footer.md @@ -1,4 +1,4 @@ -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![代码风格 black](https://img.shields.io/badge/代码风格-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) diff --git a/README.md b/README.md index 4dc7734e..26a8afbd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bancho.py -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) @@ -12,7 +12,6 @@ the project is developed primarily by the [Akatsuki](https://akatsuki.gg/) team, and our aim is to create the most easily maintainable, reliable, and feature-rich osu! server implementation available. - If you are interested in running or contributing to **bancho.py**, you should head over to the **[bancho.py wiki](https://github.com/osuAkatsuki/bancho.py/wiki)**. ## License diff --git a/README_CN.md b/README_CN.md index ad0e03da..76d09485 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,5 +1,6 @@ # bancho.py - 中文文档 -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) + +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) @@ -18,267 +19,5 @@ bancho.py 是一个还在被不断维护的osu!后端项目,不论你的水平 注意:bancho.py是一个后端!当你跟着下面的步骤部署完成后你可以正常登录 并游玩。这个项目自带api,但是没有前端(就是网页),前端的话你也可以去看 他们团队开发的前端项目。 -api文档(英语):https://github.com/JKBGL/gulag-api-docs -前端(guweb):https://github.com/Varkaria/guweb - -# 如何在自己的服务器上部署 -如果你学习过有关于Linux / 数据库(MySQL) / Python的知识,他们会对你的部署 -有很大的帮助!但是这并 不 必须 - -(有很多没有上面相关知识的朋友也成功的部署了这个项目!) - -如果在部署的时候遇到了什么问题,欢迎来我们的Discord群组!(看上面) :) - -这个教程是基于Ubuntu Linux操作系统的,如果你采用了其他操作系统可能会有一点不同~ -## 你需要先准备好下面的东西哦~ (译者注) -- 一个服务器*,他可以是你本地的服务器,也可以是一个云服务器 -- 一个你自己的域名!注意必须是一级域名!建议在部署前先去开以下的几个子域名(先解析到你的服务器),后期会方便一些: - a , b , c , ce , c4 , osu , api , assets - -举个例子:如果你购买的域名是 example.com,你就需要去开这些子域名: -a.example.com -b.example.com -c.example.com -等等...... - -*(注意!如果你在 中国大陆 并且想要和你的朋友一起玩你的私服,译者建议你购买(大陆外的,例如笔者购买的是香港的)云服务器。因为中国大陆的家庭宽带封锁了80和443端口,而且大多数家庭没有公网ipv4,会给后期部署带来麻烦) -*(译者购买的云服务器配置是 2核CPU, 2GB RAM, 30MB带宽,性能足够10人同时在线游玩,仅供配置参考) - -## 第一步:下载这个项目到你服务器本地 -```sh -# 克隆 bancho.py's -# 注意!你的服务器可能需要先安装git,尤其是全新的服务器 -git clone https://github.com/osuAkatsuki/bancho.py - -# 进入到 bancho.py 的目录 -cd bancho.py -``` - -## 第二步:安装bancho.py所需的依赖 -bancho.py 的代码库有大约15,000行,我们致力于减少我们需要的外部依赖(dependence) - -但是你还是需要安装这些哦:(别急!一步步来) -- python (bancho.py就是拿这个写的~) -- mysql (数据库~) -- redis (一种缓存数据库,与mysql不同的是,他把频繁的数据存储到缓存中,读取速度更快) -- nginx (用于反向代理) -- certbot (用于搞SSL证书) -- build-essential ( c/c++ 的 build tools ) - -当然还有些别的,跟着下面的步骤走就可以全都安装咯~ -```sh -# python3.9 现在并不能直接装, -# 这里我们拿deadsnakes来搞 -# https://github.com/deadsnakes/python3.9 -sudo add-apt-repository -y ppa:deadsnakes - -# 安装所有的依赖(dependence) -sudo apt install -y python3.9-dev python3.9-distutils \ - build-essential \ - mysql-server redis-server \ - nginx certbot - - -# 安装python的包管理器, pip -# pip是用来安装和python有关的包 -wget https://bootstrap.pypa.io/get-pip.py -python3.9 get-pip.py && rm get-pip.py - -# 更新python3.9和pip到最新 -python3.9 -m pip install -U pip setuptools pipenv - -# 安装所有bancho.py使用的与python有关的包(外部依赖) -# (如果你想要使用开发环境,那么下面请使用`make install-dev`) -make install -``` - -## 第三步:给bancho.py开一个数据库! -你需要给bancho.py开一个数据库去存相关的数据: - -元数据(metadata) 以及 日志(logs), 例如:用户账户和统计(user accounts -and stats), 譜面(beatmaps and beatmapsets), 聊天(chat channels)等等 - -```sh -# 开启数据库服务 -sudo service mysql start - -# 以 root 用户登录mysql(注意如果你已经是root用户的话直接输mysql -# 然后回车就可以啦) - -# 现在请小心谨慎,因为你给他的错误命令他会在很短的时间内执行完毕, -# 不给你后悔的机会 - -sudo mysql -``` - -现在,我们会: -- 创建一个数据库 -- 创建用户 -- 给你新建的用户放全部的数据库权限 - -不要忘记分号(";")哦~ -(一会我们会去改bancho.py的配置文件来连接这个数据库) -```sql -# !你需要改这些东西并且 记 好 他 们 !: -# - YOUR_DB_NAME -> 改成你想创建的数据库名字 -# - YOUR_DB_USER -> 改成你想创建的数据库用户名 -# - YOUR_DB_PASSWORD -> 改成你希望的数据库密码 - -# 给bancho.py创建一个数据库 -CREATE DATABASE YOUR_DB_NAME; - -# 创建一个操作这个数据库的用户 -CREATE USER 'YOUR_DB_USER'@'localhost' IDENTIFIED BY 'YOUR_DB_PASSWORD'; - -# 给用户放所有的权限让他可以去操作数据库 -GRANT ALL PRIVILEGES ON YOUR_DB_NAME.* TO 'YOUR_DB_USER'@'localhost'; - -# 确保上面的权限变更已经操作好了 -FLUSH PRIVILEGES; - -# 退出mysql,回到系统命令行 -quit -``` - -## 第四步:把刚刚我们新建的空数据库变成我们想要的样子 -我们现在已经建立了一个空的数据库。你可以把数据库理解成一个巨大的表格 - -bancho.py 有很多 表 (tables) 去存各种东西, 例如, 名为 `users` 以及 `scores` -的表用来存储他们相关的东西(字面意思) - -有很多 列 (columns) (竖直的) 存着 `user` or `score`里面不同的数据 - -这个地方你可以直接把他理解成写着全班同学成绩的成绩表,最上面横行是姓 -名,分数,竖着往下看是不同同学的分 - -有一个基础模板存在 `ext/base.sql`;他可以把我们刚刚新建的数据库搞成我们 -想要的样子: -```sh -# 你需要改这些: -# - YOUR_DB_NAME -> 改成你刚刚创建的数据库名字 -# - YOUR_DB_USER -> 改成你刚刚创建的数据库用户名 - -# 把bancho.py的数据库框架导入到我们刚刚创建的新数据库 - -mysql -u YOUR_DB_USER -p YOUR_DB_NAME < migrations/base.sql -``` - -## 第五步:搞一个SSL证书! (这样我们就有https啦!) -```sh -# 你需要改这些: -# - YOUR_EMAIL_ADDRESS -> 改成你的邮箱地址 -# - YOUR_DOMAIN -> 改成你自己的域名 - -# 下面的指令会给我们搞一个SSL证书 -sudo certbot certonly \ - --manual \ - --preferred-challenges=dns \ - --email YOUR_EMAIL_ADDRESS \ - --server https://acme-v02.api.letsencrypt.org/directory \ - --agree-tos \ - -d *.YOUR_DOMAIN -``` - -## 第六步:配置反向代理 (我们使用Nginx) -bancho.py 需要使用反向代理来使用https,在这里我们使用Nginx这个开源的 -web服务器。当然,你也可以尝试一下其他的例如 caddy 以及 h2o. - -```sh -# 把nginx配置文件样例复制到 /etc/nginx/sites-available, -# 然后建立符号连接到 /etc/nginx/sites-enabled -sudo cp ext/nginx.conf.example /etc/nginx/sites-available/bancho.conf -sudo ln -s /etc/nginx/sites-available/bancho.conf /etc/nginx/sites-enabled/bancho.conf - -# 现在你可以去编辑配置文件咯 -# 你需要更改的地方已经被标识在文件里了 -sudo nano /etc/nginx/sites-available/bancho.conf - -# 重载配置文件 -sudo nginx -s reload -``` - -## 第七步:配置 bancho.py -你可以在 `.env` 文件里解决所有与bancho.py程序相关的配置 -我们提供了一个样例文件 `.env.example`,你可以参考它来设置你自己的 -```sh -# 把我们提供的样例文件制造一个副本,直接编辑他就可以了。你所有需要改 -# 的地方都在里面有标注/空缺。不用担心,就算你失败了,你也可以再来一次 -cp .env.example .env - -# 你需要 至少 修改 DB_DSN (*就是数据库连接的URL), -# 如果你想要查看譜面信息,那么请把osu_api_key给填上(在osu官网可以申请) - -# 打开配置文件来编辑: -nano .env -``` - -- *这里设定数据库URL是一个重点!参考下面的来设定!(直接把中文替换即可) -DB_DSN=mysql://数据库用户名:数据库密码@localhost:3306/数据库的名字 - -## 最后一步啦!运行bancho.py吧! - -如果前面的设定都没问题,那么你输入下面的指令就可以运行私服啦! - -```sh -# 运行私服啦 -make run -``` - -如果你看到了下面的提示,那么恭喜!你成功了 - -![tada](https://cdn.discordapp.com/attachments/616400094408736779/993705619498467369/ld-iZXysVXqwhM8.png) - -# 文件目录 - . - ├── app # 服务 - 处理逻辑, 类 和 对象 - | ├── api # 处理外部请求的部分 - | | ├── domains # 外部访问可到达的endpoints (终点,指向web服务的api,此处为url,下译为"终点") - | | | ├── cho.py # 处理在这个终点的请求: https://c.cmyui.xyz - | | | ├── map.py # 处理在这个终点的请求: https://b.cmyui.xyz - | | | └── osu.py # 处理在这个终点的请求: https://osu.cmyui.xyz - | | | - | | ├── v1 - | | | └── api.py # 处理在这个终点的请求: https://api.cmyui.xyz/v1 - | | | - | | ├── v2 - | | | ├── clans.py # 处理在这个终点的请求: https://api.cmyui.xyz/v2/clans - | | | ├── maps.py # 处理在这个终点的请求: https://api.cmyui.xyz/v2/maps - | | | ├── players.py # 处理在这个终点的请求: https://api.cmyui.xyz/v2/players - | | | └── scores.py # 处理在这个终点的请求: https://api.cmyui.xyz/v2/scores - | | | - | | ├── init_api.py # 初始化api服务 - | | └── middlewares.py # 围绕终点的逻辑部分(中间件) - | | - | ├── constants # 服务器端静态类/对象的数据和逻辑实现 - | | ├── clientflags.py # osu!客户端使用的反作弊flags - | | ├── gamemodes.py # osu!游戏模式, 支持 relax/autopilot - | | ├── mods.py # osu!游戏mods - | | ├── privileges.py # 用户特权(玩家,服主,支持者,开发者等等) - | | └── regexes.py # 整个代码库中的正则表达式 - | | - | ├── objects # 服务器端动态类/对象的数据和逻辑实现 - | | ├── achievement.py # 有关个人成就achievement - | | ├── beatmap.py # 有关个人的谱面 - | | ├── channel.py # 有关个人的聊天频道(chat) - | | ├── clan.py # 有关个人的地区(clans) - | | ├── collection.py # 动态类的集合 (存储在内存中) - | | ├── match.py # 多人比赛 - | | ├── menu.py # (-正在制作中-) 聊天频道中的交互菜单 - | | ├── models.py # api请求主体(bodies)的结构 - | | ├── player.py # 关于个人的players - | | └── score.py # 有关个人的score - | | - | ├── state # 和服务器实时状态有关的对象 - | | ├── cache.py # 为最优化而保存的数据 - | | ├── services.py # 外部依赖实例 (e.g. 数据库) - | | └── sessions.py # 活动的sessions (players, channels, matches, etc.) - | | - | ├── bg_loops.py # 服务运时运行的循环 - | ├── commands.py # 在osu!的chat里可用的指令 - | ├── packets.py # 用于序列化/反序列化的模块 - | └── settings.py # 管理用户设置 - | - ├── ext # 运行服务时使用的外部依赖(内容: nginx的配置文件) - ├── migrations # 迁移数据库 - updates to schema - ├── tools # 在bancho.py开发过程中曾经制作出来的工具 - └── main.py # 运行服务的入口 +api文档(英语): +前端(guweb): diff --git a/README_DE.MD b/README_DE.MD index 99f6015f..3c24f9c7 100644 --- a/README_DE.MD +++ b/README_DE.MD @@ -1,5 +1,6 @@ # bancho.py -[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/) + +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Code Stil: schwarz](https://img.shields.io/badge/Code%20Stil-Schwarz-black)](https://github.com/ambv/black) [![pre-commit.ci Status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) @@ -11,243 +12,3 @@ private(n) osu-Server-Instanz(en) zu hosten Das Projekt wird hauptsächlich vom [Akatsuki](https://akatsuki.pw/)-Team entwickelt, und unser Ziel ist es, die am einfachsten zu wartende, zuverlässigste und funktionsreichste osu!-Server-Implementierung auf dem Markt zu schaffen. - -# Einrichtung -Kenntnisse in Linux, Python und Datenbanken sind sicherlich hilfreich, aber -keinesfalls erforderlich. - -(Viele Leute haben diesen Server ohne vorherige Programmiererfahrung -installiert!) - -Wenn du an irgendeinem Punkt des Prozesses nicht weiterkommst - -wir haben oben einen öffentlichen Discord :) - -Diese Anleitung ist auf Ubuntu ausgerichtet - andere Distributionen können leicht -abweichende Installationsprozesse haben. - -## Laden Sie die osu! Server-Codebasis auf Ihren Rechner herunter -```sh -# Klone das Repository von bancho.py -git clone https://github.com/osuAkatsuki/bancho.py - -# gebe das neue Verzeichnis von bancho.py ein -cd bancho.py -``` - -## Installation der Anforderungen von bancho.py -bancho.py ist eine ~15.000 Zeilen lange Codebasis, die auf den Schultern von Riesen gebaut wurde. - -Wir versuchen, unsere Abhängigkeiten zu minimieren, sind aber immer noch auf Abhängigkeiten wie -- python (Programmiersprache) -- mysql (relationale Datenbank) -- redis (speicherinterne Datenbank) -- nginx (http(s)-Reverse-Proxy) -- certbot (ssl-Zertifikatstool) -- build-essential (Build-Werkzeuge für C/C++) - -als auch einige andere angewiesen. -```sh -# python3.9 ist oft nicht nativ verfügbar, -# aber wir können uns auf Deadsnakes verlassen, um es bereitzustellen. -# https://github.com/deadsnakes/python3.9 -sudo add-apt-repository -y ppa:deadsnakes - -# Installiere die erforderlichen Programme für die Ausführung von bancho.py -sudo apt install -y python3.9-dev python3.9-distutils \ - build-essential \ - mysql-server redis-server \ - nginx certbot - - -# Installieren Sie den Paketmanager von Python, pip -# Er wird verwendet, um python-spezifische Abhängigkeiten zu installieren -wget https://bootstrap.pypa.io/get-pip.py -python3.9 get-pip.py && rm get-pip.py - -# Stellen Sie sicher, dass pip und setuptools auf dem neuesten Stand sind. -python3.9 -m pip install -U pip setuptools pipenv - -# Installiere die python-spezifischen Abhängigkeiten von bancho.py -# (wenn Sie planen, als Entwickler zu arbeiten, können Sie `make install-dev` verwenden) -make install -``` - -## Erstellen einer Datenbank für bancho.py -Sie müssen eine Datenbank für bancho.py erstellen, um persistente Daten zu speichern. - -Der Server benutzt diese Datenbank, um Metadaten und Logs zu speichern, wie z.B. Benutzerkonten -und Statistiken, Beatmaps und Beatmapsets, Chat-Kanäle, Turnier-Mappools und mehr. - -```sh -# Starten Sie Ihren Datenbankserver -sudo service mysql start - -# Melden Sie sich in der Shell von mysql mit root an - dem Standard-Admin-Konto. - -# Beachten Sie, dass diese Shell ziemlich gefährlich sein kann - sie erlaubt Benutzern -# beliebige SQL-Befehle auszuführen, um mit der Datenbank zu interagieren. - -# Sie ist aber auch sehr nützlich, mächtig und schnell, wenn sie richtig benutzt wird. -mysql -u root -p -``` - -Von dieser mysql-Shell aus wollen wir eine Datenbank anlegen, ein Benutzerkonto erstellen, -und dem Benutzer volle Rechte für die Datenbank geben. - -Später werden wir dann bancho.py so konfigurieren, dass es diese Datenbank ebenfalls verwendet. -```sql -# Sie müssen ändern: -# - IHR_DB_NAME -# - IHR_DB_NUTZER -# - IHR_DB_PASSWORT - -# Erstelle eine Datenbank, die bancho.py verwenden kann. -CREATE DATABASE IHR_DB_NAME; - -# Lege einen Benutzer an, der die bancho.py-Datenbank benutzt. -CREATE USER 'IHR_DB_NUTZER'@'localhost' IDENTIFIED BY 'IHR_DB_PASSWORT'; - -# Gewähre dem Benutzer vollen Zugriff auf alle Tabellen in der bancho.py-Datenbank. -GRANT ALL PRIVILEGES ON IHR_DB_NAME.* TO 'IHR_DB_NUTZER'@'localhost'; - -# Stellen Sie sicher, dass die Änderungen der Privilegien sofort übernommen werden. -FLUSH PRIVILEGES; - -# Verlasse die mysql-Shell, um zurück zur bash zu gelangen. -quit -``` - -## Einrichten der Datenbankstruktur für bancho.py -Haben wir nun eine leere Datenbank erstellt - Datenbanken sind voll von 2-dimensionalen -Tabellen mit Daten. - -bancho.py hat viele Tabellen, die es benutzt, um Informationen zu organisieren, zum Beispiel gibt es -gibt es Tabellen wie `users` und `scores` zum Speichern der jeweiligen Informationen. - -Die Spalten (vertikal) stellen die Arten von Daten dar, die für einen `user` oder eine `score` gespeichert werden. -Zum Beispiel die Anzahl der 300er in einer Punktzahl oder die Berechtigungen eines Benutzers. - -Die Zeilen (horizontal) stellen die einzelnen Elemente oder Ereignisse in einer Tabelle dar. -Zum Beispiel eine einzelne Punktzahl in der Tabelle "Punkte". - -Dieser Grundzustand der Datenbank wird in `ext/base.sql` gespeichert; es ist ein Bündel von -sql-Befehlen, die nacheinander ausgeführt werden können, um den gewünschten Grundzustand zu erzeugen. -```sh -# Sie müssen ändern: -# - IHR_DB_NAME -# - IHR_DB_NUTZER - -# Importiere die mysql-Struktur von bancho.py in unsere neue db. -# Dies führt den Inhalt der Datei als SQL-Befehle aus. -mysql -u IHR_DB_NUTZER -p IHR_DB_NAME < migrations/base.sql -``` - -## Erstellung eines SSL-Zertifikats (um https-Verkehr zu ermöglichen) -```sh -# Sie müssen ändern: -# - IHRE_EMAIL_ADRESSE -# - IHRE_DOMAIN - -# Erstellen eines SSL-Zertifikats für Ihre Domain: -sudo certbot certonly \ - --manual \ - --preferred-challenges=dns \ - --email IHRE_EMAIL_ADRESSE \ - --server https://acme-v02.api.letsencrypt.org/directory \ - --agree-tos \ - -d *.IHRE_DOMAIN -``` - -## Konfigurieren eines Reverse-Proxys (wir verwenden nginx) -bancho.py benötigt einen Reverse-Proxy für die tls (https)-Unterstützung und für die Benutzerfreundlichkeit der Konfiguration. -nginx ist ein effizienter Open-Source-Webserver, den wir für diese Anleitung verwenden. -Sie können sich auch andere effiziente Open-Source-Webserver ansehen, wie z.B. caddy und h2o. - -```sh -# Kopieren Sie die Beispielkonfiguration von nginx nach /etc/nginx/sites-available, -# und erstellen Sie einen symbolischen Link zu /etc/nginx/sites-enabled. -sudo cp ext/nginx.conf.example /etc/nginx/sites-available/bancho.conf -sudo ln -s /etc/nginx/sites-available/bancho.conf /etc/nginx/sites-enabled/bancho.conf - -# Jetzt können Sie die Konfigurationsdatei bearbeiten. -# Die Stellen, die Sie ändern müssen, sind markiert. -sudo nano /etc/nginx/sites-available/bancho.conf - -# Laden Sie die Konfiguration von der Festplatte neu. -sudo nginx -s reload -``` - -## Konfiguration von bancho.py -Die gesamte Konfiguration des osu! Servers (bancho.py) selbst kann in der Datei -`.env`-Datei vorgenommen werden. Wir stellen eine Beispiel-Datei `.env.example` zur Verfügung, die Sie als Basis verwenden können. -```sh -# Erstelle eine Konfigurationsdatei aus dem mitgelieferten Beispiel. -cp .env.example .env - -# Sie sollten *mindestens* DB_DSN (die Datenbankverbindungsurl) konfigurieren, -# sowie den OSU_API_KEY setzen, wenn Sie Informationen aus der osu! v1 api benötigen. -# (z.B. Beatmaps). - -# Öffnen Sie die Konfigurationsdatei zum Bearbeiten. -nano .env -``` - -## Glückwunsch! Sie haben soeben einen privaten osu!-Server eingerichtet! - -Wenn alles gut gegangen ist, sollten Sie in der Lage sein, Ihren Server zu starten - -```sh -# Starte den Server. -make run -``` - -und Sie sollten sowas in der Art sehen: - -![tada](https://cdn.discordapp.com/attachments/616400094408736779/993705619498467369/ld-iZXysVXqwhM8.png) - -# Verzeichnisstruktur - . - ├── app # der Server - Logik, Klassen und Objekte - | ├── api # Code für die Bearbeitung externer Anfragen - | | ├── domains # Endpunkte, die von außen erreicht werden können - | | | ├── api.py # endpoints available @ https://api.ppy.sh - | | | ├── cho.py # endpoints available @ https://c.ppy.sh - | | | ├── map.py # endpoints available @ https://b.ppy.sh - | | | └── osu.py # endpoints available @ https://osu.ppy.sh - | | | - | | ├── init_api.py # Logik zum Zusammenstellen des Servers - | | └── middlewares.py # Logik, die sich um die Endpunkte wickelt - | | - | ├── constants # logic & data for constant server-side classes & objects - | | ├── clientflags.py # anticheat flags used by the osu! client - | | ├── gamemodes.py # osu! gamemodes, mit relax/autopilot Unterstützung - | | ├── mods.py # osu! gameplay modifiers - | | ├── privileges.py # Privilegien für Spieler, global & in Clans - | | └── regexes.py # Regexe, die in der gesamten Codebasis verwendet werden - | | - | ├── objects # Logik & Daten für dynamische serverseitige Klassen & Objekte - | | ├── achievement.py # Darstellung der einzelnen Leistungen - | | ├── beatmap.py # Darstellung einzelner Map(set)s - | | ├── channel.py # Darstellung individueller Chat-Kanäle - | | ├── clan.py # Darstellung der einzelnen Clans - | | ├── collection.py # Sammlungen von dynamischen Objekten (zur Speicherung im Speicher) - | | ├── match.py # individuelle Multiplayer-Matches - | | ├── menu.py # (WIP) Konzept für interaktive Menüs in Chat-Kanälen - | | ├── models.py # Strukturen von Api-Anfragekörpern - | | ├── player.py # Darstellung der einzelnen Spieler - | | └── score.py # Darstellung einzelner Spielstände - | | - | ├── state # Objekte, die den Live-Server-Status darstellen - | | ├── cache.py # zu Optimierungszwecken gespeicherte Daten - | | ├── services.py # Instanzen von 3rd-Party-Diensten (z. B. Datenbanken) - | | └── sessions.py # aktive Sitzungen (Spieler, Kanäle, Matches usw.) - | | - | ├── bg_loops.py # Schleifen, die laufen, während der Server läuft - | ├── commands.py # Befehle, die im Chat von osu! verfügbar sind - | ├── packets.py # ein Modul zur (De-)Serialisierung von osu!-Paketen - | └── settings.py # verwaltet Konfigurationswerte des Benutzers - | - ├── ext # externe Entitäten, die beim Betrieb des Servers verwendet werden - ├── migrations # Datenbankmigrationen - Aktualisierungen des Schemas - ├── tools # verschiedene Tools aus der Geschichte von bancho.py - └── main.py # ein Einstiegspunkt (Skript) zur Ausführung des Servers diff --git a/app/utils.py b/app/utils.py index 86f452a7..bfa2818e 100644 --- a/app/utils.py +++ b/app/utils.py @@ -349,10 +349,10 @@ def ensure_supported_platform() -> int: ) return 1 - if sys.version_info < (3, 9): + if sys.version_info < (3, 11): log( "bancho.py uses many modern python features, " - "and the minimum python version is 3.9.", + "and the minimum python version is 3.11.", Ansi.LRED, ) return 1 From b3e9f439c801739c38cce9846c2e736ddd49f661 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Thu, 21 Sep 2023 19:12:38 +0300 Subject: [PATCH 18/24] docs: document breaking change --- .github/docs/wiki/Breaking-changes.md | 4 ++++ .github/docs/wiki/locale/de-DE/Breaking-changes-de-DE.md | 4 ++++ .github/docs/wiki/locale/zh-CN/Breaking-changes-zh-CN.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.github/docs/wiki/Breaking-changes.md b/.github/docs/wiki/Breaking-changes.md index ea624dca..d759f468 100644 --- a/.github/docs/wiki/Breaking-changes.md +++ b/.github/docs/wiki/Breaking-changes.md @@ -1,5 +1,9 @@ # Breaking changes +## 2023-09-21 + +The minimum Python version has been changed to 3.11. + ## 2023-07-19 The behaviour of overall pp and accuracy has changed to encapsulate all of a user's scores rather than their top 100, in order to match Bancho's behaviour. This means a recalculation of user's stats is necessary. bancho.py has a recalculation tool which can be found in `tools/recalc.py` in order to facilitate this. diff --git a/.github/docs/wiki/locale/de-DE/Breaking-changes-de-DE.md b/.github/docs/wiki/locale/de-DE/Breaking-changes-de-DE.md index 9ad50a55..d500222d 100644 --- a/.github/docs/wiki/locale/de-DE/Breaking-changes-de-DE.md +++ b/.github/docs/wiki/locale/de-DE/Breaking-changes-de-DE.md @@ -1,5 +1,9 @@ # Breaking changes +## 2023-09-21 + +Die minimale Python-Version wurde auf 3.11 geändert. + ## 2023-07-19 Die Funktionsweise von Gesamt-PP und Genauigkeit hat sich geändert, um alle Scores eines Benutzers zu umfassen, anstatt die Top 100, um das Verhalten von Bancho zu entsprechen. Dies bedeutet, dass eine Neuberechnung der Statistiken eines Benutzers erforderlich ist. Bancho.py verfügt über ein Neuberechnungstool, das in `tools/recalc.py` zu finden ist, um dies zu erleichtern. diff --git a/.github/docs/wiki/locale/zh-CN/Breaking-changes-zh-CN.md b/.github/docs/wiki/locale/zh-CN/Breaking-changes-zh-CN.md index fa4013f8..573efcc4 100644 --- a/.github/docs/wiki/locale/zh-CN/Breaking-changes-zh-CN.md +++ b/.github/docs/wiki/locale/zh-CN/Breaking-changes-zh-CN.md @@ -1,5 +1,9 @@ # Breaking changes +## 2023-09-21 + +The minimum Python version has been changed to 3.11. + ## 2023-07-19 The behaviour of overall pp and accuracy has changed to encapsulate all of a user's scores rather than their top 100, in order to match Bancho's behaviour. This means a recalculation of user's stats is necessary. bancho.py has a recalculation tool which can be found in tools/recalc.py in order to facilitate this. From 1efb8c79f4a7679516c269bd0ea15d00fd23c2f2 Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Thu, 21 Sep 2023 15:05:45 -0400 Subject: [PATCH 19/24] fix typo --- .github/docs/wiki/_Footer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/docs/wiki/_Footer.md b/.github/docs/wiki/_Footer.md index 45836f36..93ce3dae 100644 --- a/.github/docs/wiki/_Footer.md +++ b/.github/docs/wiki/_Footer.md @@ -1,4 +1,4 @@ -[![Python 11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/osuAkatsuki/bancho.py/master.svg)](https://results.pre-commit.ci/latest/github/osuAkatsuki/bancho.py/master) [![Discord](https://discordapp.com/api/guilds/748687781605408908/widget.png?style=shield)](https://discord.gg/ShEQgUx) From 684fd6cab5d6ac60728bae2a1cc4689260e970db Mon Sep 17 00:00:00 2001 From: Alonso <33896771+7mochi@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:55:18 -0500 Subject: [PATCH 20/24] impl: implement unset pattern (#499) * impl: implement unset pattern * fix: apply suggested changes * fix: remove pop method * fix: apply suggestions Co-authored-by: Josh Smith * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: fix return type * fix --------- Co-authored-by: Josh Smith Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- app/_typing.py | 20 +++ app/objects/collections.py | 2 +- app/repositories/achievements.py | 86 +++++++----- app/repositories/channels.py | 89 +++++++----- app/repositories/clans.py | 82 ++++++----- app/repositories/maps.py | 230 +++++++++++++++++++------------ app/repositories/players.py | 179 +++++++++++++++--------- app/repositories/scores.py | 103 ++++++++++---- app/repositories/stats.py | 170 ++++++++++++++--------- 9 files changed, 622 insertions(+), 339 deletions(-) diff --git a/app/_typing.py b/app/_typing.py index 8db2792e..aff56186 100644 --- a/app/_typing.py +++ b/app/_typing.py @@ -2,6 +2,26 @@ from ipaddress import IPv4Address from ipaddress import IPv6Address +from typing import Any +from typing import TypeVar from typing import Union IPAddress = Union[IPv4Address, IPv6Address] +T = TypeVar("T") + + +class _UnsetSentinel: + def __repr__(self) -> str: + return "Unset" + + def __copy__(self: T) -> T: + return self + + def __reduce__(self) -> str: + return "Unset" + + def __deepcopy__(self: T, _: Any) -> T: + return self + + +UNSET = _UnsetSentinel() diff --git a/app/objects/collections.py b/app/objects/collections.py index 543585a4..6b602ecb 100644 --- a/app/objects/collections.py +++ b/app/objects/collections.py @@ -559,7 +559,7 @@ async def initialize_ram_caches(db_conn: databases.core.Connection) -> None: desc=row["desc"], # NOTE: achievement conditions are stored as stringified python # expressions in the database to allow for extensive customizability. - cond=eval(f'lambda score, mode_vn: {row.pop("cond")}'), + cond=eval(f'lambda score, mode_vn: {row["cond"]}'), ) app.state.sessions.achievements.append(achievement) diff --git a/app/repositories/achievements.py b/app/repositories/achievements.py index 1c8b7e8e..effd71dd 100644 --- a/app/repositories/achievements.py +++ b/app/repositories/achievements.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +-------+--------------+------+-----+---------+----------------+ # | Field | Type | Null | Key | Default | Extra | @@ -23,12 +27,27 @@ ) +class Achievement(TypedDict): + id: int + file: str + name: str + desc: str + cond: str + + +class AchievementUpdateFields(TypedDict, total=False): + file: str + name: str + desc: str + cond: str + + async def create( file: str, name: str, desc: str, cond: str, -) -> dict[str, Any]: +) -> Achievement: """Create a new achievement.""" query = """\ INSERT INTO achievements (file, name, desc, cond) @@ -50,16 +69,16 @@ async def create( params = { "id": rec_id, } + achievement = await app.state.services.database.fetch_one(query, params) - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + assert achievement is not None + return cast(Achievement, achievement) async def fetch_one( id: int | None = None, name: str | None = None, -) -> dict[str, Any] | None: +) -> Achievement | None: """Fetch a single achievement.""" if id is None and name is None: raise ValueError("Must provide at least one parameter.") @@ -74,9 +93,9 @@ async def fetch_one( "id": id, "name": name, } + achievement = await app.state.services.database.fetch_one(query, params) - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + return cast(Achievement, achievement) if achievement is not None else None async def fetch_count() -> int: @@ -95,7 +114,7 @@ async def fetch_count() -> int: async def fetch_many( page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Achievement]: """Fetch a list of achievements.""" query = f"""\ SELECT {READ_PARAMS} @@ -111,34 +130,35 @@ async def fetch_many( params["page_size"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + achievements = await app.state.services.database.fetch_all(query, params) + return cast(list[Achievement], achievements) if achievements is not None else None async def update( id: int, - file: str | None = None, - name: str | None = None, - desc: str | None = None, - cond: str | None = None, -) -> dict[str, Any] | None: + file: str | _UnsetSentinel = UNSET, + name: str | _UnsetSentinel = UNSET, + desc: str | _UnsetSentinel = UNSET, + cond: str | _UnsetSentinel = UNSET, +) -> Achievement | None: """Update an existing achievement.""" - query = """\ + update_fields = AchievementUpdateFields = {} + if not isinstance(file, _UnsetSentinel): + update_fields["file"] = file + if not isinstance(name, _UnsetSentinel): + update_fields["name"] = name + if not isinstance(desc, _UnsetSentinel): + update_fields["desc"] = desc + if not isinstance(cond, _UnsetSentinel): + update_fields["cond"] = cond + + query = f"""\ UPDATE achievements - SET file = COALESCE(:file, file), - name = COALESCE(:name, name), - desc = COALESCE(:desc, desc), - cond = COALESCE(:cond, cond) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE id = :id """ - params = { - "id": id, - "file": file, - "name": name, - "desc": desc, - "cond": cond, - } - await app.state.services.database.execute(query, params) + values = {"id": id} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -148,13 +168,13 @@ async def update( params = { "id": id, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + achievement = await app.state.services.database.fetch_one(query, params) + return cast(Achievement, achievement) if achievement is not None else None async def delete( id: int, -) -> dict[str, Any] | None: +) -> Achievement | None: """Delete an existing achievement.""" query = f"""\ SELECT {READ_PARAMS} @@ -175,5 +195,5 @@ async def delete( params = { "id": id, } - await app.state.services.database.execute(query, params) - return dict(rec) + achievement = await app.state.services.database.execute(query, params) + return cast(Achievement, achievement) if achievement is not None else None diff --git a/app/repositories/channels.py b/app/repositories/channels.py index 5eaa8e5d..a1b9033c 100644 --- a/app/repositories/channels.py +++ b/app/repositories/channels.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +------------+--------------+------+-----+---------+----------------+ # | Field | Type | Null | Key | Default | Extra | @@ -24,13 +28,30 @@ ) +class Channel(TypedDict): + id: int + name: str + topic: str + read_priv: int + write_priv: int + auto_join: bool + + +class ChannelUpdateFields(TypedDict, total=False): + name: str + topic: str + read_priv: int + write_priv: int + auto_join: bool + + async def create( name: str, topic: str, read_priv: int, write_priv: int, auto_join: bool, -) -> dict[str, Any]: +) -> Channel: """Create a new channel.""" query = """\ INSERT INTO channels (name, topic, read_priv, write_priv, auto_join) @@ -55,15 +76,16 @@ async def create( "id": rec_id, } - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + channel = await app.state.services.database.fetch_one(query, params) + + assert channel is not None + return cast(Channel, channel) async def fetch_one( id: int | None = None, name: str | None = None, -) -> dict[str, Any] | None: +) -> Channel | None: """Fetch a single channel.""" if id is None and name is None: raise ValueError("Must provide at least one parameter.") @@ -77,9 +99,9 @@ async def fetch_one( "id": id, "name": name, } + channel = await app.state.services.database.fetch_one(query, params) - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + return cast(Channel, channel) if channel is not None else None async def fetch_count( @@ -114,7 +136,7 @@ async def fetch_many( auto_join: bool | None = None, page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Channel]: """Fetch multiple channels from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -137,34 +159,35 @@ async def fetch_many( params["limit"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + channels = await app.state.services.database.fetch_all(query, params) + return cast(list[Channel], channels) if channels is not None else None async def update( name: str, - topic: str | None = None, - read_priv: int | None = None, - write_priv: int | None = None, - auto_join: bool | None = None, -) -> dict[str, Any] | None: + topic: str | _UnsetSentinel = UNSET, + read_priv: int | _UnsetSentinel = UNSET, + write_priv: int | _UnsetSentinel = UNSET, + auto_join: bool | _UnsetSentinel = UNSET, +) -> Channel | None: """Update a channel in the database.""" - query = """\ + update_fields = ChannelUpdateFields = {} + if not isinstance(topic, _UnsetSentinel): + update_fields["topic"] = topic + if not isinstance(read_priv, _UnsetSentinel): + update_fields["read_priv"] = read_priv + if not isinstance(write_priv, _UnsetSentinel): + update_fields["write_priv"] = write_priv + if not isinstance(auto_join, _UnsetSentinel): + update_fields["auto_join"] = auto_join + + query = f"""\ UPDATE channels - SET topic = COALESCE(:topic, topic), - read_priv = COALESCE(:read_priv, read_priv), - write_priv = COALESCE(:write_priv, write_priv), - auto_join = COALESCE(:auto_join, auto_join) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE name = :name """ - params = { - "name": name, - "topic": topic, - "read_priv": read_priv, - "write_priv": write_priv, - "auto_join": auto_join, - } - await app.state.services.database.execute(query, params) + values = {"name": name} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -174,13 +197,13 @@ async def update( params = { "name": name, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + channel = await app.state.services.database.fetch_one(query, params) + return cast(Channel, channel) if channel is not None else None async def delete( name: str, -) -> dict[str, Any] | None: +) -> Channel | None: """Delete a channel from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -201,5 +224,5 @@ async def delete( params = { "name": name, } - await app.state.services.database.execute(query, params) - return dict(rec) + channel = await app.state.services.database.execute(query, params) + return cast(Channel, channel) if channel is not None else None diff --git a/app/repositories/clans.py b/app/repositories/clans.py index 0e627df1..b64f47f4 100644 --- a/app/repositories/clans.py +++ b/app/repositories/clans.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +------------+-------------+------+-----+---------+----------------+ # | Field | Type | Null | Key | Default | Extra | @@ -23,11 +27,25 @@ ) +class Clan(TypedDict): + id: int + name: str + tag: str + owner: int + created_at: str + + +class ClanUpdateFields(TypedDict, total=False): + name: str + tag: str + owner: int + + async def create( name: str, tag: str, owner: int, -) -> dict[str, Any]: +) -> Clan: """Create a new clan in the database.""" query = f"""\ INSERT INTO clans (name, tag, owner, created_at) @@ -48,9 +66,10 @@ async def create( params = { "id": rec_id, } - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + clan = await app.state.services.database.fetch_one(query, params) + + assert clan is not None + return cast(Clan, clan) async def fetch_one( @@ -58,7 +77,7 @@ async def fetch_one( name: str | None = None, tag: str | None = None, owner: int | None = None, -) -> dict[str, Any] | None: +) -> Clan | None: """Fetch a single clan from the database.""" if id is None and name is None and tag is None and owner is None: raise ValueError("Must provide at least one parameter.") @@ -72,8 +91,9 @@ async def fetch_one( AND owner = COALESCE(:owner, owner) """ params = {"id": id, "name": name, "tag": tag, "owner": owner} - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + clan = await app.state.services.database.fetch_one(query, params) + + return cast(Clan, clan) if clan is not None else None async def fetch_count() -> int: @@ -90,7 +110,7 @@ async def fetch_count() -> int: async def fetch_many( page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Clan]: """Fetch many clans from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -106,32 +126,32 @@ async def fetch_many( params["limit"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + clans = await app.state.services.database.fetch_all(query, params) + return cast(list[Clan], clans) if clans is not None else None async def update( id: int, - name: str | None = None, - tag: str | None = None, - owner: int | None = None, -) -> dict[str, Any] | None: + name: str | _UnsetSentinel = UNSET, + tag: str | _UnsetSentinel = UNSET, + owner: int | _UnsetSentinel = UNSET, +) -> Clan | None: """Update a clan in the database.""" - query = """\ + update_fields: ClanUpdateFields = {} + if not isinstance(name, _UnsetSentinel): + update_fields["name"] = name + if not isinstance(tag, _UnsetSentinel): + update_fields["tag"] = tag + if not isinstance(owner, _UnsetSentinel): + update_fields["owner"] = owner + + query = f"""\ UPDATE clans - SET name = :name, - tag = :tag, - owner = :owner + SET {",".join(f"{k} = :{k}" for k in update_fields)} WHERE id = :id """ - params = { - "id": id, - "name": name, - "tag": tag, - "owner": owner, - } - - await app.state.services.database.execute(query, params) + values = {"id": id} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -141,11 +161,11 @@ async def update( params = { "id": id, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + clan = await app.state.services.database.fetch_one(query, params) + return cast(Clan, clan) if clan is not None else None -async def delete(id: int) -> dict[str, Any] | None: +async def delete(id: int) -> Clan | None: """Delete a clan from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -166,5 +186,5 @@ async def delete(id: int) -> dict[str, Any] | None: params = { "id": id, } - await app.state.services.database.execute(query, params) - return dict(rec) + clan = await app.state.services.database.execute(query, params) + return cast(Clan, clan) if clan is not None else None diff --git a/app/repositories/maps.py b/app/repositories/maps.py index 8fdc7fa1..bbcfe7ab 100644 --- a/app/repositories/maps.py +++ b/app/repositories/maps.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +--------------+------------------------+------+-----+---------+-------+ # | Field | Type | Null | Key | Default | Extra | @@ -43,6 +47,57 @@ ) +class Map(TypedDict): + id: int + server: str + set_id: int + status: int + md5: str + artist: str + title: str + version: str + creator: str + filename: str + last_update: str + total_length: int + max_combo: int + frozen: bool + plays: int + passes: int + mode: int + bpm: float + cs: float + ar: float + od: float + hp: float + diff: float + + +class MapUpdateFields(TypedDict, total=False): + server: str + set_id: int + status: int + md5: str + artist: str + title: str + version: str + creator: str + filename: str + last_update: str + total_length: int + max_combo: int + frozen: bool + plays: int + passes: int + mode: int + bpm: float + cs: float + ar: float + od: float + hp: float + diff: float + + async def create( id: int, server: str, @@ -114,16 +169,17 @@ async def create( params = { "id": rec_id, } - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + map = await app.state.services.database.fetch_one(query, params) + + assert map is not None + return cast(Map, map) async def fetch_one( id: int | None = None, md5: str | None = None, filename: str | None = None, -) -> dict[str, Any] | None: +) -> Map | None: """Fetch a beatmap entry from the database.""" if id is None and md5 is None and filename is None: raise ValueError("Must provide at least one parameter.") @@ -140,8 +196,9 @@ async def fetch_one( "md5": md5, "filename": filename, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + map = await app.state.services.database.fetch_one(query, params) + + return cast(Map, map) if map is not None else None async def fetch_count( @@ -194,7 +251,7 @@ async def fetch_many( frozen: bool | None = None, page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Map]: """Fetch a list of maps from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -227,88 +284,89 @@ async def fetch_many( params["limit"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + maps = await app.state.services.database.fetch_all(query, params) + return cast(list[Map], maps) if maps is not None else None async def update( id: int, - server: str | None = None, - set_id: int | None = None, - status: int | None = None, - md5: str | None = None, - artist: str | None = None, - title: str | None = None, - version: str | None = None, - creator: str | None = None, - filename: str | None = None, - last_update: str | None = None, - total_length: int | None = None, - max_combo: int | None = None, - frozen: bool | None = None, - plays: int | None = None, - passes: int | None = None, - mode: int | None = None, - bpm: float | None = None, - cs: float | None = None, - ar: float | None = None, - od: float | None = None, - hp: float | None = None, - diff: float | None = None, -) -> dict[str, Any] | None: + server: str | _UnsetSentinel = UNSET, + set_id: int | _UnsetSentinel = UNSET, + status: int | _UnsetSentinel = UNSET, + md5: str | _UnsetSentinel = UNSET, + artist: str | _UnsetSentinel = UNSET, + title: str | _UnsetSentinel = UNSET, + version: str | _UnsetSentinel = UNSET, + creator: str | _UnsetSentinel = UNSET, + filename: str | _UnsetSentinel = UNSET, + last_update: str | _UnsetSentinel = UNSET, + total_length: int | _UnsetSentinel = UNSET, + max_combo: int | _UnsetSentinel = UNSET, + frozen: bool | _UnsetSentinel = UNSET, + plays: int | _UnsetSentinel = UNSET, + passes: int | _UnsetSentinel = UNSET, + mode: int | _UnsetSentinel = UNSET, + bpm: float | _UnsetSentinel = UNSET, + cs: float | _UnsetSentinel = UNSET, + ar: float | _UnsetSentinel = UNSET, + od: float | _UnsetSentinel = UNSET, + hp: float | _UnsetSentinel = UNSET, + diff: float | _UnsetSentinel = UNSET, +) -> Map | None: """Update a beatmap entry in the database.""" - query = """\ + update_fields: MapUpdateFields = {} + if not isinstance(server, _UnsetSentinel): + update_fields["server"] = server + if not isinstance(set_id, _UnsetSentinel): + update_fields["set_id"] = set_id + if not isinstance(status, _UnsetSentinel): + update_fields["status"] = status + if not isinstance(md5, _UnsetSentinel): + update_fields["md5"] = md5 + if not isinstance(artist, _UnsetSentinel): + update_fields["artist"] = artist + if not isinstance(title, _UnsetSentinel): + update_fields["title"] = title + if not isinstance(version, _UnsetSentinel): + update_fields["version"] = version + if not isinstance(creator, _UnsetSentinel): + update_fields["creator"] = creator + if not isinstance(filename, _UnsetSentinel): + update_fields["filename"] = filename + if not isinstance(last_update, _UnsetSentinel): + update_fields["last_update"] = last_update + if not isinstance(total_length, _UnsetSentinel): + update_fields["total_length"] = total_length + if not isinstance(max_combo, _UnsetSentinel): + update_fields["max_combo"] = max_combo + if not isinstance(frozen, _UnsetSentinel): + update_fields["frozen"] = frozen + if not isinstance(plays, _UnsetSentinel): + update_fields["plays"] = plays + if not isinstance(passes, _UnsetSentinel): + update_fields["passes"] = passes + if not isinstance(mode, _UnsetSentinel): + update_fields["mode"] = mode + if not isinstance(bpm, _UnsetSentinel): + update_fields["bpm"] = bpm + if not isinstance(cs, _UnsetSentinel): + update_fields["cs"] = cs + if not isinstance(ar, _UnsetSentinel): + update_fields["ar"] = ar + if not isinstance(od, _UnsetSentinel): + update_fields["od"] = od + if not isinstance(hp, _UnsetSentinel): + update_fields["hp"] = hp + if not isinstance(diff, _UnsetSentinel): + update_fields["diff"] = diff + + query = f"""\ UPDATE maps - SET server = COALESCE(:server, server), - set_id = COALESCE(:set_id, set_id), - status = COALESCE(:status, status), - md5 = COALESCE(:md5, md5), - artist = COALESCE(:artist, artist), - title = COALESCE(:title, title), - version = COALESCE(:version, version), - creator = COALESCE(:creator, creator), - filename = COALESCE(:filename, filename), - last_update = COALESCE(:last_update, last_update), - total_length = COALESCE(:total_length, total_length), - max_combo = COALESCE(:max_combo, max_combo), - frozen = COALESCE(:frozen, frozen), - plays = COALESCE(:plays, plays), - passes = COALESCE(:passes, passes), - mode = COALESCE(:mode, mode), - bpm = COALESCE(:bpm, bpm), - cs = COALESCE(:cs, cs), - ar = COALESCE(:ar, ar), - od = COALESCE(:od, od), - hp = COALESCE(:hp, hp), - diff = COALESCE(:diff, diff) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE id = :id """ - params = { - "id": id, - "server": server, - "set_id": set_id, - "status": status, - "md5": md5, - "artist": artist, - "title": title, - "version": version, - "creator": creator, - "filename": filename, - "last_update": last_update, - "total_length": total_length, - "max_combo": max_combo, - "frozen": frozen, - "plays": plays, - "passes": passes, - "mode": mode, - "bpm": bpm, - "cs": cs, - "ar": ar, - "od": od, - "hp": hp, - "diff": diff, - } - await app.state.services.database.execute(query, params) + values = {"id": id} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -318,11 +376,11 @@ async def update( params = { "id": id, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + map = await app.state.services.database.fetch_one(query, params) + return cast(Map, map) if map is not None else None -async def delete(id: int) -> dict[str, Any] | None: +async def delete(id: int) -> Map | None: """Delete a beatmap entry from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -343,5 +401,5 @@ async def delete(id: int) -> dict[str, Any] | None: params = { "id": id, } - await app.state.services.database.execute(query, params) - return dict(rec) + map = await app.state.services.database.execute(query, params) + return cast(Map, map) if map is not None else None diff --git a/app/repositories/players.py b/app/repositories/players.py index 2639cbf7..6c4e6fda 100644 --- a/app/repositories/players.py +++ b/app/repositories/players.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET from app.utils import make_safe_name # +-------------------+---------------+------+-----+---------+----------------+ @@ -40,12 +44,51 @@ ) +class Player(TypedDict): + id: int + name: str + safe_name: str + priv: int + country: str + silence_end: int + donor_end: int + creation_time: int + latest_activity: int + clan_id: int + clan_priv: int + preferred_mode: int + play_style: int + custom_badge_name: str | None + custom_badge_icon: str | None + userpage_content: str | None + api_key: str | None + + +class PlayerUpdateFields(TypedDict, total=False): + name: str + email: str + priv: int + country: str + silence_end: int + donor_end: int + creation_time: int + latest_activity: int + clan_id: int + clan_priv: int + preferred_mode: int + play_style: int + custom_badge_name: str | None + custom_badge_icon: str | None + userpage_content: str | None + api_key: str | None + + async def create( name: str, email: str, pw_bcrypt: bytes, country: str, -) -> dict[str, Any]: +) -> Player: """Create a new player in the database.""" query = f"""\ INSERT INTO users (name, safe_name, email, pw_bcrypt, country, creation_time, latest_activity) @@ -68,9 +111,10 @@ async def create( params = { "id": rec_id, } - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + player = await app.state.services.database.fetch_one(query, params) + + assert player is not None + return cast(Player, player) async def fetch_one( @@ -78,7 +122,7 @@ async def fetch_one( name: str | None = None, email: str | None = None, fetch_all_fields: bool = False, # TODO: probably remove this if possible -) -> dict[str, Any] | None: +) -> Player | None: """Fetch a single player from the database.""" if id is None and name is None and email is None: raise ValueError("Must provide at least one parameter.") @@ -95,8 +139,8 @@ async def fetch_one( "safe_name": make_safe_name(name) if name is not None else None, "email": email, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + player = await app.state.services.database.fetch_one(query, params) + return cast(Player, player) if player is not None else None async def fetch_count( @@ -140,7 +184,7 @@ async def fetch_many( play_style: int | None = None, page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Player]: """Fetch multiple players from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -169,72 +213,71 @@ async def fetch_many( params["limit"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + players = await app.state.services.database.fetch_all(query, params) + return cast(list[Player], players) if players is not None else None async def update( id: int, - name: str | None = None, - email: str | None = None, - priv: int | None = None, - country: str | None = None, - silence_end: int | None = None, - donor_end: int | None = None, - creation_time: int | None = None, - latest_activity: int | None = None, - clan_id: int | None = None, - clan_priv: int | None = None, - preferred_mode: int | None = None, - play_style: int | None = None, - custom_badge_name: str | None = None, - custom_badge_icon: str | None = None, - userpage_content: str | None = None, - api_key: str | None = None, -) -> dict[str, Any] | None: + name: str | _UnsetSentinel = UNSET, + email: str | _UnsetSentinel = UNSET, + priv: int | _UnsetSentinel = UNSET, + country: str | _UnsetSentinel = UNSET, + silence_end: int | _UnsetSentinel = UNSET, + donor_end: int | _UnsetSentinel = UNSET, + creation_time: _UnsetSentinel | _UnsetSentinel = UNSET, + latest_activity: int | _UnsetSentinel = UNSET, + clan_id: int | _UnsetSentinel = UNSET, + clan_priv: int | _UnsetSentinel = UNSET, + preferred_mode: int | _UnsetSentinel = UNSET, + play_style: int | _UnsetSentinel = UNSET, + custom_badge_name: str | None | _UnsetSentinel = UNSET, + custom_badge_icon: str | None | _UnsetSentinel = UNSET, + userpage_content: str | None | _UnsetSentinel = UNSET, + api_key: str | None | _UnsetSentinel = UNSET, +) -> Player | None: """Update a player in the database.""" - query = """\ + update_fields = PlayerUpdateFields = {} + if not isinstance(name, _UnsetSentinel): + update_fields["name"] = name + if not isinstance(email, _UnsetSentinel): + update_fields["email"] = email + if not isinstance(priv, _UnsetSentinel): + update_fields["priv"] = priv + if not isinstance(country, _UnsetSentinel): + update_fields["country"] = country + if not isinstance(silence_end, _UnsetSentinel): + update_fields["silence_end"] = silence_end + if not isinstance(donor_end, _UnsetSentinel): + update_fields["donor_end"] = donor_end + if not isinstance(creation_time, _UnsetSentinel): + update_fields["creation_time"] = creation_time + if not isinstance(latest_activity, _UnsetSentinel): + update_fields["latest_activity"] = latest_activity + if not isinstance(clan_id, _UnsetSentinel): + update_fields["clan_id"] = clan_id + if not isinstance(clan_priv, _UnsetSentinel): + update_fields["clan_priv"] = clan_priv + if not isinstance(preferred_mode, _UnsetSentinel): + update_fields["preferred_mode"] = preferred_mode + if not isinstance(play_style, _UnsetSentinel): + update_fields["play_style"] = play_style + if not isinstance(custom_badge_name, _UnsetSentinel): + update_fields["custom_badge_name"] = custom_badge_name + if not isinstance(custom_badge_icon, _UnsetSentinel): + update_fields["custom_badge_icon"] = custom_badge_icon + if not isinstance(userpage_content, _UnsetSentinel): + update_fields["userpage_content"] = userpage_content + if not isinstance(api_key, _UnsetSentinel): + update_fields["api_key"] = api_key + + query = f"""\ UPDATE users - SET name = COALESCE(:name, name), - safe_name = COALESCE(:safe_name, safe_name), - email = COALESCE(:email, email), - priv = COALESCE(:priv, priv), - country = COALESCE(:country, country), - silence_end = COALESCE(:silence_end, silence_end), - donor_end = COALESCE(:donor_end, donor_end), - creation_time = COALESCE(:creation_time, creation_time), - latest_activity = COALESCE(:latest_activity, latest_activity), - clan_id = COALESCE(:clan_id, clan_id), - clan_priv = COALESCE(:clan_priv, clan_priv), - preferred_mode = COALESCE(:preferred_mode, preferred_mode), - play_style = COALESCE(:play_style, play_style), - custom_badge_name = COALESCE(:custom_badge_name, custom_badge_name), - custom_badge_icon = COALESCE(:custom_badge_icon, custom_badge_icon), - userpage_content = COALESCE(:userpage_content, userpage_content), - api_key = COALESCE(:api_key, api_key) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE id = :id """ - params = { - "id": id, - "name": name, - "safe_name": make_safe_name(name) if name is not None else None, - "email": email, - "priv": priv, - "country": country, - "silence_end": silence_end, - "donor_end": donor_end, - "creation_time": creation_time, - "latest_activity": latest_activity, - "clan_id": clan_id, - "clan_priv": clan_priv, - "preferred_mode": preferred_mode, - "play_style": play_style, - "custom_badge_name": custom_badge_name, - "custom_badge_icon": custom_badge_icon, - "userpage_content": userpage_content, - "api_key": api_key, - } - await app.state.services.database.execute(query, params) + values = {"id": id} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -244,8 +287,8 @@ async def update( params = { "id": id, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + player = await app.state.services.database.fetch_one(query, params) + return cast(Player, player) if player is not None else None # TODO: delete? diff --git a/app/repositories/scores.py b/app/repositories/scores.py index e40d5c66..e901bb54 100644 --- a/app/repositories/scores.py +++ b/app/repositories/scores.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +-----------------+-----------------+------+-----+---------+----------------+ # | Field | Type | Null | Key | Default | Extra | @@ -41,6 +45,55 @@ ) +class Score(TypedDict): + id: int + map_md5: str + score: int + pp: float + acc: float + max_combo: int + mods: int + n300: int + n100: int + n50: int + nmiss: int + ngeki: int + nkatu: int + grade: str + status: int + mode: int + play_time: str + time_elapsed: int + client_flags: int + userid: int + perfect: int + online_checksum: str + + +class ScoreUpdateFields(TypedDict, total=False): + map_md5: str + score: int + pp: float + acc: float + max_combo: int + mods: int + n300: int + n100: int + n50: int + nmiss: int + ngeki: int + nkatu: int + grade: str + status: int + mode: int + play_time: str + time_elapsed: int + client_flags: int + userid: int + perfect: int + online_checksum: str + + async def create( map_md5: str, score: int, @@ -63,7 +116,7 @@ async def create( user_id: int, perfect: int, online_checksum: str, -) -> dict[str, Any]: +) -> Score: query = """\ INSERT INTO scores (map_md5, score, pp, acc, max_combo, mods, n300, n100, n50, nmiss, ngeki, nkatu, grade, status, @@ -105,20 +158,22 @@ async def create( WHERE id = :id """ params = {"id": rec_id} - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + score = await app.state.services.database.fetch_one(query, params) + + assert score is not None + return cast(Score, score) -async def fetch_one(id: int) -> dict[str, Any] | None: +async def fetch_one(id: int) -> Score | None: query = f"""\ SELECT {READ_PARAMS} FROM scores WHERE id = :id """ params = {"id": id} - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + score = await app.state.services.database.fetch_one(query, params) + + return cast(Score, score) if score is not None else None async def fetch_count( @@ -157,7 +212,7 @@ async def fetch_many( user_id: int | None = None, page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Score]: query = f"""\ SELECT {READ_PARAMS} FROM scores @@ -182,26 +237,28 @@ async def fetch_many( params["page_size"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + scores = await app.state.services.database.fetch_all(query, params) + return cast(list[Score], scores) if scores is not None else None async def update( id: int, - pp: float | None = None, - status: int | None = None, -) -> dict[str, Any] | None: - query = """\ + pp: float | _UnsetSentinel = UNSET, + status: int | _UnsetSentinel = UNSET, +) -> Score | None: + """Update an existing score.""" + update_fields = ScoreUpdateFields = {} + if not isinstance(pp, _UnsetSentinel): + update_fields["pp"] = pp + if not isinstance(status, _UnsetSentinel): + update_fields["status"] = status + + query = f"""\ UPDATE scores - SET pp = COALESCE(:pp, pp), - status = COALESCE(:status, status) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE id = :id """ - params = { - "id": id, - "pp": pp, - "status": status, - } + values = {"id": id} | update_fields await app.state.services.database.execute(query, params) query = f"""\ @@ -210,8 +267,8 @@ async def update( WHERE id = :id """ params = {"id": id} - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + score = await app.state.services.database.fetch_one(query, params) + return cast(Score, score) if score is not None else None # TODO: delete diff --git a/app/repositories/stats.py b/app/repositories/stats.py index d234bd62..0bab3c40 100644 --- a/app/repositories/stats.py +++ b/app/repositories/stats.py @@ -2,9 +2,13 @@ import textwrap from typing import Any +from typing import cast from typing import Optional +from typing import TypedDict import app.state.services +from app._typing import _UnsetSentinel +from app._typing import UNSET # +--------------+-----------------+------+-----+---------+----------------+ # | Field | Type | Null | Key | Default | Extra | @@ -35,11 +39,47 @@ ) +class Stat(TypedDict): + id: int + mode: int + tscore: int + rscore: int + pp: int + plays: int + playtime: int + acc: float + max_combo: int + total_hits: int + replay_views: int + xh_count: int + x_count: int + sh_count: int + s_count: int + a_count: int + + +class StatUpdateFields(TypedDict, total=False): + tscore: int + rscore: int + pp: int + plays: int + playtime: int + acc: float + max_combo: int + total_hits: int + replay_views: int + xh_count: int + x_count: int + sh_count: int + s_count: int + a_count: int + + async def create( player_id: int, mode: int, # TODO: should we allow init with values? -) -> dict[str, Any]: +) -> Stat: """Create a new player stats entry in the database.""" query = f"""\ INSERT INTO stats (id, mode) @@ -59,12 +99,13 @@ async def create( params = { "id": rec_id, } - rec = await app.state.services.database.fetch_one(query, params) - assert rec is not None - return dict(rec) + stat = await app.state.services.database.fetch_one(query, params) + + assert stat is not None + return cast(Stat, stat) -async def create_all_modes(player_id: int) -> list[dict[str, Any]]: +async def create_all_modes(player_id: int) -> list[Stat]: """Create new player stats entries for each game mode in the database.""" query = f"""\ INSERT INTO stats (id, mode) @@ -93,11 +134,11 @@ async def create_all_modes(player_id: int) -> list[dict[str, Any]]: params = { "id": player_id, } - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + stats = await app.state.services.database.fetch_all(query, params) + return cast(list[Stat], stats) if stats is not None else None -async def fetch_one(player_id: int, mode: int) -> dict[str, Any] | None: +async def fetch_one(player_id: int, mode: int) -> Stat | None: """Fetch a player stats entry from the database.""" query = f"""\ SELECT {READ_PARAMS} @@ -109,8 +150,9 @@ async def fetch_one(player_id: int, mode: int) -> dict[str, Any] | None: "id": player_id, "mode": mode, } - rec = await app.state.services.database.fetch_one(query, params) - return dict(rec) if rec is not None else None + stat = await app.state.services.database.fetch_one(query, params) + + return cast(Stat, stat) if stat is not None else None async def fetch_count( @@ -137,7 +179,7 @@ async def fetch_many( mode: int | None = None, page: int | None = None, page_size: int | None = None, -) -> list[dict[str, Any]]: +) -> list[Stat]: query = f"""\ SELECT {READ_PARAMS} FROM stats @@ -157,67 +199,67 @@ async def fetch_many( params["limit"] = page_size params["offset"] = (page - 1) * page_size - recs = await app.state.services.database.fetch_all(query, params) - return [dict(rec) for rec in recs] + stats = await app.state.services.database.fetch_all(query, params) + return cast(list[Stat], stats) if stats is not None else None async def update( player_id: int, mode: int, - tscore: int | None = None, - rscore: int | None = None, - pp: int | None = None, - plays: int | None = None, - playtime: int | None = None, - acc: float | None = None, - max_combo: int | None = None, - total_hits: int | None = None, - replay_views: int | None = None, - xh_count: int | None = None, - x_count: int | None = None, - sh_count: int | None = None, - s_count: int | None = None, - a_count: int | None = None, -): + tscore: int | _UnsetSentinel = UNSET, + rscore: int | _UnsetSentinel = UNSET, + pp: int | _UnsetSentinel = UNSET, + plays: int | _UnsetSentinel = UNSET, + playtime: int | _UnsetSentinel = UNSET, + acc: float | _UnsetSentinel = UNSET, + max_combo: int | _UnsetSentinel = UNSET, + total_hits: int | _UnsetSentinel = UNSET, + replay_views: int | _UnsetSentinel = UNSET, + xh_count: int | _UnsetSentinel = UNSET, + x_count: int | _UnsetSentinel = UNSET, + sh_count: int | _UnsetSentinel = UNSET, + s_count: int | _UnsetSentinel = UNSET, + a_count: int | _UnsetSentinel = UNSET, +) -> Stat | None: """Update a player stats entry in the database.""" - query = """\ + update_fields = AchievementUpdateFields = {} + if not isinstance(tscore, _UnsetSentinel): + update_fields["tscore"] = tscore + if not isinstance(rscore, _UnsetSentinel): + update_fields["rscore"] = rscore + if not isinstance(pp, _UnsetSentinel): + update_fields["pp"] = pp + if not isinstance(plays, _UnsetSentinel): + update_fields["plays"] = plays + if not isinstance(playtime, _UnsetSentinel): + update_fields["playtime"] = playtime + if not isinstance(acc, _UnsetSentinel): + update_fields["acc"] = acc + if not isinstance(max_combo, _UnsetSentinel): + update_fields["max_combo"] = max_combo + if not isinstance(total_hits, _UnsetSentinel): + update_fields["total_hits"] = total_hits + if not isinstance(replay_views, _UnsetSentinel): + update_fields["replay_views"] = replay_views + if not isinstance(xh_count, _UnsetSentinel): + update_fields["xh_count"] = xh_count + if not isinstance(x_count, _UnsetSentinel): + update_fields["x_count"] = x_count + if not isinstance(sh_count, _UnsetSentinel): + update_fields["sh_count"] = sh_count + if not isinstance(s_count, _UnsetSentinel): + update_fields["s_count"] = s_count + if not isinstance(a_count, _UnsetSentinel): + update_fields["a_count"] = a_count + + query = f"""\ UPDATE stats - SET tscore = COALESCE(:tscore, tscore), - rscore = COALESCE(:rscore, rscore), - pp = COALESCE(:pp, pp), - plays = COALESCE(:plays, plays), - playtime = COALESCE(:playtime, playtime), - acc = COALESCE(:acc, acc), - max_combo = COALESCE(:max_combo, max_combo), - total_hits = COALESCE(:total_hits, total_hits), - replay_views = COALESCE(:replay_views, replay_views), - xh_count = COALESCE(:xh_count, xh_count), - x_count = COALESCE(:x_count, x_count), - sh_count = COALESCE(:sh_count, sh_count), - s_count = COALESCE(:s_count, s_count), - a_count = COALESCE(:a_count, a_count) + SET {",".join(f"{k} = COALESCE(:{k}, {k})" for k in update_fields)} WHERE id = :id AND mode = :mode """ - params = { - "id": player_id, - "mode": mode, - "tscore": tscore, - "rscore": rscore, - "pp": pp, - "plays": plays, - "playtime": playtime, - "acc": acc, - "max_combo": max_combo, - "total_hits": total_hits, - "replay_views": replay_views, - "xh_count": xh_count, - "x_count": x_count, - "sh_count": sh_count, - "s_count": s_count, - "a_count": a_count, - } - await app.state.services.database.execute(query, params) + values = {"id": player_id, "mode": mode} | update_fields + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} @@ -229,8 +271,8 @@ async def update( "id": player_id, "mode": mode, } - rec = await app.state.services.database.fetch_one(query, params) - return rec + stat = await app.state.services.database.fetch_one(query, params) + return cast(Stat, stat) if stat is not None else None # TODO: delete? From 92a4e1c58e9e1509009f4fe3ef6d99f8811affc7 Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Sat, 23 Sep 2023 17:09:38 -0400 Subject: [PATCH 21/24] unused imports (#500) * unused imports Co-authored-by: James * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: James Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- app/api/domains/cho.py | 1 - app/api/domains/osu.py | 1 - app/api/v1/api.py | 1 - app/api/v2/common/responses.py | 2 -- app/api/v2/maps.py | 2 -- app/api/v2/models/players.py | 2 -- app/api/v2/models/scores.py | 1 - app/api/v2/players.py | 2 -- app/api/v2/scores.py | 2 -- app/commands.py | 4 ---- app/discord.py | 2 -- app/logging.py | 1 - app/objects/beatmap.py | 1 - app/objects/clan.py | 3 --- app/objects/collections.py | 1 - app/objects/match.py | 3 --- app/objects/menu.py | 2 -- app/objects/player.py | 2 -- app/objects/score.py | 1 - app/packets.py | 2 -- app/repositories/achievements.py | 2 -- app/repositories/channels.py | 2 -- app/repositories/clans.py | 2 -- app/repositories/maps.py | 1 - app/repositories/players.py | 2 -- app/repositories/scores.py | 2 -- app/repositories/stats.py | 2 -- app/state/cache.py | 1 - app/state/services.py | 1 - app/usecases/performance.py | 1 - app/utils.py | 2 -- 31 files changed, 54 deletions(-) diff --git a/app/api/domains/cho.py b/app/api/domains/cho.py index a1fe9e49..4fe15bf0 100644 --- a/app/api/domains/cho.py +++ b/app/api/domains/cho.py @@ -11,7 +11,6 @@ from datetime import datetime from pathlib import Path from typing import Literal -from typing import Optional from typing import TypedDict import bcrypt diff --git a/app/api/domains/osu.py b/app/api/domains/osu.py index 4bbbee2e..6a155af1 100644 --- a/app/api/domains/osu.py +++ b/app/api/domains/osu.py @@ -17,7 +17,6 @@ from pathlib import Path as SystemPath from typing import Any from typing import Literal -from typing import Optional from typing import TypeVar from typing import Union from urllib.parse import unquote diff --git a/app/api/v1/api.py b/app/api/v1/api.py index d6ece1e1..2f4a2ec7 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -5,7 +5,6 @@ import struct from pathlib import Path as SystemPath from typing import Literal -from typing import Optional from fastapi import APIRouter from fastapi import Depends diff --git a/app/api/v2/common/responses.py b/app/api/v2/common/responses.py index 1f9096f5..09c536ae 100644 --- a/app/api/v2/common/responses.py +++ b/app/api/v2/common/responses.py @@ -3,9 +3,7 @@ from typing import Any from typing import Generic from typing import Literal -from typing import Optional from typing import TypeVar -from typing import Union from pydantic import BaseModel diff --git a/app/api/v2/maps.py b/app/api/v2/maps.py index 3531aae0..07cbe42f 100644 --- a/app/api/v2/maps.py +++ b/app/api/v2/maps.py @@ -1,8 +1,6 @@ """ bancho.py's v2 apis for interacting with maps """ from __future__ import annotations -from typing import Optional - from fastapi import APIRouter from fastapi import status from fastapi.param_functions import Query diff --git a/app/api/v2/models/players.py b/app/api/v2/models/players.py index b03918b2..b91c4d79 100644 --- a/app/api/v2/models/players.py +++ b/app/api/v2/models/players.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Optional - from . import BaseModel diff --git a/app/api/v2/models/scores.py b/app/api/v2/models/scores.py index 1f2d4892..823f5f9c 100644 --- a/app/api/v2/models/scores.py +++ b/app/api/v2/models/scores.py @@ -1,7 +1,6 @@ from __future__ import annotations from datetime import datetime -from typing import Optional from . import BaseModel diff --git a/app/api/v2/players.py b/app/api/v2/players.py index 9c7fd15a..4488098a 100644 --- a/app/api/v2/players.py +++ b/app/api/v2/players.py @@ -1,8 +1,6 @@ """ bancho.py's v2 apis for interacting with players """ from __future__ import annotations -from typing import Optional - from fastapi import APIRouter from fastapi import status from fastapi.param_functions import Query diff --git a/app/api/v2/scores.py b/app/api/v2/scores.py index e86a9867..001ce275 100644 --- a/app/api/v2/scores.py +++ b/app/api/v2/scores.py @@ -1,8 +1,6 @@ """ bancho.py's v2 apis for interacting with scores """ from __future__ import annotations -from typing import Optional - from fastapi import APIRouter from fastapi import status from fastapi.param_functions import Query diff --git a/app/commands.py b/app/commands.py index 54377ff7..a7674071 100644 --- a/app/commands.py +++ b/app/commands.py @@ -1,13 +1,11 @@ from __future__ import annotations -import copy import importlib.metadata import os import pprint import random import secrets import signal -import struct import time import traceback import uuid @@ -28,7 +26,6 @@ from typing import TYPE_CHECKING from typing import TypedDict from typing import TypeVar -from typing import Union from urllib.parse import urlparse import psutil @@ -42,7 +39,6 @@ import app.usecases.performance import app.utils from app.constants import regexes -from app.constants.gamemodes import GameMode from app.constants.gamemodes import GAMEMODE_REPR_LIST from app.constants.mods import Mods from app.constants.mods import SPEED_CHANGING_MODS diff --git a/app/discord.py b/app/discord.py index e7508e7e..4395d7b5 100644 --- a/app/discord.py +++ b/app/discord.py @@ -1,8 +1,6 @@ """Functionality related to Discord interactivity.""" from __future__ import annotations -from typing import Optional - import aiohttp import orjson diff --git a/app/logging.py b/app/logging.py index ff8c2f2f..c3817b01 100644 --- a/app/logging.py +++ b/app/logging.py @@ -3,7 +3,6 @@ import colorsys import datetime from enum import IntEnum -from typing import Optional from typing import overload from typing import Union from zoneinfo import ZoneInfo diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 2c8412b6..22cd7729 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -10,7 +10,6 @@ from enum import unique from pathlib import Path from typing import Any -from typing import Optional from typing import TypedDict import aiohttp diff --git a/app/objects/clan.py b/app/objects/clan.py index 1f4146fe..0a69e591 100644 --- a/app/objects/clan.py +++ b/app/objects/clan.py @@ -1,11 +1,8 @@ from __future__ import annotations from datetime import datetime -from typing import Optional from typing import TYPE_CHECKING -import databases.core - import app.state from app.constants.privileges import ClanPrivileges from app.repositories import clans as clans_repo diff --git a/app/objects/collections.py b/app/objects/collections.py index 6b602ecb..590cc221 100644 --- a/app/objects/collections.py +++ b/app/objects/collections.py @@ -7,7 +7,6 @@ from collections.abc import Sequence from typing import Optional from typing import overload -from typing import Union import databases.core diff --git a/app/objects/match.py b/app/objects/match.py index 1aec4809..dba75146 100644 --- a/app/objects/match.py +++ b/app/objects/match.py @@ -7,11 +7,8 @@ from datetime import timedelta as timedelta from enum import IntEnum from enum import unique -from typing import Optional -from typing import overload from typing import TYPE_CHECKING from typing import TypedDict -from typing import Union import databases.core diff --git a/app/objects/menu.py b/app/objects/menu.py index a5a9815f..fefe8a49 100644 --- a/app/objects/menu.py +++ b/app/objects/menu.py @@ -8,9 +8,7 @@ from enum import IntEnum from enum import unique from typing import NamedTuple -from typing import Optional from typing import TYPE_CHECKING -from typing import Union if TYPE_CHECKING: diff --git a/app/objects/player.py b/app/objects/player.py index a1babfc3..c561c1b6 100644 --- a/app/objects/player.py +++ b/app/objects/player.py @@ -9,10 +9,8 @@ from enum import unique from functools import cached_property from typing import Any -from typing import Optional from typing import TYPE_CHECKING from typing import TypedDict -from typing import Union import databases.core diff --git a/app/objects/score.py b/app/objects/score.py index 6788fb72..31e14250 100644 --- a/app/objects/score.py +++ b/app/objects/score.py @@ -6,7 +6,6 @@ from enum import IntEnum from enum import unique from pathlib import Path -from typing import Optional from typing import TYPE_CHECKING import app.state diff --git a/app/packets.py b/app/packets.py index 87f9cec7..5dc9e6aa 100644 --- a/app/packets.py +++ b/app/packets.py @@ -15,9 +15,7 @@ from functools import lru_cache from typing import Any from typing import NamedTuple -from typing import Optional from typing import TYPE_CHECKING -from typing import Union # from app.objects.beatmap import BeatmapInfo diff --git a/app/repositories/achievements.py b/app/repositories/achievements.py index effd71dd..5334b9b8 100644 --- a/app/repositories/achievements.py +++ b/app/repositories/achievements.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/channels.py b/app/repositories/channels.py index a1b9033c..2f552292 100644 --- a/app/repositories/channels.py +++ b/app/repositories/channels.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/clans.py b/app/repositories/clans.py index b64f47f4..dcfe4934 100644 --- a/app/repositories/clans.py +++ b/app/repositories/clans.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/maps.py b/app/repositories/maps.py index bbcfe7ab..9e3c13e8 100644 --- a/app/repositories/maps.py +++ b/app/repositories/maps.py @@ -3,7 +3,6 @@ import textwrap from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/players.py b/app/repositories/players.py index 6c4e6fda..8e2c8289 100644 --- a/app/repositories/players.py +++ b/app/repositories/players.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/scores.py b/app/repositories/scores.py index e901bb54..f66f59b4 100644 --- a/app/repositories/scores.py +++ b/app/repositories/scores.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/repositories/stats.py b/app/repositories/stats.py index 0bab3c40..66eaafb5 100644 --- a/app/repositories/stats.py +++ b/app/repositories/stats.py @@ -1,9 +1,7 @@ from __future__ import annotations import textwrap -from typing import Any from typing import cast -from typing import Optional from typing import TypedDict import app.state.services diff --git a/app/state/cache.py b/app/state/cache.py index 661cb964..10be1dd0 100644 --- a/app/state/cache.py +++ b/app/state/cache.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from typing import Union if TYPE_CHECKING: from app.objects.beatmap import Beatmap, BeatmapSet diff --git a/app/state/services.py b/app/state/services.py index e09ea58f..a270312f 100644 --- a/app/state/services.py +++ b/app/state/services.py @@ -9,7 +9,6 @@ from collections.abc import Mapping from collections.abc import MutableMapping from pathlib import Path -from typing import Optional from typing import TYPE_CHECKING from typing import TypedDict diff --git a/app/usecases/performance.py b/app/usecases/performance.py index 86136da7..4e1dc78a 100644 --- a/app/usecases/performance.py +++ b/app/usecases/performance.py @@ -3,7 +3,6 @@ import math from collections.abc import Iterable from dataclasses import dataclass -from typing import Optional from typing import TypedDict from akatsuki_pp_py import Beatmap diff --git a/app/utils.py b/app/utils.py index bfa2818e..fb122959 100644 --- a/app/utils.py +++ b/app/utils.py @@ -6,14 +6,12 @@ import os import shutil import socket -import subprocess import sys import types import zipfile from collections.abc import Callable from pathlib import Path from typing import Any -from typing import Optional from typing import TypedDict from typing import TypeVar From 17ccbf68303c8e2874285a9621a6216522110ced Mon Sep 17 00:00:00 2001 From: Josh Smith Date: Sat, 23 Sep 2023 22:38:36 -0400 Subject: [PATCH 22/24] Fix typing errors & introduce mypy as a required CI step (#501) * typing fixes Co-authored-by: James * fix performance typing Co-authored-by: James * more typing fixes Co-authored-by: James * [CI] Generated new Pipfile.lock * update akatsuki-pp-py to 0.9.8 - typing fixes Co-authored-by: James * basic lint in CI * [CI] Generated new Pipfile.lock * ignore missing imports in mypy ci * temp test * pipenv run mypy Co-authored-by: James * test Co-authored-by: James * fixed all type errors; added mypy config * run mypy on change to any py files * better comment * todone * pattern for clarity * a bit of prevention of circular imports * weird but less weird * remove __getitem__ implementations from collections * type params better * make type-check --------- Co-authored-by: James Co-authored-by: CI --- .github/workflows/lint.yaml | 24 ++++ Makefile | 6 +- Pipfile | 1 + Pipfile.lock | 88 +++++++----- app/_typing.py | 4 +- app/api/domains/cho.py | 55 +++---- app/api/domains/map.py | 2 +- app/api/domains/osu.py | 238 ++++++++++++------------------- app/api/v1/api.py | 86 ++++++----- app/api/v2/common/responses.py | 2 +- app/api/v2/models/__init__.py | 6 +- app/bg_loops.py | 2 +- app/commands.py | 46 +++--- app/constants/mods.py | 2 + app/discord.py | 36 ++--- app/logging.py | 21 +-- app/objects/achievement.py | 6 +- app/objects/beatmap.py | 28 ++-- app/objects/collections.py | 140 ++++-------------- app/objects/match.py | 27 ++-- app/objects/player.py | 31 ++-- app/objects/score.py | 31 ++-- app/packets.py | 13 +- app/repositories/__init__.py | 0 app/repositories/achievements.py | 39 +++-- app/repositories/channels.py | 27 ++-- app/repositories/clans.py | 30 ++-- app/repositories/maps.py | 26 ++-- app/repositories/players.py | 24 ++-- app/repositories/scores.py | 42 +++--- app/repositories/stats.py | 29 ++-- app/settings.py | 3 +- app/state/__init__.py | 8 +- app/state/services.py | 52 ++++--- app/state/sessions.py | 3 +- app/usecases/performance.py | 30 ++-- app/utils.py | 19 ++- main.py | 19 +-- mypy.ini | 19 +++ requirements-dev.txt | 4 +- requirements.txt | 2 +- tools/newstats.py | 85 ----------- tools/recalc.py | 9 +- 43 files changed, 642 insertions(+), 723 deletions(-) create mode 100644 .github/workflows/lint.yaml create mode 100644 app/repositories/__init__.py create mode 100644 mypy.ini delete mode 100755 tools/newstats.py diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..9bf679a8 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,24 @@ +name: Lint +on: + push: + paths: + - '*.py' + +jobs: + mypy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + - name: Install pipenv + run: python -m pip install --upgrade pipenv wheel + # TODO: add caching of Pipfile.lock + - name: Install dependencies + run: pipenv install --dev --deploy + - name: Run mypy + run: pipenv run mypy . diff --git a/Makefile b/Makefile index 02c2794c..b8ae4395 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ test: lint: pipenv run pre-commit run --all-files +type-check: + pipenv run mypy . + install: PIPENV_VENV_IN_PROJECT=1 pipenv install @@ -27,7 +30,4 @@ clean: pipenv clean run: - pipenv run python main.py - -run-prod: pipenv run ./scripts/start_server.sh diff --git a/Pipfile b/Pipfile index b25541cd..bc318951 100644 --- a/Pipfile +++ b/Pipfile @@ -32,6 +32,7 @@ pre-commit = "*" black = "*" reorder-python-imports = "*" mypy = "*" +autoflake = "*" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 0e1766ff..327d5d2a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e53e4c3feb118befbf6f337a07168bfb8bbb6c2ad39b541106ff17b68f158eb3" + "sha256": "82b0b736bcac265a6584ba7efb5bcd253b912ddc17288b99847a7257195daeda" }, "pipfile-spec": 6, "requires": { @@ -127,41 +127,46 @@ }, "akatsuki-pp-py": { "hashes": [ - "sha256:0c7b5b75924edb4da0d5db36efd04372020b06a1a1a4d0a9aa3ed7d8bf6c7414", - "sha256:14607691db7d8e356b48f5e854dd83e7c16c6543ef682a077eecd2b9e56652bd", - "sha256:181f317a3eabc83406e60281de76de7e61a1eb58757dfccf561fffa3e582ee09", - "sha256:1b4a1e54b60eda295077ecd2337f1864ef5352db90e84ca649baab6140393c1d", - "sha256:32f7ede478a3de8b5c1ea031af93adb68d86f26ac296dccae9c4e2508871bfe8", - "sha256:34638c49bffabe97fef3bf554ea5caf3a1fb4f5b4bbb3326202c4e9b61b8a542", - "sha256:35841c00a72ec35565a014f4aaaa0ac62c5c492d22ce88aa95379e79cbad86a3", - "sha256:42897cc3a969fc6cda62cdb70047ae142e6e8ccc6c80e7dde7198a4d1ebab66c", - "sha256:4ee1a994e70d12dccb9a985bfb91325807065c8736f139ce1dfc251d955cfe33", - "sha256:6522acefa1129f9b999201876412c1121492bc4cd5d8e478e6386bd27d4fffbe", - "sha256:6615ef75493de0a0c10e771c77364b7374b2d5974b8617280264ee1ac3cef9a7", - "sha256:6cdfbe8b1899138c9622e06373b89ea4f542a9211c1bf7a3e706e9c78b6e062c", - "sha256:6e6f2ec4afcccabc6cbc82accefffc29f24ce7c0ebb8af9b20158f27d103ccc2", - "sha256:783480561226bdbcee8eb91480ff8a59aea8e5dbf5eb83b5751eac3fc9897fd8", - "sha256:8019210301884f0a02200f1b5cec048b305cf22e3cdd88caeae6356565ba5ecb", - "sha256:86101805c25886cd486827750a9bd2dc2f0df528227d7c0ae1e5468e38a08b11", - "sha256:881674d79fbdcd9cef7c7935ac1a8356fd14ea5bbf14e3fd1f0fbdae8f419f96", - "sha256:931b762952ee474048eac37fc7e36b420e6357c426c2b60107f7439711d3c9a6", - "sha256:931f4dc916d1dc4bf93501f6e7b222024e48a53c282e02e8352a333c8cc5ff91", - "sha256:a377743a76a033fefbb4bd8b207a9cc52ca5848c46b3e65b75a34023f04767de", - "sha256:b1fae2665193faf481c4508041985a59a5caab7cef11dfa5fb3032bb7504e560", - "sha256:b8cfcd5b1e40cfb015604bd48873970cb67e6acc498c93f6fb056b9e1a6e8d01", - "sha256:becfa2ad21051297c2d2b1329186e3243943a3b55c0f92d28a48a4bf20b08c02", - "sha256:bfc9a4b25d87a0b6945687cb3ae60ee10ef90bad04f0580a11ea716c94004cc0", - "sha256:c71e42a3a28787f1c9b9cd955af317b1a79f52e3c6eb3b60cb216430af31594a", - "sha256:c86dd032378c648bb8acae33a4803ecfe9f8aee9a2fecb2b3de07f9c4674a3a0", - "sha256:ca6f1ea1ec89beeca6215f6d5a848da225ff5d1edef9cdfd60387126d32379fd", - "sha256:cf6368be43066eb32f56a9a73fc8701aa3145ccd879a3ffb4b1ef22031720e67", - "sha256:d0fa42a853384def2d277a15ec47b8afbea43f56e8cf3dfb69d8bcb9d1261f2e", - "sha256:de29d75ad416fab953d5dcd3116bca4aaef113e4907975fe057894bc251bf93a", - "sha256:feddd245c858a566b48efcff6758964492ecd0a6fec342a6c302b1866e73ca9a" + "sha256:0ab412c8238d2c59e64615e78049d3918a85966ea254f39567922dca035c9a83", + "sha256:0eaf94e18fd5bbbc81910ea5ed66b359a99a7144c96c72c69ffffbbdb769ccd0", + "sha256:15171781948cb0be72b0e7b65e8ec049f030f6a2ebc8d13314843001c3cf3c5e", + "sha256:1f9b056e93ab42b15dff0c59cc628cdee6570c4091ff021c7cee2dd5177a8520", + "sha256:2494cc28c8478aa9698e4b209f760d6fb1686ad3a086f2d8733f03aa7467d09d", + "sha256:33fe54648b70c331bbc82026d5df4fc9c3c5dc794863d55549040bd26443ea2e", + "sha256:3602e52b4dfcd1427b1b713861b3dca47d25dc61a68ebb8fec5670902d5249b6", + "sha256:389c96dd909f661e0a1871e2f6b2f583f549e2c27fbf3c7865d3ac192e5b4edf", + "sha256:39a5cdbf88d1513e91308105d7916e0bff7c364dc6efb97cbe184b1685a7e1f6", + "sha256:3b4316c86b82a58f2822c8cff2fc41d767a512dd8cf32fb1b923d8f7a153bc7b", + "sha256:403165e6548277d20dd60ba6866e73993d01308ed3143ce64c4ae6b3943733b4", + "sha256:41162333579bfdc15d5ca86e3e08c6c3b03403970f658093b79bb01ff083b0ef", + "sha256:4b1f1cc1905a2adf3197f5fe0aabb787daef709f024b42cbe120e00ffcbb2a85", + "sha256:4ec656a82e759189abe7852335402304f3326b5ae80bd167a6d1c0caa28eaacd", + "sha256:4f0fb79330eafea7d59fbc0e73ce511b6df318af36302160b1414fa5db5fc6f3", + "sha256:4f1ea080e9ee91bb07008b8f06a8dfb94c1ac56984f67a87d84f5c68c32bdb6e", + "sha256:5446eb32aa3983b4aa3c79235eb4e4e200d257b836c8fc3e3f7c1f867269bc7c", + "sha256:63de426d6182e3e91fbd1f57576e57cfb8d2aab5efd08effd04cbf5611fba251", + "sha256:70a3fe10bdda92fe8f10cac0e286c9e494e73a5642eeda8d66c77120b18a252a", + "sha256:73eea2a1012fe340598f3896be3b2525de70388c2e1b109543c20ec753e223db", + "sha256:748bbbcc5c4c0233fcbbcba330cb12cf1a1b0e2b4dab9869015e118c73d1c4f3", + "sha256:929ba23fdc1436daa10e8371d5a4d5503cd88bfac2fe506c93773ecaafe61ce0", + "sha256:96c763153ec05fd4b10b131fe8a9308f4ce62272f553e97519fa04cc25698c91", + "sha256:b32d3b5518e2475af276b8eebd3400aba6b291052247efa1a42809aeda1b9384", + "sha256:c2df9a3c86f4b755ac5ac767aac31755cb31931d74f9b2fc24f51221ecbc8b2f", + "sha256:c5a1fb34a3d16629ccc5621c74be781e8ab580260ac1028a167f5fa47cd0ae5c", + "sha256:d735c9a1da152ac66bda575b393998b14c00be86d7a52aba90a20d267f3859ae", + "sha256:dce31e5b3414790e195a555c2425e48127a698c6df3401e35435742116b1cb9f", + "sha256:e84ed7bac4bfe8b67a0afbbe4baac9a81facb9613f2b8fc056d16b600116e053", + "sha256:e9839db2a9dd350637b135d98bdf89035eeb78c5d5558352baa06e376225c153", + "sha256:f181436cf9d8017e2d10166c3e14715f3e3f44910f21a56052f304b5001b99a1", + "sha256:f1a8a9814c9a79f94360dcaaafa3c10f2137df75188d1c0a51d1e14ac5e93e74", + "sha256:f2b6082b00aa844d62a2d88cc7c85244de86370b224a01e858dc45043b0780c3", + "sha256:f36101483729e23b87d6206b4bdbcd51ed25e7b166318eb63ecaa23f0ca8280d", + "sha256:f5b197a408ced600551475751e1e715a460ee6381dfb45295e4ff448761e7e41", + "sha256:ffd127ebd0d011359d414531c84089b0c3e68679f46b485c435d71d936b8daee" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.9.5" + "version": "==0.9.8" }, "annotated-types": { "hashes": [ @@ -1181,6 +1186,15 @@ } }, "develop": { + "autoflake": { + "hashes": [ + "sha256:265cde0a43c1f44ecfb4f30d95b0437796759d07be7706a2f70e4719234c0f79", + "sha256:62b7b6449a692c3c9b0c916919bbc21648da7281e8506bcf8d3f8280e431ebc1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.2.1" + }, "black": { "hashes": [ "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f", @@ -1356,6 +1370,14 @@ "markers": "python_version >= '3.8'", "version": "==3.4.0" }, + "pyflakes": { + "hashes": [ + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.0" + }, "pytest": { "hashes": [ "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", diff --git a/app/_typing.py b/app/_typing.py index aff56186..f6ccd747 100644 --- a/app/_typing.py +++ b/app/_typing.py @@ -4,11 +4,11 @@ from ipaddress import IPv6Address from typing import Any from typing import TypeVar -from typing import Union -IPAddress = Union[IPv4Address, IPv6Address] T = TypeVar("T") +IPAddress = IPv4Address | IPv6Address + class _UnsetSentinel: def __repr__(self) -> str: diff --git a/app/api/domains/cho.py b/app/api/domains/cho.py index 4fe15bf0..5c3ed21c 100644 --- a/app/api/domains/cho.py +++ b/app/api/domains/cho.py @@ -41,6 +41,7 @@ from app.objects.beatmap import Beatmap from app.objects.beatmap import ensure_local_osu_file from app.objects.channel import Channel +from app.objects.clan import Clan from app.objects.match import Match from app.objects.match import MatchTeams from app.objects.match import MatchTeamTypes @@ -107,7 +108,7 @@ async def bancho_http_handler(): @router.get("/online") -async def bancho_list_user(): +async def bancho_view_online_users(): """see who's online""" new_line = "\n" @@ -130,7 +131,7 @@ async def bancho_list_user(): @router.get("/matches") -async def bancho_list_user(): +async def bancho_view_matches(): """ongoing matches""" new_line = "\n" @@ -318,7 +319,7 @@ async def handle(self, player: Player) -> None: else: return - t_chan = app.state.sessions.channels[f"#spec_{spec_id}"] + t_chan = app.state.sessions.channels.get_by_name(f"#spec_{spec_id}") elif recipient == "#multiplayer": if not player.match: # they're not in a match? @@ -326,7 +327,7 @@ async def handle(self, player: Player) -> None: t_chan = player.match.chat else: - t_chan = app.state.sessions.channels[recipient] + t_chan = app.state.sessions.channels.get_by_name(recipient) if not t_chan: log(f"{player} wrote to non-existent {recipient}.", Ansi.LYELLOW) @@ -654,8 +655,6 @@ async def login( ), } - user_info = dict(user_info) # make a mutable copy - if osu_version.stream == "tourney" and not ( user_info["priv"] & Privileges.DONATOR and user_info["priv"] & Privileges.UNRESTRICTED @@ -669,7 +668,6 @@ async def login( # get our bcrypt cache bcrypt_cache = app.state.cache.bcrypt pw_bcrypt = user_info["pw_bcrypt"].encode() - user_info["pw_bcrypt"] = pw_bcrypt # check credentials against db. algorithms like these are intentionally # designed to be slow; we'll cache the results to speed up subsequent logins. @@ -773,15 +771,13 @@ async def login( """ All checks passed, player is safe to login """ # get clan & clan priv if we're in a clan + clan: Clan | None = None + clan_priv: ClanPrivileges | None = None if user_info["clan_id"] != 0: - clan = app.state.sessions.clans.get(id=user_info.pop("clan_id")) - clan_priv = ClanPrivileges(user_info.pop("clan_priv")) - else: - del user_info["clan_id"] - del user_info["clan_priv"] - clan = clan_priv = None + clan = app.state.sessions.clans.get(id=user_info["clan_id"]) + clan_priv = ClanPrivileges(user_info["clan_priv"]) - db_country = user_info.pop("country") + db_country = user_info["country"] geoloc = await app.state.services.fetch_geoloc(ip, headers) @@ -796,8 +792,6 @@ async def login( ), } - user_info["geoloc"] = geoloc - if db_country == "xx": # bugfix for old bancho.py versions when # country wasn't stored on registration. @@ -806,7 +800,7 @@ async def login( await db_conn.execute( "UPDATE users SET country = :country WHERE id = :user_id", { - "country": user_info["geoloc"]["country"]["acronym"], + "country": geoloc["country"]["acronym"], "user_id": user_info["id"], }, ) @@ -822,14 +816,21 @@ async def login( ) player = Player( - **user_info, # {id, name, priv, pw_bcrypt, silence_end, api_key, geoloc?} + id=user_info["id"], + name=user_info["name"], + priv=user_info["priv"], + pw_bcrypt=pw_bcrypt, + clan=clan, + clan_priv=clan_priv, + geoloc=geoloc, utc_offset=login_data["utc_offset"], pm_private=login_data["pm_private"], + silence_end=user_info["silence_end"], + donor_end=user_info["donor_end"], + client_details=client_details, login_time=login_time, - clan=clan, - clan_priv=clan_priv, tourney_client=osu_version.stream == "tourney", - client_details=client_details, + api_key=user_info["api_key"], ) data = bytearray(app.packets.protocol_version(19)) @@ -1173,7 +1174,7 @@ async def handle(self, player: Player) -> None: if target is not app.state.sessions.bot: # target is not bot, send the message normally if online - if target.online: + if target.is_online: target.send(msg, sender=player) else: # inform user they're offline, but @@ -1355,7 +1356,7 @@ async def handle(self, player: Player) -> None: map_md5=self.match_data.map_md5, # TODO: validate no security hole exists host_id=self.match_data.host_id, - mode=self.match_data.mode, + mode=GameMode(self.match_data.mode), mods=Mods(self.match_data.mods), win_condition=MatchWinConditions(self.match_data.win_condition), team_type=MatchTeamTypes(self.match_data.team_type), @@ -1599,12 +1600,12 @@ async def handle(self, player: Player) -> None: player.match.map_id = bmap.id player.match.map_md5 = bmap.md5 player.match.map_name = bmap.full_name - player.match.mode = player.match.host.status.mode.as_vanilla + player.match.mode = GameMode(player.match.host.status.mode.as_vanilla) else: player.match.map_id = self.match_data.map_id player.match.map_md5 = self.match_data.map_md5 player.match.map_name = self.match_data.map_name - player.match.mode = self.match_data.mode + player.match.mode = GameMode(self.match_data.mode) if player.match.team_type != self.match_data.team_type: # if theres currently a scrim going on, only allow @@ -1870,7 +1871,7 @@ async def handle(self, player: Player) -> None: if self.name in IGNORED_CHANNELS: return - channel = app.state.sessions.channels[self.name] + channel = app.state.sessions.channels.get_by_name(self.name) if not channel or not player.join_channel(channel): log(f"{player} failed to join {self.name}.", Ansi.LYELLOW) @@ -2036,7 +2037,7 @@ async def handle(self, player: Player) -> None: if self.name in IGNORED_CHANNELS: return - channel = app.state.sessions.channels[self.name] + channel = app.state.sessions.channels.get_by_name(self.name) if not channel: log(f"{player} failed to leave {self.name}.", Ansi.LYELLOW) diff --git a/app/api/domains/map.py b/app/api/domains/map.py index 39dfbded..04fd074e 100644 --- a/app/api/domains/map.py +++ b/app/api/domains/map.py @@ -14,7 +14,7 @@ # forward any unmatched request to osu! # eventually if we do bmap submission, we'll need this. @router.get("/{file_path:path}") -async def everything(request: Request): +async def everything(request: Request) -> RedirectResponse: return RedirectResponse( url=f"https://b.ppy.sh{request['path']}", status_code=status.HTTP_301_MOVED_PERMANENTLY, diff --git a/app/api/domains/osu.py b/app/api/domains/osu.py index 6a155af1..4614a865 100644 --- a/app/api/domains/osu.py +++ b/app/api/domains/osu.py @@ -5,7 +5,6 @@ import hashlib import random import secrets -import time from base64 import b64decode from collections import defaultdict from collections.abc import Awaitable @@ -17,8 +16,6 @@ from pathlib import Path as SystemPath from typing import Any from typing import Literal -from typing import TypeVar -from typing import Union from urllib.parse import unquote from urllib.parse import unquote_plus @@ -47,6 +44,7 @@ import app.settings import app.state import app.utils +from app._typing import UNSET from app.constants import regexes from app.constants.clientflags import LastFMFlags from app.constants.gamemodes import GameMode @@ -377,7 +375,7 @@ async def lastFM( ) # refresh their client state - if player.online: + if player.is_online: player.logout() return b"-3" @@ -396,7 +394,7 @@ async def lastFM( ) # refresh their client state - if player.online: + if player.is_online: player.logout() return b"-3" @@ -554,16 +552,18 @@ async def osuSearchSetHandler( return # invalid args # Get all set data. - bmapset = await app.state.services.database.fetch_one( + rec = await app.state.services.database.fetch_one( "SELECT DISTINCT set_id, artist, " "title, status, creator, last_update " f"FROM maps WHERE {k} = :v", {"v": v}, ) - if not bmapset: + if rec is None: # TODO: get from osu! - return + return None + + bmapset = dict(rec._mapping) return ( ( @@ -577,11 +577,8 @@ async def osuSearchSetHandler( # 0s are threadid, has_vid, has_story, filesize, filesize_novid -T = TypeVar("T", bound=Union[int, float]) - - -def chart_entry(name: str, before: T | None, after: T) -> str: - return f"{name}Before:{before or ''}|{name}After:{after}" +def chart_entry(name: str, before: float | None, after: float | None) -> str: + return f"{name}Before:{before or ''}|{name}After:{after or ''}" def parse_form_data_score_params( @@ -859,7 +856,7 @@ async def osuSubmitModularSelector( ) if score.rank == 1 and not score.player.restricted: - announce_chan = app.state.sessions.channels["#announce"] + announce_chan = app.state.sessions.channels.get_by_name("#announce") ann = [ f"\x01ACTION achieved #1 on {score.bmap.embed}", @@ -957,7 +954,7 @@ async def osuSubmitModularSelector( admin=app.state.sessions.bot, reason="submitted score with no replay", ) - if score.player.online: + if score.player.is_online: score.player.logout() """ Update the user's & beatmap's stats """ @@ -1059,19 +1056,19 @@ async def osuSubmitModularSelector( await stats_repo.update( score.player.id, score.mode.value, - plays=stats_updates.get("plays"), - playtime=stats_updates.get("playtime"), - tscore=stats_updates.get("tscore"), - total_hits=stats_updates.get("total_hits"), - max_combo=stats_updates.get("max_combo"), - xh_count=stats_updates.get("xh_count"), - x_count=stats_updates.get("x_count"), - sh_count=stats_updates.get("sh_count"), - s_count=stats_updates.get("s_count"), - a_count=stats_updates.get("a_count"), - rscore=stats_updates.get("rscore"), - acc=stats_updates.get("acc"), - pp=stats_updates.get("pp"), + plays=stats_updates.get("plays", UNSET), + playtime=stats_updates.get("playtime", UNSET), + tscore=stats_updates.get("tscore", UNSET), + total_hits=stats_updates.get("total_hits", UNSET), + max_combo=stats_updates.get("max_combo", UNSET), + xh_count=stats_updates.get("xh_count", UNSET), + x_count=stats_updates.get("x_count", UNSET), + sh_count=stats_updates.get("sh_count", UNSET), + s_count=stats_updates.get("s_count", UNSET), + a_count=stats_updates.get("a_count", UNSET), + rscore=stats_updates.get("rscore", UNSET), + acc=stats_updates.get("acc", UNSET), + pp=stats_updates.get("pp", UNSET), ) if not score.player.restricted: @@ -1206,7 +1203,7 @@ async def getReplay( return # increment replay views for this score - if player.id != score.player.id: + if score.player is not None and player.id != score.player.id: app.state.loop.create_task(score.increment_replay_views()) return FileResponse(file) @@ -1282,7 +1279,7 @@ async def get_leaderboard_scores( mods: Mods, player: Player, scoring_metric: Literal["pp", "score"], -) -> tuple[list[Mapping[str, Any]], Mapping[str, Any] | None]: +) -> tuple[list[dict[str, Any]], dict[str, Any] | None]: query = [ f"SELECT s.id, s.{scoring_metric} AS _score, " "s.max_combo, s.n50, s.n100, s.n300, " @@ -1296,7 +1293,11 @@ async def get_leaderboard_scores( "AND (u.priv & 1 OR u.id = :user_id) AND mode = :mode", ] - params = {"map_md5": map_md5, "user_id": player.id, "mode": mode} + params: dict[str, Any] = { + "map_md5": map_md5, + "user_id": player.id, + "mode": mode, + } if leaderboard_type == LeaderboardType.Mods: query.append("AND s.mods = :mods") @@ -1311,10 +1312,13 @@ async def get_leaderboard_scores( # TODO: customizability of the number of scores query.append("ORDER BY _score DESC LIMIT 50") - score_rows = await app.state.services.database.fetch_all( - " ".join(query), - params, - ) + score_rows = [ + dict(r._mapping) + for r in await app.state.services.database.fetch_all( + " ".join(query), + params, + ) + ] if score_rows: # None or [] # fetch player's personal best score @@ -1347,7 +1351,7 @@ async def get_leaderboard_scores( ) # attach rank to personal best row - personal_best_score_row = dict(personal_best_score_row) + personal_best_score_row = dict(personal_best_score_row._mapping) personal_best_score_row["rank"] = p_best_rank else: personal_best_score_row = None @@ -1413,7 +1417,9 @@ async def getScores( if not player.restricted: app.state.sessions.players.enqueue(app.packets.user_stats(player)) - scoring_metric = "pp" if mode >= GameMode.RELAX_OSU else "score" + scoring_metric: Literal["pp", "score"] = ( + "pp" if mode >= GameMode.RELAX_OSU else "score" + ) bmap = await Beatmap.from_md5(map_md5, set_id=map_set_id) has_set_id = map_set_id > 0 @@ -1548,19 +1554,22 @@ async def osuComment( ): if action == "get": # client is requesting all comments - comments = await app.state.services.database.fetch_all( - "SELECT c.time, c.target_type, c.colour, " - "c.comment, u.priv FROM comments c " - "INNER JOIN users u ON u.id = c.userid " - "WHERE (c.target_type = 'replay' AND c.target_id = :score_id) " - "OR (c.target_type = 'song' AND c.target_id = :set_id) " - "OR (c.target_type = 'map' AND c.target_id = :map_id) ", - { - "score_id": score_id, - "set_id": map_set_id, - "map_id": map_id, - }, - ) + comments = [ + dict(c._mapping) + for c in await app.state.services.database.fetch_all( + "SELECT c.time, c.target_type, c.colour, " + "c.comment, u.priv FROM comments c " + "INNER JOIN users u ON u.id = c.userid " + "WHERE (c.target_type = 'replay' AND c.target_id = :score_id) " + "OR (c.target_type = 'song' AND c.target_id = :set_id) " + "OR (c.target_type = 'map' AND c.target_id = :map_id) ", + { + "score_id": score_id, + "set_id": map_set_id, + "map_id": map_id, + }, + ) + ] ret: list[str] = [] @@ -1617,17 +1626,18 @@ async def osuComment( ) player.update_latest_activity_soon() - return # empty resp is fine + + return Response(content=b"") # empty resp is fine @router.get("/web/osu-markasread.php") async def osuMarkAsRead( player: Player = Depends(authenticate_player_session(Query, "u", "h")), channel: str = Query(..., min_length=0, max_length=32), -): +) -> Response: target_name = unquote(channel) # TODO: unquote needed? if not target_name: - return # no channel specified + return Response(content=b"") # no channel specified target = await app.state.sessions.players.from_cache_or_sql(name=target_name) if target: @@ -1639,9 +1649,11 @@ async def osuMarkAsRead( {"to": player.id, "from": target.id}, ) + return Response(content=b"") + @router.get("/web/osu-getseasonal.php") -async def osuSeasonal(): +async def osuSeasonal() -> Response: return ORJSONResponse(app.settings.SEASONAL_BGS) @@ -1655,8 +1667,8 @@ async def banchoConnect( net_framework_vers: str | None = Query(None, alias="fx"), # delimited by | client_hash: str | None = Query(None, alias="ch"), retrying: bool | None = Query(None, alias="retry"), # '0' or '1' -): - return b"" # TODO +) -> Response: + return Response(content=b"") # TODO _checkupdates_cache = { # default timeout is 1h, set on request. @@ -1672,38 +1684,8 @@ async def checkUpdates( request: Request, action: Literal["check", "path", "error"], stream: Literal["cuttingedge", "stable40", "beta40", "stable"], -): - return b"" - - # NOTE: this code is unused now. - # it was only used with server switchers, - # which bancho.py has deprecated support for. - - if action == "error": - # client is just reporting an error updating - return - - cache = _checkupdates_cache[stream] - current_time = int(time.time()) - - if cache[action] and cache["timeout"] > current_time: - return cache[action] - - url = "https://old.ppy.sh/web/check-updates.php" - async with app.state.services.http_client.get( - url, - params=request.query_params, - ) as resp: - if not resp or resp.status != 200: - return (503, b"") # failed to get data from osu - - result = await resp.read() - - # update the cached result. - cache[action] = result - cache["timeout"] = current_time + 3600 - - return result +) -> Response: + return Response(content=b"") """ Misc handlers """ @@ -1711,7 +1693,7 @@ async def checkUpdates( if app.settings.REDIRECT_OSU_URLS: # NOTE: this will likely be removed with the addition of a frontend. - async def osu_redirect(request: Request, _: int = Path(...)): + async def osu_redirect(request: Request, _: int = Path(...)) -> Response: return RedirectResponse( url=f"https://osu.ppy.sh{request['path']}", status_code=status.HTTP_301_MOVED_PERMANENTLY, @@ -1730,7 +1712,7 @@ async def osu_redirect(request: Request, _: int = Path(...)): async def get_screenshot( screenshot_id: str = Path(..., regex=r"[a-zA-Z0-9-_]{8}"), extension: Literal["jpg", "jpeg", "png"] = Path(...), -): +) -> Response: """Serve a screenshot from the server, by filename.""" screenshot_path = SCREENSHOTS_PATH / f"{screenshot_id}.{extension}" @@ -1742,14 +1724,14 @@ async def get_screenshot( return FileResponse( path=screenshot_path, - media_type=app.utils.get_media_type(extension), # type: ignore + media_type=app.utils.get_media_type(extension), ) @router.get("/d/{map_set_id}") async def get_osz( map_set_id: str = Path(...), -): +) -> Response: """Handle a map download request (osu.ppy.sh/d/*).""" no_video = map_set_id[-1] == "n" if no_video: @@ -1768,61 +1750,24 @@ async def get_updated_beatmap( request: Request, map_filename: str, host: str = Header(...), -): +) -> Response: """Send the latest .osu file the server has for a given map.""" - if host != "osu.ppy.sh": - return RedirectResponse( - url=f"https://osu.ppy.sh{request['raw_path'].decode()}", - status_code=status.HTTP_301_MOVED_PERMANENTLY, - ) - - return - - # NOTE: this code is unused now. ඞ - # it was only used with server switchers, - # which bancho.py has deprecated support for. + if host == "osu.ppy.sh": + return Response("bancho.py only supports the -devserver connection method") - # server switcher, use old method - map_filename = unquote(map_filename) - - if not ( - res := await app.state.services.database.fetch_one( - "SELECT id, md5 FROM maps WHERE filename = :filename", - {"filename": map_filename}, - ) - ): - return Response(status_code=status.HTTP_400_BAD_REQUEST) - - osu_file_path = BEATMAPS_PATH / f'{res["id"]}.osu' - - if ( - osu_file_path.exists() - and res["md5"] == hashlib.md5(osu_file_path.read_bytes()).hexdigest() - ): - # up-to-date map found on disk. - content = osu_file_path.read_bytes() - else: - # map not found, or out of date; get from osu! - url = f"https://old.ppy.sh/osu/{res['id']}" - - async with app.state.services.http_client.get(url) as resp: - if not resp or resp.status != 200: - log(f"Could not find map {osu_file_path}!", Ansi.LRED) - return (404, b"") # couldn't find on osu!'s server - - content = await resp.read() - - # save it to disk for future - osu_file_path.write_bytes(content) - - return content + return RedirectResponse( + url=f"https://osu.ppy.sh{request['raw_path'].decode()}", + status_code=status.HTTP_301_MOVED_PERMANENTLY, + ) @router.get("/p/doyoureallywanttoaskpeppy") -async def peppyDMHandler(): - return ( - b"This user's ID is usually peppy's (when on bancho), " - b"and is blocked from being messaged by the osu! client." +async def peppyDMHandler() -> Response: + return Response( + content=( + b"This user's ID is usually peppy's (when on bancho), " + b"and is blocked from being messaged by the osu! client." + ), ) @@ -1840,7 +1785,7 @@ async def register_account( # TODO: allow nginx to be optional forwarded_ip: str = Header(..., alias="X-Forwarded-For"), real_ip: str = Header(..., alias="X-Real-IP"), -): +) -> Response: if not all((username, email, pw_plaintext)): return Response( content=b"Missing required params", @@ -1911,6 +1856,7 @@ async def register_account( ip = app.state.services.ip_resolver.get_ip(request.headers) geoloc = await app.state.services.fetch_geoloc(ip, request.headers) + country = geoloc["country"]["acronym"] if geoloc is not None else "XX" async with app.state.services.database.transaction(): # add to `users` table. @@ -1918,7 +1864,7 @@ async def register_account( name=username, email=email, pw_bcrypt=pw_bcrypt, - country=geoloc["country"]["acronym"], + country=country, ) # add to `stats` table. @@ -1929,11 +1875,11 @@ async def register_account( log(f"<{username} ({player['id']})> has registered!", Ansi.LGREEN) - return b"ok" # success + return Response(content=b"ok") # success @router.post("/difficulty-rating") -async def difficultyRatingHandler(request: Request): +async def difficultyRatingHandler(request: Request) -> Response: return RedirectResponse( url=f"https://osu.ppy.sh{request['path']}", status_code=status.HTTP_307_TEMPORARY_REDIRECT, diff --git a/app/api/v1/api.py b/app/api/v1/api.py index 2f4a2ec7..17aeab5d 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -86,7 +86,7 @@ def format_player_basic(player: Player) -> dict[str, object]: "name": player.name, "country": player.geoloc["country"]["acronym"], "clan": format_clan_basic(player.clan) if player.clan else None, - "online": player.online, + "online": player.is_online, } @@ -128,7 +128,7 @@ async def api_calculate_pp( mode: int = Query(0, min=0, max=11), combo: int = Query(None, max=2_147_483_647), acclist: list[float] = Query([100, 99, 98, 95], alias="acc"), -): +) -> Response: """Calculates the PP of a specified map with specified score parameters.""" if token is None or app.state.sessions.api_keys.get(token.credentials) is None: @@ -180,17 +180,16 @@ async def api_calculate_pp( ) # "Inject" the accuracy into the list of results - results = [ + final_results = [ performance_result | {"accuracy": score.acc} for performance_result, score in zip(results, scores) ] return ORJSONResponse( - results + # XXX: change the output type based on the inputs from user + final_results if all(x is None for x in [ngeki, nkatu, n100, n50]) - else results[ - 0 - ], # It's okay to change the output type as the user explicitly either requests + else final_results[0], status_code=status.HTTP_200_OK, # a list via the acclist parameter or a single score via n100 and n50 ) @@ -198,7 +197,7 @@ async def api_calculate_pp( @router.get("/search_players") async def api_search_players( search: str | None = Query(None, alias="q", min=2, max=32), -): +) -> Response: """Search for users on the server by name.""" rows = await app.state.services.database.fetch_all( "SELECT id, name " @@ -219,7 +218,7 @@ async def api_search_players( @router.get("/get_player_count") -async def api_get_player_count(): +async def api_get_player_count() -> Response: """Get the current amount of online players.""" return ORJSONResponse( { @@ -238,7 +237,7 @@ async def api_get_player_info( scope: Literal["stats", "info", "all"], user_id: int | None = Query(None, alias="id", ge=3, le=2_147_483_647), username: str | None = Query(None, alias="name", regex=regexes.USERNAME.pattern), -): +) -> Response: """Return information about a given player.""" if not (username or user_id) or (username and user_id): return ORJSONResponse( @@ -279,18 +278,35 @@ async def api_get_player_info( f"bancho:leaderboard:{idx}", str(resolved_user_id), ) - mode_stats["rank"] = rank + 1 if rank is not None else 0 - country_rank = await app.state.services.redis.zrevrank( f"bancho:leaderboard:{idx}:{resolved_country}", str(resolved_user_id), ) - mode_stats["country_rank"] = ( - country_rank + 1 if country_rank is not None else 0 - ) - mode = str(mode_stats.pop("mode")) - api_data["stats"][mode] = mode_stats + # NOTE: this dict-like return is intentional. + # but quite cursed. + stats_key = str(mode_stats["mode"]) + api_data["stats"][stats_key] = { + "id": mode_stats["id"], + "mode": mode_stats["mode"], + "tscore": mode_stats["tscore"], + "rscore": mode_stats["rscore"], + "pp": mode_stats["pp"], + "plays": mode_stats["plays"], + "playtime": mode_stats["playtime"], + "acc": mode_stats["acc"], + "max_combo": mode_stats["max_combo"], + "total_hits": mode_stats["total_hits"], + "replay_views": mode_stats["replay_views"], + "xh_count": mode_stats["xh_count"], + "x_count": mode_stats["x_count"], + "sh_count": mode_stats["sh_count"], + "s_count": mode_stats["s_count"], + "a_count": mode_stats["a_count"], + # extra fields are added to the api response + "rank": rank + 1 if rank is not None else 0, + "country_rank": country_rank + 1 if country_rank is not None else 0, + } return ORJSONResponse({"status": "success", "player": api_data}) @@ -299,7 +315,7 @@ async def api_get_player_info( async def api_get_player_status( user_id: int | None = Query(None, alias="id", ge=3, le=2_147_483_647), username: str | None = Query(None, alias="name", regex=regexes.USERNAME.pattern), -): +) -> Response: """Return a players current status, if they are online.""" if username and user_id: return ORJSONResponse( @@ -374,7 +390,7 @@ async def api_get_player_scores( limit: int = Query(25, ge=1, le=100), include_loved: bool = False, include_failed: bool = True, -): +) -> Response: """Return a list of a given user's recent/best scores.""" if mode_arg in ( GameMode.RELAX_MANIA, @@ -413,12 +429,11 @@ async def api_get_player_scores( mode = GameMode(mode_arg) + strong_equality = True if mods_arg is not None: if mods_arg[0] in ("~", "="): # weak/strong equality strong_equality = mods_arg[0] == "=" mods_arg = mods_arg[1:] - else: # use strong as default - strong_equality = True if mods_arg.isdecimal(): # parse from int form @@ -446,7 +461,7 @@ async def api_get_player_scores( } if mods is not None: - if strong_equality: # type: ignore + if strong_equality: query.append("AND t.mods & :mods = :mods") else: query.append("AND t.mods & :mods != 0") @@ -508,7 +523,7 @@ async def api_get_player_most_played( username: str | None = Query(None, alias="name", regex=regexes.USERNAME.pattern), mode_arg: int = Query(0, alias="mode", ge=0, le=11), limit: int = Query(25, ge=1, le=100), -): +) -> Response: """Return the most played beatmaps of a given player.""" # NOTE: this will almost certainly not scale well, lol. if mode_arg in ( @@ -568,7 +583,7 @@ async def api_get_player_most_played( async def api_get_map_info( map_id: int | None = Query(None, alias="id", ge=3, le=2_147_483_647), md5: str | None = Query(None, alias="md5", min_length=32, max_length=32), -): +) -> Response: """Return information about a given beatmap.""" if map_id is not None: bmap = await Beatmap.from_bid(map_id) @@ -602,7 +617,7 @@ async def api_get_map_scores( mods_arg: str | None = Query(None, alias="mods"), mode_arg: int = Query(0, alias="mode", ge=0, le=11), limit: int = Query(50, ge=1, le=100), -): +) -> Response: """Return the top n scores on a given beatmap.""" if mode_arg in ( GameMode.RELAX_MANIA, @@ -635,12 +650,11 @@ async def api_get_map_scores( mode = GameMode(mode_arg) + strong_equality = True if mods_arg is not None: - if mods_arg[0] in ("~", "="): # weak/strong equality + if mods_arg[0] in ("~", "="): strong_equality = mods_arg[0] == "=" mods_arg = mods_arg[1:] - else: # use strong as default - strong_equality = True if mods_arg.isdecimal(): # parse from int form @@ -673,7 +687,7 @@ async def api_get_map_scores( } if mods is not None: - if strong_equality: # type: ignore + if strong_equality: query.append("AND mods & :mods = :mods") else: query.append("AND mods & :mods != 0") @@ -703,7 +717,7 @@ async def api_get_map_scores( @router.get("/get_score_info") async def api_get_score_info( score_id: int = Query(..., alias="id", ge=0, le=9_223_372_036_854_775_807), -): +) -> Response: """Return information about a given score.""" score = await scores_repo.fetch_one(score_id) @@ -722,7 +736,7 @@ async def api_get_score_info( async def api_get_replay( score_id: int = Query(..., alias="id", ge=0, le=9_223_372_036_854_775_807), include_headers: bool = True, -): +) -> Response: """Return a given replay (including headers).""" # fetch replay file & make sure it exists replay_file = REPLAYS_PATH / f"{score_id}.osr" @@ -825,7 +839,7 @@ async def api_get_replay( 'attachment; filename="{username} - ' "{artist} - {title} [{version}] " '({play_time:%Y-%m-%d}).osr"' - ).format(**row), + ).format(**dict(row._mapping)), }, ) @@ -833,7 +847,7 @@ async def api_get_replay( @router.get("/get_match") async def api_get_match( match_id: int = Query(..., alias="id", ge=1, le=64), -): +) -> Response: """Return information of a given multiplayer match.""" # TODO: eventually, this should contain recent score info. @@ -887,7 +901,7 @@ async def api_get_global_leaderboard( limit: int = Query(50, ge=1, le=100), offset: int = Query(0, min=0, max=2_147_483_647), country: str | None = Query(None, min_length=2, max_length=2), -): +) -> Response: if mode_arg in ( GameMode.RELAX_MANIA, GameMode.AUTOPILOT_CATCH, @@ -929,7 +943,7 @@ async def api_get_global_leaderboard( @router.get("/get_clan") async def api_get_clan( clan_id: int = Query(..., alias="id", ge=1, le=2_147_483_647), -): +) -> Response: """Return information of a given clan.""" # TODO: fetching by name & tag (requires safe_name, safe_tag) @@ -978,7 +992,7 @@ async def api_get_clan( @router.get("/get_mappool") async def api_get_pool( pool_id: int = Query(..., alias="id", ge=1, le=2_147_483_647), -): +) -> Response: """Return information of a given mappool.""" # TODO: fetching by name (requires safe_name) diff --git a/app/api/v2/common/responses.py b/app/api/v2/common/responses.py index 09c536ae..a25cf4a9 100644 --- a/app/api/v2/common/responses.py +++ b/app/api/v2/common/responses.py @@ -41,7 +41,7 @@ def failure( # TODO: error code message: str, status_code: int = 400, - headers: dict | None = None, + headers: dict[str, Any] | None = None, ) -> Any: data = {"status": "error", "error": message} return json.ORJSONResponse(data, status_code, headers) diff --git a/app/api/v2/models/__init__.py b/app/api/v2/models/__init__.py index 5cbb2447..7b08fd51 100644 --- a/app/api/v2/models/__init__.py +++ b/app/api/v2/models/__init__.py @@ -7,7 +7,7 @@ from pydantic import BaseModel as _pydantic_BaseModel -T = TypeVar("T", bound=type["BaseModel"]) +T = TypeVar("T", bound="BaseModel") class BaseModel(_pydantic_BaseModel): @@ -15,5 +15,5 @@ class Config: str_strip_whitespace = True @classmethod - def from_mapping(cls: T, mapping: Mapping[str, Any]) -> T: - return cls(**{k: mapping[k] for k in cls.__fields__}) + def from_mapping(cls: type[T], mapping: Mapping[str, Any]) -> T: + return cls(**{k: mapping[k] for k in cls.model_fields}) diff --git a/app/bg_loops.py b/app/bg_loops.py index 60722125..5a8a0dbe 100644 --- a/app/bg_loops.py +++ b/app/bg_loops.py @@ -61,7 +61,7 @@ async def _remove_expired_donation_privileges(interval: int) -> None: {"id": player.id}, ) - if player.online: + if player.is_online: player.enqueue( app.packets.notification("Your supporter status has expired."), ) diff --git a/app/commands.py b/app/commands.py index a7674071..6c72c14e 100644 --- a/app/commands.py +++ b/app/commands.py @@ -66,6 +66,9 @@ from app.objects.channel import Channel +R = TypeVar("R") + + BEATMAPS_PATH = Path.cwd() / ".data/osu" @@ -393,17 +396,20 @@ async def top(ctx: Context) -> str | None: # !top rx!std mode = GAMEMODE_REPR_LIST.index(ctx.args[0]) - scores = await app.state.services.database.fetch_all( - "SELECT s.pp, b.artist, b.title, b.version, b.set_id map_set_id, b.id map_id " - "FROM scores s " - "LEFT JOIN maps b ON b.md5 = s.map_md5 " - "WHERE s.userid = :user_id " - "AND s.mode = :mode " - "AND s.status = 2 " - "AND b.status in (2, 3) " - "ORDER BY s.pp DESC LIMIT 10", - {"user_id": player.id, "mode": mode}, - ) + scores = [ + dict(s._mapping) + for s in await app.state.services.database.fetch_all( + "SELECT s.pp, b.artist, b.title, b.version, b.set_id map_set_id, b.id map_id " + "FROM scores s " + "LEFT JOIN maps b ON b.md5 = s.map_md5 " + "WHERE s.userid = :user_id " + "AND s.mode = :mode " + "AND s.status = 2 " + "AND b.status in (2, 3) " + "ORDER BY s.pp DESC LIMIT 10", + {"user_id": player.id, "mode": mode}, + ) + ] if not scores: return "No scores" @@ -856,7 +862,11 @@ async def user(ctx: Context) -> str | None: else: last_np = None - osu_version = player.client_details.osu_version.date if player.online else "Unknown" + if player.is_online and player.client_details is not None: + osu_version = player.client_details.osu_version.date + else: + osu_version = "Unknown" + donator_info = ( f"True (ends {timeago.format(player.donor_end)})" if player.priv & Privileges.DONATOR != 0 @@ -906,7 +916,7 @@ async def restrict(ctx: Context) -> str | None: await target.restrict(admin=ctx.player, reason=reason) # refresh their client state - if target.online: + if target.is_online: target.logout() return f"{target} was restricted." @@ -937,7 +947,7 @@ async def unrestrict(ctx: Context) -> str | None: await target.unrestrict(ctx.player, reason) # refresh their client state - if target.online: + if target.is_online: target.logout() return f"{target} was unrestricted." @@ -1339,8 +1349,6 @@ async def py(ctx: Context) -> str | None: # Most commands are open to player usage. """ -R = TypeVar("R", bound=Optional[str]) - def ensure_match( f: Callable[[Context, Match], Awaitable[R | None]], @@ -2265,7 +2273,7 @@ async def pool_info(ctx: Context) -> str | None: for (mods, slot), bmap in sorted( pool.maps.items(), - key=lambda x: (Mods.to_string(x[0][0]), x[0][1]), + key=lambda x: (repr(x[0][0]), x[0][1]), ): l.append(f"{mods!r}{slot}: {bmap.embed}") @@ -2350,7 +2358,7 @@ async def clan_create(ctx: Context) -> str | None: ) # announce clan creation - announce_chan = app.state.sessions.channels["#announce"] + announce_chan = app.state.sessions.channels.get_by_name("#announce") if announce_chan: msg = f"\x01ACTION founded {clan!r}." announce_chan.send(msg, sender=ctx.player, to_self=True) @@ -2390,7 +2398,7 @@ async def clan_disband(ctx: Context) -> str | None: member.clan_priv = None # announce clan disbanding - announce_chan = app.state.sessions.channels["#announce"] + announce_chan = app.state.sessions.channels.get_by_name("#announce") if announce_chan: msg = f"\x01ACTION disbanded {clan!r}." announce_chan.send(msg, sender=ctx.player, to_self=True) diff --git a/app/constants/mods.py b/app/constants/mods.py index 2e4576d8..ec186fca 100644 --- a/app/constants/mods.py +++ b/app/constants/mods.py @@ -120,6 +120,8 @@ def filter_invalid_combos(self, mode_vn: int) -> Mods: first_keymod = mod break + assert first_keymod is not None + # remove all but the first keymod. self &= ~(keymods_used & ~first_keymod) diff --git a/app/discord.py b/app/discord.py index 4395d7b5..8cf7b5b9 100644 --- a/app/discord.py +++ b/app/discord.py @@ -1,6 +1,8 @@ """Functionality related to Discord interactivity.""" from __future__ import annotations +from typing import Any + import aiohttp import orjson @@ -20,14 +22,14 @@ class Footer: - def __init__(self, text: str, **kwargs) -> None: + def __init__(self, text: str, **kwargs: Any) -> None: self.text = text self.icon_url = kwargs.get("icon_url") self.proxy_icon_url = kwargs.get("proxy_icon_url") class Image: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Any) -> None: self.url = kwargs.get("url") self.proxy_url = kwargs.get("proxy_url") self.height = kwargs.get("height") @@ -35,7 +37,7 @@ def __init__(self, **kwargs) -> None: class Thumbnail: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Any) -> None: self.url = kwargs.get("url") self.proxy_url = kwargs.get("proxy_url") self.height = kwargs.get("height") @@ -43,20 +45,20 @@ def __init__(self, **kwargs) -> None: class Video: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Any) -> None: self.url = kwargs.get("url") self.height = kwargs.get("height") self.width = kwargs.get("width") class Provider: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: str) -> None: self.url = kwargs.get("url") self.name = kwargs.get("name") class Author: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: str) -> None: self.name = kwargs.get("name") self.url = kwargs.get("url") self.icon_url = kwargs.get("icon_url") @@ -71,7 +73,7 @@ def __init__(self, name: str, value: str, inline: bool = False) -> None: class Embed: - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Any) -> None: self.title = kwargs.get("title") self.type = kwargs.get("type") self.description = kwargs.get("description") @@ -88,22 +90,22 @@ def __init__(self, **kwargs) -> None: self.fields: list[Field] = kwargs.get("fields", []) - def set_footer(self, **kwargs) -> None: + def set_footer(self, **kwargs: Any) -> None: self.footer = Footer(**kwargs) - def set_image(self, **kwargs) -> None: + def set_image(self, **kwargs: Any) -> None: self.image = Image(**kwargs) - def set_thumbnail(self, **kwargs) -> None: + def set_thumbnail(self, **kwargs: Any) -> None: self.thumbnail = Thumbnail(**kwargs) - def set_video(self, **kwargs) -> None: + def set_video(self, **kwargs: Any) -> None: self.video = Video(**kwargs) - def set_provider(self, **kwargs) -> None: + def set_provider(self, **kwargs: Any) -> None: self.provider = Provider(**kwargs) - def set_author(self, **kwargs) -> None: + def set_author(self, **kwargs: Any) -> None: self.author = Author(**kwargs) def add_field(self, name: str, value: str, inline: bool = False) -> None: @@ -113,7 +115,7 @@ def add_field(self, name: str, value: str, inline: bool = False) -> None: class Webhook: """A class to represent a single-use Discord webhook.""" - def __init__(self, url: str, **kwargs) -> None: + def __init__(self, url: str, **kwargs: Any) -> None: self.url = url self.content = kwargs.get("content") self.username = kwargs.get("username") @@ -126,7 +128,7 @@ def add_embed(self, embed: Embed) -> None: self.embeds.append(embed) @property - def json(self): + def json(self) -> str: if not any([self.content, self.file, self.embeds]): raise Exception( "Webhook must contain at least one " "of (content, file, embeds).", @@ -135,7 +137,7 @@ def json(self): if self.content and len(self.content) > 2000: raise Exception("Webhook content must be under " "2000 characters.") - payload = {"embeds": []} + payload: dict[str, Any] = {"embeds": []} for key in ("content", "username", "avatar_url", "tts", "file"): val = getattr(self, key) @@ -174,7 +176,7 @@ async def post(self, http_client: aiohttp.ClientSession | None = None) -> None: # use multipart/form-data instead of json payload. headers = {"Content-Type": "application/json"} async with _http_client.post(self.url, data=self.json, headers=headers) as resp: - if not resp or resp.status != 204: + if resp.status != 204: return # failed if not http_client: diff --git a/app/logging.py b/app/logging.py index c3817b01..538c99dd 100644 --- a/app/logging.py +++ b/app/logging.py @@ -3,8 +3,6 @@ import colorsys import datetime from enum import IntEnum -from typing import overload -from typing import Union from zoneinfo import ZoneInfo @@ -36,15 +34,7 @@ def __repr__(self) -> str: class RGB: - @overload - def __init__(self, rgb: int) -> None: - ... - - @overload - def __init__(self, r: int, g: int, b: int) -> None: - ... - - def __init__(self, *args) -> None: + def __init__(self, *args: int) -> None: largs = len(args) if largs == 3: @@ -69,10 +59,10 @@ class _Rainbow: Rainbow = _Rainbow() -Colour_Types = Union[Ansi, RGB, _Rainbow] +Colour_Types = Ansi | RGB | _Rainbow -def get_timestamp(full: bool = False, tz: datetime.tzinfo | None = None) -> str: +def get_timestamp(full: bool = False, tz: ZoneInfo | None = None) -> str: fmt = "%d/%m/%Y %I:%M:%S%p" if full else "%I:%M:%S%p" return f"{datetime.datetime.now(tz=tz):{fmt}}" @@ -82,7 +72,7 @@ def get_timestamp(full: bool = False, tz: datetime.tzinfo | None = None) -> str: _log_tz = ZoneInfo("GMT") # default -def set_timezone(tz: datetime.tzinfo) -> None: +def set_timezone(tz: ZoneInfo) -> None: global _log_tz _log_tz = tz @@ -153,8 +143,9 @@ def print_rainbow(msg: str, rainbow_end: float = 2 / 3, end: str = "\n") -> None def magnitude_fmt_time(t: int | float) -> str: # in nanosec + suffix = None for suffix in TIME_ORDER_SUFFIXES: if t < 1000: break t /= 1000 - return f"{t:.2f} {suffix}" # type: ignore + return f"{t:.2f} {suffix}" diff --git a/app/objects/achievement.py b/app/objects/achievement.py index 5b949301..41b0f99b 100644 --- a/app/objects/achievement.py +++ b/app/objects/achievement.py @@ -1,6 +1,10 @@ from __future__ import annotations from collections.abc import Callable +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from app.objects.score import Score __all__ = ("Achievement",) @@ -14,7 +18,7 @@ def __init__( file: str, name: str, desc: str, - cond: Callable, + cond: Callable[[Score, int], bool], # (score, mode) -> unlocked ) -> None: self.id = id self.file = file diff --git a/app/objects/beatmap.py b/app/objects/beatmap.py index 22cd7729..56425422 100644 --- a/app/objects/beatmap.py +++ b/app/objects/beatmap.py @@ -10,6 +10,7 @@ from enum import unique from pathlib import Path from typing import Any +from typing import cast from typing import TypedDict import aiohttp @@ -224,8 +225,8 @@ class Beatmap: maintaining a low overhead. The only methods you should need are: - await Beatmap.from_md5(md5: str, set_id: int = -1) -> Optional[Beatmap] - await Beatmap.from_bid(bid: int) -> Optional[Beatmap] + await Beatmap.from_md5(md5: str, set_id: int = -1) -> Beatmap | None + await Beatmap.from_bid(bid: int) -> Beatmap | None Properties: Beatmap.full -> str # Artist - Title [Version] @@ -237,11 +238,11 @@ class Beatmap: Beatmap.as_dict -> dict[str, object] Lower level API: - Beatmap._from_md5_cache(md5: str, check_updates: bool = True) -> Optional[Beatmap] - Beatmap._from_bid_cache(bid: int, check_updates: bool = True) -> Optional[Beatmap] + Beatmap._from_md5_cache(md5: str, check_updates: bool = True) -> Beatmap | None + Beatmap._from_bid_cache(bid: int, check_updates: bool = True) -> Beatmap | None - Beatmap._from_md5_sql(md5: str) -> Optional[Beatmap] - Beatmap._from_bid_sql(bid: int) -> Optional[Beatmap] + Beatmap._from_md5_sql(md5: str) -> Beatmap | None + Beatmap._from_bid_sql(bid: int) -> Beatmap | None Beatmap._parse_from_osuapi_resp(osuapi_resp: dict[str, object]) -> None @@ -531,7 +532,7 @@ async def fetch_rating(self) -> float | None: if row is None: return None - return row["rating"] + return cast(float | None, row["rating"]) class BeatmapSet: @@ -545,7 +546,7 @@ class BeatmapSet: information, while maintaining a low overhead. The only methods you should need are: - await BeatmapSet.from_bsid(bsid: int) -> Optional[BeatmapSet] + await BeatmapSet.from_bsid(bsid: int) -> BeatmapSet | None BeatmapSet.all_officially_ranked_or_approved() -> bool BeatmapSet.all_officially_loved() -> bool @@ -554,9 +555,9 @@ class BeatmapSet: BeatmapSet.url -> str # https://osu.cmyui.xyz/beatmapsets/123 Lower level API: - await BeatmapSet._from_bsid_cache(bsid: int) -> Optional[BeatmapSet] - await BeatmapSet._from_bsid_sql(bsid: int) -> Optional[BeatmapSet] - await BeatmapSet._from_bsid_osuapi(bsid: int) -> Optional[BeatmapSet] + await BeatmapSet._from_bsid_cache(bsid: int) -> BeatmapSet | None + await BeatmapSet._from_bsid_sql(bsid: int) -> BeatmapSet | None + await BeatmapSet._from_bsid_osuapi(bsid: int) -> BeatmapSet | None BeatmapSet._cache_expired() -> bool await BeatmapSet._update_if_available() -> None @@ -670,6 +671,9 @@ async def _update_if_available(self) -> None: updated_maps: list[Beatmap] = [] # TODO: optimize map_md5s_to_delete: set[str] = set() + # temp value for building the new beatmap + bmap: Beatmap + # find maps in our current state that've been deleted, or need updates for old_id, old_map in old_maps.items(): if old_id not in new_maps: @@ -696,7 +700,7 @@ async def _update_if_available(self) -> None: for new_id, new_map in new_maps.items(): if new_id not in old_maps: # new map we don't have locally, add it - bmap: Beatmap = Beatmap.__new__(Beatmap) + bmap = Beatmap.__new__(Beatmap) bmap.id = new_id bmap._parse_from_osuapi_resp(new_map) diff --git a/app/objects/collections.py b/app/objects/collections.py index 590cc221..a65be066 100644 --- a/app/objects/collections.py +++ b/app/objects/collections.py @@ -5,8 +5,7 @@ from collections.abc import Iterable from collections.abc import Iterator from collections.abc import Sequence -from typing import Optional -from typing import overload +from typing import Any import databases.core @@ -48,7 +47,7 @@ class Channels(list[Channel]): def __iter__(self) -> Iterator[Channel]: return super().__iter__() - def __contains__(self, o: Channel | str) -> bool: + def __contains__(self, o: object) -> bool: """Check whether internal list contains `o`.""" # Allow string to be passed to compare vs. name. if isinstance(o, str): @@ -56,29 +55,6 @@ def __contains__(self, o: Channel | str) -> bool: else: return super().__contains__(o) - @overload - def __getitem__(self, index: int) -> Channel: - ... - - @overload - def __getitem__(self, index: str) -> Channel: - ... - - @overload - def __getitem__(self, index: slice) -> list[Channel]: - ... - - def __getitem__( - self, - index: int | slice | str, - ) -> Channel | list[Channel]: - # XXX: can be either a string (to get by name), - # or a slice, for indexing the internal array. - if isinstance(index, str): - return self.get_by_name(index) # type: ignore - else: - return super().__getitem__(index) - def __repr__(self) -> str: # XXX: we use the "real" name, aka # #multi_1 instead of #multiplayer @@ -129,7 +105,7 @@ async def prepare(self, db_conn: databases.core.Connection) -> None: ) -class Matches(list[Optional[Match]]): +class Matches(list[Match | None]): """The currently active multiplayer matches on the server.""" def __init__(self) -> None: @@ -149,25 +125,7 @@ def get_free(self) -> int | None: return None - def append(self, match: Match) -> bool: - """Append `match` to the list.""" - free = self.get_free() - if free is not None: - # set the id of the match to the lowest available free. - match.id = free - self[free] = match - - if app.settings.DEBUG: - log(f"{match} added to matches list.") - - return True - else: - log(f"Match list is full! Could not add {match}.") - return False - - # TODO: extend - - def remove(self, match: Match) -> None: + def remove(self, match: Match | None) -> None: """Remove `match` from the list.""" for i, _m in enumerate(self): if match is _m: @@ -181,13 +139,13 @@ def remove(self, match: Match) -> None: class Players(list[Player]): """The currently active players on the server.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) def __iter__(self) -> Iterator[Player]: return super().__iter__() - def __contains__(self, player: Player | str) -> bool: + def __contains__(self, player: object) -> bool: # allow us to either pass in the player # obj, or the player name as a string. if isinstance(player, str): @@ -259,26 +217,31 @@ async def get_sql( if player is None: return None - # encode pw_bcrypt from str -> bytes. - player["pw_bcrypt"] = player["pw_bcrypt"].encode() - + clan: Clan | None = None + clan_priv: ClanPrivileges | None = None if player["clan_id"] != 0: - player["clan"] = app.state.sessions.clans.get(id=player["clan_id"]) - player["clan_priv"] = ClanPrivileges(player["clan_priv"]) - else: - player["clan"] = player["clan_priv"] = None - - # country from acronym to {acronym, numeric} - player["geoloc"] = { - "latitude": 0.0, # TODO - "longitude": 0.0, - "country": { - "acronym": player["country"], - "numeric": app.state.services.country_codes[player["country"]], + clan = app.state.sessions.clans.get(id=player["clan_id"]) + clan_priv = ClanPrivileges(player["clan_priv"]) + + return Player( + id=player["id"], + name=player["name"], + pw_bcrypt=player["pw_bcrypt"].encode(), + priv=Privileges(player["priv"]), + clan=clan, + clan_priv=clan_priv, + geoloc={ + "latitude": 0.0, + "longitude": 0.0, + "country": { + "acronym": player["country"], + "numeric": app.state.services.country_codes[player["country"]], + }, }, - } - - return Player(**player, token="") + silence_end=player["silence_end"], + donor_end=player["donor_end"], + api_key=player["api_key"], + ) async def from_cache_or_sql( self, @@ -343,28 +306,6 @@ class MapPools(list[MapPool]): def __iter__(self) -> Iterator[MapPool]: return super().__iter__() - @overload - def __getitem__(self, index: int) -> MapPool: - ... - - @overload - def __getitem__(self, index: str) -> MapPool: - ... - - @overload - def __getitem__(self, index: slice) -> list[MapPool]: - ... - - def __getitem__( - self, - index: int | slice | str, - ) -> MapPool | list[MapPool]: - """Allow slicing by either a string (for name), or slice.""" - if isinstance(index, str): - return self.get_by_name(index) # type: ignore - else: - return super().__getitem__(index) - def get( self, id: int | None = None, @@ -381,7 +322,7 @@ def get( return None - def __contains__(self, o: MapPool | str) -> bool: + def __contains__(self, o: object) -> bool: """Check whether internal list contains `o`.""" # Allow string to be passed to compare vs. name. if isinstance(o, str): @@ -444,26 +385,7 @@ class Clans(list[Clan]): def __iter__(self) -> Iterator[Clan]: return super().__iter__() - @overload - def __getitem__(self, index: int) -> Clan: - ... - - @overload - def __getitem__(self, index: str) -> Clan: - ... - - @overload - def __getitem__(self, index: slice) -> list[Clan]: - ... - - def __getitem__(self, index: int | str | slice): - """Allow slicing by either a string (for name), or slice.""" - if isinstance(index, str): - return self.get(name=index) - else: - return super().__getitem__(index) - - def __contains__(self, o: Clan | str) -> bool: + def __contains__(self, o: object) -> bool: """Check whether internal list contains `o`.""" # Allow string to be passed to compare vs. name. if isinstance(o, str): diff --git a/app/objects/match.py b/app/objects/match.py index dba75146..9d3a865e 100644 --- a/app/objects/match.py +++ b/app/objects/match.py @@ -190,7 +190,7 @@ class Match: slots: list[`Slot`] A list of 16 `Slot` objects representing the match's slots. - starting: Optional[dict[str, `TimerHandle`]] + starting: dict[str, `TimerHandle`] | None Used when the match is started with !mp start . It stores both the starting timer, and the chat alert timers. @@ -269,7 +269,7 @@ def url(self) -> str: return f"osump://{self.id}/{self.passwd}" @property - def map_url(self): + def map_url(self) -> str: """The osu! beatmap url for `self`'s map.""" return f"https://osu.{app.settings.DOMAIN}/beatmapsets/#/{self.map_id}" @@ -349,7 +349,7 @@ def enqueue( """Add data to be sent to all clients in the match.""" self.chat.enqueue(data, immune) - lchan = app.state.sessions.channels["#lobby"] + lchan = app.state.sessions.channels.get_by_name("#lobby") if lobby and lchan and lchan.players: lchan.enqueue(data) @@ -360,7 +360,7 @@ def enqueue_state(self, lobby: bool = True) -> None: # send password only to users currently in the match. self.chat.enqueue(app.packets.update_match(self, send_pw=True)) - lchan = app.state.sessions.channels["#lobby"] + lchan = app.state.sessions.channels.get_by_name("#lobby") if lobby and lchan and lchan.players: lchan.enqueue(app.packets.update_match(self, send_pw=False)) @@ -399,7 +399,7 @@ async def await_submissions( """Await score submissions from all players in completed state.""" scores: dict[MatchTeams | Player, int] = defaultdict(int) didnt_submit: list[Player] = [] - time_waited = 0 # allow up to 10s (total, not per player) + time_waited = 0.0 # allow up to 10s (total, not per player) ffa = self.team_type in (MatchTeamTypes.head_to_head, MatchTeamTypes.tag_coop) @@ -418,20 +418,24 @@ async def await_submissions( # continue trying to fetch each player's # scores until they've all been submitted. while True: + assert s.player is not None rc_score = s.player.recent_score + assert rc_score is not None + max_age = datetime.now() - timedelta( seconds=bmap.total_length + time_waited + 0.5, ) + assert rc_score.bmap is not None if ( rc_score and rc_score.bmap.md5 == self.map_md5 and rc_score.server_time > max_age ): # score found, add to our scores dict if != 0. - score = getattr(rc_score, win_cond) + score: int = getattr(rc_score, win_cond) if score: - key = s.player if ffa else s.team + key: MatchTeams | Player = s.player if ffa else s.team scores[key] += score break @@ -506,6 +510,8 @@ def add_suffix(score: int | float) -> str | int | float: return str(score) if ffa: + assert isinstance(winner, Player) + msg.append( f"{winner.name} takes the point! ({add_suffix(scores[winner])} " f"[Match avg. {add_suffix(sum(scores.values()) / len(scores))}])", @@ -531,6 +537,8 @@ def add_suffix(score: int | float) -> str | int | float: del m else: # teams + assert isinstance(winner, MatchTeams) + r_match = regexes.TOURNEY_MATCHNAME.match(self.name) if r_match: match_name = r_match["name"] @@ -543,7 +551,10 @@ def add_suffix(score: int | float) -> str | int | float: team_names = {MatchTeams.blue: "Blue", MatchTeams.red: "Red"} # teams are binary, so we have a loser. - loser = MatchTeams({1: 2, 2: 1}[winner]) + if winner is MatchTeams.blue: + loser = MatchTeams.red + else: + loser = MatchTeams.blue # from match name if available, else blue/red. wname = team_names[winner] diff --git a/app/objects/player.py b/app/objects/player.py index c561c1b6..4c70017b 100644 --- a/app/objects/player.py +++ b/app/objects/player.py @@ -9,6 +9,7 @@ from enum import unique from functools import cached_property from typing import Any +from typing import cast from typing import TYPE_CHECKING from typing import TypedDict @@ -36,7 +37,6 @@ from app.objects.menu import MenuCommands from app.objects.menu import MenuFunction from app.objects.score import Grade -from app.objects.score import Score from app.repositories import stats as stats_repo from app.utils import escape_enum from app.utils import make_safe_name @@ -46,6 +46,7 @@ from app.objects.achievement import Achievement from app.objects.beatmap import Beatmap from app.objects.clan import Clan + from app.objects.score import Score from app.constants.privileges import ClanPrivileges __all__ = ("ModeData", "Status", "Player") @@ -253,7 +254,7 @@ def __init__( # generate a token if not given token = extras.get("token", None) - if token is not None: + if token is not None and isinstance(token, str): self.token = token else: self.token = self.generate_token() @@ -334,7 +335,7 @@ def __repr__(self) -> str: return f"<{self.name} ({self.id})>" @property - def online(self) -> bool: + def is_online(self) -> bool: return self.token != "" @property @@ -479,7 +480,7 @@ async def add_privs(self, bits: Privileges) -> None: if "bancho_priv" in self.__dict__: del self.bancho_priv # wipe cached_property - if self.online: + if self.is_online: # if they're online, send a packet # to update their client-side privileges self.enqueue(app.packets.bancho_privileges(self.bancho_priv)) @@ -496,7 +497,7 @@ async def remove_privs(self, bits: Privileges) -> None: if "bancho_priv" in self.__dict__: del self.bancho_priv # wipe cached_property - if self.online: + if self.is_online: # if they're online, send a packet # to update their client-side privileges self.enqueue(app.packets.bancho_privileges(self.bancho_priv)) @@ -532,7 +533,7 @@ async def restrict(self, admin: Player, reason: str) -> None: await webhook.post(app.state.services.http_client) # refresh their client state - if self.online: + if self.is_online: self.logout() async def unrestrict(self, admin: Player, reason: str) -> None: @@ -546,7 +547,7 @@ async def unrestrict(self, admin: Player, reason: str) -> None: {"from": admin.id, "to": self.id, "action": "unrestrict", "msg": reason}, ) - if not self.online: + if not self.is_online: async with app.state.services.database.connection() as db_conn: await self.stats_from_sql_full(db_conn) @@ -569,7 +570,7 @@ async def unrestrict(self, admin: Player, reason: str) -> None: webhook = Webhook(webhook_url, content=log_msg) await webhook.post(app.state.services.http_client) - if self.online: + if self.is_online: # log the user out if they're offline, this # will simply relog them and refresh their app.state self.logout() @@ -658,7 +659,7 @@ def join_match(self, match: Match, passwd: str) -> bool: log(f"{self} failed to join {match.chat}.", Ansi.LYELLOW) return False - lobby = app.state.sessions.channels["#lobby"] + lobby = app.state.sessions.channels.get_by_name("#lobby") if lobby in self.channels: self.leave_channel(lobby) @@ -713,7 +714,7 @@ def leave_match(self) -> None: app.state.sessions.matches.remove(self.match) - lobby = app.state.sessions.channels["#lobby"] + lobby = app.state.sessions.channels.get_by_name("#lobby") if lobby: lobby.enqueue(app.packets.dispose_match(self.match.id)) @@ -828,7 +829,7 @@ def add_spectator(self, player: Player) -> None: """Attempt to add `player` to `self`'s spectators.""" chan_name = f"#spec_{self.id}" - spec_chan = app.state.sessions.channels[chan_name] + spec_chan = app.state.sessions.channels.get_by_name(chan_name) if not spec_chan: # spectator chan doesn't exist, create it. spec_chan = Channel( @@ -869,7 +870,9 @@ def remove_spectator(self, player: Player) -> None: self.spectators.remove(player) player.spectating = None - channel = app.state.sessions.channels[f"#spec_{self.id}"] + channel = app.state.sessions.channels.get_by_name(f"#spec_{self.id}") + assert channel is not None + player.leave_channel(channel) if not self.spectators: @@ -1003,7 +1006,7 @@ async def get_global_rank(self, mode: GameMode) -> int: f"bancho:leaderboard:{mode.value}", str(self.id), ) - return rank + 1 if rank is not None else 0 + return cast(int, rank) + 1 if rank is not None else 0 async def get_country_rank(self, mode: GameMode) -> int: if self.restricted: @@ -1015,7 +1018,7 @@ async def get_country_rank(self, mode: GameMode) -> int: str(self.id), ) - return rank + 1 if rank is not None else 0 + return cast(int, rank) + 1 if rank is not None else 0 async def update_rank(self, mode: GameMode) -> int: country = self.geoloc["country"]["acronym"] diff --git a/app/objects/score.py b/app/objects/score.py index 31e14250..fa8d3c3f 100644 --- a/app/objects/score.py +++ b/app/objects/score.py @@ -6,6 +6,7 @@ from enum import IntEnum from enum import unique from pathlib import Path +from typing import cast from typing import TYPE_CHECKING import app.state @@ -88,10 +89,10 @@ class Score: Possibly confusing attributes ----------- - bmap: Optional[`Beatmap`] + bmap: `Beatmap | None` A beatmap obj representing the osu map. - player: Optional[`Player`] + player: `Player | None` A player obj of the player who submitted the score. grade: `Grade` @@ -109,7 +110,7 @@ class Score: client_flags: `int` osu!'s old anticheat flags. - prev_best: Optional[`Score`] + prev_best: `Score | None` The previous best score before this play was submitted. NOTE: just because a score has a `prev_best` attribute does mean the score is our best score on the map! the `status` @@ -159,6 +160,7 @@ def __init__(self) -> None: def __repr__(self) -> str: # TODO: i really need to clean up my reprs try: + assert self.bmap is not None return ( f"<{self.acc:.2f}% {self.max_combo}x {self.nmiss}M " f"#{self.rank} on {self.bmap.full_name} for {self.pp:,.2f}pp>" @@ -187,7 +189,7 @@ async def from_sql(cls, score_id: int) -> Score | None: s.pp = rec["pp"] s.score = rec["score"] s.max_combo = rec["max_combo"] - s.mods = rec["mods"] + s.mods = Mods(rec["mods"]) s.acc = rec["acc"] s.n300 = rec["n300"] s.n100 = rec["n100"] @@ -195,23 +197,16 @@ async def from_sql(cls, score_id: int) -> Score | None: s.nmiss = rec["nmiss"] s.ngeki = rec["ngeki"] s.nkatu = rec["nkatu"] - s.grade = rec["grade"] - s.perfect = rec["perfect"] - s.status = rec["status"] - s.mode = rec["mode"] + s.grade = Grade.from_str(rec["grade"]) + s.perfect = rec["perfect"] == 1 + s.status = SubmissionStatus(rec["status"]) + s.passed = s.status != SubmissionStatus.FAILED + s.mode = GameMode(rec["mode"]) s.server_time = rec["play_time"] s.time_elapsed = rec["time_elapsed"] - s.client_flags = rec["client_flags"] + s.client_flags = ClientFlags(rec["client_flags"]) s.client_checksum = rec["online_checksum"] - # fix some types - s.passed = s.status != 0 - s.status = SubmissionStatus(s.status) - s.grade = Grade.from_str(s.grade) - s.mods = Mods(s.mods) - s.mode = GameMode(s.mode) - s.client_flags = ClientFlags(s.client_flags) - if s.bmap: s.rank = await s.calculate_placement() @@ -323,7 +318,7 @@ async def calculate_placement(self) -> int: ) # TODO: idk if returns none - return better_scores + 1 # if better_scores is not None else 1 + return cast(int, better_scores) + 1 # if better_scores is not None else 1 def calculate_performance(self, osu_file_path: Path) -> tuple[float, float]: """Calculate PP and star rating for our score.""" diff --git a/app/packets.py b/app/packets.py index 5dc9e6aa..2e168ef3 100644 --- a/app/packets.py +++ b/app/packets.py @@ -14,6 +14,7 @@ from functools import cache from functools import lru_cache from typing import Any +from typing import cast from typing import NamedTuple from typing import TYPE_CHECKING @@ -404,23 +405,23 @@ def read_u64(self) -> int: def read_f16(self) -> float: (val,) = struct.unpack_from(" float: (val,) = struct.unpack_from(" float: (val,) = struct.unpack_from(" tuple[int]: + def read_i32_list_i16l(self) -> tuple[int, ...]: length = int.from_bytes(self.body_view[:2], "little") self.body_view = self.body_view[2:] @@ -428,7 +429,7 @@ def read_i32_list_i16l(self) -> tuple[int]: self.body_view = self.body_view[length * 4 :] return val - def read_i32_list_i32l(self) -> tuple[int]: + def read_i32_list_i32l(self) -> tuple[int, ...]: length = int.from_bytes(self.body_view[:4], "little") self.body_view = self.body_view[4:] @@ -645,6 +646,7 @@ def write_match(m: Match, send_pw: bool = True) -> bytearray: for s in m.slots: if s.status & 0b01111100 != 0: # SlotStatus.has_player + assert s.player is not None ret += s.player.id.to_bytes(4, "little") ret += m.host.id.to_bytes(4, "little") @@ -1172,6 +1174,7 @@ def restart_server(ms: int) -> bytes: # packet id: 88 def match_invite(player: Player, target_name: str) -> bytes: + assert player.match is not None msg = f"Come join my game: {player.match.embed}." return write( ServerPackets.MATCH_INVITE, diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/repositories/achievements.py b/app/repositories/achievements.py index 5334b9b8..50d8a4f7 100644 --- a/app/repositories/achievements.py +++ b/app/repositories/achievements.py @@ -1,6 +1,7 @@ from __future__ import annotations import textwrap +from typing import Any from typing import cast from typing import TypedDict @@ -51,7 +52,7 @@ async def create( INSERT INTO achievements (file, name, desc, cond) VALUES (:file, :name, :desc, :cond) """ - params = { + params: dict[str, Any] = { "file": file, "name": name, "desc": desc, @@ -70,7 +71,7 @@ async def create( achievement = await app.state.services.database.fetch_one(query, params) assert achievement is not None - return cast(Achievement, achievement) + return cast(Achievement, dict(achievement._mapping)) async def fetch_one( @@ -87,13 +88,17 @@ async def fetch_one( WHERE id = COALESCE(:id, id) OR name = COALESCE(:name, name) """ - params = { + params: dict[str, Any] = { "id": id, "name": name, } achievement = await app.state.services.database.fetch_one(query, params) - return cast(Achievement, achievement) if achievement is not None else None + return ( + cast(Achievement, dict(achievement._mapping)) + if achievement is not None + else None + ) async def fetch_count() -> int: @@ -102,11 +107,11 @@ async def fetch_count() -> int: SELECT COUNT(*) AS count FROM achievements """ - params = {} + params: dict[str, Any] = {} rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -118,7 +123,7 @@ async def fetch_many( SELECT {READ_PARAMS} FROM achievements """ - params = {} + params: dict[str, Any] = {} if page is not None and page_size is not None: query += """\ @@ -129,7 +134,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size achievements = await app.state.services.database.fetch_all(query, params) - return cast(list[Achievement], achievements) if achievements is not None else None + return cast(list[Achievement], [dict(a._mapping) for a in achievements]) async def update( @@ -140,7 +145,7 @@ async def update( cond: str | _UnsetSentinel = UNSET, ) -> Achievement | None: """Update an existing achievement.""" - update_fields = AchievementUpdateFields = {} + update_fields: AchievementUpdateFields = {} if not isinstance(file, _UnsetSentinel): update_fields["file"] = file if not isinstance(name, _UnsetSentinel): @@ -163,7 +168,7 @@ async def update( FROM achievements WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } achievement = await app.state.services.database.fetch_one(query, params) @@ -179,11 +184,11 @@ async def delete( FROM achievements WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } - rec = await app.state.services.database.fetch_one(query, params) - if rec is None: + achievement = await app.state.services.database.fetch_one(query, params) + if achievement is None: return None query = """\ @@ -193,5 +198,9 @@ async def delete( params = { "id": id, } - achievement = await app.state.services.database.execute(query, params) - return cast(Achievement, achievement) if achievement is not None else None + await app.state.services.database.execute(query, params) + return ( + cast(Achievement, dict(achievement._mapping)) + if achievement is not None + else None + ) diff --git a/app/repositories/channels.py b/app/repositories/channels.py index 2f552292..04d0962c 100644 --- a/app/repositories/channels.py +++ b/app/repositories/channels.py @@ -1,6 +1,7 @@ from __future__ import annotations import textwrap +from typing import Any from typing import cast from typing import TypedDict @@ -56,7 +57,7 @@ async def create( VALUES (:name, :topic, :read_priv, :write_priv, :auto_join) """ - params = { + params: dict[str, Any] = { "name": name, "topic": topic, "read_priv": read_priv, @@ -77,7 +78,7 @@ async def create( channel = await app.state.services.database.fetch_one(query, params) assert channel is not None - return cast(Channel, channel) + return cast(Channel, dict(channel._mapping)) async def fetch_one( @@ -93,13 +94,13 @@ async def fetch_one( WHERE id = COALESCE(:id, id) AND name = COALESCE(:name, name) """ - params = { + params: dict[str, Any] = { "id": id, "name": name, } channel = await app.state.services.database.fetch_one(query, params) - return cast(Channel, channel) if channel is not None else None + return cast(Channel, dict(channel._mapping)) if channel is not None else None async def fetch_count( @@ -117,7 +118,7 @@ async def fetch_count( AND write_priv = COALESCE(:write_priv, write_priv) AND auto_join = COALESCE(:auto_join, auto_join) """ - params = { + params: dict[str, Any] = { "read_priv": read_priv, "write_priv": write_priv, "auto_join": auto_join, @@ -125,7 +126,7 @@ async def fetch_count( rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -143,7 +144,7 @@ async def fetch_many( AND write_priv = COALESCE(:write_priv, write_priv) AND auto_join = COALESCE(:auto_join, auto_join) """ - params = { + params: dict[str, Any] = { "read_priv": read_priv, "write_priv": write_priv, "auto_join": auto_join, @@ -158,7 +159,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size channels = await app.state.services.database.fetch_all(query, params) - return cast(list[Channel], channels) if channels is not None else None + return cast(list[Channel], [dict(c._mapping) for c in channels]) async def update( @@ -169,7 +170,7 @@ async def update( auto_join: bool | _UnsetSentinel = UNSET, ) -> Channel | None: """Update a channel in the database.""" - update_fields = ChannelUpdateFields = {} + update_fields: ChannelUpdateFields = {} if not isinstance(topic, _UnsetSentinel): update_fields["topic"] = topic if not isinstance(read_priv, _UnsetSentinel): @@ -192,11 +193,11 @@ async def update( FROM channels WHERE name = :name """ - params = { + params: dict[str, Any] = { "name": name, } channel = await app.state.services.database.fetch_one(query, params) - return cast(Channel, channel) if channel is not None else None + return cast(Channel, dict(channel._mapping)) if channel is not None else None async def delete( @@ -208,7 +209,7 @@ async def delete( FROM channels WHERE name = :name """ - params = { + params: dict[str, Any] = { "name": name, } rec = await app.state.services.database.fetch_one(query, params) @@ -223,4 +224,4 @@ async def delete( "name": name, } channel = await app.state.services.database.execute(query, params) - return cast(Channel, channel) if channel is not None else None + return cast(Channel, dict(channel._mapping)) if channel is not None else None diff --git a/app/repositories/clans.py b/app/repositories/clans.py index dcfe4934..fc11ef67 100644 --- a/app/repositories/clans.py +++ b/app/repositories/clans.py @@ -1,6 +1,8 @@ from __future__ import annotations import textwrap +from datetime import datetime +from typing import Any from typing import cast from typing import TypedDict @@ -30,7 +32,7 @@ class Clan(TypedDict): name: str tag: str owner: int - created_at: str + created_at: datetime class ClanUpdateFields(TypedDict, total=False): @@ -49,7 +51,7 @@ async def create( INSERT INTO clans (name, tag, owner, created_at) VALUES (:name, :tag, :owner, NOW()) """ - params = { + params: dict[str, Any] = { "name": name, "tag": tag, "owner": owner, @@ -67,7 +69,7 @@ async def create( clan = await app.state.services.database.fetch_one(query, params) assert clan is not None - return cast(Clan, clan) + return cast(Clan, dict(clan._mapping)) async def fetch_one( @@ -88,10 +90,10 @@ async def fetch_one( AND tag = COALESCE(:tag, tag) AND owner = COALESCE(:owner, owner) """ - params = {"id": id, "name": name, "tag": tag, "owner": owner} + params: dict[str, Any] = {"id": id, "name": name, "tag": tag, "owner": owner} clan = await app.state.services.database.fetch_one(query, params) - return cast(Clan, clan) if clan is not None else None + return cast(Clan, dict(clan._mapping)) if clan is not None else None async def fetch_count() -> int: @@ -102,7 +104,7 @@ async def fetch_count() -> int: """ rec = await app.state.services.database.fetch_one(query) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -114,7 +116,7 @@ async def fetch_many( SELECT {READ_PARAMS} FROM clans """ - params = {} + params: dict[str, Any] = {} if page is not None and page_size is not None: query += """\ @@ -125,7 +127,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size clans = await app.state.services.database.fetch_all(query, params) - return cast(list[Clan], clans) if clans is not None else None + return cast(list[Clan], [dict(c._mapping) for c in clans]) async def update( @@ -156,11 +158,11 @@ async def update( FROM clans WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } clan = await app.state.services.database.fetch_one(query, params) - return cast(Clan, clan) if clan is not None else None + return cast(Clan, dict(clan._mapping)) if clan is not None else None async def delete(id: int) -> Clan | None: @@ -170,7 +172,7 @@ async def delete(id: int) -> Clan | None: FROM clans WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } rec = await app.state.services.database.fetch_one(query, params) @@ -181,8 +183,6 @@ async def delete(id: int) -> Clan | None: DELETE FROM clans WHERE id = :id """ - params = { - "id": id, - } + params = {"id": id} clan = await app.state.services.database.execute(query, params) - return cast(Clan, clan) if clan is not None else None + return cast(Clan, dict(clan._mapping)) if clan is not None else None diff --git a/app/repositories/maps.py b/app/repositories/maps.py index 9e3c13e8..1d55000c 100644 --- a/app/repositories/maps.py +++ b/app/repositories/maps.py @@ -121,7 +121,7 @@ async def create( od: float, hp: float, diff: float, -) -> dict[str, Any]: +) -> Map: """Create a new beatmap entry in the database.""" query = f"""\ INSERT INTO maps (id, server, set_id, status, md5, artist, title, @@ -133,7 +133,7 @@ async def create( :max_combo, :frozen, :plays, :passes, :mode, :bpm, :cs, :ar, :od, :hp, :diff) """ - params = { + params: dict[str, Any] = { "id": id, "server": server, "set_id": set_id, @@ -171,7 +171,7 @@ async def create( map = await app.state.services.database.fetch_one(query, params) assert map is not None - return cast(Map, map) + return cast(Map, dict(map._mapping)) async def fetch_one( @@ -190,14 +190,14 @@ async def fetch_one( AND md5 = COALESCE(:md5, md5) AND filename = COALESCE(:filename, filename) """ - params = { + params: dict[str, Any] = { "id": id, "md5": md5, "filename": filename, } map = await app.state.services.database.fetch_one(query, params) - return cast(Map, map) if map is not None else None + return cast(Map, dict(map._mapping)) if map is not None else None async def fetch_count( @@ -224,7 +224,7 @@ async def fetch_count( AND frozen = COALESCE(:frozen, frozen) """ - params = { + params: dict[str, Any] = { "server": server, "set_id": set_id, "status": status, @@ -236,7 +236,7 @@ async def fetch_count( } rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -264,7 +264,7 @@ async def fetch_many( AND mode = COALESCE(:mode, mode) AND frozen = COALESCE(:frozen, frozen) """ - params = { + params: dict[str, Any] = { "server": server, "set_id": set_id, "status": status, @@ -284,7 +284,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size maps = await app.state.services.database.fetch_all(query, params) - return cast(list[Map], maps) if maps is not None else None + return cast(list[Map], [dict(m._mapping) for m in maps]) async def update( @@ -372,11 +372,11 @@ async def update( FROM maps WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } map = await app.state.services.database.fetch_one(query, params) - return cast(Map, map) if map is not None else None + return cast(Map, dict(map._mapping)) if map is not None else None async def delete(id: int) -> Map | None: @@ -386,7 +386,7 @@ async def delete(id: int) -> Map | None: FROM maps WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } rec = await app.state.services.database.fetch_one(query, params) @@ -401,4 +401,4 @@ async def delete(id: int) -> Map | None: "id": id, } map = await app.state.services.database.execute(query, params) - return cast(Map, map) if map is not None else None + return cast(Map, dict(map._mapping)) if map is not None else None diff --git a/app/repositories/players.py b/app/repositories/players.py index 8e2c8289..38c75a10 100644 --- a/app/repositories/players.py +++ b/app/repositories/players.py @@ -1,6 +1,7 @@ from __future__ import annotations import textwrap +from typing import Any from typing import cast from typing import TypedDict @@ -47,6 +48,7 @@ class Player(TypedDict): name: str safe_name: str priv: int + pw_bcrypt: str country: str silence_end: int donor_end: int @@ -92,7 +94,7 @@ async def create( INSERT INTO users (name, safe_name, email, pw_bcrypt, country, creation_time, latest_activity) VALUES (:name, :safe_name, :email, :pw_bcrypt, :country, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()) """ - params = { + params: dict[str, Any] = { "name": name, "safe_name": make_safe_name(name), "email": email, @@ -112,7 +114,7 @@ async def create( player = await app.state.services.database.fetch_one(query, params) assert player is not None - return cast(Player, player) + return cast(Player, dict(player._mapping)) async def fetch_one( @@ -132,13 +134,13 @@ async def fetch_one( AND safe_name = COALESCE(:safe_name, safe_name) AND email = COALESCE(:email, email) """ - params = { + params: dict[str, Any] = { "id": id, "safe_name": make_safe_name(name) if name is not None else None, "email": email, } player = await app.state.services.database.fetch_one(query, params) - return cast(Player, player) if player is not None else None + return cast(Player, dict(player._mapping)) if player is not None else None async def fetch_count( @@ -160,7 +162,7 @@ async def fetch_count( AND preferred_mode = COALESCE(:preferred_mode, preferred_mode) AND play_style = COALESCE(:play_style, play_style) """ - params = { + params: dict[str, Any] = { "priv": priv, "country": country, "clan_id": clan_id, @@ -170,7 +172,7 @@ async def fetch_count( } rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -194,7 +196,7 @@ async def fetch_many( AND preferred_mode = COALESCE(:preferred_mode, preferred_mode) AND play_style = COALESCE(:play_style, play_style) """ - params = { + params: dict[str, Any] = { "priv": priv, "country": country, "clan_id": clan_id, @@ -212,7 +214,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size players = await app.state.services.database.fetch_all(query, params) - return cast(list[Player], players) if players is not None else None + return cast(list[Player], [dict(p._mapping) for p in players]) async def update( @@ -235,7 +237,7 @@ async def update( api_key: str | None | _UnsetSentinel = UNSET, ) -> Player | None: """Update a player in the database.""" - update_fields = PlayerUpdateFields = {} + update_fields: PlayerUpdateFields = {} if not isinstance(name, _UnsetSentinel): update_fields["name"] = name if not isinstance(email, _UnsetSentinel): @@ -282,11 +284,11 @@ async def update( FROM users WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": id, } player = await app.state.services.database.fetch_one(query, params) - return cast(Player, player) if player is not None else None + return cast(Player, dict(player._mapping)) if player is not None else None # TODO: delete? diff --git a/app/repositories/scores.py b/app/repositories/scores.py index f66f59b4..dc440c4a 100644 --- a/app/repositories/scores.py +++ b/app/repositories/scores.py @@ -1,6 +1,8 @@ from __future__ import annotations import textwrap +from datetime import datetime +from typing import Any from typing import cast from typing import TypedDict @@ -60,7 +62,7 @@ class Score(TypedDict): grade: str status: int mode: int - play_time: str + play_time: datetime time_elapsed: int client_flags: int userid: int @@ -84,7 +86,7 @@ class ScoreUpdateFields(TypedDict, total=False): grade: str status: int mode: int - play_time: str + play_time: datetime time_elapsed: int client_flags: int userid: int @@ -108,7 +110,7 @@ async def create( grade: str, status: int, mode: int, - play_time: str, + play_time: datetime, time_elapsed: int, client_flags: int, user_id: int, @@ -125,7 +127,7 @@ async def create( :mode, :play_time, :time_elapsed, :client_flags, :userid, :perfect, :online_checksum) """ - params = { + params: dict[str, Any] = { "map_md5": map_md5, "score": score, "pp": pp, @@ -156,10 +158,10 @@ async def create( WHERE id = :id """ params = {"id": rec_id} - score = await app.state.services.database.fetch_one(query, params) + rec = await app.state.services.database.fetch_one(query, params) - assert score is not None - return cast(Score, score) + assert rec is not None + return cast(Score, dict(rec._mapping)) async def fetch_one(id: int) -> Score | None: @@ -168,10 +170,10 @@ async def fetch_one(id: int) -> Score | None: FROM scores WHERE id = :id """ - params = {"id": id} - score = await app.state.services.database.fetch_one(query, params) + params: dict[str, Any] = {"id": id} + rec = await app.state.services.database.fetch_one(query, params) - return cast(Score, score) if score is not None else None + return cast(Score, dict(rec._mapping)) if rec is not None else None async def fetch_count( @@ -190,7 +192,7 @@ async def fetch_count( AND mode = COALESCE(:mode, mode) AND userid = COALESCE(:userid, userid) """ - params = { + params: dict[str, Any] = { "map_md5": map_md5, "mods": mods, "status": status, @@ -199,7 +201,7 @@ async def fetch_count( } rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -220,7 +222,7 @@ async def fetch_many( AND mode = COALESCE(:mode, mode) AND userid = COALESCE(:userid, userid) """ - params = { + params: dict[str, Any] = { "map_md5": map_md5, "mods": mods, "status": status, @@ -235,8 +237,8 @@ async def fetch_many( params["page_size"] = page_size params["offset"] = (page - 1) * page_size - scores = await app.state.services.database.fetch_all(query, params) - return cast(list[Score], scores) if scores is not None else None + recs = await app.state.services.database.fetch_all(query, params) + return cast(list[Score], [dict(r._mapping) for r in recs]) async def update( @@ -245,7 +247,7 @@ async def update( status: int | _UnsetSentinel = UNSET, ) -> Score | None: """Update an existing score.""" - update_fields = ScoreUpdateFields = {} + update_fields: ScoreUpdateFields = {} if not isinstance(pp, _UnsetSentinel): update_fields["pp"] = pp if not isinstance(status, _UnsetSentinel): @@ -257,16 +259,16 @@ async def update( WHERE id = :id """ values = {"id": id} | update_fields - await app.state.services.database.execute(query, params) + await app.state.services.database.execute(query, values) query = f"""\ SELECT {READ_PARAMS} FROM scores WHERE id = :id """ - params = {"id": id} - score = await app.state.services.database.fetch_one(query, params) - return cast(Score, score) if score is not None else None + params: dict[str, Any] = {"id": id} + rec = await app.state.services.database.fetch_one(query, params) + return cast(Score, dict(rec._mapping)) if rec is not None else None # TODO: delete diff --git a/app/repositories/stats.py b/app/repositories/stats.py index 66eaafb5..6dc623ff 100644 --- a/app/repositories/stats.py +++ b/app/repositories/stats.py @@ -1,6 +1,7 @@ from __future__ import annotations import textwrap +from typing import Any from typing import cast from typing import TypedDict @@ -83,7 +84,7 @@ async def create( INSERT INTO stats (id, mode) VALUES (:id, :mode) """ - params = { + params: dict[str, Any] = { "id": player_id, "mode": mode, } @@ -100,7 +101,7 @@ async def create( stat = await app.state.services.database.fetch_one(query, params) assert stat is not None - return cast(Stat, stat) + return cast(Stat, dict(stat._mapping)) async def create_all_modes(player_id: int) -> list[Stat]: @@ -129,11 +130,11 @@ async def create_all_modes(player_id: int) -> list[Stat]: FROM stats WHERE id = :id """ - params = { + params: dict[str, Any] = { "id": player_id, } stats = await app.state.services.database.fetch_all(query, params) - return cast(list[Stat], stats) if stats is not None else None + return cast(list[Stat], [dict(s._mapping) for s in stats]) async def fetch_one(player_id: int, mode: int) -> Stat | None: @@ -144,13 +145,13 @@ async def fetch_one(player_id: int, mode: int) -> Stat | None: WHERE id = :id AND mode = :mode """ - params = { + params: dict[str, Any] = { "id": player_id, "mode": mode, } stat = await app.state.services.database.fetch_one(query, params) - return cast(Stat, stat) if stat is not None else None + return cast(Stat, dict(stat._mapping)) if stat is not None else None async def fetch_count( @@ -163,13 +164,13 @@ async def fetch_count( WHERE id = COALESCE(:id, id) AND mode = COALESCE(:mode, mode) """ - params = { + params: dict[str, Any] = { "id": player_id, "mode": mode, } rec = await app.state.services.database.fetch_one(query, params) assert rec is not None - return rec["count"] + return cast(int, rec._mapping["count"]) async def fetch_many( @@ -184,7 +185,7 @@ async def fetch_many( WHERE id = COALESCE(:id, id) AND mode = COALESCE(:mode, mode) """ - params = { + params: dict[str, Any] = { "id": player_id, "mode": mode, } @@ -198,7 +199,7 @@ async def fetch_many( params["offset"] = (page - 1) * page_size stats = await app.state.services.database.fetch_all(query, params) - return cast(list[Stat], stats) if stats is not None else None + return cast(list[Stat], [dict(s._mapping) for s in stats]) async def update( @@ -220,7 +221,7 @@ async def update( a_count: int | _UnsetSentinel = UNSET, ) -> Stat | None: """Update a player stats entry in the database.""" - update_fields = AchievementUpdateFields = {} + update_fields: StatUpdateFields = {} if not isinstance(tscore, _UnsetSentinel): update_fields["tscore"] = tscore if not isinstance(rscore, _UnsetSentinel): @@ -265,12 +266,12 @@ async def update( WHERE id = :id AND mode = :mode """ - params = { + params: dict[str, Any] = { "id": player_id, "mode": mode, } - stat = await app.state.services.database.fetch_one(query, params) - return cast(Stat, stat) if stat is not None else None + stats = await app.state.services.database.fetch_one(query, params) + return cast(Stat, dict(stats._mapping)) if stats is not None else None # TODO: delete? diff --git a/app/settings.py b/app/settings.py index 50c4aeac..a78f82e3 100644 --- a/app/settings.py +++ b/app/settings.py @@ -23,11 +23,10 @@ def read_list(value: str) -> list[str]: # this is a bit weird, to allow "" as a value for backwards compatibility. # perhaps can remove in future. +SERVER_PORT = None _server_port = os.environ["SERVER_PORT"] if _server_port: SERVER_PORT = int(_server_port) -else: - SERVER_PORT = None DB_HOST = os.environ["DB_HOST"] DB_PORT = int(os.environ["DB_PORT"]) diff --git a/app/state/__init__.py b/app/state/__init__.py index f8e29da9..b0f09af5 100644 --- a/app/state/__init__.py +++ b/app/state/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Literal from typing import TYPE_CHECKING from . import cache @@ -8,7 +9,12 @@ if TYPE_CHECKING: from asyncio import AbstractEventLoop + from app.packets import ClientPackets + from app.packets import BasePacket loop: AbstractEventLoop -packets = {"all": {}, "restricted": {}} +packets: dict[Literal["all", "restricted"], dict[ClientPackets, type[BasePacket]]] = { + "all": {}, + "restricted": {}, +} shutting_down = False diff --git a/app/state/services.py b/app/state/services.py index a270312f..81841ddd 100644 --- a/app/state/services.py +++ b/app/state/services.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio import ipaddress import pickle import re @@ -53,8 +52,6 @@ ip_resolver: IPResolver -housekeeping_tasks: list[asyncio.Task] = [] - """ session usecases """ @@ -134,27 +131,33 @@ async def fetch_geoloc( ip: IPAddress, headers: Mapping[str, str] | None = None, ) -> Geolocation | None: - """Fetch geolocation data based on ip.""" - geoloc = fetch_geoloc_cloudflare(ip, headers) + """Attempt to fetch geolocation data by any means necessary.""" + geoloc = None + if headers is not None: + geoloc = _fetch_geoloc_from_headers(headers) if geoloc is None: - geoloc = fetch_geoloc_nginx(ip, headers) + geoloc = await _fetch_geoloc_from_ip(ip) + + return geoloc + + +def _fetch_geoloc_from_headers(headers: Mapping[str, str]) -> Geolocation | None: + """Attempt to fetch geolocation data from http headers.""" + geoloc = __fetch_geoloc_cloudflare(headers) if geoloc is None: - geoloc = await fetch_geoloc_web(ip) + geoloc = __fetch_geoloc_nginx(headers) return geoloc -def fetch_geoloc_cloudflare( - ip: IPAddress, - headers: Mapping[str, str], -) -> Geolocation | None: - """Fetch geolocation data based on ip (using cloudflare headers).""" +def __fetch_geoloc_cloudflare(headers: Mapping[str, str]) -> Geolocation | None: + """Attempt to fetch geolocation data from cloudflare headers.""" if not all( key in headers for key in ("CF-IPCountry", "CF-IPLatitude", "CF-IPLongitude") ): - return + return None country_code = headers["CF-IPCountry"].lower() latitude = float(headers["CF-IPLatitude"]) @@ -170,15 +173,12 @@ def fetch_geoloc_cloudflare( } -def fetch_geoloc_nginx( - ip: IPAddress, - headers: Mapping[str, str], -) -> Geolocation | None: - """Fetch geolocation data based on ip (using nginx headers).""" +def __fetch_geoloc_nginx(headers: Mapping[str, str]) -> Geolocation | None: + """Attempt to fetch geolocation data from nginx headers.""" if not all( key in headers for key in ("X-Country-Code", "X-Latitude", "X-Longitude") ): - return + return None country_code = headers["X-Country-Code"].lower() latitude = float(headers["X-Latitude"]) @@ -194,15 +194,16 @@ def fetch_geoloc_nginx( } -async def fetch_geoloc_web(ip: IPAddress) -> Geolocation | None: +async def _fetch_geoloc_from_ip(ip: IPAddress) -> Geolocation | None: """Fetch geolocation data based on ip (using ip-api).""" if not ip.is_private: url = f"http://ip-api.com/line/{ip}" else: url = "http://ip-api.com/line/" + assert http_client is not None async with http_client.get(url) as resp: - if not resp or resp.status != 200: + if resp.status != 200: log("Failed to get geoloc data: request failed.", Ansi.LRED) return None @@ -234,6 +235,7 @@ async def log_strange_occurrence(obj: object) -> None: if app.settings.AUTOMATICALLY_REPORT_PROBLEMS: # automatically reporting problems to cmyui's server + assert http_client is not None async with http_client.post( url="https://log.cmyui.xyz/", headers={ @@ -286,7 +288,10 @@ def __repr__(self) -> str: def __hash__(self) -> int: return self.as_tuple.__hash__() - def __eq__(self, other: Version) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, Version): + return NotImplemented + return self.as_tuple == other.as_tuple def __lt__(self, other: Version) -> bool: @@ -339,6 +344,7 @@ async def _get_latest_dependency_versions() -> AsyncGenerator[ # TODO: split up and do the requests asynchronously url = f"https://pypi.org/pypi/{dependency_name}/json" + assert http_client is not None async with http_client.get(url) as resp: json = await resp.json() if resp.status == 200 and json: @@ -453,7 +459,7 @@ async def run_sql_migrations() -> None: try: await db_conn.execute(query) except pymysql.err.MySQLError as exc: - log(f"Failed: {query}", Ansi.GRAY) # type: ignore + log(f"Failed: {query}", Ansi.GRAY) log(repr(exc)) log( "SQL failed to update - unless you've been " diff --git a/app/state/sessions.py b/app/state/sessions.py index 8e3ee3b7..88ae2ae7 100644 --- a/app/state/sessions.py +++ b/app/state/sessions.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from typing import Any from typing import TYPE_CHECKING from app.logging import Ansi @@ -24,7 +25,7 @@ api_keys: dict[str, int] = {} -housekeeping_tasks: set[asyncio.Task] = set() +housekeeping_tasks: set[asyncio.Task[Any]] = set() bot: Player diff --git a/app/usecases/performance.py b/app/usecases/performance.py index 4e1dc78a..e5801b89 100644 --- a/app/usecases/performance.py +++ b/app/usecases/performance.py @@ -30,25 +30,25 @@ class ScoreParams: class PerformanceRating(TypedDict): pp: float - pp_acc: float - pp_aim: float - pp_speed: float - pp_flashlight: float - effective_miss_count: int - pp_difficulty: float + pp_acc: float | None + pp_aim: float | None + pp_speed: float | None + pp_flashlight: float | None + effective_miss_count: float | None + pp_difficulty: float | None class DifficultyRating(TypedDict): stars: float - aim: float - speed: float - flashlight: float - slider_factor: float - speed_note_count: float - stamina: float - color: float - rhythm: float - peak: float + aim: float | None + speed: float | None + flashlight: float | None + slider_factor: float | None + speed_note_count: float | None + stamina: float | None + color: float | None + rhythm: float | None + peak: float | None class PerformanceResult(TypedDict): diff --git a/app/utils.py b/app/utils.py index fb122959..9f70a0b5 100644 --- a/app/utils.py +++ b/app/utils.py @@ -42,7 +42,6 @@ "escape_enum", "ensure_supported_platform", "ensure_directory_structure", - "ensure_dependencies_and_requirements", "setup_runtime_environment", "_install_debugging_hooks", "display_startup_dialog", @@ -53,6 +52,9 @@ "has_png_headers_and_trailers", ) +T = TypeVar("T") + + DATA_PATH = Path.cwd() / ".data" ACHIEVEMENTS_ASSETS_PATH = DATA_PATH / "assets/medals/client" DEFAULT_AVATAR_PATH = DATA_PATH / "avatars/default.jpg" @@ -237,7 +239,7 @@ def _excepthook( type_: type[BaseException], value: BaseException, traceback: types.TracebackType | None, - ): + ) -> None: if type_ is KeyboardInterrupt: print("\33[2K\r", end="Aborted startup.") return @@ -261,7 +263,7 @@ def _excepthook( f"bancho.py v{app.settings.VERSION} ran into an issue before starting up :(", Ansi.RED, ) - real_excepthook(type_, value, traceback) # type: ignore + real_excepthook(type_, value, traceback) sys.excepthook = _excepthook @@ -313,15 +315,12 @@ def is_valid_unix_address(address: str) -> bool: return address.endswith(".sock") # TODO: improve -T = TypeVar("T") - - def pymysql_encode( conv: Callable[[Any, dict[object, object] | None], str], -) -> Callable[[T], T]: +) -> Callable[[type[T]], type[T]]: """Decorator to allow for adding to pymysql's encoders.""" - def wrapper(cls: T) -> T: + def wrapper(cls: type[T]) -> type[T]: pymysql.converters.encoders[cls] = conv return cls @@ -427,7 +426,7 @@ def create_config_from_default() -> None: shutil.copy("ext/config.sample.py", "config.py") -def orjson_serialize_to_str(*args, **kwargs) -> str: +def orjson_serialize_to_str(*args: Any, **kwargs: Any) -> str: return orjson.dumps(*args, **kwargs).decode() @@ -448,5 +447,5 @@ def has_jpeg_headers_and_trailers(data_view: memoryview) -> bool: def has_png_headers_and_trailers(data_view: memoryview) -> bool: return ( data_view[:8] == b"\x89PNG\r\n\x1a\n" - and data_view[-8] == b"\x49END\xae\x42\x60\x82" + and data_view[-8:] == b"\x49END\xae\x42\x60\x82" ) diff --git a/main.py b/main.py index 504cb862..a5e1df74 100755 --- a/main.py +++ b/main.py @@ -26,7 +26,6 @@ import logging import sys from collections.abc import Sequence - import uvicorn import app.utils @@ -76,21 +75,21 @@ def main(argv: Sequence[str]) -> int: # the server supports both inet and unix sockets. + uds = None + host = None + port = None + if ( app.utils.is_valid_inet_address(app.settings.SERVER_ADDR) and app.settings.SERVER_PORT is not None ): - server_arguments = { - "host": app.settings.SERVER_ADDR, - "port": app.settings.SERVER_PORT, - } + host = app.settings.SERVER_ADDR + port = app.settings.SERVER_PORT elif ( app.utils.is_valid_unix_address(app.settings.SERVER_ADDR) and app.settings.SERVER_PORT is None ): - server_arguments = { - "uds": app.settings.SERVER_ADDR, - } + uds = app.settings.SERVER_ADDR # make sure the socket file does not exist on disk and can be bound # (uvicorn currently does not do this for us, and will raise an exc) @@ -124,7 +123,9 @@ def main(argv: Sequence[str]) -> int: # but i would prefer Bancho-Version to keep # with standards. perhaps look into this. headers=[("bancho-version", app.settings.VERSION)], - **server_arguments, + uds=uds, + host=host or "127.0.0.1", # uvicorn defaults + port=port or 8000, # uvicorn defaults ) return 0 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..3631598b --- /dev/null +++ b/mypy.ini @@ -0,0 +1,19 @@ +[mypy] +strict = True +disallow_untyped_calls = False +exclude = ["tests"] + +[mypy-aiomysql] +ignore_missing_imports = True + +[mypy-pymysql] +ignore_missing_imports = True + +[mypy-mitmproxy] +ignore_missing_imports = True + +[mypy-py3rijndael] +ignore_missing_imports = True + +[mypy-requests] +ignore_missing_imports = True diff --git a/requirements-dev.txt b/requirements-dev.txt index 5af6b117..f62cc4cd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -i https://pypi.org/simple +autoflake==2.2.1; python_version >= '3.8' black==23.9.1; python_version >= '3.8' cfgv==3.4.0; python_version >= '3.8' classify-imports==4.2.0; python_version >= '3.7' @@ -15,6 +16,7 @@ pathspec==0.11.2; python_version >= '3.7' platformdirs==3.10.0; python_version >= '3.7' pluggy==1.3.0; python_version >= '3.8' pre-commit==3.4.0; python_version >= '3.8' +pyflakes==3.1.0; python_version >= '3.8' pytest==7.4.2; python_version >= '3.7' pyyaml==6.0.1; python_version >= '3.6' reorder-python-imports==3.11.0; python_version >= '3.8' @@ -24,7 +26,7 @@ virtualenv==20.24.5; python_version >= '3.7' aiohttp==3.8.5; python_version >= '3.6' aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5; python_version >= '3.7' +akatsuki-pp-py==0.9.8; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' async-timeout==4.0.3; python_version >= '3.7' diff --git a/requirements.txt b/requirements.txt index a9b17ead..22a4d9b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ aiohttp==3.8.5; python_version >= '3.6' aiomysql==0.2.0 aiosignal==1.3.1; python_version >= '3.7' -akatsuki-pp-py==0.9.5; python_version >= '3.7' +akatsuki-pp-py==0.9.8; python_version >= '3.7' annotated-types==0.5.0; python_version >= '3.7' anyio==3.7.1; python_version >= '3.7' async-timeout==4.0.3; python_version >= '3.7' diff --git a/tools/newstats.py b/tools/newstats.py deleted file mode 100755 index 82a4ad5d..00000000 --- a/tools/newstats.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3.11 -""" -tool to merge from gulag 3.5.2 stats to 3.5.3 stats -this tool is destructive, don't run it if you don't know why it exists :) -""" -from __future__ import annotations - -import asyncio - -import aiomysql -from cmyui.mysql import AsyncSQLPool # module not in requirements.txt - deprecated - -TABLE_COLUMNS = ["tscore", "rscore", "pp", "plays", "playtime", "acc", "max_combo"] - - -async def main(): - pool = AsyncSQLPool() - await pool.connect( - {"db": "gulag_old", "host": "localhost", "password": "lol123", "user": "cmyui"}, - ) - - async with pool.pool.acquire() as conn: - async with conn.cursor(aiomysql.DictCursor) as main_cursor: - # rename current table to old_table - await main_cursor.execute("RENAME TABLE stats TO old_stats") - - # create new stats table - await main_cursor.execute( - """ - create table stats - ( - id int auto_increment, - mode tinyint(1) not null, - tscore bigint unsigned default 0 not null, - rscore bigint unsigned default 0 not null, - pp int unsigned default 0 not null, - plays int unsigned default 0 not null, - playtime int unsigned default 0 not null, - acc float(6,3) default 0.000 not null, - max_combo int unsigned default 0 not null, - xh_count int unsigned default 0 not null, - x_count int unsigned default 0 not null, - sh_count int unsigned default 0 not null, - s_count int unsigned default 0 not null, - a_count int unsigned default 0 not null, - primary key (id, mode) - ); - """, - ) - - # move existing data to new stats table - await main_cursor.execute("SELECT * FROM old_stats") - - async with conn.cursor(aiomysql.DictCursor) as insert_cursor: - async for row in main_cursor: - # create 7 new rows per user, one for each mode - for mode_num, mode_str in enumerate( - ( - "vn_std", - "vn_taiko", - "vn_catch", - "vn_mania", - "rx_std", - "rx_taiko", - "rx_catch", - "ap_std", - ), - ): - row_values = [ - row[f"{column}_{mode_str}"] for column in TABLE_COLUMNS - ] - - await insert_cursor.execute( - "INSERT INTO stats " - "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, " - "0, 0, 0, 0, 0)", # grades (new stuff) - [row["id"], mode_num, *row_values], - ) - - print("success, old table left at old_stats") - - await pool.close() - - -asyncio.run(main()) diff --git a/tools/recalc.py b/tools/recalc.py index e8323784..f138a260 100644 --- a/tools/recalc.py +++ b/tools/recalc.py @@ -13,7 +13,7 @@ from dataclasses import field from pathlib import Path from typing import Any -from typing import Optional +from typing import TypeVar import aiohttp import databases @@ -35,6 +35,9 @@ print("\x1b[;91mMust run from tools/ directory\x1b[m") raise +T = TypeVar("T") + + DEBUG = False BEATMAPS_PATH = Path.cwd() / ".data/osu" @@ -46,7 +49,7 @@ class Context: beatmaps: dict[int, Beatmap] = field(default_factory=dict) -def divide_chunks(values: list, n: int) -> Iterator[list]: +def divide_chunks(values: list[T], n: int) -> Iterator[list[T]]: for i in range(0, len(values), n): yield values[i : i + n] @@ -75,7 +78,7 @@ async def recalculate_score( ) attrs = calculator.performance(beatmap) - new_pp: float = attrs.pp # type: ignore + new_pp: float = attrs.pp if math.isnan(new_pp) or math.isinf(new_pp): new_pp = 0.0 From a7618d9e14ec0517d1582b6340aa62d83870055c Mon Sep 17 00:00:00 2001 From: cmyui Date: Sat, 23 Sep 2023 22:46:56 -0400 Subject: [PATCH 23/24] fix typing bug --- app/objects/score.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/objects/score.py b/app/objects/score.py index fa8d3c3f..4c4518cd 100644 --- a/app/objects/score.py +++ b/app/objects/score.py @@ -6,7 +6,6 @@ from enum import IntEnum from enum import unique from pathlib import Path -from typing import cast from typing import TYPE_CHECKING import app.state @@ -293,7 +292,7 @@ def compute_online_checksum( """Methods to calculate internal data for a score.""" - async def calculate_placement(self) -> int: + async def calculate_placement(self) -> int | None: assert self.bmap is not None if self.mode >= GameMode.RELAX_OSU: @@ -303,7 +302,7 @@ async def calculate_placement(self) -> int: scoring_metric = "score" score = self.score - better_scores = await app.state.services.database.fetch_val( + num_better_scores: int | None = await app.state.services.database.fetch_val( "SELECT COUNT(*) AS c FROM scores s " "INNER JOIN users u ON u.id = s.userid " "WHERE s.map_md5 = :map_md5 AND s.mode = :mode " @@ -316,9 +315,10 @@ async def calculate_placement(self) -> int: }, column=0, # COUNT(*) ) + if num_better_scores is None: + return None - # TODO: idk if returns none - return cast(int, better_scores) + 1 # if better_scores is not None else 1 + return num_better_scores + 1 def calculate_performance(self, osu_file_path: Path) -> tuple[float, float]: """Calculate PP and star rating for our score.""" From f77f201708f49b9250d979946f937db115e5ea12 Mon Sep 17 00:00:00 2001 From: cmyui Date: Sat, 23 Sep 2023 22:48:57 -0400 Subject: [PATCH 24/24] improve api with assert for case that should always happen --- app/objects/score.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/objects/score.py b/app/objects/score.py index 4c4518cd..9f03c83e 100644 --- a/app/objects/score.py +++ b/app/objects/score.py @@ -292,7 +292,7 @@ def compute_online_checksum( """Methods to calculate internal data for a score.""" - async def calculate_placement(self) -> int | None: + async def calculate_placement(self) -> int: assert self.bmap is not None if self.mode >= GameMode.RELAX_OSU: @@ -315,9 +315,7 @@ async def calculate_placement(self) -> int | None: }, column=0, # COUNT(*) ) - if num_better_scores is None: - return None - + assert num_better_scores is not None return num_better_scores + 1 def calculate_performance(self, osu_file_path: Path) -> tuple[float, float]: