diff --git a/.github/workflows/library-tests.yml b/.github/workflows/library-tests.yml index fca5ea5..b5fad2c 100644 --- a/.github/workflows/library-tests.yml +++ b/.github/workflows/library-tests.yml @@ -9,14 +9,11 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - - env: - VALORANT_KEY: ${{ secrets.VALORANT_KEY }} + with: + python-version: '3.9' - - name: Install local package - run: | - pip install -e . --user + - env: + VALPY_KEY: ${{ secrets.VALPY_KEY }} - name: Run library tests - run: | - python -m tests + - run: python -m tests diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0c488a8..3416454 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.6, 3.7, 3.8] + python-version: [3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 994a3e2..2ab61a1 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,24 @@ # valorant.py -[![GitHub Actions](https://camo.githubusercontent.com/0fc9226929794d4d4dfb9ac05a1786942f8e4b4300207224277ac49e22e9fdb6/68747470733a2f2f7472617669732d63692e636f6d2f7073662f626c61636b2e7376673f6272616e63683d6d6173746572)](https://github.com/frissyn/valorant.py/actions) -[![valorant on PyPI](https://img.shields.io/pypi/v/valorant.svg)](https://pypi.python.org/pypi/valorant) -[![Downloads](https://pepy.tech/badge/valorant/month)](https://pepy.tech/project/valorant) -[![License](https://img.shields.io/pypi/l/valorant.svg)](https://pypi.python.org/pypi/valorant) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Contribute](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/frissyn/valorant.py/issues) -[![Discord Chat](https://img.shields.io/badge/discord-join-7389D8?style=flat&logo=discord)](https://discord.gg/b3qjk4epPr) +[![Actions](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffrissyn%2Fvalorant.py%2Fbadge%3Fref%3Dmaster&style=flat-square)](https://actions-badge.atrox.dev/frissyn/valorant.py/goto?ref=master) +[![PyPI](https://img.shields.io/pypi/v/valorant?style=flat-square)](https://pypi.python.org/pypi/valorant) +[![Downloads](https://img.shields.io/pypi/dm/valorant?style=flat-square)](https://pepy.tech/project/valorant) +![LICENSE](https://img.shields.io/github/license/frissyn/valorant.py?style=flat-square) +[![Discord](https://img.shields.io/badge/discord-join-7389D8?style=flat-square&logo=discord)](https://discord.gg/b3qjk4epPr) `valorant.py` is an unofficial API wrapper for Riot Games' Valorant API endpoints. It's modern, easy to use, feature-rich, and intuitive! ## Features -**Simple:** High-level abstraction of API interactions; easy to use and easy to customize. ++ **Simple:** High-level abstraction of API interactions; easy to use and easy to customize. -**Lightweight:** Doesn't rely on any external dependencies, minimal package size. ++ **Lightweight:** Doesn't rely on any external dependencies, minimal package size. -**Extensive:** Covers all Valorant related endpoints from the Riot Games API. Also includes Account coverage. ++ **Extensive:** Covers all Valorant related endpoints from the Riot Games API. Also includes Account coverage. -**Fast:** HTTP requests and object instancing optimized to use minimal resources and complete tasks quickly! ++ **Fast:** HTTP requests and object instancing optimized to use minimal resources and complete tasks quickly! -**Intuitive:** Complete auto-completion, docstrings, and type-hinting for all library objects and variables. ++ **Intuitive:** Complete auto-completion, docstrings, and type-hinting for all library objects and variables. ## Installation @@ -30,7 +28,6 @@ |:----------:|:------------------------| |PIP |`pip install valorant` | |Poetry |`poetry add valorant` | -|PIPEnv |`pipenv install valorant`| ## Usage @@ -69,14 +66,8 @@ Use `bash bin/docs` to start the documentation server locally. This uses Ruby's ## Help and Questions -Have a bug or issue? Need help with the API? Open an [issue](https://github.com/frissyn/valorant.py/issues) or hop in the [#valorant-py](https://discord.gg/b3qjk4epPr) channel of my Community Discord Server. +Have a bug or issue? Need help with the API? Open an [issue](https://github.com/frissyn/valorant.py/issues) or hop in the [#valorant-py](https://discord.gg/b3qjk4epPr) channel of the Frisscraft Community Discord Server. ## Contributing -1. Fork the repository: [`Fork`](https://github.com/frissyn/valorant.py/fork) -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -a -m 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create a new Pull Request! 🎉 - -You can also re-create these steps with GitHub Desktop, Visual Studio Code, or whatever `git` version control UI you prefer. You don't have to, but I use prefixes for all my commits (i.e `✨: add asyncio run to package namespace`). I have a personal style guide that I use, which you can find [`here`](https://github.com/frissyn/commit-prefixes). +Head over to the [**Contributing Guide**](https://github.com/frissyn/valorant.py/blob/master/.github/CONTRIBUTING.md) page. \ No newline at end of file diff --git a/bin/format b/bin/format new file mode 100644 index 0000000..5f04801 --- /dev/null +++ b/bin/format @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +pip install black > /dev/null 2>&1 +black valorant tests \ No newline at end of file diff --git a/docs/pages/api.rst b/docs/pages/api.rst index 08e1607..25ae0da 100644 --- a/docs/pages/api.rst +++ b/docs/pages/api.rst @@ -127,6 +127,13 @@ LeaderboardDTO :undoc-members: +LeaderboardIterator +~~~~~~~~~~~~~~~~~~~ +.. autoclass:: LeaderboardIterator + :members: + :undoc-members: + + LeaderboardPlayerDTO ~~~~~~~~~~~~~~~~~~~~ .. autoclass:: LeaderboardPlayerDTO diff --git a/tests/__init__.py b/tests/__init__.py index 95a07e5..04f22ae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,7 +5,7 @@ class BaseTest(unittest.TestCase): def setUp(self): - KEY = os.environ["VALPY-KEY"] + KEY = os.environ["VALPY_KEY"] self.access_key = KEY self.client = valorant.Client(KEY, locale=None, load_content=False) diff --git a/valorant/__init__.py b/valorant/__init__.py index c598dc3..6fa417d 100644 --- a/valorant/__init__.py +++ b/valorant/__init__.py @@ -20,11 +20,7 @@ class Version(t.NamedTuple): release: t.Literal["alpha", "beta", "dev"] -version_info = Version(major=1, minor=0, micro=2, release="") - -if not version_info.release: - tag = "" -else: - tag = "-" + version_info.release +version_info = Version(major=1, minor=0, micro=3, 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/caller.py b/valorant/caller.py index 04ae211..d875972 100644 --- a/valorant/caller.py +++ b/valorant/caller.py @@ -41,14 +41,19 @@ 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.Mapping[str, t.Any]: + ) -> t.Optional[t.Mapping[str, t.Any]]: prefix = self.base.format(root=self.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 + r.raise_for_status() return r.json() diff --git a/valorant/client.py b/valorant/client.py index 1759e69..387675d 100644 --- a/valorant/client.py +++ b/valorant/client.py @@ -15,6 +15,7 @@ LeaderboardDTO, LeaderboardIterator, PlatformDataDTO, + MatchDTO, ) @@ -73,9 +74,7 @@ def __init__( self.content = None def _content_if_cache(self) -> ContentDTO: - content = getattr(self, "content", None) - - if content: + if content := getattr(self, "content", None): return content else: return ContentDTO(self.handle.call("GET", "content")) @@ -105,9 +104,7 @@ def asset( content = self._content_if_cache() for name in Lex.CONTENT_NAMES: - el = getattr(content, name).get(**attributes) - - if el: + if el := getattr(content, name).get(**attributes): return el return None @@ -192,7 +189,7 @@ def get_current_act(self) -> t.Optional[ActDTO]: :rtype: Optional[ActDTO] """ - return self.get_acts().find(isActive=True) + return self.get_acts().get(isActive=True) def get_equips(self) -> t.List[ContentItemDTO]: """Get a :class:`ContentList` of :class:`ContentItemDTO` objects that each @@ -218,7 +215,51 @@ def get_leaderboard( pages: t.Optional[int] = None, actID: t.Text = "", ) -> t.Union[LeaderboardDTO, LeaderboardIterator]: - actID = self.get_current_act().id if not actID else actID + """Get the ranked leaderboard for an Act in VALORANT. + + :param size: + Size of the leaderboard players to include. Can be between ``1`` and ``100``. + If this value is greater than ``100``, the remaining items in leaderboard will + be ``None``. + :type size: int + :param page: + Page of the leaderboard to retrieve. For example, page 4 of a leaderboard + with a size of 50 will skip the first 200 players. + :param pages: + Number of pages to retrieve from the leaderboard. If specified, the ``page`` + parameter will be ignored. This will return a :class:`LeaderboardIterator` + of the retrieved pages. + :type page: Optional[int] + :param actID: + ID of the Act to get the leaderboard from. This defaults to the currently + active Act. + :type actID: str + + **Examples:** + + .. code-block:: python + + # Get players from 101-200th rank on the leaderboard. + lb = client.get_leaderboard(size=100, page=2) + + .. code-block:: python + + # Loop through multiple leaderboard pages. + pages = client.get_leaderboard(size=50, pages=3) + + for page in pages: + print(page.totalPlayers) + + .. note:: + + The :class:`LeaderboardIterator` will request the next page of the leaderboard + after each iteration. Be wary of running into ratelimits when iterating over + a large amount of pages. + + :rtype: Union[LeaderboardDTO, LeaderboardIterator] + """ + + actID = actID or self.get_current_act().id if pages: return LeaderboardIterator(self.handle, pages=pages, size=size, actID=actID) @@ -237,7 +278,16 @@ def get_maps(self) -> t.List[ContentItemDTO]: """ return self._content_if_cache().maps + def get_match(self, id: t.Text) -> t.Optional[MatchDTO]: + r = self.handle.call("GET", "match", matchID=id, escape_if=(400, 404)) + + return MatchDTO(r) if r else None + def get_platform_status(self) -> PlatformDataDTO: + """Get status of VALORANT for the given platform. + + :rtype: PlatformDataDTO + """ r = self.handle.call("GET", "status") return PlatformDataDTO(r) @@ -305,15 +355,11 @@ def get_user( :rtype: Optional[AccountDTO] """ - try: - r = self.handle.call("GET", "puuid", route=True, puuid=puuid) - except requests.exceptions.HTTPError as e: - if e.response.status_code in (400, 404): - return None - else: - e.response.raise_for_status() + r = self.handle.call( + "GET", "puuid", route=True, puuid=puuid, escape_if=(400, 404) + ) - return AccountDTO(r, self.handle) + return AccountDTO(r, self.handle) if r else None def get_user_by_name( self, name: t.Text, route: t.Text = "americas" @@ -334,14 +380,13 @@ def get_user_by_name( vals = name.split("#") vals = [urllib.parse.quote(v, safe=Lex.SAFES) for v in vals] - try: - r = self.handle.call( - "GET", "game-name", route=True, name=vals[0], tag=vals[1] - ) - except requests.exceptions.HTTPError as e: - if e.response.status_code in (400, 404): - return None - else: - e.response.raise_for_status() - - return AccountDTO(r, self.handle) + r = self.handle.call( + "GET", + "game-name", + route=True, + name=vals[0], + tag=vals[1], + escape_if=(400, 404), + ) + + return AccountDTO(r, self.handle) if r else None diff --git a/valorant/local/client.py b/valorant/local/client.py index 685d379..f227635 100644 --- a/valorant/local/client.py +++ b/valorant/local/client.py @@ -69,12 +69,6 @@ def get_presences(self, user=False) -> dict: if user: puuid = self.get_session()["puuid"] - for u in data["presences"]: - if u["puuid"] == puuid: - return u - else: - pass - - return {} + return next((u for u in data["presences"] if u["puuid"] == puuid), {}) else: return data diff --git a/valorant/objects/dto.py b/valorant/objects/dto.py index c04777b..dbe3740 100644 --- a/valorant/objects/dto.py +++ b/valorant/objects/dto.py @@ -4,10 +4,7 @@ class DTOEncoder(json.JSONEncoder): def default(self, o): - if isinstance(o, DTO): - return o._json - - return json.JSONEncoder.default(self, o) + return o._json if isinstance(o, DTO) else json.JSONEncoder.default(self, o) class DTO(object): @@ -33,10 +30,7 @@ def __repr__(self) -> t.Text: @classmethod def optional(cls, obj: t.Optional[t.Mapping]) -> t.Optional: - if obj != None: - return cls(obj) - - return None + return cls(obj) if obj != None else None def json(self) -> dict: """Return a JSON (dictionary) representation of this DTO. diff --git a/valorant/objects/ranked.py b/valorant/objects/ranked.py index f147c40..4e2200a 100644 --- a/valorant/objects/ranked.py +++ b/valorant/objects/ranked.py @@ -35,8 +35,13 @@ def __init__(self, obj): class LeaderboardIterator: + """Simple iterator utility for getting multiple leaderboard pages. + Each iteraction returns a :class:`LeaderboardDTO`. See + :func:`Client.get_leaderboard` for more info. + """ + def __init__(self, caller: WebCaller, pages: int = 1, **params): - self.handle = caller + self._handle = caller self.kwargs = params self.index, self.pages = 0, pages @@ -46,17 +51,16 @@ def __iter__(self): def __next__(self) -> LeaderboardDTO: if self.index >= self.pages: raise StopIteration - else: - self.index += 1 - payload = { - "actID": self.kwargs["actID"], - "params": { - "size": self.kwargs["size"], - "startIndex": (self.index - 1) * self.kwargs["size"], - }, - } + payload = { + "actID": self.kwargs["actID"], + "params": { + "size": self.kwargs["size"], + "startIndex": (self.index) * self.kwargs["size"], + }, + } - data = self.handle.call("GET", "leaderboard", **payload) + self.index += 1 + data = self._handle.call("GET", "leaderboard", **payload) - return LeaderboardDTO(data) + return LeaderboardDTO(data)