diff --git a/.github/workflows/library-tests.yml b/.github/workflows/library-tests.yml index f511703..a8823e9 100644 --- a/.github/workflows/library-tests.yml +++ b/.github/workflows/library-tests.yml @@ -17,17 +17,14 @@ on: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.10'] env: VALPY_KEY: ${{ secrets.VALPY_KEY }} steps: - uses: actions/checkout@v3 - - name: Setup Python v${{ matrix.python-version }} + - name: Setup Python v3.10 uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python-version }} + python-version: '3.10' - name: Install missing dependencies run: | python3 -m pip install --upgrade pip diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3416454..d834a6f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9] + python-version: ['3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 41385d6..89f6b14 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ name="valorant", license="MIT", author="frissyn", + description=valorant.__doc__, version=valorant.__version__, @@ -16,16 +17,16 @@ "Source Code": url, "Pull Requests": url + "/pulls", "Issue Tracker": url + "/issues", - "Documentation": "https://valorantpy.readthedocs.io/" + "Documentation": "https://valorantpy.readthedocs.io/", }, long_description=readme, long_description_content_type="text/markdown", python_requires=">=3.8.0", - zip_safe=False, - packages=["valorant", "valorant/local", "valorant/objects"], + + packages=["valorant", "valorant/local", "valorant/objects", "valorant/callers"], classifiers=[ "License :: OSI Approved :: MIT License", @@ -33,6 +34,6 @@ "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Topic :: Software Development" - ] -) \ No newline at end of file + "Topic :: Software Development", + ], +) diff --git a/valorant/__init__.py b/valorant/__init__.py index 76e1025..da938a5 100644 --- a/valorant/__init__.py +++ b/valorant/__init__.py @@ -1,5 +1,6 @@ import typing as t +from .aio import AsyncClient from .client import Client from .lexicon import Lex from .local import LocalClient @@ -20,7 +21,7 @@ class Version(t.NamedTuple): release: t.Literal["alpha", "beta", "dev"] -version_info = Version(major=1, minor=0, micro=3, release="") +version_info = Version(major=1, minor=1, micro=0, release="dev") tag = f"-{version_info.release}" if version_info.release else "" __version__ = ".".join(str(i) for i in version_info[:3]) + tag diff --git a/valorant/aio.py b/valorant/aio.py new file mode 100644 index 0000000..152514e --- /dev/null +++ b/valorant/aio.py @@ -0,0 +1,44 @@ +import typing as t +import urllib.parse + +from .lexicon import Lex + +from .callers import AsyncCaller + +from .objects import ( + DTO, + ActDTO, + AccountDTO, + ContentDTO, + ContentItemDTO, + LeaderboardDTO, + LeaderboardIterator, + PlatformDataDTO, + MatchDTO, +) + + +class AsyncClient(object): + def __init__( + self, + key: t.Text, + locale: t.Optional[t.Text] = Lex.LOCALE, + region: t.Text = "na", + route: t.Text = "americas", + ): + self.key = key + self.route = route + self.locale = locale + self.region = region + self.handle = AsyncCaller(key, locale=locale, region=region, route=route) + + async def _content_from_cache(self, from_cache=True): + if from_cache and getattr(self, "content", False): + return self.content + else: + self.content = ContentDTO(await self.handle.call("GET", "content")) + + return self.content + + async def get_content(self, cache: bool=False) -> ContentDTO: + return await self._content_from_cache(from_cache=cache) diff --git a/valorant/caller.py b/valorant/caller.py deleted file mode 100644 index ec56e76..0000000 --- a/valorant/caller.py +++ /dev/null @@ -1,57 +0,0 @@ -import requests -import typing as t - -from .lexicon import Lex - - -def value_check(*args: t.List[t.Text]) -> bool: - KEYS = Lex.ROUTES + Lex.LOCALES + Lex.REGIONS - - for arg in args: - if arg not in KEYS: - raise ValueError( - f"`{arg}` is either an unspported or invalid geographical value." - ) - else: - return True - - return False - - -class WebCaller(object): - def __init__(self, token: t.Text, locale: t.Text, region: t.Text, route: t.Text): - self.base = "https://{root}.api.riotgames.com/" - self.eps = Lex.ENDPOINTS["web"] - self.sess = requests.Session() - self.sess.params.update({"locale": locale}) - self.sess.headers.update( - { - "Accept-Charset": "application/x-www-form-urlencoded; charset=UTF-8", - "User-Agent": "Mozilla/5.0", - "X-Riot-Token": token, - } - ) - - if value_check(region, route): - self.locale = locale - self.region = region - self.route = route - - def call( - self, - m: t.Text, - ep: t.Text, - escape_if: t.Tuple[int, ...] = (), - params: t.Optional[t.Mapping] = None, - route: t.Optional[t.Text] = False, - **kw, - ) -> t.Optional[t.Mapping[str, t.Any]]: - prefix = self.base.format(root=route if route else self.region) - url = prefix + self.eps[ep].format(**kw) - - r = self.sess.request(m, url, params=params) - - if r.status_code in escape_if: - return None - - return r.raise_for_status() or r.json() diff --git a/valorant/callers/__init__.py b/valorant/callers/__init__.py new file mode 100644 index 0000000..8a67922 --- /dev/null +++ b/valorant/callers/__init__.py @@ -0,0 +1,11 @@ +import aiohttp +import requests +import typing as t + +from .async_ import AsyncCaller +from .sync import WebCaller + +SessionType = t.Union[requests.Session, aiohttp.ClientSession] +CallerType = t.Union[AsyncCaller, WebCaller] + +__all__ = ["AsyncCaller", "CallerType", "SessionType", "WebCaller"] diff --git a/valorant/callers/async_.py b/valorant/callers/async_.py new file mode 100644 index 0000000..eb2926f --- /dev/null +++ b/valorant/callers/async_.py @@ -0,0 +1,28 @@ +import aiohttp +import typing as t + +from .base import BaseCaller + + +class AsyncCaller(BaseCaller): + def __init__(self, key: t.Text, **options): + self.params = {"locale": options.get("locale")} + + super().__init__(key, aiohttp.ClientSession, async_=True, **options) + + async def call( + self, + method: t.Text, + endpoint: t.Text, + escapes: t.Tuple[int, ...] = (), + params: t.Mapping = {}, + route: t.Optional[t.Text] = False, + **extras, + ) -> t.Optional[t.Mapping[str, t.Any]]: + url = self.build_url(endpoint, route, **extras) + r = await self.session.request(method, url, params=self.params.update(params)) + + if r.status in escapes: + return None + + return r.raise_for_status() or (await r.json()) diff --git a/valorant/callers/base.py b/valorant/callers/base.py new file mode 100644 index 0000000..cf44dab --- /dev/null +++ b/valorant/callers/base.py @@ -0,0 +1,44 @@ +import typing as t + +from ..lexicon import Lex + + +class BaseCaller(object): + headers: t.Mapping[str, str] = { + "Accept-Charset": "application/x-www-form-urlencoded; charset=UTF-8", + "User-Agent": "Mozilla/5.0", + "X-Riot-Token": None, + } + + def __init__( + self, + key: t.Text, + ctx: t.Any, + headers: t.Mapping = {}, + async_: bool = False, + **options: t.Mapping, + ): + self.token = key + self.async_ = async_ + self.endpoints = Lex.ENDPOINTS["web"].copy() + self.base = "https://{code}.api.riotgames.com/" + self.headers["X-Riot-Token"] = key + + try: + self.session = ctx(headers=self.headers) + except TypeError: + self.session = ctx() + self.session.headers = self.headers + + valids = Lex.ROUTES + Lex.LOCALES + Lex.REGIONS + + for name, value in options.items(): + if value not in valids and not (value is None and name == 'locale'): + raise ValueError(f"'{value}' is not a valid {name}.") + + self.__setattr__(name, value) + + def build_url(self, endpoint: t.Text, route: t.Text, **extras) -> t.Text: + url = self.base.format(code=route if route else self.region) + + return url + self.endpoints[endpoint].format(**extras) diff --git a/valorant/callers/sync.py b/valorant/callers/sync.py new file mode 100644 index 0000000..390d001 --- /dev/null +++ b/valorant/callers/sync.py @@ -0,0 +1,28 @@ +import requests +import typing as t + +from .base import BaseCaller + + +class WebCaller(BaseCaller): + def __init__(self, key, **options): + self.params = {"locale": options.get("locale")} + + super().__init__(key, requests.Session, **options) + + def call( + self, + method: t.Text, + endpoint: t.Text, + escapes: t.Tuple[int, ...] = (), + params: t.Mapping = {}, + route: t.Optional[t.Text] = False, + **extras, + ) -> t.Optional[t.Mapping[str, t.Any]]: + url = self.build_url(endpoint, route, **extras) + r = self.session.request(method, url, params=self.params.update(params)) + + if r.status_code in escapes: + return None + + return r.raise_for_status() or r.json() diff --git a/valorant/client.py b/valorant/client.py index b77937a..6c2d38b 100644 --- a/valorant/client.py +++ b/valorant/client.py @@ -1,10 +1,9 @@ -import requests import typing as t import urllib.parse from .lexicon import Lex -from .caller import WebCaller +from .callers import WebCaller from .objects import ( DTO, @@ -66,22 +65,22 @@ def __init__( self.route = route self.locale = locale self.region = region - self.handle = WebCaller(key, locale, region, route) + self.handle = WebCaller(key, locale=locale, region=region, route=route) if load_content: self.get_content(cache=True) else: self.content = None + def __getattribute__(self, name): + return super(Client, self).__getattribute__(name) + def _content_if_cache(self) -> ContentDTO: if content := getattr(self, "content", None): return content else: return ContentDTO(self.handle.call("GET", "content")) - def __getattribute__(self, name): - return super(Client, self).__getattribute__(name) - def asset( self, **attributes: t.Mapping[t.Text, t.Any] ) -> t.Optional[t.Union[ActDTO, ContentItemDTO]]: @@ -161,7 +160,7 @@ def get_chromas(self, strip: bool = False) -> t.List[ContentItemDTO]: return chromas - def get_content(self, cache: bool = True) -> ContentDTO: + def get_content(self, cache: bool = False) -> ContentDTO: """Get complete content data from VALORANT. :param cache: If set to ``True``, the Client will cache the response data, @@ -377,8 +376,7 @@ def get_user_by_name( :type route: str :rtype: Optional[AccountDTO] """ - vals = name.split("#") - vals = [urllib.parse.quote(v, safe=Lex.SAFES) for v in vals] + vals = [urllib.parse.quote(v, safe=Lex.SAFES) for v in name.split("#")] r = self.handle.call( "GET", diff --git a/valorant/objects/match.py b/valorant/objects/match.py index 895772d..9e61efd 100644 --- a/valorant/objects/match.py +++ b/valorant/objects/match.py @@ -3,6 +3,7 @@ import typing as t from datetime import datetime +from ..callers import CallerType from .content import ContentList from .dto import DTO from .player import PlayerDTO, LocationDTO, PlayerStatsDTO, PlayerLocationsDTO @@ -99,20 +100,29 @@ class MatchlistEntryDTO(DTO): matchId: str gameStartMillis: int teamId: str + timestamp: datetime + + def __init__(self, obj, handle: CallerType): + super().__init__(obj) - def __init__(self, obj, handle): - self._json = obj self._handle = handle self.id = obj["matchId"] - self.set_attributes(obj) + self.timestamp = datetime.fromtimestamp(self.gameStartMillis / 1000.0) - def get(self) -> MatchDTO: + if handle.async_: + self.__setattr__("get", self._async_get) + else: + self.__setattr__("get", self._sync_get) + + def _sync_get(self) -> MatchDTO: match = self._handle.call("GET", "match", matchID=self.id) return MatchDTO(match) - def timestamp(self) -> datetime: - return datetime.fromtimestamp(self.gameStartMillis / 1000.0) + async def _async_get(self) -> MatchDTO: + match = await self._handle.call("GET", "match", matchID=self.id) + + return MatchDTO(match) class PlayerRoundStatsDTO(DTO): diff --git a/valorant/objects/ranked.py b/valorant/objects/ranked.py index 4e2200a..81a8b59 100644 --- a/valorant/objects/ranked.py +++ b/valorant/objects/ranked.py @@ -3,7 +3,7 @@ from .content import ContentList from .dto import DTO -from ..caller import WebCaller +from ..callers import CallerType class LeaderboardPlayerDTO(DTO): @@ -40,8 +40,8 @@ class LeaderboardIterator: :func:`Client.get_leaderboard` for more info. """ - def __init__(self, caller: WebCaller, pages: int = 1, **params): - self._handle = caller + def __init__(self, caller: CallerType, pages: int = 1, **params): + self.handle = caller self.kwargs = params self.index, self.pages = 0, pages @@ -64,3 +64,9 @@ def __next__(self) -> LeaderboardDTO: data = self._handle.call("GET", "leaderboard", **payload) return LeaderboardDTO(data) + + def __aiter__(self): + pass + + def __anext__(self): + pass \ No newline at end of file