diff --git a/README.md b/README.md index c8bbc27..1453eca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # The project The goal is to make a wrapper around the various oldschool runescape api's. -# osrs hiscores +## osrs hiscores ```py import asyncio @@ -24,10 +24,11 @@ async def main(): ) print(player_stats) - -loop = asyncio.get_running_loop() -await loop.create_task(main()) +# Run the asynchronous main function +if __name__ == "__main__": + asyncio.run(main()) ``` +## osrs itemdb (Catalogue & Grand Exchange) ```py import asyncio from aiohttp import ClientSession @@ -63,6 +64,7 @@ async def main(): print("Item Detail:", item_detail) # Example 3: Fetching historical trade data (price graph) for a specific item + item_id = 4151 # Example item ID (Abyssal whip in OSRS) trade_history = await graph_instance.get_graph( session, item_id=item_id, @@ -71,5 +73,36 @@ async def main(): print("Trade History:", trade_history) # Run the asynchronous main function -asyncio.run(main()) -``` \ No newline at end of file +if __name__ == "__main__": + asyncio.run(main()) +``` +```py +import asyncio +from aiohttp import ClientSession +from osrs.async_api.wiki.prices import WikiPrices, AveragePrices, LatestPrices, TimeSeries, ItemMapping + +async def main(): + wiki_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) + print("Item Mappings:", mappings) + + # Fetch latest prices + latest_prices = await wiki_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) + 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) + print("Time Series Data:", time_series) + +# Run the asynchronous main function +if __name__ == "__main__": + asyncio.run(main()) +``` \ No newline at end of file diff --git a/osrs/async_api/osrs/hiscores.py b/osrs/async_api/osrs/hiscores.py index dbe4f3e..d394c4a 100644 --- a/osrs/async_api/osrs/hiscores.py +++ b/osrs/async_api/osrs/hiscores.py @@ -80,10 +80,8 @@ async def get(self, mode: Mode, player: str, session: ClientSession) -> PlayerSt error_msg = ( f"Redirection occured: {response.url} - {response.history[0].url}" ) - logger.error(error_msg) raise UnexpectedRedirection(error_msg) elif response.status == 404: - logger.error(f"player: {player} does not exist.") raise PlayerDoesNotExist(f"player: {player} does not exist.") elif response.status != 200: # raises ClientResponseError diff --git a/osrs/async_api/runelite/__init__.py b/osrs/async_api/runelite/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/osrs/async_api/wiki/__init__.py b/osrs/async_api/wiki/__init__.py new file mode 100644 index 0000000..a2007cd --- /dev/null +++ b/osrs/async_api/wiki/__init__.py @@ -0,0 +1,9 @@ +from osrs.async_api.wiki.prices import ( + AveragePrices, + ItemMapping, + LatestPrices, + TimeSeries, + WikiPrices, +) + +__all__ = ["WikiPrices", "AveragePrices", "LatestPrices", "TimeSeries", "ItemMapping"] diff --git a/osrs/async_api/wiki/prices.py b/osrs/async_api/wiki/prices.py new file mode 100644 index 0000000..2fab6a1 --- /dev/null +++ b/osrs/async_api/wiki/prices.py @@ -0,0 +1,212 @@ +import logging +from enum import Enum + +from aiohttp import ClientSession +from pydantic import BaseModel + +from osrs.exceptions import Undefined +from osrs.utils import RateLimiter + +logger = logging.getLogger(__name__) + + +class Interval(str, Enum): + FIVE_MIN = "5m" + ONE_HOUR = "1h" + SIX_HOUR = "6h" + DAY = "24h" + + +class ItemMapping(BaseModel): + examine: str + id: int + members: bool + lowalch: int | None = None + limit: int | None = None + value: int + highalch: int | None = None + icon: str + name: str + + +class PriceData(BaseModel): + high: int | None + highTime: int | None + low: int | None + lowTime: int | None + + +class LatestPrices(BaseModel): + data: dict[str, PriceData] + + +class AveragePriceData(BaseModel): + avgHighPrice: int | None + highPriceVolume: int | None + avgLowPrice: int | None + lowPriceVolume: int | None + + +class AveragePrices(BaseModel): + data: dict[str, AveragePriceData] + + +class TimeSeriesData(BaseModel): + timestamp: int + avgHighPrice: int | None + avgLowPrice: int | None + highPriceVolume: int | None + lowPriceVolume: int | None + + +class TimeSeries(BaseModel): + data: list[TimeSeriesData] + + +class WikiPrices: + BASE_URL = "https://prices.runescape.wiki/api/v1" + + def __init__( + self, + user_agent: str = None, + proxy: str = "", + rate_limiter: RateLimiter = RateLimiter(), + ) -> None: + """ + Initializes the WikiPrices client for accessing OSRS price data. + + Args: + user_agent (str): User-Agent like 'price_tracker - @username on Discord' + proxy (str): Optional proxy URL for requests. + rate_limiter (RateLimiter): Rate limiter to control request frequency. + """ + self.proxy = proxy + self.rate_limiter = rate_limiter + + if user_agent: + self.user_agent = user_agent + else: + inp = input(""" + User-Agent like 'price_tracker - @username on Discord'\n + user_agent: + """) + if not inp: + raise Exception("invalid input") + self.user_agent = inp + + async def fetch_data(self, session: ClientSession, url: str, params: dict = {}): + """ + Utility method to fetch data from a specific endpoint, with ratelimiter, + and basic error handling + + Args: + session (ClientSession): The HTTP session for making the request. + url (str): The URL endpoint to fetch data from. + params (Optional[dict]): Query parameters for the request. + + Returns: + dict: JSON-parsed data from the API response. + """ + await self.rate_limiter.check() + + 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() + + async def get_mapping(self, session: ClientSession): + """ + Fetches item mappings containing metadata. + + Args: + session (ClientSession): The HTTP session for making the request. + + Returns: + List[ItemMapping]: List of ItemMapping objects with metadata for each item. + + Example: + >>> session = ClientSession() + >>> wiki_prices = WikiPrices() + >>> mappings = await wiki_prices.get_mapping(session) + >>> print(mappings[0].name) # e.g., '3rd age amulet' + """ + url = f"{self.BASE_URL}/osrs/mapping" + 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: + """ + Fetches the latest prices for all items. + + Args: + session (ClientSession): The HTTP session for making the request. + + Returns: + LatestPrices: A dictionary of item IDs to PriceData. + + Example: + >>> session = ClientSession() + >>> wiki_prices = WikiPrices() + >>> latest_prices = await wiki_prices.get_latest_prices(session) + >>> print(latest_prices.data["2"].high) # e.g., 240 + """ + url = f"{self.BASE_URL}/osrs/latest" + data = await self.fetch_data(session=session, url=url) + return LatestPrices(**data) + + async def get_average_prices( + self, + session: ClientSession, + interval: Interval, + timestamp: int | None = None, + ) -> AveragePrices: + """ + Fetches average prices at a specified interval (5-minute, 1-hour, etc.). + + Args: + session (ClientSession): The HTTP session for the request. + interval (Interval): The time interval ('5m', '1h', etc.) for averaging. + timestamp (Optional[int]): Optional Unix timestamp to retrieve prices for a specific time. + + Returns: + AveragePrices: A dictionary of item IDs to AveragePriceData. + + Example: + >>> session = ClientSession() + >>> wiki_prices = WikiPrices() + >>> avg_prices = await wiki_prices.get_average_prices(session, Interval.ONE_HOUR) + >>> print(avg_prices.data["2"].avgHighPrice) # e.g., 235 + """ + url = f"{self.BASE_URL}/osrs/{interval.value}" + params = {"timestamp": timestamp} if timestamp else {} + data = await self.fetch_data(session=session, url=url, params=params) + return AveragePrices(**data) + + async def get_time_series( + self, session: ClientSession, item_id: int, timestep: Interval + ) -> TimeSeries: + """ + Fetches time-series data for a specific item and timestep. + + Args: + session (ClientSession): The HTTP session for the request. + item_id (int): The item ID. + timestep (Interval): The timestep (e.g., '5m', '1h'). + + Returns: + TimeSeries: A list of TimeSeriesData entries for the specified item and timestep. + + Example: + >>> session = ClientSession() + >>> wiki_prices = WikiPrices() + >>> time_series = await wiki_prices.get_time_series(session, 2, Interval.ONE_HOUR) + >>> print(time_series.data[0].avgHighPrice) # e.g., 1310000 + """ + url = f"{self.BASE_URL}/osrs/timeseries" + params = {"id": item_id, "timestep": timestep.value} + data = await self.fetch_data(session=session, url=url, params=params) + return TimeSeries(**data) diff --git a/tests/test_async_hiscore.py b/tests/test_async_osrs_hiscore.py similarity index 100% rename from tests/test_async_hiscore.py rename to tests/test_async_osrs_hiscore.py diff --git a/tests/test_async_itemdb.py b/tests/test_async_osrs_itemdb.py similarity index 100% rename from tests/test_async_itemdb.py rename to tests/test_async_osrs_itemdb.py diff --git a/tests/test_async_wiki_prices.py b/tests/test_async_wiki_prices.py new file mode 100644 index 0000000..4b8b18f --- /dev/null +++ b/tests/test_async_wiki_prices.py @@ -0,0 +1,109 @@ +import pytest +from aiohttp import ClientSession + +from osrs.async_api.wiki.prices import ( + AveragePrices, + Interval, + ItemMapping, + LatestPrices, + TimeSeries, + WikiPrices, +) + + +@pytest.mark.asyncio +async def test_get_mapping_valid(): + """Test fetching item mappings successfully.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + mappings = await wiki_prices_instance.get_mapping(session=session) + + assert isinstance(mappings, list), "Mappings should be a list" + assert len(mappings) > 0, "Mappings list should not be empty" + assert isinstance( + mappings[0], ItemMapping + ), "First mapping should be of type ItemMapping" + + +@pytest.mark.asyncio +async def test_get_latest_prices_valid(): + """Test fetching the latest prices successfully.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + latest_prices = await wiki_prices_instance.get_latest_prices(session=session) + + assert isinstance( + latest_prices, LatestPrices + ), "The returned object is not of type LatestPrices" + assert latest_prices.data, "Latest prices data should not be empty" + + +@pytest.mark.asyncio +async def test_get_average_prices_valid(): + """Test fetching average prices with valid parameters.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + average_prices = await wiki_prices_instance.get_average_prices( + session=session, interval=Interval.FIVE_MIN + ) + + assert isinstance( + average_prices, AveragePrices + ), "The returned object is not of type AveragePrices" + assert average_prices.data, "Average prices data should not be empty" + + +@pytest.mark.asyncio +async def test_get_average_prices_invalid(): + """Test fetching average prices with an invalid interval.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + with pytest.raises(Exception): + await wiki_prices_instance.get_average_prices( + session=session, + interval="invalid_interval", + ) + + +@pytest.mark.asyncio +async def test_get_time_series_valid(): + """Test fetching time series data for a valid item ID and timestep.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + item_id = 4151 + time_series = await wiki_prices_instance.get_time_series( + session=session, item_id=item_id, timestep=Interval.ONE_HOUR + ) + + assert isinstance( + time_series, TimeSeries + ), "The returned object is not of type TimeSeries" + assert len(time_series.data) > 0, "Time series data should not be empty" + + +@pytest.mark.asyncio +async def test_get_time_series_invalid(): + """Test fetching time series data for an invalid item ID.""" + wiki_prices_instance = WikiPrices( + user_agent="tests - https://github.com/Bot-detector/osrs" + ) + async with ClientSession() as session: + invalid_item_id = 9999999 + + time_series = await wiki_prices_instance.get_time_series( + session=session, item_id=invalid_item_id, timestep=Interval.ONE_HOUR + ) + assert isinstance( + time_series, TimeSeries + ), "The returned object is not of type TimeSeries" + assert len(time_series.data) == 0, "Time series data should be empty"