From 42cb30bc19913e7fa5dde2c99dd0c54e72ff0b77 Mon Sep 17 00:00:00 2001 From: extreme4all <40169115+extreme4all@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:37:35 +0100 Subject: [PATCH 1/4] improved naming --- README.md | 38 +++++++++++++------ osrs/__init__.py | 4 -- osrs/async_api/osrs/__init__.py | 4 -- osrs/asyncio/__init__.py | 23 +++++++++++ osrs/asyncio/osrs/__init__.py | 4 ++ osrs/{async_api => asyncio}/osrs/hiscores.py | 0 osrs/{async_api => asyncio}/osrs/itemdb.py | 0 osrs/{async_api => asyncio}/wiki/__init__.py | 2 +- osrs/{async_api => asyncio}/wiki/prices.py | 0 osrs/{async_api => sync}/__init__.py | 0 osrs/{sync_api => sync/osrs}/__init__.py | 0 .../osrs => sync/runelite}/__init__.py | 0 osrs/sync_api/runelite/__init__.py | 0 tests/test_async_osrs_hiscore.py | 7 ++-- tests/test_async_osrs_itemdb.py | 18 ++++----- tests/test_async_wiki_prices.py | 5 +-- 16 files changed, 68 insertions(+), 37 deletions(-) delete mode 100644 osrs/async_api/osrs/__init__.py create mode 100644 osrs/asyncio/__init__.py create mode 100644 osrs/asyncio/osrs/__init__.py rename osrs/{async_api => asyncio}/osrs/hiscores.py (100%) rename osrs/{async_api => asyncio}/osrs/itemdb.py (100%) rename osrs/{async_api => asyncio}/wiki/__init__.py (81%) rename osrs/{async_api => asyncio}/wiki/prices.py (100%) rename osrs/{async_api => sync}/__init__.py (100%) rename osrs/{sync_api => sync/osrs}/__init__.py (100%) rename osrs/{sync_api/osrs => sync/runelite}/__init__.py (100%) delete mode 100644 osrs/sync_api/runelite/__init__.py diff --git a/README.md b/README.md index 9cbdd17..2c95a56 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ import asyncio from aiohttp import ClientSession -from osrs.async_api.osrs.hiscores import Mode, PlayerStats, Hiscore, RateLimiter +from osrs.asyncio import Hiscore, HSMode +from osrs.utils import RateLimiter from osrs.exceptions import PlayerDoesNotExist @@ -18,7 +19,7 @@ async def main(): async with ClientSession() as session: player_stats = await hiscore_instance.get( - mode=Mode.OLDSCHOOL, + mode=HSMode.OLDSCHOOL, player="extreme4all", session=session, ) @@ -32,7 +33,8 @@ if __name__ == "__main__": ```py import asyncio from aiohttp import ClientSession -from osrs.async_api.osrs.itemdb import Mode, Catalogue, Graph, RateLimiter +from ors.asyncio import ItemDBMode, Catalogue, Graph +from osrs.utils import RateLimiter async def main(): # Initialize the Catalogue with optional proxy and rate limiter @@ -49,7 +51,7 @@ async def main(): session, alpha=alpha, page=page, - mode=Mode.OLDSCHOOL, + mode=ItemDBMode.OLDSCHOOL, category=category ) print("Fetched Items:", items) @@ -59,7 +61,7 @@ async def main(): item_detail = await catalogue_instance.get_detail( session, item_id=item_id, - mode=Mode.OLDSCHOOL + mode=ItemDBMode.OLDSCHOOL ) print("Item Detail:", item_detail) @@ -68,7 +70,7 @@ async def main(): trade_history = await graph_instance.get_graph( session, item_id=item_id, - mode=Mode.OLDSCHOOL + mode=ItemDBMode.OLDSCHOOL ) print("Trade History:", trade_history) @@ -81,27 +83,39 @@ the wiki via runelite collects item price, which they expose via an api. ```py import asyncio from aiohttp import ClientSession -from osrs.async_api.wiki.prices import WikiPrices, AveragePrices, LatestPrices, TimeSeries, ItemMapping +from osrs.asyncio import WikiPrices, Interval async def main(): - wiki_prices_instance = WikiPrices(user_agent="Your User Agent") + prices_instance = WikiPrices(user_agent="Your User Agent") async with ClientSession() as session: # Fetch item mappings - mappings = await wiki_prices_instance.get_mapping(session=session) + mappings = await prices_instance.get_mapping( + session=session + ) print("Item Mappings:", mappings) # Fetch latest prices - latest_prices = await wiki_prices_instance.get_latest_prices(session=session) + latest_prices = await prices_instance.get_latest_prices( + session=session + ) print("Latest Prices:", latest_prices) # Fetch average prices - average_prices = await wiki_prices_instance.get_average_prices(session=session, interval=Interval.FIVE_MIN) + average_prices = await prices_instance.get_average_prices( + session=session, + interval=Interval.FIVE_MIN + ) + print("Average Prices:", average_prices) # Fetch time series data item_id = 4151 # Example item ID (Abyssal whip in OSRS) - time_series = await wiki_prices_instance.get_time_series(session=session, item_id=item_id, timestep=Interval.ONE_HOUR) + time_series = await prices_instance.get_time_series( + session=session, + item_id=item_id, + timestep=Interval.ONE_HOUR + ) print("Time Series Data:", time_series) # Run the asynchronous main function diff --git a/osrs/__init__.py b/osrs/__init__.py index 0bdce72..e69de29 100644 --- a/osrs/__init__.py +++ b/osrs/__init__.py @@ -1,4 +0,0 @@ -from osrs.async_api.osrs.hiscores import Hiscore -from osrs.utils import RateLimiter - -__all__ = ["Hiscore", "RateLimiter"] diff --git a/osrs/async_api/osrs/__init__.py b/osrs/async_api/osrs/__init__.py deleted file mode 100644 index 6512d41..0000000 --- a/osrs/async_api/osrs/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from osrs.async_api.osrs.hiscores import Hiscore -from osrs.async_api.osrs.itemdb import Catalogue, Graph - -__all__ = ["Hiscore", "Catalogue", "Graph"] diff --git a/osrs/asyncio/__init__.py b/osrs/asyncio/__init__.py new file mode 100644 index 0000000..bb9c9bb --- /dev/null +++ b/osrs/asyncio/__init__.py @@ -0,0 +1,23 @@ +from osrs.asyncio.osrs.hiscores import Hiscore +from osrs.asyncio.osrs.hiscores import Mode as HSMode +from osrs.asyncio.osrs.itemdb import Catalogue, Graph +from osrs.asyncio.osrs.itemdb import Mode as ItemDBMode +from osrs.asyncio.wiki.prices import ( + Interval, + WikiPrices, +) + +__all__ = ["WikiPrices", "AveragePrices", "LatestPrices", "TimeSeries", "ItemMapping"] + +__all__ = [ + # osrs.hiscore + "Hiscore", + "HSMode", + # osrs.itemdb + "Catalogue", + "ItemDBMode", + "Graph", + # wiki + "Interval", + "WikiPrices", +] diff --git a/osrs/asyncio/osrs/__init__.py b/osrs/asyncio/osrs/__init__.py new file mode 100644 index 0000000..5cf6c43 --- /dev/null +++ b/osrs/asyncio/osrs/__init__.py @@ -0,0 +1,4 @@ +from osrs.asyncio.osrs.hiscores import Hiscore +from osrs.asyncio.osrs.itemdb import Catalogue, Graph + +__all__ = ["Hiscore", "Catalogue", "Graph"] diff --git a/osrs/async_api/osrs/hiscores.py b/osrs/asyncio/osrs/hiscores.py similarity index 100% rename from osrs/async_api/osrs/hiscores.py rename to osrs/asyncio/osrs/hiscores.py diff --git a/osrs/async_api/osrs/itemdb.py b/osrs/asyncio/osrs/itemdb.py similarity index 100% rename from osrs/async_api/osrs/itemdb.py rename to osrs/asyncio/osrs/itemdb.py diff --git a/osrs/async_api/wiki/__init__.py b/osrs/asyncio/wiki/__init__.py similarity index 81% rename from osrs/async_api/wiki/__init__.py rename to osrs/asyncio/wiki/__init__.py index a2007cd..98f7a79 100644 --- a/osrs/async_api/wiki/__init__.py +++ b/osrs/asyncio/wiki/__init__.py @@ -1,4 +1,4 @@ -from osrs.async_api.wiki.prices import ( +from osrs.asyncio.wiki.prices import ( AveragePrices, ItemMapping, LatestPrices, diff --git a/osrs/async_api/wiki/prices.py b/osrs/asyncio/wiki/prices.py similarity index 100% rename from osrs/async_api/wiki/prices.py rename to osrs/asyncio/wiki/prices.py diff --git a/osrs/async_api/__init__.py b/osrs/sync/__init__.py similarity index 100% rename from osrs/async_api/__init__.py rename to osrs/sync/__init__.py diff --git a/osrs/sync_api/__init__.py b/osrs/sync/osrs/__init__.py similarity index 100% rename from osrs/sync_api/__init__.py rename to osrs/sync/osrs/__init__.py diff --git a/osrs/sync_api/osrs/__init__.py b/osrs/sync/runelite/__init__.py similarity index 100% rename from osrs/sync_api/osrs/__init__.py rename to osrs/sync/runelite/__init__.py diff --git a/osrs/sync_api/runelite/__init__.py b/osrs/sync_api/runelite/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_async_osrs_hiscore.py b/tests/test_async_osrs_hiscore.py index e2e991f..a88e6fa 100644 --- a/tests/test_async_osrs_hiscore.py +++ b/tests/test_async_osrs_hiscore.py @@ -1,7 +1,8 @@ import pytest from aiohttp import ClientSession -from osrs.async_api.osrs.hiscores import Hiscore, Mode, PlayerStats +from osrs.asyncio import Hiscore, HSMode +from osrs.asyncio.osrs.hiscores import PlayerStats from osrs.exceptions import PlayerDoesNotExist @@ -10,7 +11,7 @@ async def test_get_valid(): hiscore_instance = Hiscore() async with ClientSession() as session: player_stats = await hiscore_instance.get( - mode=Mode.OLDSCHOOL, + mode=HSMode.OLDSCHOOL, player="extreme4all", session=session, ) @@ -29,7 +30,7 @@ async def test_get_invalid(): async with ClientSession() as session: with pytest.raises(PlayerDoesNotExist): _ = await hiscore_instance.get( - mode=Mode.OLDSCHOOL, + mode=HSMode.OLDSCHOOL, player="This_is_not_a_valid_name", session=session, ) diff --git a/tests/test_async_osrs_itemdb.py b/tests/test_async_osrs_itemdb.py index 58c7f80..e400df7 100644 --- a/tests/test_async_osrs_itemdb.py +++ b/tests/test_async_osrs_itemdb.py @@ -1,12 +1,10 @@ import pytest from aiohttp import ClientSession -from osrs.async_api.osrs.itemdb import ( - Catalogue, +from osrs.asyncio import Catalogue, Graph, ItemDBMode +from osrs.asyncio.osrs.itemdb import ( Detail, - Graph, Items, - Mode, TradeHistory, ) @@ -20,7 +18,7 @@ async def test_get_items_valid(): session=session, alpha="a", # Assume items starting with "A" exist page=1, - mode=Mode.OLDSCHOOL, + mode=ItemDBMode.OLDSCHOOL, category=1, ) @@ -39,7 +37,7 @@ async def test_get_items_invalid_page(): session=session, alpha="A", page=9999, # Assume this page does not exist - mode=Mode.OLDSCHOOL, + mode=ItemDBMode.OLDSCHOOL, category=1, ) @@ -55,7 +53,7 @@ async def test_get_detail_valid(): async with ClientSession() as session: item_id = 4151 # Assume this is a valid item ID item_detail = await catalogue_instance.get_detail( - session=session, item_id=item_id, mode=Mode.OLDSCHOOL + session=session, item_id=item_id, mode=ItemDBMode.OLDSCHOOL ) # Assertions to confirm the response is correct @@ -75,7 +73,7 @@ async def test_get_detail_invalid(): Exception ): # Replace Exception with a specific exception if defined await catalogue_instance.get_detail( - session=session, item_id=invalid_item_id, mode=Mode.OLDSCHOOL + session=session, item_id=invalid_item_id, mode=ItemDBMode.OLDSCHOOL ) @@ -86,7 +84,7 @@ async def test_get_graph_valid(): async with ClientSession() as session: item_id = 4151 # Assume this is a valid item ID trade_history = await catalogue_instance.get_graph( - session=session, item_id=item_id, mode=Mode.OLDSCHOOL + session=session, item_id=item_id, mode=ItemDBMode.OLDSCHOOL ) # Assertions to confirm the response is correct @@ -107,5 +105,5 @@ async def test_get_graph_invalid(): Exception ): # Replace Exception with a specific exception if defined await catalogue_instance.get_graph( - session=session, item_id=invalid_item_id, mode=Mode.OLDSCHOOL + session=session, item_id=invalid_item_id, mode=ItemDBMode.OLDSCHOOL ) diff --git a/tests/test_async_wiki_prices.py b/tests/test_async_wiki_prices.py index f3e1af3..8b7777c 100644 --- a/tests/test_async_wiki_prices.py +++ b/tests/test_async_wiki_prices.py @@ -1,13 +1,12 @@ import pytest from aiohttp import ClientSession -from osrs.async_api.wiki.prices import ( +from osrs.asyncio import Interval, WikiPrices +from osrs.asyncio.wiki.prices import ( AveragePrices, - Interval, ItemMapping, LatestPrices, TimeSeries, - WikiPrices, ) From 586ff0ef4ea382d98b1cf5c2386d7884f778b2bf Mon Sep 17 00:00:00 2001 From: extreme4all <40169115+extreme4all@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:39:26 +0100 Subject: [PATCH 2/4] fixed typos --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c95a56..8e61422 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,10 @@ if __name__ == "__main__": ## osrs itemdb (Catalogue & Grand Exchange) ```py import asyncio + from aiohttp import ClientSession -from ors.asyncio import ItemDBMode, Catalogue, Graph + +from osrs.asyncio import ItemDBMode, Catalogue, Graph from osrs.utils import RateLimiter async def main(): @@ -82,11 +84,15 @@ if __name__ == "__main__": the wiki via runelite collects item price, which they expose via an api. ```py import asyncio + from aiohttp import ClientSession + from osrs.asyncio import WikiPrices, Interval +from osrs.utils import RateLimiter async def main(): - prices_instance = WikiPrices(user_agent="Your User Agent") + limiter = RateLimiter(calls_per_interval=100, interval=60) + prices_instance = WikiPrices(user_agent="Your User Agent", rate_limiter=limiter) async with ClientSession() as session: # Fetch item mappings From d236aec3007454d62b5e9aa05c87bd80f6a54216 Mon Sep 17 00:00:00 2001 From: extreme4all <40169115+extreme4all@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:39:12 +0100 Subject: [PATCH 3/4] no session support --- osrs/asyncio/osrs/hiscores.py | 17 +++++++++++--- osrs/asyncio/osrs/itemdb.py | 39 ++++++++++++++++++++++++-------- osrs/asyncio/wiki/prices.py | 28 +++++++++++++++++------ tests/test_async_osrs_hiscore.py | 12 ++++++++++ tests/test_async_osrs_itemdb.py | 18 +++++++++++++++ 5 files changed, 94 insertions(+), 20 deletions(-) diff --git a/osrs/asyncio/osrs/hiscores.py b/osrs/asyncio/osrs/hiscores.py index ccdcd6b..51c30d3 100644 --- a/osrs/asyncio/osrs/hiscores.py +++ b/osrs/asyncio/osrs/hiscores.py @@ -49,7 +49,12 @@ def __init__( self.proxy = proxy self.rate_limiter = rate_limiter - async def get(self, mode: Mode, player: str, session: ClientSession) -> PlayerStats: + async def get( + self, + player: str, + mode: Mode = Mode.OLDSCHOOL, + session: ClientSession | None = None, + ) -> PlayerStats: """ Fetches player stats from the OSRS hiscores API. @@ -73,7 +78,9 @@ async def get(self, mode: Mode, player: str, session: ClientSession) -> PlayerSt url = f"{self.BASE_URL}/m={mode.value}/index_lite.json" params = {"player": player} - async with session.get(url, proxy=self.proxy, params=params) as response: + _session = ClientSession() if session is None else session + + async with _session.get(url, proxy=self.proxy, params=params) as response: # when the HS are down it will redirect to the main page. # after redirction it will return a 200, so we must check for redirection first if response.history and any(r.status == 302 for r in response.history): @@ -88,4 +95,8 @@ async def get(self, mode: Mode, player: str, session: ClientSession) -> PlayerSt response.raise_for_status() raise Undefined() data = await response.json() - return PlayerStats(**data) + + if session is None: + await _session.close() + + return PlayerStats(**data) diff --git a/osrs/asyncio/osrs/itemdb.py b/osrs/asyncio/osrs/itemdb.py index d7f81f8..d738cc7 100644 --- a/osrs/asyncio/osrs/itemdb.py +++ b/osrs/asyncio/osrs/itemdb.py @@ -98,10 +98,10 @@ def __init__( async def get_items( self, - session: ClientSession, alpha: str, page: int | None = 1, mode: Mode = Mode.OLDSCHOOL, + session: ClientSession | None = None, category: int = 1, ) -> Items: """Fetch items from the RuneScape item catalog based on alphabetical filter. @@ -124,16 +124,22 @@ async def get_items( logger.debug(f"[GET]: {url=}, {params=}") - async with session.get(url, proxy=self.proxy, params=params) as response: + _session = ClientSession() if session is None else session + + async with _session.get(url, proxy=self.proxy, params=params) as response: response.raise_for_status() data = await response.text() - return Items(**json.loads(data)) + + if session is None: + await _session.close() + + return Items(**json.loads(data)) async def get_detail( self, - session: ClientSession, item_id: int, mode: Mode = Mode.OLDSCHOOL, + session: ClientSession | None = None, ) -> Detail: """Fetch detailed information about a specific item. @@ -152,17 +158,24 @@ async def get_detail( logger.debug(f"[GET]: {url=}, {params=}") - async with session.get(url, proxy=self.proxy, params=params) as response: + _session = ClientSession() if session is None else session + + async with _session.get(url, proxy=self.proxy, params=params) as response: response.raise_for_status() data = await response.text() - return Detail(**json.loads(data)) + + if session is None: + await _session.close() + return Detail(**json.loads(data)) class Graph: BASE_URL = "https://secure.runescape.com" def __init__( - self, proxy: str = "", rate_limiter: RateLimiter = RateLimiter() + self, + proxy: str = "", + rate_limiter: RateLimiter = RateLimiter(), ) -> None: """Initialize the Catalogue with an optional proxy and rate limiter. @@ -176,9 +189,9 @@ def __init__( async def get_graph( self, - session: ClientSession, item_id: int, mode: Mode = Mode.OLDSCHOOL, + session: ClientSession | None = None, ) -> TradeHistory: """Fetch trade history graph data for a specific item. @@ -196,7 +209,13 @@ async def get_graph( logger.debug(f"[GET]: {url=}") - async with session.get(url, proxy=self.proxy) as response: + _session = ClientSession() if session is None else session + + async with _session.get(url, proxy=self.proxy) as response: response.raise_for_status() data = await response.text() - return TradeHistory(**json.loads(data)) + + if session is None: + await _session.close() + + return TradeHistory(**json.loads(data)) diff --git a/osrs/asyncio/wiki/prices.py b/osrs/asyncio/wiki/prices.py index 8ff895f..bfe640d 100644 --- a/osrs/asyncio/wiki/prices.py +++ b/osrs/asyncio/wiki/prices.py @@ -94,7 +94,12 @@ def __init__( raise Exception("invalid input") self.user_agent = inp - async def fetch_data(self, session: ClientSession, url: str, params: dict = {}): + async def fetch_data( + self, + url: str, + session: ClientSession | None = None, + params: dict = {}, + ): """ Utility method to fetch data from a specific endpoint, with ratelimiter, and basic error handling @@ -109,16 +114,23 @@ async def fetch_data(self, session: ClientSession, url: str, params: dict = {}): """ await self.rate_limiter.check() - async with session.get(url, proxy=self.proxy, params=params) as response: + _session = ClientSession() if session is None else session + + async with _session.get(url, proxy=self.proxy, params=params) as response: if response.status == 400: error = await response.json() raise Exception(error) elif response.status != 200: response.raise_for_status() raise Undefined("Unexpected error.") - return await response.json() + data = await response.json() + + if session is None: + await _session.close() + + return data - async def get_mapping(self, session: ClientSession): + async def get_mapping(self, session: ClientSession | None = None): """ Fetches item mappings containing metadata. @@ -138,7 +150,9 @@ async def get_mapping(self, session: ClientSession): data = await self.fetch_data(session=session, url=url) return [ItemMapping(**item) for item in data] - async def get_latest_prices(self, session: ClientSession) -> LatestPrices: + async def get_latest_prices( + self, session: ClientSession | None = None + ) -> LatestPrices: """ Fetches the latest prices for all items. @@ -160,8 +174,8 @@ async def get_latest_prices(self, session: ClientSession) -> LatestPrices: async def get_average_prices( self, - session: ClientSession, interval: Interval, + session: ClientSession | None = None, timestamp: int | None = None, ) -> AveragePrices: """ @@ -187,7 +201,7 @@ async def get_average_prices( return AveragePrices(**data) async def get_time_series( - self, session: ClientSession, item_id: int, timestep: Interval + self, item_id: int, timestep: Interval, session: ClientSession | None = None ) -> TimeSeries: """ Fetches time-series data for a specific item and timestep. diff --git a/tests/test_async_osrs_hiscore.py b/tests/test_async_osrs_hiscore.py index a88e6fa..98e590a 100644 --- a/tests/test_async_osrs_hiscore.py +++ b/tests/test_async_osrs_hiscore.py @@ -34,3 +34,15 @@ async def test_get_invalid(): player="This_is_not_a_valid_name", session=session, ) + + +@pytest.mark.asyncio +async def test_get_default_no_session(): + hiscore_instance = Hiscore() + player_stats = await hiscore_instance.get(player="extreme4all") + # Assertions to confirm the response is correct + assert isinstance( + player_stats, PlayerStats + ), "The returned object is not of type PlayerStats" + assert player_stats.skills, "Skills data should not be empty" + assert player_stats.activities, "Activities data should not be empty" diff --git a/tests/test_async_osrs_itemdb.py b/tests/test_async_osrs_itemdb.py index e400df7..a6a1d6d 100644 --- a/tests/test_async_osrs_itemdb.py +++ b/tests/test_async_osrs_itemdb.py @@ -95,6 +95,24 @@ async def test_get_graph_valid(): assert trade_history.average, "Average trade history should not be empty" +@pytest.mark.asyncio +async def test_get_graph_valid_no_session(): + """Test fetching trade history for a valid item ID""" + catalogue_instance = Graph() + + item_id = 4151 # Assume this is a valid item ID + trade_history = await catalogue_instance.get_graph( + item_id=item_id, mode=ItemDBMode.OLDSCHOOL + ) + + # Assertions to confirm the response is correct + assert isinstance( + trade_history, TradeHistory + ), "The returned object is not of type TradeHistory" + assert trade_history.daily, "Daily trade history should not be empty" + assert trade_history.average, "Average trade history should not be empty" + + @pytest.mark.asyncio async def test_get_graph_invalid(): """Test fetching trade history for an invalid item ID""" From 1268c5b52b5d3828eb488031c08979aae16a8aa0 Mon Sep 17 00:00:00 2001 From: extreme4all <40169115+extreme4all@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:45:03 +0100 Subject: [PATCH 4/4] noting the speed improvement --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 8e61422..1e075e4 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ async def main(): ) print(player_stats) + # if you do not provide a session we'll make one for you, this session will not be reused + # for multiple requests we advice doing that within one session like the example above + player_stats = await hiscore_instance.get( + mode=HSMode.OLDSCHOOL, + player="extreme4all", + ) + print(player_stats) # Run the asynchronous main function if __name__ == "__main__": asyncio.run(main())