From 035d7e420c65329e7e6787dc2270abc3f6348ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:48:34 +0000 Subject: [PATCH 01/10] add and update project metadata regarding types and linting --- .github/workflows/coverage_and_lint.yml | 61 +++++++++++++++++++++++++ .github/workflows/publish.yml | 4 +- MANIFEST.in | 4 ++ pyproject.toml | 32 +++++++++++++ pyxivapi/py.typed | 0 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/coverage_and_lint.yml create mode 100644 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 pyxivapi/py.typed diff --git a/.github/workflows/coverage_and_lint.yml b/.github/workflows/coverage_and_lint.yml new file mode 100644 index 0000000..1616fc6 --- /dev/null +++ b/.github/workflows/coverage_and_lint.yml @@ -0,0 +1,61 @@ +name: Type Coverage and Linting + +on: + push: + branches: + - master + pull_request: + branches: + - master + types: + - opened + - synchronize + +jobs: + job: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ] + + name: "Type Coverage and Linting @ ${{ matrix.python-version }}" + steps: + - name: "Checkout Repository" + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: "Setup Python @ ${{ matrix.python-version }}" + uses: actions/setup-python@v3 + with: + python-version: "${{ matrix.python-version }}" + + - name: "Install Python deps @ ${{ matrix.python-version }}" + env: + PY_VER: "${{ matrix.python-version }}" + run: | + pip install -U -r requirements.txt + + - uses: actions/setup-node@v3 + with: + node-version: "17" + - run: npm install --location=global pyright@latest + + - name: "Type Coverage @ ${{ matrix.python-version }}" + run: | + pyright + pyright --ignoreexternal --lib --verifytypes pyxivapi + + - name: Lint + if: ${{ github.event_name != 'pull_request' }} + uses: github/super-linter/slim@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEFAULT_BRANCH: main + VALIDATE_ALL_CODEBASE: false + VALIDATE_PYTHON_BLACK: true + VALIDATE_PYTHON_ISORT: true + LINTER_RULES_PATH: / + PYTHON_ISORT_CONFIG_FILE: pyproject.toml + PYTHON_BLACK_CONFIG_FILE: pyproject.toml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 856285c..794d84d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,9 +8,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..579dc94 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +include LICENSE +include requirements.txt +include pyxivapi/py.typed diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..afd17fe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[tool.black] +line-length = 125 +target-version = ["py37"] + +[tool.isort] +profile = "black" +src_paths = ["mystbin"] +lines_after_imports = 2 + +[tool.pyright] +include = ["pyxivapi/**/*.py"] +useLibraryCodeForTypes = true +typeCheckingMode = "basic" +pythonVersion = "3.7" +strictListInference = true +strictDictionaryInference = true +strictSetInference = true +strictParameterNoneValue = true +reportMissingImports = "error" +reportUnusedImport = "error" +reportUnusedClass = "error" +reportUnusedFunction = "error" +reportUnusedVariable = "error" +reportUnusedExpression = "error" +reportGeneralTypeIssues = "error" +reportDuplicateImport = "error" +reportUntypedFunctionDecorator = "error" +reportUnnecessaryTypeIgnoreComment = "warning" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/pyxivapi/py.typed b/pyxivapi/py.typed new file mode 100644 index 0000000..e69de29 From 1bee3e02265e789d6061ab6ef8ff8b3af9e9e05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:48:51 +0000 Subject: [PATCH 02/10] Add license header and clean up imports to init --- pyxivapi/__init__.py | 51 +++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/pyxivapi/__init__.py b/pyxivapi/__init__.py index bbcda5f..38aaaa8 100644 --- a/pyxivapi/__init__.py +++ b/pyxivapi/__init__.py @@ -1,21 +1,32 @@ -__title__ = 'pyxivapi' -__author__ = 'Lethys' -__license__ = 'MIT' -__copyright__ = 'Copyright 2019 (c) Lethys' -__version__ = '0.5.1' +""" +MIT License -from .client import XIVAPIClient -from .exceptions import ( - XIVAPIForbidden, - XIVAPIBadRequest, - XIVAPINotFound, - XIVAPIServiceUnavailable, - XIVAPIInvalidLanguage, - XIVAPIInvalidIndex, - XIVAPIInvalidColumns, - XIVAPIInvalidFilter, - XIVAPIInvalidWorlds, - XIVAPIInvalidDatacenter, - XIVAPIError, - XIVAPIInvalidAlgo -) +Copyright (c) 2019 Lethys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +__title__ = "pyxivapi" +__author__ = "Lethys" +__license__ = "MIT" +__copyright__ = "Copyright 2019 (c) Lethys" +__version__ = "0.5.1" + +from .client import * +from .exceptions import * From 18b9c9619a7c8151ba46b5547655be0632263b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:49:15 +0000 Subject: [PATCH 03/10] add license header, format code, type code and cleanup code around client --- pyxivapi/client.py | 382 +++++++++++++++++++++++++++------------------ 1 file changed, 230 insertions(+), 152 deletions(-) diff --git a/pyxivapi/client.py b/pyxivapi/client.py index 467af90..26ff300 100644 --- a/pyxivapi/client.py +++ b/pyxivapi/client.py @@ -1,17 +1,53 @@ +""" +MIT License + +Copyright (c) 2019 Lethys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + import logging -from typing import List, Optional +from typing import Any, ClassVar, List, Optional, TypeVar, Union -from aiohttp import ClientSession +import aiohttp +from .decorators import timed from .exceptions import ( - XIVAPIBadRequest, XIVAPIForbidden, XIVAPINotFound, XIVAPIServiceUnavailable, - XIVAPIInvalidLanguage, XIVAPIError, XIVAPIInvalidIndex, XIVAPIInvalidColumns, - XIVAPIInvalidAlgo + XIVAPIBadRequest, + XIVAPIError, + XIVAPIForbidden, + XIVAPIInvalidAlgo, + XIVAPIInvalidColumns, + XIVAPIInvalidIndex, + XIVAPIInvalidLanguage, + XIVAPINotFound, + XIVAPIServiceUnavailable, ) -from .decorators import timed from .models import Filter, Sort -__log__ = logging.getLogger(__name__) + +LOGGER = logging.getLogger(__name__) + +__all__ = ("XIVAPIClient",) + +T = TypeVar("T") class XIVAPIClient: @@ -24,28 +60,44 @@ class XIVAPIClient: session: Optional[ClientSession] Optionally include your aiohttp session """ - base_url = "https://xivapi.com" - languages = ["en", "fr", "de", "ja"] - - def __init__(self, api_key: str, session: Optional[ClientSession] = None) -> None: - self.api_key = api_key - self._session = session - self.base_url = "https://xivapi.com" - self.languages = ["en", "fr", "de", "ja"] - self.string_algos = [ - "custom", "wildcard", "wildcard_plus", "fuzzy", "term", "prefix", "match", "match_phrase", - "match_phrase_prefix", "multi_match", "query_string" + __slots__ = ( + "api_key", + "_session", + "languages", + "string_algos", + ) + + base_url: ClassVar[str] = "https://xivapi.com" + + def __init__(self, api_key: str, session: Optional[aiohttp.ClientSession] = None) -> None: + self.api_key: str = api_key + self._session: Optional[aiohttp.ClientSession] = session + + self.languages: list[str] = ["en", "fr", "de", "ja"] + self.string_algos: list[str] = [ + "custom", + "wildcard", + "wildcard_plus", + "fuzzy", + "term", + "prefix", + "match", + "match_phrase", + "match_phrase_prefix", + "multi_match", + "query_string", ] - @property - def session(self) -> ClientSession: - if self._session is None or self._session.closed: - self._session = ClientSession() - return self._session + async def handle_request(self, http_verb: str, request_url: str, **kwargs: Any) -> aiohttp.ClientResponse: + if not self._session: + self._session = aiohttp.ClientSession() + + async with self._session.request(http_verb.upper(), request_url, **kwargs) as response: + return response @timed - async def character_search(self, world, forename, surname, page=1): + async def character_search(self, world: str, forename: str, surname: str, page: int = 1) -> Any: """|coro| Search for character data directly from the Lodestone. Parameters @@ -56,15 +108,28 @@ async def character_search(self, world, forename, surname, page=1): The character's forename. surname: str The character's surname. - Optional[page: int] + page: int The page of results to return. Defaults to 1. """ - url = f'{self.base_url}/character/search?name={forename}%20{surname}&server={world}&page={page}&private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/character/search?name={forename}%20{surname}&server={world}&page={page}&private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def character_by_id(self, lodestone_id: int, extended=False, include_achievements=False, include_minions_mounts=False, include_classjobs=False, include_friendslist=False, include_freecompany=False, include_freecompany_members=False, include_pvpteam=False, language="en"): + async def character_by_id( + self, + lodestone_id: int, + extended: bool = False, + include_achievements: bool = False, + include_minions_mounts: bool = False, + include_classjobs: bool = False, + include_friendslist: bool = False, + include_freecompany: bool = False, + include_freecompany_members: bool = False, + include_pvpteam: bool = False, + language: str = "en", + ) -> Any: """|coro| Request character data from XIVAPI.com Please see XIVAPI documentation for more information about character sync state https://xivapi.com/docs/Character#character @@ -74,10 +139,7 @@ async def character_by_id(self, lodestone_id: int, extended=False, include_achie The character's Lodestone ID. """ - params = { - "private_key": self.api_key, - "language": language - } + params: dict[str, Union[str, int]] = {"private_key": self.api_key, "language": language} if language.lower() not in self.languages: raise XIVAPIInvalidLanguage(f'"{language}" is not a valid language code for XIVAPI.') @@ -110,12 +172,12 @@ async def character_by_id(self, lodestone_id: int, extended=False, include_achie if len(data) > 0: params["data"] = ",".join(data) - url = f'{self.base_url}/character/{lodestone_id}' - async with self.session.get(url, params=params) as response: - return await self.process_response(response) + url = f"{self.base_url}/character/{lodestone_id}" + response = await self.handle_request("GET", url) + return await self.process_response(response) @timed - async def freecompany_search(self, world, name, page=1): + async def freecompany_search(self, world: str, name: str, page: int = 1) -> Any: """|coro| Search for Free Company data directly from the Lodestone. Parameters @@ -124,15 +186,21 @@ async def freecompany_search(self, world, name, page=1): The world that the Free Company is attributed to. name: str The Free Company's name. - Optional[page: int] + page: int The page of results to return. Defaults to 1. """ - url = f'{self.base_url}/freecompany/search?name={name}&server={world}&page={page}&private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/freecompany/search?name={name}&server={world}&page={page}&private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def freecompany_by_id(self, lodestone_id: int, extended=False, include_freecompany_members=False): + async def freecompany_by_id( + self, + lodestone_id: int, + extended: bool = False, + include_freecompany_members: bool = False, + ) -> Any: """|coro| Request Free Company data from XIVAPI.com by Lodestone ID Please see XIVAPI documentation for more information about Free Company info at https://xivapi.com/docs/Free-Company#profile @@ -142,9 +210,7 @@ async def freecompany_by_id(self, lodestone_id: int, extended=False, include_fre The Free Company's Lodestone ID. """ - params = { - "private_key": self.api_key - } + params: dict[str, Union[str, int]] = {"private_key": self.api_key} if extended is True: params["extended"] = 1 @@ -156,12 +222,13 @@ async def freecompany_by_id(self, lodestone_id: int, extended=False, include_fre if len(data) > 0: params["data"] = ",".join(data) - url = f'{self.base_url}/freecompany/{lodestone_id}' - async with self.session.get(url, params=params) as response: - return await self.process_response(response) + url = f"{self.base_url}/freecompany/{lodestone_id}" + response = await self.handle_request("GET", url, params=params) + + return await self.process_response(response) @timed - async def linkshell_search(self, world, name, page=1): + async def linkshell_search(self, world: str, name: str, page: int = 1) -> Any: """|coro| Search for Linkshell data directly from the Lodestone. Parameters @@ -170,15 +237,16 @@ async def linkshell_search(self, world, name, page=1): The world that the Linkshell is attributed to. name: str The Linkshell's name. - Optional[page: int] + page: int The page of results to return. Defaults to 1. """ - url = f'{self.base_url}/linkshell/search?name={name}&server={world}&page={page}&private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/linkshell/search?name={name}&server={world}&page={page}&private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def linkshell_by_id(self, lodestone_id: int): + async def linkshell_by_id(self, lodestone_id: int) -> Any: """|coro| Request Linkshell data from XIVAPI.com by Lodestone ID Parameters @@ -186,12 +254,13 @@ async def linkshell_by_id(self, lodestone_id: int): lodestone_id: int The Linkshell's Lodestone ID. """ - url = f'{self.base_url}/linkshell/{lodestone_id}?private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/linkshell/{lodestone_id}?private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def pvpteam_search(self, world, name, page=1): + async def pvpteam_search(self, world: str, name: str, page: int = 1) -> Any: """|coro| Search for PvPTeam data directly from the Lodestone. Parameters @@ -200,15 +269,16 @@ async def pvpteam_search(self, world, name, page=1): The world that the PvPTeam is attributed to. name: str The PvPTeam's name. - Optional[page: int] + page: int The page of results to return. Defaults to 1. """ - url = f'{self.base_url}/pvpteam/search?name={name}&server={world}&page={page}&private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/pvpteam/search?name={name}&server={world}&page={page}&private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def pvpteam_by_id(self, lodestone_id): + async def pvpteam_by_id(self, lodestone_id: int) -> None: """|coro| Request PvPTeam data from XIVAPI.com by Lodestone ID Parameters @@ -216,12 +286,25 @@ async def pvpteam_by_id(self, lodestone_id): lodestone_id: str The PvPTeam's Lodestone ID. """ - url = f'{self.base_url}/pvpteam/{lodestone_id}?private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/pvpteam/{lodestone_id}?private_key={self.api_key}" + response = await self.handle_request("GET", url) + + return await self.process_response(response) @timed - async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] = (), sort: Sort = None, page=0, per_page=10, language="en", string_algo="match"): + async def index_search( + self, + *, + name: str, + indexes: List[str] = [], + language: str = "en", + columns: List[str] = [], + filters: List[Filter] = [], + sort: Optional[Sort] = None, + page: int = 0, + per_page: int = 10, + string_algo: Optional[str] = "match", + ) -> Any: """|coro| Search for data from on specific indexes. Parameters @@ -231,6 +314,9 @@ async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] indexes: list A named list of indexes to search XIVAPI. At least one must be specified. e.g. ["Recipe", "Item"] + language: str + The two character length language code that indicates the language to return the response in. Defaults to English (en). + Valid values are "en", "fr", "de" & "ja" Optional[columns: list] A named list of columns to return in the response. ID, Name, Icon & ItemDescription will be returned by default. e.g. ["ID", "Name", "Icon"] @@ -241,9 +327,6 @@ async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] The name of the column to sort on. Optional[page: int] The page of results to return. Defaults to 1. - Optional[language: str] - The two character length language code that indicates the language to return the response in. Defaults to English (en). - Valid values are "en", "fr", "de" & "ja" Optional[string_algo: str] The search algorithm to use for string matching (default = "match") Valid values are "custom", "wildcard", "wildcard_plus", "fuzzy", "term", "prefix", "match", "match_phrase", @@ -251,7 +334,7 @@ async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] """ if len(indexes) == 0: - raise XIVAPIInvalidIndex("Please specify at least one index to search for, e.g. [\"Recipe\"]") + raise XIVAPIInvalidIndex('Please specify at least one index to search for, e.g. ["Recipe"]') if language.lower() not in self.languages: raise XIVAPIInvalidLanguage(f'"{language}" is not a valid language code for XIVAPI.') @@ -262,54 +345,59 @@ async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] if string_algo not in self.string_algos: raise XIVAPIInvalidAlgo(f'"{string_algo}" is not a supported string_algo for XIVAPI') - body = { + body: dict[str, Any] = { "indexes": ",".join(list(set(indexes))), "columns": "ID", "body": { "query": { "bool": { - "should": [{ - string_algo: { - "NameCombined_en": { - "query": name, - "fuzziness": "AUTO", - "prefix_length": 1, - "max_expansions": 50 + "should": [ + { + string_algo: { + "NameCombined_en": { + "query": name, + "fuzziness": "AUTO", + "prefix_length": 1, + "max_expansions": 50, + } } - } - }, { - string_algo: { - "NameCombined_de": { - "query": name, - "fuzziness": "AUTO", - "prefix_length": 1, - "max_expansions": 50 + }, + { + string_algo: { + "NameCombined_de": { + "query": name, + "fuzziness": "AUTO", + "prefix_length": 1, + "max_expansions": 50, + } } - } - }, { - string_algo: { - "NameCombined_fr": { - "query": name, - "fuzziness": "AUTO", - "prefix_length": 1, - "max_expansions": 50 + }, + { + string_algo: { + "NameCombined_fr": { + "query": name, + "fuzziness": "AUTO", + "prefix_length": 1, + "max_expansions": 50, + } } - } - }, { - string_algo: { - "NameCombined_ja": { - "query": name, - "fuzziness": "AUTO", - "prefix_length": 1, - "max_expansions": 50 + }, + { + string_algo: { + "NameCombined_ja": { + "query": name, + "fuzziness": "AUTO", + "prefix_length": 1, + "max_expansions": 50, + } } - } - }] + }, + ] } }, "from": page, - "size": per_page - } + "size": per_page, + }, } if len(columns) > 0: @@ -318,27 +406,20 @@ async def index_search(self, name, indexes=(), columns=(), filters: List[Filter] if len(filters) > 0: filts = [] for f in filters: - filts.append({ - "range": { - f.Field: { - f.Comparison: f.Value - } - } - }) + filts.append({"range": {f.field: {f.comparison: f.value}}}) body["body"]["query"]["bool"]["filter"] = filts if sort: - body["body"]["sort"] = [{ - sort.Field: "asc" if sort.Ascending else "desc" - }] + body["body"]["sort"] = [{sort.field: "asc" if sort.ascending else "desc"}] - url = f'{self.base_url}/search?language={language}&private_key={self.api_key}' - async with self.session.post(url, json=body) as response: - return await self.process_response(response) + url = f"{self.base_url}/search?language={language}&private_key={self.api_key}" + response = await self.handle_request("POST", url, json=body) + + return await self.process_response(response) @timed - async def index_by_id(self, index, content_id: int, columns=(), language="en"): + async def index_by_id(self, index, content_id: int, columns: List[str], language: str = "en") -> Any: """|coro| Request data from a given index by ID. Parameters @@ -347,33 +428,31 @@ async def index_by_id(self, index, content_id: int, columns=(), language="en"): The index to which the content is attributed. content_id: int The ID of the content - Optional[columns: list] + columns: list[str] A named list of columns to return in the response. ID, Name, Icon & ItemDescription will be returned by default. e.g. ["ID", "Name", "Icon"] - Optional[language: str] + language: str The two character length language code that indicates the language to return the response in. Defaults to English (en). Valid values are "en", "fr", "de" & "ja" """ if index == "": - raise XIVAPIInvalidIndex("Please specify an index to search on, e.g. \"Item\"") + raise XIVAPIInvalidIndex('Please specify an index to search on, e.g. "Item"') if len(columns) == 0: raise XIVAPIInvalidColumns("Please specify at least one column to return in the resulting data.") - params = { - "private_key": self.api_key, - "language": language - } + params = {"private_key": self.api_key, "language": language} if len(columns) > 0: params["columns"] = ",".join(list(set(columns))) - url = f'{self.base_url}/{index}/{content_id}' - async with self.session.get(url, params=params) as response: - return await self.process_response(response) + url = f"{self.base_url}/{index}/{content_id}" + response = await self.handle_request("GET", url, params=params) + + return await self.process_response(response) @timed - async def lore_search(self, query, language="en"): + async def lore_search(self, query: str, language: str = "en") -> Any: """|coro| Search cutscene subtitles, quest dialog, item, achievement, mount & minion descriptions and more for any text that matches query. Parameters @@ -384,42 +463,41 @@ async def lore_search(self, query, language="en"): The two character length language code that indicates the language to return the response in. Defaults to English (en). Valid values are "en", "fr", "de" & "ja" """ - params = { - "private_key": self.api_key, - "language": language, - "string": query - } + params = {"private_key": self.api_key, "language": language, "string": query} + + url = f"{self.base_url}/lore" + response = await self.handle_request("GET", url, params=params) - url = f'{self.base_url}/lore' - async with self.session.get(url, params=params) as response: - return await self.process_response(response) + return await self.process_response(response) @timed - async def lodestone_worldstatus(self): + async def lodestone_worldstatus(self) -> Any: """|coro| Request world status post from the Lodestone. """ - url = f'{self.base_url}/lodestone/worldstatus?private_key={self.api_key}' - async with self.session.get(url) as response: - return await self.process_response(response) + url = f"{self.base_url}/lodestone/worldstatus?private_key={self.api_key}" + response = await self.handle_request("GET", url) + return await self.process_response(response) - async def process_response(self, response): - __log__.info(f'{response.status} from {response.url}') + async def process_response(self, response: aiohttp.ClientResponse) -> Any: + LOGGER.info(f"{response.status} from {response.url}") if response.status == 200: return await response.json() - if response.status == 400: + elif response.status == 400: raise XIVAPIBadRequest("Request was bad. Please check your parameters.") - if response.status == 401: + elif response.status == 401: raise XIVAPIForbidden("Request was refused. Possibly due to an invalid API key.") - if response.status == 404: + elif response.status == 404: raise XIVAPINotFound("Resource not found.") - if response.status == 500: + elif response.status == 500: raise XIVAPIError("An internal server error has occured on XIVAPI.") - if response.status == 503: - raise XIVAPIServiceUnavailable("Service is unavailable. This could be because the Lodestone is under maintenance.") + elif response.status == 503: + raise XIVAPIServiceUnavailable( + "Service is unavailable. This could be because the Lodestone is under maintenance." + ) From 5ac2c31ceab62d0deb52dc81e9c5930118a3bd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:49:36 +0000 Subject: [PATCH 04/10] add license header, type decorator and cleanup around decorators --- pyxivapi/decorators.py | 49 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/pyxivapi/decorators.py b/pyxivapi/decorators.py index 772127e..3679317 100644 --- a/pyxivapi/decorators.py +++ b/pyxivapi/decorators.py @@ -1,16 +1,57 @@ +""" +MIT License + +Copyright (c) 2019 Lethys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +from __future__ import annotations + import logging from functools import wraps from time import time +from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar -__log__ = logging.getLogger(__name__) +if TYPE_CHECKING: + from typing_extensions import ParamSpec -def timed(func): + P = ParamSpec("P") +else: + P = TypeVar("P") + +C = TypeVar("C") +T = TypeVar("T") + +LOGGER = logging.getLogger(__name__) + + +def timed(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]: """This decorator prints the execution time for the decorated function.""" + @wraps(func) - async def wrapper(*args, **kwargs): + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: start = time() result = await func(*args, **kwargs) - __log__.info("{} executed in {}s".format(func.__name__, round(time() - start, 2))) + LOGGER.info("{} executed in {}s".format(func.__name__, round(time() - start, 2))) return result + return wrapper From 69eb3e1cb7e1022b135a132fd4a494baefbd529e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:49:54 +0000 Subject: [PATCH 05/10] add license headers and all dunder for easier importing --- pyxivapi/exceptions.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pyxivapi/exceptions.py b/pyxivapi/exceptions.py index efcf9e9..74b74ca 100644 --- a/pyxivapi/exceptions.py +++ b/pyxivapi/exceptions.py @@ -1,7 +1,48 @@ +""" +MIT License + +Copyright (c) 2019 Lethys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +__all__ = ( + "XIVAPIForbidden", + "XIVAPIBadRequest", + "XIVAPINotFound", + "XIVAPIServiceUnavailable", + "XIVAPIInvalidLanguage", + "XIVAPIInvalidIndex", + "XIVAPIInvalidColumns", + "XIVAPIInvalidFilter", + "XIVAPIInvalidWorlds", + "XIVAPIInvalidDatacenter", + "XIVAPIError", + "XIVAPIInvalidAlgo", +) + + class XIVAPIForbidden(Exception): """ XIVAPI Forbidden Request error """ + pass @@ -9,6 +50,7 @@ class XIVAPIBadRequest(Exception): """ XIVAPI Bad Request error """ + pass @@ -16,6 +58,7 @@ class XIVAPINotFound(Exception): """ XIVAPI not found error """ + pass @@ -23,6 +66,7 @@ class XIVAPIServiceUnavailable(Exception): """ XIVAPI service unavailable error """ + pass @@ -30,6 +74,7 @@ class XIVAPIInvalidLanguage(Exception): """ XIVAPI invalid language error """ + pass @@ -37,6 +82,7 @@ class XIVAPIInvalidIndex(Exception): """ XIVAPI invalid index error """ + pass @@ -44,6 +90,7 @@ class XIVAPIInvalidColumns(Exception): """ XIVAPI invalid columns error """ + pass @@ -51,6 +98,7 @@ class XIVAPIInvalidFilter(Exception): """ XIVAPI invalid filter error """ + pass @@ -58,6 +106,7 @@ class XIVAPIInvalidWorlds(Exception): """ XIVAPI invalid world(s) error """ + pass @@ -65,6 +114,7 @@ class XIVAPIInvalidDatacenter(Exception): """ XIVAPI invalid datacenter error """ + pass @@ -72,6 +122,7 @@ class XIVAPIError(Exception): """ XIVAPI error """ + pass @@ -79,4 +130,5 @@ class XIVAPIInvalidAlgo(Exception): """ Invalid String Algo """ + pass From dd322ec49eb8388ad55ac00b332a5120b14227e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:50:29 +0000 Subject: [PATCH 06/10] add license header and cleanup types around models --- pyxivapi/models.py | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/pyxivapi/models.py b/pyxivapi/models.py index 36653b3..a3a62d6 100644 --- a/pyxivapi/models.py +++ b/pyxivapi/models.py @@ -1,3 +1,30 @@ +""" +MIT License + +Copyright (c) 2019 Lethys + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + + +from typing import ClassVar, List + from .exceptions import XIVAPIInvalidFilter @@ -6,17 +33,17 @@ class Filter: Model class for DQL filters """ - comparisons = ["gt", "gte", "lt", "lte"] + comparisons: ClassVar[List[str]] = ["gt", "gte", "lt", "lte"] - def __init__(self, field: str, comparison: str, value: int): + def __init__(self, field: str, comparison: str, value: int) -> None: comparison = comparison.lower() if comparison not in self.comparisons: raise XIVAPIInvalidFilter(f'"{comparison}" is not a valid DQL filter comparison.') - self.Field = field - self.Comparison = comparison - self.Value = value + self.field = field + self.comparison = comparison + self.value = value class Sort: @@ -24,6 +51,6 @@ class Sort: Model class for sort field """ - def __init__(self, field: str, ascending: bool): - self.Field = field - self.Ascending = ascending + def __init__(self, field: str, ascending: bool) -> None: + self.field = field + self.ascending = ascending From be1395afebf3072f38cc204e17b8790ccf17221a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:53:26 +0000 Subject: [PATCH 07/10] install self during workflow --- .github/workflows/coverage_and_lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage_and_lint.yml b/.github/workflows/coverage_and_lint.yml index 1616fc6..59b1695 100644 --- a/.github/workflows/coverage_and_lint.yml +++ b/.github/workflows/coverage_and_lint.yml @@ -36,6 +36,7 @@ jobs: PY_VER: "${{ matrix.python-version }}" run: | pip install -U -r requirements.txt + pip install -U . - uses: actions/setup-node@v3 with: From 518dbc201d163bffe5fb46dcc83255eb15a2038b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 17:58:09 +0000 Subject: [PATCH 08/10] remove boilerplate error --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index afd17fe..e70175c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,6 @@ target-version = ["py37"] [tool.isort] profile = "black" -src_paths = ["mystbin"] lines_after_imports = 2 [tool.pyright] From 54baef07d859ba8394c066d45e59c34fce83c74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 18:01:23 +0000 Subject: [PATCH 09/10] more cleanup and formatting --- pyxivapi/client.py | 1 - pyxivapi/decorators.py | 1 - pyxivapi/models.py | 22 ++++++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/pyxivapi/client.py b/pyxivapi/client.py index 26ff300..9250240 100644 --- a/pyxivapi/client.py +++ b/pyxivapi/client.py @@ -22,7 +22,6 @@ SOFTWARE. """ - import logging from typing import Any, ClassVar, List, Optional, TypeVar, Union diff --git a/pyxivapi/decorators.py b/pyxivapi/decorators.py index 3679317..1da2d26 100644 --- a/pyxivapi/decorators.py +++ b/pyxivapi/decorators.py @@ -22,7 +22,6 @@ SOFTWARE. """ - from __future__ import annotations import logging diff --git a/pyxivapi/models.py b/pyxivapi/models.py index a3a62d6..073b94a 100644 --- a/pyxivapi/models.py +++ b/pyxivapi/models.py @@ -22,7 +22,6 @@ SOFTWARE. """ - from typing import ClassVar, List from .exceptions import XIVAPIInvalidFilter @@ -33,6 +32,12 @@ class Filter: Model class for DQL filters """ + __slots__ = ( + "field", + "comparison", + "value", + ) + comparisons: ClassVar[List[str]] = ["gt", "gte", "lt", "lte"] def __init__(self, field: str, comparison: str, value: int) -> None: @@ -41,9 +46,9 @@ def __init__(self, field: str, comparison: str, value: int) -> None: if comparison not in self.comparisons: raise XIVAPIInvalidFilter(f'"{comparison}" is not a valid DQL filter comparison.') - self.field = field - self.comparison = comparison - self.value = value + self.field: str = field + self.comparison: str = comparison + self.value: int = value class Sort: @@ -51,6 +56,11 @@ class Sort: Model class for sort field """ + __slots__ = ( + "field", + "ascending", + ) + def __init__(self, field: str, ascending: bool) -> None: - self.field = field - self.ascending = ascending + self.field: str = field + self.ascending: bool = ascending From c381e9425575aa1c6e6b78b0fb959abeabc77c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20N=C3=B8rgaard?= Date: Wed, 14 Dec 2022 18:04:46 +0000 Subject: [PATCH 10/10] fix default branch specifier in boilerplate --- .github/workflows/coverage_and_lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage_and_lint.yml b/.github/workflows/coverage_and_lint.yml index 59b1695..ec43b43 100644 --- a/.github/workflows/coverage_and_lint.yml +++ b/.github/workflows/coverage_and_lint.yml @@ -53,7 +53,7 @@ jobs: uses: github/super-linter/slim@v4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEFAULT_BRANCH: main + DEFAULT_BRANCH: master VALIDATE_ALL_CODEBASE: false VALIDATE_PYTHON_BLACK: true VALIDATE_PYTHON_ISORT: true