From e5e02e29cf88dc1522822acc1a760d7766a9184f Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Wed, 2 Oct 2024 02:56:46 -0400 Subject: [PATCH] improve testing added more tests and a dummy api client for testing without credentials --- .github/workflows/pytest.yml | 2 +- otdb/api/tests.py | 238 ----------------------- otdb/api/tests/__init__.py | 0 otdb/api/tests/conftest.py | 179 +++++++++++++++++ otdb/api/tests/test_mappools.py | 144 ++++++++++++++ otdb/api/tests/test_tournaments.py | 112 +++++++++++ otdb/api/tests/test_users.py | 18 ++ otdb/api/tests/util.py | 11 ++ otdb/api/views/mappools.py | 10 +- otdb/api/views/tournaments.py | 6 +- otdb/common/dummy_api.py | 33 ++++ otdb/common/dummy_api_data/beatmaps.json | 1 + otdb/common/dummy_api_data/users.json | 1 + otdb/common/middleware.py | 13 +- otdb/otdb/settings.py | 21 +- otdb/otdb/urls.py | 3 +- otdb/pytest.ini | 2 +- requirements.txt | 1 + 18 files changed, 537 insertions(+), 258 deletions(-) delete mode 100644 otdb/api/tests.py create mode 100644 otdb/api/tests/__init__.py create mode 100644 otdb/api/tests/conftest.py create mode 100644 otdb/api/tests/test_mappools.py create mode 100644 otdb/api/tests/test_tournaments.py create mode 100644 otdb/api/tests/test_users.py create mode 100644 otdb/api/tests/util.py create mode 100644 otdb/common/dummy_api.py create mode 100644 otdb/common/dummy_api_data/beatmaps.json create mode 100644 otdb/common/dummy_api_data/users.json diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index cdd86d4..95db380 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,6 +1,6 @@ name: Pytest -on: workflow_dispatch +on: [push, pull_request] permissions: contents: read diff --git a/otdb/api/tests.py b/otdb/api/tests.py deleted file mode 100644 index 396c4ee..0000000 --- a/otdb/api/tests.py +++ /dev/null @@ -1,238 +0,0 @@ -from django.test import RequestFactory -from django.contrib.auth import get_user_model -from django.db import connection -from django.conf import settings - -from . import views - -import json -import pytest -import os -from asgiref.sync import sync_to_async - - -OsuUser = get_user_model() -SQL_DIR = os.path.join(os.path.split(settings.BASE_DIR)[0], "sql") - - -def auser(user): - async def get(): - return user - - return get - - -class Client: - __slots__ = ("factory", "_user", "mappool") - - def __init__(self): - self.factory = RequestFactory() - self._user = None - self.mappool = None - - def _get_user(self): - return OsuUser.objects.get(id=8640970) - - async def get_user(self): - if self._user is None: - self._user = await sync_to_async(self._get_user)() - - return self._user - - async def _fill_req(self, req): - req.auser = auser(await self.get_user()) - return req - - async def get(self, *args, **kwargs): - return await self._fill_req(self.factory.get(*args, **kwargs)) - - async def post(self, *args, content_type="application/json", **kwargs): - return await self._fill_req(self.factory.post(*args, content_type=content_type, **kwargs)) - - -@pytest.fixture(scope="session") -def django_db_keepdb(): - yield settings.IS_GITHUB_WORKFLOW - - -@pytest.fixture(scope="session") -def django_db_createdb(): - yield not settings.IS_GITHUB_WORKFLOW - - -def create_user(): - user = OsuUser( - id=8640970, - username="enri", - avatar="", - cover="" - ) - user.save() - - -def create_psql_functions(): - with connection.cursor() as cursor: - for file in os.listdir(SQL_DIR): - with open(os.path.join(SQL_DIR, file), "r") as f: - cursor.execute(f.read()) - - -@pytest.fixture(scope="session") -def django_db_setup(django_db_setup, django_db_blocker): - with django_db_blocker.unblock(): - create_user() - create_psql_functions() - - -@pytest.fixture -def sample_mappool(): - yield { - "name": "test mappool", - "description": "this is a description", - "beatmaps": [ - { - "id": 3993830, - "slot": "EZ1", - "mods": ["EZ"] - }, - { - "id": 4021669, - "slot": "EZ2", - "mods": ["EZ"] - }, - { - "id": 2964073, - "slot": "EZ3", - "mods": ["EZ"] - }, - { - "id": 3316178, - "slot": "EZ4", - "mods": ["EZ"] - }, - { - "id": 4031511, - "slot": "HD1", - "mods": ["EZ", "HD"] - }, - { - "id": 2369018, - "slot": "HD2", - "mods": ["EZ", "HD"] - }, - { - "id": 4154290, - "slot": "DT1", - "mods": ["EZ", "DT"] - }, - { - "id": 49612, - "slot": "DT2", - "mods": ["EZ", "DT"] - }, - { - "id": 2478754, - "slot": "HT1", - "mods": ["EZ", "HT"] - }, - { - "id": 2447573, - "slot": "HT2", - "mods": ["EZ", "HT"] - } - ] - } - - -@pytest.fixture(scope="session") -def client(): - yield Client() - - -def parse_resp(resp): - assert resp.status_code == 200, resp.content.decode("utf-8") - return json.loads(resp.content) if resp.content else None - - -@pytest.mark.django_db -class TestTournaments: - @pytest.mark.asyncio - async def test_tournaments_list(self, client): - req = await client.get("/api/tournaments/") - parse_resp(await views.tournaments(req)) - - -@pytest.mark.django_db -class TestMappools: - @pytest.mark.asyncio - async def test_create_mappool(self, client, sample_mappool): - req = await client.post("/api/mappools/", data=json.dumps(sample_mappool)) - mappool = parse_resp(await views.mappools(req)) - - assert isinstance(mappool, dict) - assert isinstance(mappool["id"], int) - - assert mappool["name"] == sample_mappool["name"] - assert mappool["description"] == sample_mappool["description"] - - client.mappool = mappool - - @pytest.mark.asyncio - async def test_get_mappool(self, client, sample_mappool): - mappool = client.mappool - assert mappool is not None, "test_create_mappool failed; no mappool" - - req = await client.get(f"/api/mappools/{mappool['id']}/") - data = parse_resp(await views.mappools(req, mappool['id'])) - - assert data["id"] == mappool["id"] - assert data["name"] == mappool["name"] - assert data["description"] == mappool["description"] - for sample_beatmap in sample_mappool["beatmaps"]: - beatmap_conn = next(filter( - lambda c: c["beatmap"]["beatmap_metadata"]["id"] == sample_beatmap["id"], - data["beatmap_connections"] - )) - - assert sample_beatmap["slot"] == beatmap_conn["slot"] - - mods = [mod["acronym"] for mod in beatmap_conn["beatmap"]["mods"]] - for mod in sample_beatmap["mods"]: - assert mod in mods - - @pytest.mark.asyncio - async def test_favorite_mappool(self, client): - mappool = client.mappool - assert mappool is not None, "test_create_mappool failed; no mappool" - - req = await client.post(f"/api/mappools/{mappool['id']}/favorite/", data=json.dumps({"favorite": True})) - parse_resp(await views.favorite_mappool(req, mappool["id"])) - - async def _test_mappool_list(self, client, sort): - mappool = client.mappool - assert mappool is not None, "test_create_mappool failed; no mappool" - - req = await client.get(f"/api/mappools/?s={sort}") - data = parse_resp(await views.mappools(req)) - - assert isinstance(data, dict) - assert data["total_pages"] == 1 - - mappools = data["data"] - assert isinstance(mappools, list) - assert len(mappools) == 1 - assert mappools[0]["id"] == mappool["id"] - assert mappools[0]["name"] == mappool["name"] - assert mappools[0]["favorite_count"] == 1, "test_favorite_mappool failed; no favorites on the mappool" - - @pytest.mark.asyncio - async def test_recent_list(self, client): - await self._test_mappool_list(client, "recent") - - @pytest.mark.asyncio - async def test_favorite_list(self, client): - await self._test_mappool_list(client, "favorites") - - @pytest.mark.asyncio - async def test_trending_list(self, client): - await self._test_mappool_list(client, "trending") diff --git a/otdb/api/tests/__init__.py b/otdb/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otdb/api/tests/conftest.py b/otdb/api/tests/conftest.py new file mode 100644 index 0000000..12556a7 --- /dev/null +++ b/otdb/api/tests/conftest.py @@ -0,0 +1,179 @@ +from django.test import AsyncRequestFactory +from django.db import connection +from django.conf import settings + +from asgiref.sync import sync_to_async +import os +import pytest + +from main.models import OsuUser + + +SQL_DIR = os.path.join(os.path.split(settings.BASE_DIR)[0], "sql") + + +USER = { + "id": 14895608, + "username": "Sheppsu", + "avatar": "https://a.ppy.sh/14895608?1718517008.jpeg", + "cover": "https://assets.ppy.sh/user-profile-covers/14895608/859a7bda8ad09971013e5b7d1c619d1ca7b4cb0ee9caaaad8072a18973f3bad0.jpeg", + "is_admin": True +} + + +def auser(user): + async def get(): + return user + + return get + + +class Client: + __slots__ = ("factory", "_user", "mappool", "tournament") + + def __init__(self): + self.factory = AsyncRequestFactory() + self._user = None + + self.mappool = None + self.tournament = None + + def _get_user(self): + return OsuUser.objects.get(id=USER["id"]) + + async def get_user(self): + if self._user is None: + self._user = await sync_to_async(self._get_user)() + + return self._user + + async def _fill_req(self, req): + req.auser = auser(await self.get_user()) + return req + + async def get(self, *args, **kwargs): + return await self._fill_req(self.factory.get(*args, **kwargs)) + + async def post(self, *args, content_type="application/json", **kwargs): + return await self._fill_req(self.factory.post(*args, content_type=content_type, **kwargs)) + + +@pytest.fixture(scope="session") +def django_db_keepdb(): + return settings.IS_GITHUB_WORKFLOW + + +@pytest.fixture(scope="session") +def django_db_createdb(): + return not settings.IS_GITHUB_WORKFLOW + + +def create_user(): + user = OsuUser( + id=USER["id"], + username=USER["username"], + avatar=USER["avatar"], + cover=USER["cover"] + ) + user.save() + + +def create_psql_functions(): + with connection.cursor() as cursor: + for file in os.listdir(SQL_DIR): + with open(os.path.join(SQL_DIR, file), "r") as f: + cursor.execute(f.read()) + + +@pytest.fixture(scope="session") +def django_db_setup(django_db_setup, django_db_blocker): + with django_db_blocker.unblock(): + create_user() + create_psql_functions() + + +@pytest.fixture +def sample_mappool(): + return { + "name": "test mappool", + "description": "this is a mappool description", + "beatmaps": [ + { + "id": 3993830, + "slot": "EZ1", + "mods": ["EZ"] + }, + { + "id": 4021669, + "slot": "EZ2", + "mods": ["EZ"] + }, + { + "id": 2964073, + "slot": "EZ3", + "mods": ["EZ"] + }, + { + "id": 3316178, + "slot": "EZ4", + "mods": ["EZ"] + }, + { + "id": 4031511, + "slot": "HD1", + "mods": ["EZ", "HD"] + }, + { + "id": 2369018, + "slot": "HD2", + "mods": ["EZ", "HD"] + }, + { + "id": 4154290, + "slot": "DT1", + "mods": ["EZ", "DT"] + }, + { + "id": 49612, + "slot": "DT2", + "mods": ["EZ", "DT"] + }, + { + "id": 2478754, + "slot": "HT1", + "mods": ["EZ", "HT"] + }, + { + "id": 2447573, + "slot": "HT2", + "mods": ["EZ", "HT"] + } + ] + } + + +@pytest.fixture +def sample_tournament(): + return { + "name": "test tournament", + "abbreviation": "TT", + "link": "https://www.google.com", + "description": "this is a tournament description", + "staff": [ + { + "id": 14895608, + "roles": 3 + } + ], + "mappools": [] + } + + +@pytest.fixture +def sample_user(): + return USER + + +@pytest.fixture(scope="session") +def client(): + return Client() diff --git a/otdb/api/tests/test_mappools.py b/otdb/api/tests/test_mappools.py new file mode 100644 index 0000000..1fe012a --- /dev/null +++ b/otdb/api/tests/test_mappools.py @@ -0,0 +1,144 @@ +import json +import pytest + +from .util import parse_resp, get_total_pages +from .. import views + + +@pytest.mark.django_db +class TestMappools: + @pytest.mark.asyncio + @pytest.mark.dependency() + async def test_create_mappool(self, client, sample_mappool): + req = await client.post("/api/mappools/", data=json.dumps(sample_mappool)) + mappool = parse_resp(await views.mappools(req)) + + assert isinstance(mappool, dict) + assert isinstance(mappool["id"], int) + + self._test_mappool(mappool, sample_mappool, False) + + client.mappool = mappool + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_favorite_mappool(self, client): + mappool = client.mappool + + req = await client.post(f"/api/mappools/{mappool['id']}/favorite/", data=json.dumps({"favorite": True})) + parse_resp(await views.favorite_mappool(req, mappool["id"])) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_get_mappool(self, client, sample_mappool): + mappool = client.mappool + + req = await client.get(f"/api/mappools/{mappool['id']}/") + data = parse_resp(await views.mappools(req, mappool['id'])) + + self._test_mappool(data, mappool) + assert data["favorite_count"] == 1, "test_favorite_mappool failed; no favorites on the mappool" + for sample_beatmap in sample_mappool["beatmaps"]: + beatmap_conn = next(filter( + lambda c: c["beatmap"]["beatmap_metadata"]["id"] == sample_beatmap["id"], + data["beatmap_connections"] + )) + + assert sample_beatmap["slot"] == beatmap_conn["slot"] + + mods = [mod["acronym"] for mod in beatmap_conn["beatmap"]["mods"]] + for mod in sample_beatmap["mods"]: + assert mod in mods + + async def _get_mappools_listing(self, client, query: dict[str, str | int | None]): + query_string = "&".join((f"{k}={'' if v is None else str(v)}" for k, v in query.items())) + req = await client.get(f"/api/mappools/?{query_string}") + result = parse_resp(await views.mappools(req)) + + assert isinstance(result, dict) + data = result["data"] + assert isinstance(data, list) + assert result["total_pages"] == get_total_pages(len(data)) + + return data + + def _test_mappool(self, m1, m2, include_id=True): + if include_id: + assert m1["id"] == m2["id"], "mappool id does not match" + assert m1["name"] == m2["name"], "mappool name does not match" + assert m1["description"] == m2["description"], "mappool description does not match" + + def _test_mappool_listing(self, client, mappools): + assert isinstance(mappools, list), "expected a list" + assert len(mappools) == 1, "expected one mappool" + self._test_mappool(mappools[0], client.mappool) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_recent_list(self, client): + self._test_mappool_listing( + client, + await self._get_mappools_listing(client, {"s": "recent"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_favorite_list(self, client): + self._test_mappool_listing( + client, + await self._get_mappools_listing(client, {"s": "favorites"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_trending_list(self, client): + self._test_mappool_listing( + client, + await self._get_mappools_listing(client, {"s": "trending"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_sr_filter(self, client): + mappools = await self._get_mappools_listing(client, { + "s": "recent", + "min-sr": 4, + "max-sr": 6 + }) + self._test_mappool_listing(client, mappools) + + mappools = await self._get_mappools_listing(client, { + "s": "recent", + "min-sr": 6, + "max-sr": 8 + }) + + assert isinstance(mappools, list), "expected a list" + assert len(mappools) == 0, "expected empty return" + + mappools = await self._get_mappools_listing(client, { + "s": "recent", + "min-sr": 6, + "max-sr": 4 + }) + + assert isinstance(mappools, list), "expected a list" + assert len(mappools) == 0, "expected empty return" + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestMappools::test_create_mappool"]) + async def test_mappool_search(self, client): + for word in client.mappool["name"].split(): + mappools = await self._get_mappools_listing(client, { + "s": "recent", + "q": word + }) + self._test_mappool_listing(client, mappools) + + mappools = await self._get_mappools_listing(client, { + "s": "recent", + "q": word + " wysi" + }) + + assert isinstance(mappools, list), "expected a list" + assert len(mappools) == 0, "expected empty return" diff --git a/otdb/api/tests/test_tournaments.py b/otdb/api/tests/test_tournaments.py new file mode 100644 index 0000000..d5524eb --- /dev/null +++ b/otdb/api/tests/test_tournaments.py @@ -0,0 +1,112 @@ +import pytest +import json + +from .util import parse_resp, get_total_pages +from .. import views + + +@pytest.mark.django_db +class TestTournaments: + @pytest.mark.asyncio + @pytest.mark.dependency() + async def test_create_tournament(self, client, sample_tournament): + # create tournament + req = await client.post("/api/tournaments/", data=json.dumps(sample_tournament)) + tournament = parse_resp(await views.tournaments(req)) + + assert isinstance(tournament, dict), "expected a dict" + self._test_tournament(sample_tournament, tournament, False) + + client.tournament = tournament + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_favorite_tournament(self, client): + tournament = client.tournament + + req = await client.post(f"/api/tournament/{tournament['id']}/favorite/", data=json.dumps({"favorite": True})) + parse_resp(await views.favorite_tournament(req, tournament["id"])) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_get_tournament(self, client, sample_tournament): + tournament = client.tournament + + req = await client.get(f"/api/tournaments/{tournament['id']}/") + data = parse_resp(await views.tournaments(req, tournament['id'])) + + self._test_tournament(data, tournament) + assert data["favorite_count"] == 1, "test_favorite_tournament failed; no favorites on the tournament" + for sample_staff in sample_tournament["staff"]: + assert any(( + staff["roles"] == sample_staff["roles"] and + staff["user"]["id"] == sample_staff["id"] + for staff in data["staff"] + )), "missing staff %d" % sample_staff["id"] + + async def _get_tournaments_listing(self, client, query: dict[str, str | int | None]): + query_string = "&".join((f"{k}={'' if v is None else str(v)}" for k, v in query.items())) + req = await client.get(f"/api/tournaments/?{query_string}") + result = parse_resp(await views.tournaments(req)) + + assert isinstance(result, dict) + data = result["data"] + assert isinstance(data, list) + assert result["total_pages"] == get_total_pages(len(data)) + + return data + + def _test_tournament(self, t1, t2, include_id=True): + if include_id: + assert t1["id"] == t2["id"], "tournament id does not match" + assert t1["name"] == t2["name"], "tournament name does not match" + assert t1["description"] == t2["description"], "tournament description does not match" + assert t1["link"] == t2["link"], "tournament link does not match" + assert t1["abbreviation"] == t2["abbreviation"], "tournament abbreviation does not match" + + def _test_tournament_listing(self, client, tournaments): + assert isinstance(tournaments, list), "expected a list" + assert len(tournaments) == 1, "expected one tournament item" + self._test_tournament(tournaments[0], client.tournament) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_recent_list(self, client): + self._test_tournament_listing( + client, + await self._get_tournaments_listing(client, {"s": "recent"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_favorite_list(self, client): + self._test_tournament_listing( + client, + await self._get_tournaments_listing(client, {"s": "favorites"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_trending_list(self, client): + self._test_tournament_listing( + client, + await self._get_tournaments_listing(client, {"s": "trending"}) + ) + + @pytest.mark.asyncio + @pytest.mark.dependency(depends=["TestTournaments::test_create_tournament"]) + async def test_tournament_search(self, client): + for word in client.tournament["name"].split(): + mappools = await self._get_tournaments_listing(client, { + "s": "recent", + "q": word + }) + self._test_tournament_listing(client, mappools) + + mappools = await self._get_tournaments_listing(client, { + "s": "recent", + "q": word + " wysi" + }) + + assert isinstance(mappools, list), "expected a list" + assert len(mappools) == 0, "expected empty return" diff --git a/otdb/api/tests/test_users.py b/otdb/api/tests/test_users.py new file mode 100644 index 0000000..33660db --- /dev/null +++ b/otdb/api/tests/test_users.py @@ -0,0 +1,18 @@ +import pytest + +from .util import parse_resp +from .. import views + + +@pytest.mark.django_db +class TestUsers: + @pytest.mark.asyncio + @pytest.mark.dependency() + async def test_get_user(self, client, sample_user): + req = await client.get(f"/users/{sample_user['id']}/") + user = parse_resp(await views.users(req, sample_user['id'])) + + assert user["id"] == sample_user["id"], "user id is incorrect" + assert user["username"] == sample_user["username"], "username is incorrect" + assert user["avatar"] == sample_user["avatar"], "avatar is incorrect" + assert user["cover"] == sample_user["cover"], "cover is incorrect" diff --git a/otdb/api/tests/util.py b/otdb/api/tests/util.py new file mode 100644 index 0000000..7446e67 --- /dev/null +++ b/otdb/api/tests/util.py @@ -0,0 +1,11 @@ +import json + +from api.views.listing import LISTING_ITEMS_PER_PAGE + + +def parse_resp(resp): + assert resp.status_code == 200, resp.content.decode("utf-8") + return json.loads(resp.content) if resp.content else None + +def get_total_pages(item_count): + return (item_count - 1) // LISTING_ITEMS_PER_PAGE + 1 diff --git a/otdb/api/views/mappools.py b/otdb/api/views/mappools.py index 98907e0..09348c8 100644 --- a/otdb/api/views/mappools.py +++ b/otdb/api/views/mappools.py @@ -58,13 +58,17 @@ async def get_full_mappool(user, mappool_id) -> dict | None: ) try: - mappool = await Mappool.objects.prefetch_related( + mappool = await Mappool.objects.annotate( + favorite_count=models.Count("favorites") + ).prefetch_related( *prefetch - ).select_related(*include).aget(id=mappool_id) + ).select_related( + *include + ).aget(id=mappool_id) except Mappool.DoesNotExist: return - data = mappool.serialize(includes=include+prefetch) + data = mappool.serialize(includes=include+prefetch+("favorite_count",)) if user.is_authenticated: data["is_favorited"] = await mappool.is_favorited(user.id) diff --git a/otdb/api/views/tournaments.py b/otdb/api/views/tournaments.py index 8de9911..9719c6c 100644 --- a/otdb/api/views/tournaments.py +++ b/otdb/api/views/tournaments.py @@ -23,7 +23,9 @@ class TournamentListing(Listing[Tournament]): async def get_full_tournament(user, id): try: - tournament = await Tournament.objects.prefetch_related( + tournament = await Tournament.objects.annotate( + favorite_count=models.Count("favorites") + ).prefetch_related( "involvements__user", "mappool_connections", models.Prefetch( @@ -37,7 +39,7 @@ async def get_full_tournament(user, id): return data = tournament.serialize( - includes=["involvements__user", "submitted_by", "mappool_connections__mappool__favorite_count"], + includes=["involvements__user", "submitted_by", "mappool_connections__mappool__favorite_count", "favorite_count"], excludes=["mappool_connections__tournament_id"] ) diff --git a/otdb/common/dummy_api.py b/otdb/common/dummy_api.py new file mode 100644 index 0000000..90669a5 --- /dev/null +++ b/otdb/common/dummy_api.py @@ -0,0 +1,33 @@ +import json +import os + +from osu import Beatmap, UserCompact + + +class DummyClient: + __slots__ = ("user_data", "beatmap_data") + + def __init__(self, base_dir): + dummy_data_dir = os.path.join(base_dir, "common", "dummy_api_data") + with open(os.path.join(dummy_data_dir, "users.json"), encoding="utf-8") as f: + self.user_data = json.load(f) + + with open(os.path.join(dummy_data_dir, "beatmaps.json"), encoding="utf-8") as f: + self.beatmap_data = json.load(f) + + async def get_beatmaps(self, beatmap_ids): + return [ + next((Beatmap(beatmap) for beatmap in self.beatmap_data if beatmap["id"] == beatmap_id)) + for beatmap_id in beatmap_ids + ] + + async def get_users(self, user_ids): + return [ + next((UserCompact(user) for user in self.user_data if user["id"] == user_id)) + for user_id in user_ids + ] + + + + + diff --git a/otdb/common/dummy_api_data/beatmaps.json b/otdb/common/dummy_api_data/beatmaps.json new file mode 100644 index 0000000..fbf5024 --- /dev/null +++ b/otdb/common/dummy_api_data/beatmaps.json @@ -0,0 +1 @@ +[{"beatmapset_id": 13169, "difficulty_rating": 5.01, "id": 49612, "mode": "osu", "status": "ranked", "total_length": 143, "user_id": 27343, "version": "ignore's Insane", "accuracy": 8, "ar": 8, "bpm": 162, "convert": false, "count_circles": 434, "count_sliders": 168, "count_spinners": 1, "cs": 4, "deleted_at": null, "drain": 6, "hit_length": 131, "is_scoreable": true, "last_updated": "2014-05-18T15:45:12Z", "mode_int": 0, "passcount": 27999, "playcount": 188746, "ranked": 1, "url": "https://osu.ppy.sh/beatmaps/49612", "checksum": "b261b27d536c93cd0db9ba9671715718", "beatmapset": {"artist": "07th Expansion", "artist_unicode": "07th Expansion", "covers": {"cover": "https://assets.ppy.sh/beatmaps/13169/covers/cover.jpg?1650603695", "cover@2x": "https://assets.ppy.sh/beatmaps/13169/covers/cover@2x.jpg?1650603695", "card": "https://assets.ppy.sh/beatmaps/13169/covers/card.jpg?1650603695", "card@2x": "https://assets.ppy.sh/beatmaps/13169/covers/card@2x.jpg?1650603695", "list": "https://assets.ppy.sh/beatmaps/13169/covers/list.jpg?1650603695", "list@2x": "https://assets.ppy.sh/beatmaps/13169/covers/list@2x.jpg?1650603695", "slimcover": "https://assets.ppy.sh/beatmaps/13169/covers/slimcover.jpg?1650603695", "slimcover@2x": "https://assets.ppy.sh/beatmaps/13169/covers/slimcover@2x.jpg?1650603695"}, "creator": "AngelHoney", "favourite_count": 135, "hype": null, "id": 13169, "nsfw": false, "offset": 0, "play_count": 326991, "preview_url": "//b.ppy.sh/preview/13169.mp3", "source": "Umineko no Naku Koro ni", "spotlight": false, "status": "ranked", "title": "Aci-L", "title_unicode": "Aci-L", "track_id": null, "user_id": 104401, "video": false, "bpm": 162, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": true, "last_updated": "2010-05-21T22:15:47Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/24652", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": 1, "ranked_date": "2010-06-02T18:15:18Z", "storyboard": false, "submitted_date": "2010-02-16T22:27:54Z", "tags": "ep5 -45 iidx ignorethis AHO", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 42, 8, 7, 11, 14, 23, 30, 107, 160, 879]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 534, 646, 1009, 2415, 2753, 2914, 6050, 5310, 3602, 3065, 2092, 1919, 1786, 7351, 1770, 494, 874, 215, 380, 530, 448, 239, 150, 84, 82, 121, 157, 50, 101, 119, 326, 493, 193, 120, 365, 262, 1072, 172, 411, 695, 634, 1641, 1580, 3744, 1396, 466, 649, 1026, 535, 2030, 1760, 577, 408, 508, 439, 322, 155, 144, 240, 204, 91, 42, 48, 81, 74, 145, 81, 75, 32, 97, 58, 142, 81, 37, 10, 12, 34, 65, 30, 7, 12, 12, 10, 23, 7, 15, 11, 19, 20], "exit": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 116, 6536, 2250, 2186, 2854, 1604, 4113, 5670, 3523, 2146, 2252, 2024, 1809, 4926, 5721, 1241, 1078, 950, 549, 817, 738, 600, 722, 626, 800, 472, 945, 300, 311, 190, 924, 2636, 1159, 444, 447, 376, 713, 820, 364, 443, 817, 967, 1068, 963, 1631, 319, 431, 467, 361, 738, 924, 471, 211, 414, 258, 1288, 972, 393, 460, 691, 277, 124, 115, 89, 140, 127, 147, 71, 44, 50, 40, 47, 85, 79, 27, 27, 158, 219, 121, 27, 112, 130, 54, 37, 44, 88, 73, 274, 853]}, "max_combo": 807}, {"beatmapset_id": 1134448, "difficulty_rating": 5.81, "id": 2369018, "mode": "osu", "status": "graveyard", "total_length": 247, "user_id": 53378, "version": "Special", "accuracy": 8.5, "ar": 8.5, "bpm": 122, "convert": false, "count_circles": 721, "count_sliders": 200, "count_spinners": 2, "cs": 5, "deleted_at": null, "drain": 5.5, "hit_length": 221, "is_scoreable": false, "last_updated": "2020-12-12T23:12:15Z", "mode_int": 0, "passcount": 1177, "playcount": 4852, "ranked": -2, "url": "https://osu.ppy.sh/beatmaps/2369018", "checksum": "de09b4fd85b648388874e6eb5b3c5781", "beatmapset": {"artist": "Saint Snow", "artist_unicode": "Saint Snow", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1134448/covers/cover.jpg?1610235015", "cover@2x": "https://assets.ppy.sh/beatmaps/1134448/covers/cover@2x.jpg?1610235015", "card": "https://assets.ppy.sh/beatmaps/1134448/covers/card.jpg?1610235015", "card@2x": "https://assets.ppy.sh/beatmaps/1134448/covers/card@2x.jpg?1610235015", "list": "https://assets.ppy.sh/beatmaps/1134448/covers/list.jpg?1610235015", "list@2x": "https://assets.ppy.sh/beatmaps/1134448/covers/list@2x.jpg?1610235015", "slimcover": "https://assets.ppy.sh/beatmaps/1134448/covers/slimcover.jpg?1610235015", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1134448/covers/slimcover@2x.jpg?1610235015"}, "creator": "ktgster", "favourite_count": 10, "hype": null, "id": 1134448, "nsfw": false, "offset": 0, "play_count": 5391, "preview_url": "//b.ppy.sh/preview/1134448.mp3", "source": "", "spotlight": false, "status": "graveyard", "title": "CRASH MIND", "title_unicode": "CRASH MIND", "track_id": null, "user_id": 53378, "video": false, "bpm": 122, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": false, "last_updated": "2020-12-12T23:12:14Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1042361", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": -2, "ranked_date": null, "storyboard": false, "submitted_date": "2020-03-28T06:29:01Z", "tags": "", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 9, 55, 128, 37, 223, 124, 0, 0, 0, 1, 0, 57, 20, 88, 185, 23, 20, 14, 12, 13, 11, 1, 1, 30, 57, 28, 0, 0, 74, 32, 15, 60, 1, 0, 0, 1, 1, 0, 0, 13, 0, 11, 10, 0, 0, 0, 2, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 1, 11, 0, 0, 1, 1, 9, 0, 1, 0, 0, 0, 0, 0, 10, 4, 0, 0, 0, 5, 0, 0, 11, 9, 0, 1, 0, 0, 0, 0], "exit": [0, 0, 0, 0, 0, 27, 94, 127, 94, 147, 271, 70, 19, 30, 2, 30, 38, 22, 80, 111, 59, 10, 18, 38, 33, 33, 42, 14, 9, 110, 82, 76, 15, 23, 39, 19, 22, 21, 19, 0, 0, 1, 0, 0, 19, 0, 9, 20, 0, 1, 11, 1, 18, 3, 2, 10, 0, 20, 0, 0, 0, 0, 0, 0, 29, 0, 58, 0, 1, 0, 0, 10, 1, 1, 0, 9, 1, 0, 1, 0, 0, 0, 0, 0, 9, 0, 19, 10, 9, 37, 18, 0, 9, 18, 9, 1, 2, 1, 0, 2]}, "max_combo": 1168}, {"beatmapset_id": 1173437, "difficulty_rating": 7.58, "id": 2447573, "mode": "osu", "status": "graveyard", "total_length": 105, "user_id": 3997580, "version": "afsdfasfasdfasdfas", "accuracy": 8, "ar": 9.6, "bpm": 192, "convert": false, "count_circles": 452, "count_sliders": 122, "count_spinners": 1, "cs": 4, "deleted_at": null, "drain": 7, "hit_length": 103, "is_scoreable": false, "last_updated": "2020-05-18T07:28:14Z", "mode_int": 0, "passcount": 1355, "playcount": 10279, "ranked": -2, "url": "https://osu.ppy.sh/beatmaps/2447573", "checksum": "8a4ada65a253e48cbd603003f5fc4382", "beatmapset": {"artist": "Shinku no bara no kishi Prim", "artist_unicode": "\u771f\u7d05\u306e\u8594\u8587\u306e\u9a0e\u58ebPrim", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1173437/covers/cover.jpg?1589786911", "cover@2x": "https://assets.ppy.sh/beatmaps/1173437/covers/cover@2x.jpg?1589786911", "card": "https://assets.ppy.sh/beatmaps/1173437/covers/card.jpg?1589786911", "card@2x": "https://assets.ppy.sh/beatmaps/1173437/covers/card@2x.jpg?1589786911", "list": "https://assets.ppy.sh/beatmaps/1173437/covers/list.jpg?1589786911", "list@2x": "https://assets.ppy.sh/beatmaps/1173437/covers/list@2x.jpg?1589786911", "slimcover": "https://assets.ppy.sh/beatmaps/1173437/covers/slimcover.jpg?1589786911", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1173437/covers/slimcover@2x.jpg?1589786911"}, "creator": "GranDSenpai", "favourite_count": 6, "hype": null, "id": 1173437, "nsfw": false, "offset": 0, "play_count": 10279, "preview_url": "//b.ppy.sh/preview/1173437.mp3", "source": "REFLEC BEAT colette", "spotlight": false, "status": "graveyard", "title": "Ai wa fushicho no yo ni", "title_unicode": "\u611b\u306f\u4e0d\u6b7b\u9ce5\u306e\u69d8\u306b", "track_id": null, "user_id": 3997580, "video": false, "bpm": 192, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": false, "last_updated": "2020-05-18T07:28:14Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1071863", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": -2, "ranked_date": null, "storyboard": false, "submitted_date": "2020-05-18T03:36:50Z", "tags": "senya csy the corrupt", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 0, 65, 255, 281, 257, 508, 285, 169, 164, 128, 72, 45, 82, 200, 208, 82, 39, 72, 83, 27, 66, 64, 94, 145, 185, 27, 9, 0, 0, 9, 0, 0, 0, 0, 9, 63, 63, 54, 36, 38, 120, 438, 356, 84, 136, 28, 195, 108, 203, 54, 18, 118, 118, 126, 27, 0, 9, 0, 0, 0, 18, 0, 0, 0, 27, 18, 9, 9, 27, 0, 18, 38, 0, 0, 9, 0, 9, 0, 0, 0, 9, 0, 37, 1, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 9], "exit": [0, 0, 0, 9, 18, 0, 0, 82, 55, 111, 154, 102, 54, 73, 18, 82, 45, 9, 81, 27, 82, 19, 18, 118, 19, 36, 73, 36, 46, 45, 109, 19, 28, 9, 27, 0, 18, 18, 0, 0, 36, 18, 18, 9, 0, 0, 27, 36, 54, 47, 18, 9, 63, 172, 54, 9, 9, 18, 46, 45, 9, 27, 18, 0, 37, 27, 27, 10, 36, 9, 18, 0, 0, 0, 0, 0, 36, 27, 0, 0, 0, 0, 18, 0, 0, 9, 0, 18, 27, 9, 0, 9, 0, 0, 9, 0, 0, 9, 54, 0]}, "max_combo": 697}, {"beatmapset_id": 1189616, "difficulty_rating": 7.43, "id": 2478754, "mode": "osu", "status": "ranked", "total_length": 188, "user_id": 4966334, "version": "Limit Break", "accuracy": 9.4, "ar": 9.7, "bpm": 190, "convert": false, "count_circles": 1038, "count_sliders": 369, "count_spinners": 4, "cs": 4.1, "deleted_at": null, "drain": 5.4, "hit_length": 183, "is_scoreable": true, "last_updated": "2021-07-01T01:05:15Z", "mode_int": 0, "passcount": 8949, "playcount": 155764, "ranked": 1, "url": "https://osu.ppy.sh/beatmaps/2478754", "checksum": "bba87cd7b5897f54cc370dcca029878c", "beatmapset": {"artist": "SEPHID", "artist_unicode": "SEPHID", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1189616/covers/cover.jpg?1650709743", "cover@2x": "https://assets.ppy.sh/beatmaps/1189616/covers/cover@2x.jpg?1650709743", "card": "https://assets.ppy.sh/beatmaps/1189616/covers/card.jpg?1650709743", "card@2x": "https://assets.ppy.sh/beatmaps/1189616/covers/card@2x.jpg?1650709743", "list": "https://assets.ppy.sh/beatmaps/1189616/covers/list.jpg?1650709743", "list@2x": "https://assets.ppy.sh/beatmaps/1189616/covers/list@2x.jpg?1650709743", "slimcover": "https://assets.ppy.sh/beatmaps/1189616/covers/slimcover.jpg?1650709743", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1189616/covers/slimcover@2x.jpg?1650709743"}, "creator": "DeviousPanda", "favourite_count": 577, "hype": null, "id": 1189616, "nsfw": false, "offset": 0, "play_count": 618587, "preview_url": "//b.ppy.sh/preview/1189616.mp3", "source": "\u6771\u65b9\u8f1d\u91dd\u57ce\u3000\uff5e Double Dealing Character.", "spotlight": false, "status": "ranked", "title": "Critical Cannonball (Extended ver.)", "title_unicode": "Critical Cannonball (Extended ver.)", "track_id": 6624, "user_id": 4966334, "video": false, "bpm": 190, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": true, "last_updated": "2021-07-01T01:05:14Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1085747", "nominations_summary": {"current": 2, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": 1, "ranked_date": "2021-07-08T01:22:34Z", "storyboard": false, "submitted_date": "2020-06-10T08:32:53Z", "tags": "o!wc owc grand finals gf hd1 \u6e80\u6708\u306e\u7af9\u6797 mangetsu no chikurin bamboo forest of the full moon zun touhou 14 stage 3 theme kishinjou \u6771\u65b9project drumstep j-core electronic instrumental video game paradogi xlolicore- my angel watame skydiver atalanta talulah waefwerf sakura blossom -_ascended_- ascended cycopton wanpachi gordon lazarus xoul zelq", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 12, 4, 3, 5, 6, 7, 16, 19, 61, 473]}, "failtimes": {"fail": [0, 0, 0, 668, 511, 2121, 1509, 655, 198, 502, 10248, 0, 9, 47, 81, 36, 27, 12, 1, 0, 0, 19, 510, 2663, 5350, 1056, 4487, 3949, 554, 1266, 45, 18, 215, 3729, 1586, 36, 36, 145, 185, 36, 606, 345, 46, 74, 55, 297, 176, 722, 326, 973, 465, 292, 1040, 660, 14185, 3477, 0, 0, 0, 0, 19, 0, 18, 11, 27, 1497, 128, 364, 698, 87, 1699, 716, 93, 1438, 801, 921, 962, 92, 116, 292, 229, 155, 832, 65, 280, 417, 372, 54, 975, 18, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0], "exit": [36, 18, 27, 1927, 2382, 7423, 7776, 2694, 2955, 1559, 9273, 3872, 474, 739, 318, 225, 129, 410, 74, 110, 231, 167, 163, 651, 979, 943, 1302, 720, 392, 1576, 1031, 233, 255, 746, 446, 319, 198, 598, 502, 82, 482, 586, 156, 268, 129, 474, 434, 855, 366, 665, 221, 171, 179, 240, 1207, 289, 976, 430, 57, 38, 0, 18, 9, 45, 11, 117, 63, 118, 121, 147, 316, 153, 82, 241, 177, 173, 127, 46, 91, 176, 94, 27, 130, 81, 272, 140, 63, 9, 371, 185, 87, 54, 19, 46, 81, 46, 0, 9, 73, 706]}, "max_combo": 1847}, {"beatmapset_id": 1440688, "difficulty_rating": 6.45, "id": 2964073, "mode": "osu", "status": "graveyard", "total_length": 152, "user_id": 998901, "version": "Extra", "accuracy": 9, "ar": 9.7, "bpm": 140, "convert": false, "count_circles": 494, "count_sliders": 321, "count_spinners": 0, "cs": 4, "deleted_at": null, "drain": 3.5, "hit_length": 146, "is_scoreable": false, "last_updated": "2021-07-31T02:15:54Z", "mode_int": 0, "passcount": 11052, "playcount": 32401, "ranked": -2, "url": "https://osu.ppy.sh/beatmaps/2964073", "checksum": "a48127ac236d69b53cc851f0484b3376", "beatmapset": {"artist": "john", "artist_unicode": "john", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1440688/covers/cover.jpg?1630117813", "cover@2x": "https://assets.ppy.sh/beatmaps/1440688/covers/cover@2x.jpg?1630117813", "card": "https://assets.ppy.sh/beatmaps/1440688/covers/card.jpg?1630117813", "card@2x": "https://assets.ppy.sh/beatmaps/1440688/covers/card@2x.jpg?1630117813", "list": "https://assets.ppy.sh/beatmaps/1440688/covers/list.jpg?1630117813", "list@2x": "https://assets.ppy.sh/beatmaps/1440688/covers/list@2x.jpg?1630117813", "slimcover": "https://assets.ppy.sh/beatmaps/1440688/covers/slimcover.jpg?1630117813", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1440688/covers/slimcover@2x.jpg?1630117813"}, "creator": "amanatu2", "favourite_count": 144, "hype": null, "id": 1440688, "nsfw": false, "offset": 0, "play_count": 32824, "preview_url": "//b.ppy.sh/preview/1440688.mp3", "source": "", "spotlight": false, "status": "graveyard", "title": "Shun-ran", "title_unicode": "\u6625\u5d50", "track_id": null, "user_id": 998901, "video": false, "bpm": 140, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": false, "last_updated": "2021-07-31T02:15:53Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1305184", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": -2, "ranked_date": null, "storyboard": false, "submitted_date": "2021-04-23T11:47:53Z", "tags": "\u521d\u97f3\u30df\u30af hatsune miku shunran", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 0, 9, 36, 36, 9, 36, 153, 225, 360, 495, 117, 99, 45, 63, 72, 27, 9, 0, 63, 153, 774, 297, 324, 279, 189, 171, 144, 72, 153, 18, 0, 0, 0, 0, 0, 18, 9, 0, 144, 99, 0, 0, 36, 90, 27, 9, 0, 0, 9, 9, 36, 27, 9, 36, 90, 18, 18, 99, 9, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 54, 27, 0, 9, 9, 9, 54, 36, 45, 99, 9, 45, 27, 36, 0, 45, 72, 0, 126, 0], "exit": [9, 9, 18, 0, 54, 279, 288, 261, 99, 198, 171, 261, 351, 477, 1485, 873, 216, 126, 126, 261, 360, 45, 45, 117, 279, 675, 477, 396, 234, 243, 261, 180, 135, 234, 324, 198, 45, 36, 54, 351, 81, 36, 18, 513, 351, 63, 36, 162, 144, 126, 18, 9, 18, 36, 45, 126, 18, 90, 18, 36, 18, 45, 54, 108, 549, 180, 81, 18, 0, 9, 0, 9, 9, 0, 9, 99, 54, 81, 27, 27, 18, 27, 18, 0, 36, 81, 99, 99, 81, 99, 18, 27, 9, 72, 18, 45, 72, 63, 90, 63]}, "max_combo": 1155}, {"beatmapset_id": 824872, "difficulty_rating": 6.18, "id": 3316178, "mode": "osu", "status": "loved", "total_length": 199, "user_id": 4109923, "version": "Halgoh's Expert", "accuracy": 8.6, "ar": 9.4, "bpm": 175, "convert": false, "count_circles": 306, "count_sliders": 515, "count_spinners": 3, "cs": 4, "deleted_at": null, "drain": 4.6, "hit_length": 178, "is_scoreable": true, "last_updated": "2021-12-02T06:59:58Z", "mode_int": 0, "passcount": 1653, "playcount": 6184, "ranked": 4, "url": "https://osu.ppy.sh/beatmaps/3316178", "checksum": "b3276ef363fc660312897e05e77f35b6", "beatmapset": {"artist": "Mameyudoufu", "artist_unicode": "Mameyudoufu", "covers": {"cover": "https://assets.ppy.sh/beatmaps/824872/covers/cover.jpg?1640849433", "cover@2x": "https://assets.ppy.sh/beatmaps/824872/covers/cover@2x.jpg?1640849433", "card": "https://assets.ppy.sh/beatmaps/824872/covers/card.jpg?1640849433", "card@2x": "https://assets.ppy.sh/beatmaps/824872/covers/card@2x.jpg?1640849433", "list": "https://assets.ppy.sh/beatmaps/824872/covers/list.jpg?1640849433", "list@2x": "https://assets.ppy.sh/beatmaps/824872/covers/list@2x.jpg?1640849433", "slimcover": "https://assets.ppy.sh/beatmaps/824872/covers/slimcover.jpg?1640849433", "slimcover@2x": "https://assets.ppy.sh/beatmaps/824872/covers/slimcover@2x.jpg?1640849433"}, "creator": "Nines", "favourite_count": 322, "hype": null, "id": 824872, "nsfw": false, "offset": 0, "play_count": 90636, "preview_url": "//b.ppy.sh/preview/824872.mp3", "source": "", "spotlight": false, "status": "loved", "title": "Call My Name feat. Yukacco", "title_unicode": "Call My Name feat. Yukacco", "track_id": 5316, "user_id": 2696453, "video": false, "bpm": 175, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": true, "last_updated": "2021-12-02T06:59:53Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/783412", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": 4, "ranked_date": "2024-06-04T01:21:30Z", "storyboard": false, "submitted_date": "2018-08-02T03:20:26Z", "tags": "hardcore future bass f\u00dcgene2 lapix 2018 megarex mrx-023 dj noriken dj poyoshi mrx-032 showcase fugene 2 arcaea memory archive ambrew gatorix thiev xlolicore- my angel watame smug nanachi halgoh altai -_frontier_- t_ony kkipalt smugna scubdomino featured artist fa", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 1, 0, 0, 2, 2, 7, 40]}, "failtimes": {"fail": [0, 0, 0, 0, 18, 297, 54, 9, 9, 9, 0, 27, 27, 0, 9, 135, 45, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 9, 9, 261, 117, 0, 0, 0, 9, 9, 0, 18, 18, 27, 0, 9, 0, 9, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 9, 0, 0, 0, 0, 81, 18, 9, 0, 0, 18, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "exit": [0, 0, 0, 45, 90, 720, 270, 108, 72, 99, 36, 90, 126, 36, 36, 108, 279, 261, 72, 0, 9, 0, 0, 27, 18, 9, 0, 18, 36, 18, 72, 36, 9, 0, 18, 0, 0, 9, 9, 135, 45, 9, 9, 9, 18, 9, 36, 9, 27, 45, 63, 18, 0, 9, 0, 18, 36, 0, 9, 0, 0, 18, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 9, 18, 0, 0, 9, 36, 0, 9, 27, 0, 0, 18, 27, 0, 0, 0, 0, 9, 0, 18, 0, 0, 9]}, "max_combo": 1397}, {"beatmapset_id": 1922323, "difficulty_rating": 6.73, "id": 3993830, "mode": "osu", "status": "ranked", "total_length": 220, "user_id": 10631018, "version": "Liyuu's Extreme", "accuracy": 9.4, "ar": 9.4, "bpm": 193, "convert": false, "count_circles": 736, "count_sliders": 309, "count_spinners": 1, "cs": 4, "deleted_at": null, "drain": 5, "hit_length": 209, "is_scoreable": true, "last_updated": "2023-06-29T07:52:53Z", "mode_int": 0, "passcount": 3013, "playcount": 30052, "ranked": 1, "url": "https://osu.ppy.sh/beatmaps/3993830", "checksum": "a2248f88047ffbfb31388d647565f070", "beatmapset": {"artist": "kessoku band", "artist_unicode": "\u7d50\u675f\u30d0\u30f3\u30c9", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1922323/covers/cover.jpg?1688025192", "cover@2x": "https://assets.ppy.sh/beatmaps/1922323/covers/cover@2x.jpg?1688025192", "card": "https://assets.ppy.sh/beatmaps/1922323/covers/card.jpg?1688025192", "card@2x": "https://assets.ppy.sh/beatmaps/1922323/covers/card@2x.jpg?1688025192", "list": "https://assets.ppy.sh/beatmaps/1922323/covers/list.jpg?1688025192", "list@2x": "https://assets.ppy.sh/beatmaps/1922323/covers/list@2x.jpg?1688025192", "slimcover": "https://assets.ppy.sh/beatmaps/1922323/covers/slimcover.jpg?1688025192", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1922323/covers/slimcover@2x.jpg?1688025192"}, "creator": "Amateurre", "favourite_count": 469, "hype": null, "id": 1922323, "nsfw": false, "offset": 0, "play_count": 357245, "preview_url": "//b.ppy.sh/preview/1922323.mp3", "source": "\u307c\u3063\u3061\u30fb\u3056\u30fb\u308d\u3063\u304f\uff01", "spotlight": false, "status": "ranked", "title": "Guitar to Kodoku to Aoi Hoshi", "title_unicode": "\u30ae\u30bf\u30fc\u3068\u5b64\u72ec\u3068\u84bc\u3044\u60d1\u661f", "track_id": null, "user_id": 7326908, "video": true, "bpm": 193, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": true, "last_updated": "2023-06-29T07:52:50Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1706438", "nominations_summary": {"current": 2, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": 1, "ranked_date": "2023-07-16T22:05:43Z", "storyboard": false, "submitted_date": "2023-01-14T14:19:54Z", "tags": "bocchi the rock btr japanese anime j-rock jrock j-pop jpop loneliness and blue planet gotoh hitori \u5f8c\u85e4\u3072\u3068\u308a yamada ryo \u5c71\u7530\u30ea\u30e7\u30a6 kita ikuyo \u559c\u591a\u90c1\u4ee3 ijichi nijika \u4f0a\u5730\u77e5\u8679\u590f shizuku- kazato asa shikibe mayu ayesha altugle wen jiysea sagu otudou rielle liyuu asagi kowari midorijeon", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 13, 0, 1, 2, 2, 1, 5, 15, 34, 330]}, "failtimes": {"fail": [0, 0, 36, 90, 297, 279, 72, 9, 18, 0, 18, 18, 18, 9, 9, 9, 0, 0, 9, 0, 9, 9, 0, 135, 207, 54, 18, 45, 117, 675, 63, 27, 81, 243, 162, 54, 18, 0, 18, 9, 9, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 9, 0, 0, 18, 18, 9, 0, 0, 27, 135, 18, 18, 45, 153, 36, 27, 36, 9, 9, 36, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 9, 243, 0, 0, 45, 18, 0, 9, 9, 0, 9, 81, 9], "exit": [0, 0, 0, 2133, 2484, 4734, 225, 189, 126, 153, 117, 261, 171, 171, 108, 216, 171, 72, 63, 27, 63, 63, 45, 171, 1125, 522, 117, 270, 324, 1170, 468, 198, 351, 459, 711, 810, 333, 99, 54, 540, 144, 54, 54, 18, 9, 45, 81, 45, 27, 18, 18, 0, 9, 0, 9, 0, 0, 27, 162, 45, 36, 27, 72, 81, 45, 18, 99, 72, 108, 90, 36, 18, 18, 153, 153, 54, 0, 18, 0, 0, 9, 0, 0, 9, 9, 0, 9, 18, 99, 0, 18, 18, 18, 27, 27, 27, 27, 0, 153, 144]}, "max_combo": 1381}, {"beatmapset_id": 1894162, "difficulty_rating": 6.93, "id": 4021669, "mode": "osu", "status": "graveyard", "total_length": 127, "user_id": 12744997, "version": "Strange Tale of Avarice", "accuracy": 9.7, "ar": 9.7, "bpm": 180, "convert": false, "count_circles": 858, "count_sliders": 207, "count_spinners": 1, "cs": 4.2, "deleted_at": null, "drain": 5.8, "hit_length": 127, "is_scoreable": false, "last_updated": "2023-02-25T22:53:46Z", "mode_int": 0, "passcount": 728, "playcount": 3439, "ranked": -2, "url": "https://osu.ppy.sh/beatmaps/4021669", "checksum": "1eadddb495f6b81f8fc5e2a565df0e3a", "beatmapset": {"artist": "Len", "artist_unicode": "Len", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1894162/covers/cover.jpg?1679785316", "cover@2x": "https://assets.ppy.sh/beatmaps/1894162/covers/cover@2x.jpg?1679785316", "card": "https://assets.ppy.sh/beatmaps/1894162/covers/card.jpg?1679785316", "card@2x": "https://assets.ppy.sh/beatmaps/1894162/covers/card@2x.jpg?1679785316", "list": "https://assets.ppy.sh/beatmaps/1894162/covers/list.jpg?1679785316", "list@2x": "https://assets.ppy.sh/beatmaps/1894162/covers/list@2x.jpg?1679785316", "slimcover": "https://assets.ppy.sh/beatmaps/1894162/covers/slimcover.jpg?1679785316", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1894162/covers/slimcover@2x.jpg?1679785316"}, "creator": "Shimarin1", "favourite_count": 11, "hype": null, "id": 1894162, "nsfw": false, "offset": 0, "play_count": 12243, "preview_url": "//b.ppy.sh/preview/1894162.mp3", "source": "\u6771\u65b9\u7d05\u9b54\u90f7\u3000\uff5e the Embodiment of Scarlet Devil.", "spotlight": false, "status": "graveyard", "title": "Kiri Kakaru Mei Yoru ~ Foggy Night ~", "title_unicode": "\u9727\u639b\u304b\u308b\u51a5\u591c \u301cFoggy Night\u301c", "track_id": null, "user_id": 12744997, "video": false, "bpm": 180, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": false, "last_updated": "2023-02-25T22:53:43Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1682900", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": -2, "ranked_date": null, "storyboard": false, "submitted_date": "2022-11-28T23:27:39Z", "tags": "u.n.\u30aa\u30fc\u30a8\u30f3\u306f\u5f7c\u5973\u306a\u306e\u304b\uff1fu.n. ooen wa kanojo na no ka? u.n. owen was her? zun onosakihito flandre embodiment of scarlet devil \u6771\u65b9project touhou th06 koumakyou eastern land flandre scarlet \u4e0a\u6d77\u30a2\u30ea\u30b9\u5e7b\u6a02\u56e3 team shanghai alice power metal instrumental japanese \u5e7b\u60f3\u90f7 gensokyo gensoukyou \u535a\u9e97\u970a\u5922 hakurei reimu \u9727\u96e8\u9b54\u7406 kirisame marisa discord registers circle doujin \u540c\u4eba\u97f3\u697d", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 180, 54, 45, 36, 63, 18, 0, 54, 90, 36, 63, 18, 63, 36, 0, 45, 36, 45, 0, 117, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 18, 18, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 9, 9, 0, 0], "exit": [0, 0, 0, 0, 0, 18, 72, 72, 63, 36, 36, 18, 0, 63, 18, 36, 90, 108, 54, 45, 81, 27, 0, 18, 36, 18, 0, 36, 27, 27, 9, 0, 63, 9, 0, 45, 54, 0, 9, 9, 36, 63, 9, 9, 9, 0, 0, 0, 27, 9, 27, 0, 0, 0, 0, 9, 0, 18, 0, 9, 0, 0, 9, 9, 18, 18, 27, 0, 9, 0, 9, 9, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 9, 0, 0, 0, 27, 0, 18, 0, 9, 0, 9, 0, 9, 9, 0, 27, 45, 0]}, "max_combo": 1307}, {"beatmapset_id": 1948012, "difficulty_rating": 6.06, "id": 4031511, "mode": "osu", "status": "graveyard", "total_length": 179, "user_id": 4694602, "version": "Psychoneurosis", "accuracy": 9, "ar": 9.2, "bpm": 140, "convert": false, "count_circles": 712, "count_sliders": 301, "count_spinners": 1, "cs": 4, "deleted_at": null, "drain": 5, "hit_length": 167, "is_scoreable": false, "last_updated": "2023-02-27T12:31:30Z", "mode_int": 0, "passcount": 2399, "playcount": 5590, "ranked": -2, "url": "https://osu.ppy.sh/beatmaps/4031511", "checksum": "4810bae923647e4c4a31b1b3d11b92b5", "beatmapset": {"artist": "Polyphia", "artist_unicode": "Polyphia", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1948012/covers/cover.jpg?1679922072", "cover@2x": "https://assets.ppy.sh/beatmaps/1948012/covers/cover@2x.jpg?1679922072", "card": "https://assets.ppy.sh/beatmaps/1948012/covers/card.jpg?1679922072", "card@2x": "https://assets.ppy.sh/beatmaps/1948012/covers/card@2x.jpg?1679922072", "list": "https://assets.ppy.sh/beatmaps/1948012/covers/list.jpg?1679922072", "list@2x": "https://assets.ppy.sh/beatmaps/1948012/covers/list@2x.jpg?1679922072", "slimcover": "https://assets.ppy.sh/beatmaps/1948012/covers/slimcover.jpg?1679922072", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1948012/covers/slimcover@2x.jpg?1679922072"}, "creator": "Down", "favourite_count": 5, "hype": null, "id": 1948012, "nsfw": false, "offset": 0, "play_count": 5590, "preview_url": "//b.ppy.sh/preview/1948012.mp3", "source": "", "spotlight": false, "status": "graveyard", "title": "Neurotica", "title_unicode": "Neurotica", "track_id": null, "user_id": 4694602, "video": false, "bpm": 140, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": false, "last_updated": "2023-02-27T12:31:30Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1727970", "nominations_summary": {"current": 0, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": -2, "ranked_date": null, "storyboard": false, "submitted_date": "2023-02-27T12:31:17Z", "tags": "eac2023 eastasiacup qf nm6", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, "failtimes": {"fail": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, 63, 27, 0, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 108, 18, 0, 0, 0, 0, 18, 0, 9, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 9, 0, 0, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 36, 0, 0, 0, 18, 9, 0, 0, 9, 0, 0, 0, 0, 9, 0, 63, 18], "exit": [0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 27, 18, 144, 81, 90, 171, 117, 54, 18, 18, 9, 27, 9, 144, 108, 72, 36, 162, 180, 90, 54, 54, 36, 36, 27, 0, 0, 27, 90, 54, 9, 18, 0, 18, 54, 0, 36, 0, 27, 9, 9, 9, 36, 18, 9, 27, 9, 27, 0, 45, 18, 0, 0, 0, 27, 18, 18, 9, 0, 9, 9, 9, 0, 0, 0, 0, 9, 0, 9, 18, 0, 0, 18, 9, 54, 18, 18, 9, 0, 9, 0, 18, 0, 9, 9, 9, 9, 0, 18, 18]}, "max_combo": 1357}, {"beatmapset_id": 1998555, "difficulty_rating": 5.28, "id": 4154290, "mode": "osu", "status": "ranked", "total_length": 263, "user_id": 700887, "version": "Jumping High!", "accuracy": 8.8, "ar": 9, "bpm": 190, "convert": false, "count_circles": 624, "count_sliders": 405, "count_spinners": 1, "cs": 4, "deleted_at": null, "drain": 5.5, "hit_length": 253, "is_scoreable": true, "last_updated": "2023-06-09T03:02:24Z", "mode_int": 0, "passcount": 1246, "playcount": 10531, "ranked": 1, "url": "https://osu.ppy.sh/beatmaps/4154290", "checksum": "219299a51a9b1c2e41fc560b2bf3ea55", "beatmapset": {"artist": "Waka, Ruka from STAR*ANIS", "artist_unicode": "\u308f\u304b\u30fb\u308b\u304b from STAR\u2606ANIS", "covers": {"cover": "https://assets.ppy.sh/beatmaps/1998555/covers/cover.jpg?1686279760", "cover@2x": "https://assets.ppy.sh/beatmaps/1998555/covers/cover@2x.jpg?1686279760", "card": "https://assets.ppy.sh/beatmaps/1998555/covers/card.jpg?1686279760", "card@2x": "https://assets.ppy.sh/beatmaps/1998555/covers/card@2x.jpg?1686279760", "list": "https://assets.ppy.sh/beatmaps/1998555/covers/list.jpg?1686279760", "list@2x": "https://assets.ppy.sh/beatmaps/1998555/covers/list@2x.jpg?1686279760", "slimcover": "https://assets.ppy.sh/beatmaps/1998555/covers/slimcover.jpg?1686279760", "slimcover@2x": "https://assets.ppy.sh/beatmaps/1998555/covers/slimcover@2x.jpg?1686279760"}, "creator": "Andrea", "favourite_count": 42, "hype": null, "id": 1998555, "nsfw": false, "offset": 0, "play_count": 17975, "preview_url": "//b.ppy.sh/preview/1998555.mp3", "source": "\u30a2\u30a4\u30ab\u30c4\uff01", "spotlight": false, "status": "ranked", "title": "Idol Katsudou! ~Ichigo & Akari Ver.~", "title_unicode": "\u30a2\u30a4\u30c9\u30eb\u6d3b\u52d5\uff01 \u301c\u3044\u3061\u3054 & \u3042\u304b\u308a Ver.\u301c", "track_id": null, "user_id": 33599, "video": false, "bpm": 190, "can_be_hyped": false, "deleted_at": null, "discussion_enabled": true, "discussion_locked": false, "is_scoreable": true, "last_updated": "2023-06-09T03:02:24Z", "legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/1769907", "nominations_summary": {"current": 2, "eligible_main_rulesets": ["osu"], "required_meta": {"main_ruleset": 2, "non_main_ruleset": 1}}, "ranked": 1, "ranked_date": "2023-06-16T16:21:44Z", "storyboard": false, "submitted_date": "2023-05-25T23:11:11Z", "tags": "sekaii sekai sekai-nyan osuplayer111 leomine leominexd pnky kanui ichigo hoshimiya \u661f\u5bae\u3044\u3061\u3054 sumire morohoshi \u8af8\u661f\u3059\u307f\u308c akari oozora \u5927\u7a7a\u3042\u304b\u308a shino shimoji \u4e0b\u5730\u7d2b\u91ce game anime insert song j-pop jpop japanese female vocals cosmos \u30b3\u30b9\u30e2\u30b9 kosumosu pop assort 2nd season second data carddass mini album duo hidekazu tanaka \u7530\u4e2d\u79c0\u548c monaca ury takanori goto \u5f8c\u85e4\u8cb4\u5fb3 aikatsu!", "availability": {"download_disabled": false, "more_information": null}, "ratings": [0, 2, 0, 0, 2, 1, 1, 0, 1, 5, 34]}, "failtimes": {"fail": [0, 0, 27, 261, 153, 45, 63, 27, 18, 27, 0, 54, 72, 180, 9, 9, 36, 18, 0, 9, 18, 0, 0, 0, 18, 27, 0, 36, 117, 90, 72, 63, 72, 36, 0, 36, 54, 9, 9, 0, 18, 36, 9, 0, 0, 0, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 0, 9, 27, 0, 27, 0, 0, 0, 0, 0, 9, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 27, 27, 9, 0, 0, 9], "exit": [0, 0, 693, 666, 531, 198, 450, 252, 207, 306, 90, 207, 414, 396, 135, 90, 54, 99, 63, 90, 180, 72, 27, 45, 99, 27, 36, 153, 126, 198, 108, 45, 135, 81, 36, 72, 54, 9, 27, 36, 81, 36, 54, 18, 72, 18, 27, 9, 18, 27, 9, 36, 9, 9, 9, 9, 18, 9, 18, 18, 27, 9, 9, 0, 9, 0, 0, 9, 9, 0, 9, 9, 9, 0, 18, 0, 9, 9, 0, 9, 9, 9, 0, 18, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9, 0, 0, 0, 27]}, "max_combo": 1453}] \ No newline at end of file diff --git a/otdb/common/dummy_api_data/users.json b/otdb/common/dummy_api_data/users.json new file mode 100644 index 0000000..b177f1b --- /dev/null +++ b/otdb/common/dummy_api_data/users.json @@ -0,0 +1 @@ +[{"avatar_url": "https://a.ppy.sh/14895608?1718517008.jpeg", "country_code": "US", "default_group": "default", "id": 14895608, "is_active": true, "is_bot": false, "is_deleted": false, "is_online": false, "is_supporter": true, "last_visit": "2024-10-01T23:00:42+00:00", "pm_friends_only": false, "profile_colour": null, "username": "Sheppsu", "country": {"code": "US", "name": "United States"}, "cover": {"custom_url": "https://assets.ppy.sh/user-profile-covers/14895608/859a7bda8ad09971013e5b7d1c619d1ca7b4cb0ee9caaaad8072a18973f3bad0.jpeg", "url": "https://assets.ppy.sh/user-profile-covers/14895608/859a7bda8ad09971013e5b7d1c619d1ca7b4cb0ee9caaaad8072a18973f3bad0.jpeg", "id": null}, "groups": [], "statistics_rulesets": {"osu": {"count_100": 1760010, "count_300": 16692758, "count_50": 219367, "count_miss": 641195, "level": {"current": 101, "progress": 1}, "global_rank": 22434, "global_rank_exp": null, "pp": 7314.34, "pp_exp": 0, "ranked_score": 31575317472, "hit_accuracy": 98.9262, "play_count": 77652, "play_time": 4704256, "total_score": 128394959445, "total_hits": 18672135, "maximum_combo": 4065, "replays_watched_by_others": 34, "is_ranked": true, "grade_counts": {"ss": 25, "ssh": 22, "s": 585, "sh": 392, "a": 1750}}, "taiko": {"count_100": 784, "count_300": 4808, "count_50": 0, "count_miss": 1376, "level": {"current": 7, "progress": 35}, "global_rank": 156513, "global_rank_exp": null, "pp": 180.443, "pp_exp": 0, "ranked_score": 1099902, "hit_accuracy": 89.5627, "play_count": 32, "play_time": 3589, "total_score": 2403101, "total_hits": 5592, "maximum_combo": 148, "replays_watched_by_others": 1, "is_ranked": true, "grade_counts": {"ss": 0, "ssh": 0, "s": 1, "sh": 0, "a": 3}}, "fruits": {"count_100": 197, "count_300": 2743, "count_50": 1651, "count_miss": 338, "level": {"current": 8, "progress": 8}, "global_rank": 607070, "global_rank_exp": null, "pp": 22.3262, "pp_exp": 0, "ranked_score": 534631, "hit_accuracy": 88.9771, "play_count": 46, "play_time": 1888, "total_score": 3190272, "total_hits": 4591, "maximum_combo": 65, "replays_watched_by_others": 0, "is_ranked": true, "grade_counts": {"ss": 0, "ssh": 0, "s": 0, "sh": 0, "a": 1}}, "mania": {"count_100": 102100, "count_300": 720885, "count_50": 4433, "count_miss": 12569, "level": {"current": 49, "progress": 67}, "global_rank": 173976, "global_rank_exp": null, "pp": 1539.26, "pp_exp": 0, "ranked_score": 272411239, "hit_accuracy": 95.5243, "play_count": 1659, "play_time": 130278, "total_score": 804852178, "total_hits": 827418, "maximum_combo": 2153, "replays_watched_by_others": 0, "is_ranked": true, "grade_counts": {"ss": 0, "ssh": 0, "s": 203, "sh": 0, "a": 92}}}}] \ No newline at end of file diff --git a/otdb/common/middleware.py b/otdb/common/middleware.py index d8328af..55bc6a4 100644 --- a/otdb/common/middleware.py +++ b/otdb/common/middleware.py @@ -5,6 +5,7 @@ import logging import asyncio +from asgiref.sync import markcoroutinefunction log = logging.getLogger(__name__) @@ -23,14 +24,15 @@ class Middleware: def __init__(self, get_response): self.get_response = get_response + markcoroutinefunction(self) async def __call__(self, *args, **kwargs): return await self.get_response(*args, **kwargs) class ExceptionHandlingMiddleware(Middleware): - - async def process_exception(self, req, exc): + # process_exception is always adapted to be synchronous by django anyways + def process_exception(self, req, exc): if isinstance(exc, ExpectedException): return JsonResponse({"error": exc.args[0]}, safe=False, status=exc.args[1]) @@ -51,3 +53,10 @@ def callback(task): log.exception(exc) asyncio.create_task(increment()).add_done_callback(callback) + + +def get_response_wrapper(response): + def get_response(): + return response + + return get_response diff --git a/otdb/otdb/settings.py b/otdb/otdb/settings.py index f8f897c..1ffecab 100644 --- a/otdb/otdb/settings.py +++ b/otdb/otdb/settings.py @@ -17,6 +17,8 @@ from pathlib import Path from osu import AsynchronousClient, AsynchronousAuthHandler, Scope +from common.dummy_api import DummyClient + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -44,8 +46,6 @@ "api.apps.ApiConfig", "admin.apps.AdminConfig", - "debug_toolbar", - "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", @@ -62,7 +62,6 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "debug_toolbar.middleware.DebugToolbarMiddleware", "common.middleware.ExceptionHandlingMiddleware", "common.middleware.TrafficStatisticsMiddleware" ] @@ -92,6 +91,7 @@ LOGGING = DEFAULT_LOGGING +LOGGING["handlers"]["console"]["level"] = "DEBUG" if DEBUG else "INFO" LOGGING["loggers"]["django.request"] = { "handlers": ["console"], "level": "DEBUG" @@ -173,9 +173,12 @@ APPEND_SLASH = True -OSU_CLIENT_ID = int(os.getenv("OSU_CLIENT_ID")) -OSU_CLIENT_SECRET = os.getenv("OSU_CLIENT_SECRET") -OSU_CLIENT_REDIRECT_URI = os.getenv("OSU_CLIENT_REDIRECT_URI") -auth = AsynchronousAuthHandler(OSU_CLIENT_ID, OSU_CLIENT_SECRET, OSU_CLIENT_REDIRECT_URI, Scope.identify()) -OSU_AUTH_URL = auth.get_auth_url() -OSU_CLIENT = AsynchronousClient(auth) +if not IS_GITHUB_WORKFLOW: + OSU_CLIENT_ID = int(os.getenv("OSU_CLIENT_ID")) + OSU_CLIENT_SECRET = os.getenv("OSU_CLIENT_SECRET") + OSU_CLIENT_REDIRECT_URI = os.getenv("OSU_CLIENT_REDIRECT_URI") + auth = AsynchronousAuthHandler(OSU_CLIENT_ID, OSU_CLIENT_SECRET, OSU_CLIENT_REDIRECT_URI, Scope.identify()) + OSU_AUTH_URL = auth.get_auth_url() + OSU_CLIENT = AsynchronousClient(auth) +else: + OSU_CLIENT = DummyClient(BASE_DIR) diff --git a/otdb/otdb/urls.py b/otdb/otdb/urls.py index 99b8447..54a7756 100644 --- a/otdb/otdb/urls.py +++ b/otdb/otdb/urls.py @@ -21,6 +21,5 @@ path("", include("main.urls")), path("db/", include("database.urls")), path("api/", include("api.urls")), - path("admin/", include("admin.urls")), - path("__debug__/", include("debug_toolbar.urls")) + path("admin/", include("admin.urls")) ] diff --git a/otdb/pytest.ini b/otdb/pytest.ini index 5c74ab4..496f4cd 100644 --- a/otdb/pytest.ini +++ b/otdb/pytest.ini @@ -1,3 +1,3 @@ [pytest] DJANGO_SETTINGS_MODULE = otdb.settings -python_files = tests.py \ No newline at end of file +testpaths = api/tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 3894535..e67101d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ requests==2.32.3 pytest==8.3.3 pytest-django==4.8.0 pytest-asyncio==0.23.7 +pytest-dependency==0.6.0 aiofiles==24.1.0 \ No newline at end of file