diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index b70b6ec..0000000 --- a/.coveragerc +++ /dev/null @@ -1,14 +0,0 @@ -[run] -branch = True -source = amazon_paapi -relative_files = True -omit = **/sdk/** - -[report] -fail_under = 80 -skip_covered = true -skip_empty = true - -[html] -directory = coverage_html_report -skip_covered = false diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..79882c3 --- /dev/null +++ b/.env.template @@ -0,0 +1,4 @@ +API_KEY= +API_SECRET= +AFFILIATE_TAG= +COUNTRY_CODE= diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 597ed9e..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = W503,E203,F401 -exclude = */sdk/* diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..2305793 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,12 @@ +#! /bin/bash -e + +docker build \ + --build-arg TAG="3.12" \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" \ + -t python-amazon-paapi . + +touch .env + +docker run -t --rm -u "$(id -u):$(id -g)" -v "${PWD}:/code" --env-file .env \ + python-amazon-paapi -c "python -m pre_commit" diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index df00d64..0000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash -e - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -./scripts/check_isort -./scripts/check_black -./scripts/check_flake8 -./scripts/check_pylint -./scripts/run_tests - -header "Proceeding with push" diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index 5f08e08..a0fa1fc 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -8,9 +8,9 @@ assignees: '' --- **Steps to reproduce** -1. -2. -3. +1. +2. +3. **Code example** ```python diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md index f74fa80..924da4f 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -8,7 +8,7 @@ assignees: '' --- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. +A clear and concise description of what the problem is. **Describe the solution you'd like** A clear and concise description of what you want to happen. diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 4a32a57..02766ee 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -2,15 +2,18 @@ name: Lint and test on: push: - pull_request: - types: [opened, reopened] permissions: pull-requests: read -jobs: +env: + API_KEY: ${{ secrets.API_KEY }} + API_SECRET: ${{ secrets.API_SECRET }} + AFFILIATE_TAG: ${{ secrets.AFFILIATE_TAG }} + COUNTRY_CODE: ${{ secrets.COUNTRY_CODE }} - isort: +jobs: + ruff: runs-on: ubuntu-latest container: image: python:3.12 @@ -18,11 +21,11 @@ jobs: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies - run: pip install isort - - name: Check imports order - run: isort -c . + run: pip install ruff + - name: Check code errors + run: ruff check --output-format=github . - black: + mypy: runs-on: ubuntu-latest container: image: python:3.12 @@ -30,35 +33,77 @@ jobs: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies - run: pip install black - - name: Check code format - run: black --check --diff --color . + run: pip install mypy + - name: Check code errors + run: mypy amazon_paapi - flake8: + test-py37: runs-on: ubuntu-latest + needs: [test-py38] container: - image: python:3.12 + image: python:3.7 steps: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies - run: pip install flake8 - - name: Check code errors - run: flake8 . + run: pip install -e . && pip install pytest + - name: Run tests + run: python -m pytest - pylint: + test-py38: runs-on: ubuntu-latest + needs: [test-py39] container: - image: python:3.12 + image: python:3.8 steps: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies - run: pip install pylint - - name: Check code errors - run: find . -type f -name '*.py' | xargs pylint --disable=missing-docstring --disable=too-few-public-methods + run: pip install -e . && pip install pytest + - name: Run tests + run: python -m pytest + + test-py39: + runs-on: ubuntu-latest + needs: [test-py310] + container: + image: python:3.9 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install dependencies + run: pip install -e . && pip install pytest + - name: Run tests + run: python -m pytest + + test-py310: + runs-on: ubuntu-latest + needs: [test-py311] + container: + image: python:3.10 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install dependencies + run: pip install -e . && pip install pytest + - name: Run tests + run: python -m pytest + + test-py311: + runs-on: ubuntu-latest + needs: [test-py312] + container: + image: python:3.11 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install dependencies + run: pip install -e . && pip install pytest + - name: Run tests + run: python -m pytest + - test: + test-py312: runs-on: ubuntu-latest container: image: python:3.12 @@ -66,9 +111,9 @@ jobs: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies - run: pip install coverage certifi six python_dateutil setuptools urllib3 + run: pip install -e . && pip install coverage pytest - name: Run tests - run: coverage run -m unittest && coverage xml && coverage report + run: coverage run -m pytest && coverage xml && coverage report - name: Save code coverage file uses: actions/upload-artifact@v4 with: @@ -77,7 +122,7 @@ jobs: sonar: runs-on: ubuntu-latest - needs: [test] + needs: [test-py312] steps: - name: Check out code uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 388bc29..21fe683 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,8 @@ # Custom folders and files to ignore .vscode/ .DS_Store -secrets.py -test.py coverage_html_report - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5e7e6d0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,58 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.12 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + - id: check-yaml + - id: check-added-large-files + - id: check-ast + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-toml + - id: name-tests-test + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/lk16/detect-missing-init + rev: v0.1.6 + hooks: + - id: detect-missing-init + args: ["--create", "--python-folders", "amazon_paapi"] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.11.2' + hooks: + - id: mypy + exclude: sdk/ + + - repo: local + hooks: + - id: mypy + name: Checking types with mypy + language: system + entry: "python -m mypy amazon_paapi" + pass_filenames: false + + - id: test + name: Running tests + language: system + entry: "python -m coverage run -m pytest -rs" + pass_filenames: false + + - id: test + name: Checking coverage + language: system + entry: "python -m coverage report" + pass_filenames: false diff --git a/.shellcheckrc b/.shellcheckrc deleted file mode 100644 index 1c2cbb5..0000000 --- a/.shellcheckrc +++ /dev/null @@ -1,3 +0,0 @@ -# ~/.shellcheckrc - -disable=SC1091 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b1ed84 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +ARG TAG="3.12" + +FROM python:${TAG} + +ARG UID="1000" +ARG GID="1000" + +ENV PRE_COMMIT_HOME="/code/.cache/pre-commit" +ENV PRE_COMMIT_COLOR="always" + +RUN groupadd --gid ${GID} user \ + && useradd --uid ${UID} --gid user --shell /bin/bash --create-home user + +USER user + +WORKDIR /code + +RUN pip install --no-cache-dir \ + coverage \ + mypy \ + pre-commit \ + pytest \ + ruff + +COPY setup.py setup.py +COPY README.md README.md +RUN pip install --no-cache-dir -e . + +ENTRYPOINT [ "bash" ] diff --git a/LICENSE b/LICENSE index 67111a9..3ac2caa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Sergio Abad +Copyright (c) 2024 Sergio Abad Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..609517a --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +export UID:=$(shell id -u) +export GID:=$(shell id -g) + +export PYTHON_TAGS = 3.7 3.8 3.9 3.10 3.11 3.12 + +setup: + @git config --unset-all core.hooksPath || true + @git config --local core.hooksPath .githooks + +build: + @docker build --build-arg TAG="3.12" --build-arg UID="${UID}" --build-arg GID="${GID}" -t python-amazon-paapi . + +coverage: build + @touch .env + @docker run -t --rm -u "${UID}:${GID}" -v "${PWD}:/code" --env-file .env python-amazon-paapi -c \ + "python -m coverage run -m pytest -rs && python -m coverage xml && python -m coverage report" + +test: build + @touch .env + @docker run -t --rm -u "${UID}:${GID}" -v "${PWD}:/code" --env-file .env python-amazon-paapi -c "python -m pytest -rs" + +test-all-python-tags: + @touch .env + @for tag in $$PYTHON_TAGS; do \ + docker build --build-arg TAG="$$tag" --build-arg UID="${UID}" --build-arg GID="${GID}" -t python-amazon-paapi .; \ + docker run -t --rm -u "${UID}:${GID}" -v "${PWD}:/code" python-amazon-paapi -c "python -m pytest -rs"; \ + done + +lint: build + @touch .env + @docker run --rm -t -u "${UID}:${GID}" -v "${PWD}:/code" --env-file .env python-amazon-paapi -c "python -m pre_commit run -a" + +pre-commit: + @./.githooks/pre-commit diff --git a/README.md b/README.md index b7055e4..11fb619 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ API](https://webservices.amazon.com/paapi5/documentation/quick-start/using-sdk.h This module allows interacting with Amazon using the official API in an easier way. [![PyPI](https://img.shields.io/pypi/v/python-amazon-paapi?color=%231182C2&label=PyPI)](https://pypi.org/project/python-amazon-paapi/) -[![Python](https://img.shields.io/badge/Python->3.6-%23FFD140)](https://www.python.org/) +[![Python](https://img.shields.io/badge/Python->3.7-%23FFD140)](https://www.python.org/) [![License](https://img.shields.io/badge/License-MIT-%23e83633)](https://github.com/sergioteula/python-amazon-paapi/blob/master/LICENSE) [![Amazon API](https://img.shields.io/badge/Amazon%20API-5.0-%23FD9B15)](https://webservices.amazon.com/paapi5/documentation/) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=sergioteula_python-amazon-paapi&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=sergioteula_python-amazon-paapi) diff --git a/amazon_paapi/__init__.py b/amazon_paapi/__init__.py index 61887fa..d46c2d6 100644 --- a/amazon_paapi/__init__.py +++ b/amazon_paapi/__init__.py @@ -1,4 +1,4 @@ -"""Amazon Product Advertising API wrapper for Python""" +"""Amazon Product Advertising API wrapper for Python.""" __author__ = "Sergio Abad" diff --git a/amazon_paapi/api.py b/amazon_paapi/api.py index a6e54d1..181ca73 100644 --- a/amazon_paapi/api.py +++ b/amazon_paapi/api.py @@ -1,10 +1,10 @@ -"""Amazon Product Advertising API wrapper for Python +"""Amazon Product Advertising API wrapper for Python. A simple Python wrapper for the last version of the Amazon Product Advertising API. """ import time -from typing import List, Union +from typing import List, Optional, Union from . import models from .errors import InvalidArgument @@ -27,6 +27,7 @@ class AmazonApi: Raises: ``InvalidArgumentException`` + """ def __init__( @@ -36,8 +37,8 @@ def __init__( tag: str, country: models.Country, throttling: float = 1, - **kwargs - ): + **kwargs, + ) -> None: self._key = key self._secret = secret self._last_query_time = time.time() - throttling @@ -50,7 +51,8 @@ def __init__( self.region = models.regions.REGIONS[country] self.marketplace = "www.amazon." + models.regions.DOMAINS[country] except KeyError as error: - raise InvalidArgument("Country code is not correct") from error + msg = "Country code is not correct" + raise InvalidArgument(msg) from error self.api = DefaultApi(key, secret, self._host, self.region) @@ -59,10 +61,10 @@ def get_items( items: Union[str, List[str]], condition: models.Condition = None, merchant: models.Merchant = None, - currency_of_preference: str = None, - languages_of_preference: List[str] = None, + currency_of_preference: Optional[str] = None, + languages_of_preference: Optional[List[str]] = None, include_unavailable: bool = False, - **kwargs + **kwargs, ) -> List[models.Item]: """Get items information from Amazon. @@ -91,8 +93,8 @@ def get_items( ``MalformedRequestException`` ``ApiRequestException`` ``ItemsNotFoundException`` - """ + """ kwargs.update( { "condition": condition, @@ -115,28 +117,28 @@ def get_items( def search_items( self, # NOSONAR - item_count: int = None, - item_page: int = None, - actor: str = None, - artist: str = None, - author: str = None, - brand: str = None, - keywords: str = None, - title: str = None, + item_count: Optional[int] = None, + item_page: Optional[int] = None, + actor: Optional[str] = None, + artist: Optional[str] = None, + author: Optional[str] = None, + brand: Optional[str] = None, + keywords: Optional[str] = None, + title: Optional[str] = None, availability: models.Availability = None, - browse_node_id: str = None, + browse_node_id: Optional[str] = None, condition: models.Condition = None, - currency_of_preference: str = None, - delivery_flags: List[str] = None, - languages_of_preference: List[str] = None, + currency_of_preference: Optional[str] = None, + delivery_flags: Optional[List[str]] = None, + languages_of_preference: Optional[List[str]] = None, merchant: models.Merchant = None, - max_price: int = None, - min_price: int = None, - min_saving_percent: int = None, - min_reviews_rating: int = None, - search_index: str = None, + max_price: Optional[int] = None, + min_price: Optional[int] = None, + min_saving_percent: Optional[int] = None, + min_reviews_rating: Optional[int] = None, + search_index: Optional[str] = None, sort_by: models.SortBy = None, - **kwargs + **kwargs, ) -> models.SearchResult: """Searches for items on Amazon based on a search query. At least one of the following parameters should be specified: ``keywords``, ``actor``, ``artist``, @@ -195,8 +197,8 @@ def search_items( ``MalformedRequestException`` ``ApiRequestException`` ``ItemsNotFoundException`` - """ + """ kwargs.update( { "item_count": item_count, @@ -231,13 +233,13 @@ def search_items( def get_variations( self, asin: str, - variation_count: int = None, - variation_page: int = None, + variation_count: Optional[int] = None, + variation_page: Optional[int] = None, condition: models.Condition = None, - currency_of_preference: str = None, - languages_of_preference: List[str] = None, + currency_of_preference: Optional[str] = None, + languages_of_preference: Optional[List[str]] = None, merchant: models.Merchant = None, - **kwargs + **kwargs, ) -> models.VariationsResult: """Returns a set of items that are the same product, but differ according to a consistent theme, for example size and color. A variation is a child ASIN. @@ -268,8 +270,8 @@ def get_variations( ``MalformedRequestException`` ``ApiRequestException`` ``ItemsNotFoundException`` - """ + """ asin = arguments.get_items_ids(asin)[0] kwargs.update( @@ -292,8 +294,8 @@ def get_variations( def get_browse_nodes( self, browse_node_ids: List[str], - languages_of_preference: List[str] = None, - **kwargs + languages_of_preference: Optional[List[str]] = None, + **kwargs, ) -> List[models.BrowseNode]: """Returns the specified browse node's information like name, children and ancestors. @@ -314,8 +316,8 @@ def get_browse_nodes( ``MalformedRequestException`` ``ApiRequestException`` ``ItemsNotFoundException`` - """ + """ kwargs.update( { "browse_node_ids": browse_node_ids, @@ -328,7 +330,7 @@ def get_browse_nodes( self._throttle() return requests.get_browse_nodes_response(self, request) - def _throttle(self): + def _throttle(self) -> None: wait_time = self.throttling - (time.time() - self._last_query_time) if wait_time > 0: time.sleep(wait_time) diff --git a/amazon_paapi/errors/exceptions.py b/amazon_paapi/errors/exceptions.py index 707d4d3..b0eba7c 100644 --- a/amazon_paapi/errors/exceptions.py +++ b/amazon_paapi/errors/exceptions.py @@ -1,10 +1,10 @@ -"""Custom exceptions module""" +"""Custom exceptions module.""" class AmazonError(Exception): """Common base class for all Amazon API exceptions.""" - def __init__(self, reason: str): + def __init__(self, reason: str) -> None: super().__init__() self.reason = reason diff --git a/amazon_paapi/helpers/arguments.py b/amazon_paapi/helpers/arguments.py index 29ef67a..269a125 100644 --- a/amazon_paapi/helpers/arguments.py +++ b/amazon_paapi/helpers/arguments.py @@ -1,17 +1,15 @@ """Module with helper functions for managing arguments.""" - from typing import List, Union -from ..errors import InvalidArgument -from ..tools import get_asin +from amazon_paapi.errors import InvalidArgument +from amazon_paapi.tools import get_asin def get_items_ids(items: Union[str, List[str]]) -> List[str]: if not isinstance(items, str) and not isinstance(items, List): - raise InvalidArgument( - "Invalid items argument, it should be a string or List of strings" - ) + msg = "Invalid items argument, it should be a string or List of strings" + raise InvalidArgument(msg) if isinstance(items, str): items_ids = items.split(",") @@ -23,12 +21,12 @@ def get_items_ids(items: Union[str, List[str]]) -> List[str]: return items_ids -def check_search_args(**kwargs): +def check_search_args(**kwargs) -> None: check_search_mandatory_args(**kwargs) check_search_pagination_args(**kwargs) -def check_search_mandatory_args(**kwargs): +def check_search_mandatory_args(**kwargs) -> None: mandatory_args = [ kwargs.get("keywords"), kwargs.get("actor"), @@ -47,7 +45,7 @@ def check_search_mandatory_args(**kwargs): raise InvalidArgument(error_message) -def check_search_pagination_args(**kwargs): +def check_search_pagination_args(**kwargs) -> None: error_message = "Args item_count and item_page should be integers between 1 and 10." pagination_args = [kwargs.get("item_count"), kwargs.get("item_page")] pagination_args = [arg for arg in pagination_args if arg] @@ -59,7 +57,7 @@ def check_search_pagination_args(**kwargs): raise InvalidArgument(error_message) -def check_variations_args(**kwargs): +def check_variations_args(**kwargs) -> None: error_message = ( "Args variation_count and variation_page should be integers between 1 and 10." ) @@ -73,7 +71,7 @@ def check_variations_args(**kwargs): raise InvalidArgument(error_message) -def check_browse_nodes_args(**kwargs): +def check_browse_nodes_args(**kwargs) -> None: if not isinstance(kwargs.get("browse_node_ids"), List): error_message = "Argument browse_node_ids should be a List of strings." raise InvalidArgument(error_message) diff --git a/amazon_paapi/helpers/generators.py b/amazon_paapi/helpers/generators.py index f0d6537..ccb56fe 100644 --- a/amazon_paapi/helpers/generators.py +++ b/amazon_paapi/helpers/generators.py @@ -1,6 +1,5 @@ """Module with helper functions for making generators.""" - from typing import Generator, List diff --git a/amazon_paapi/helpers/items.py b/amazon_paapi/helpers/items.py index 40b139f..dcb15e0 100644 --- a/amazon_paapi/helpers/items.py +++ b/amazon_paapi/helpers/items.py @@ -1,8 +1,8 @@ -"""Module to manage items""" +"""Module to manage items.""" from typing import List -from .. import models +from amazon_paapi import models def sort_items( diff --git a/amazon_paapi/helpers/requests.py b/amazon_paapi/helpers/requests.py index fa6880a..52b8db5 100644 --- a/amazon_paapi/helpers/requests.py +++ b/amazon_paapi/helpers/requests.py @@ -1,10 +1,9 @@ """Module with helper functions for creating requests.""" - import inspect from typing import List -from ..errors import ( +from amazon_paapi.errors import ( AssociateValidationError, InvalidArgument, ItemsNotFound, @@ -12,20 +11,20 @@ RequestError, TooManyRequests, ) -from ..models.browse_nodes_result import BrowseNode -from ..models.item_result import Item -from ..models.search_result import SearchResult -from ..models.variations_result import VariationsResult -from ..sdk.models.get_browse_nodes_request import GetBrowseNodesRequest -from ..sdk.models.get_browse_nodes_resource import GetBrowseNodesResource -from ..sdk.models.get_items_request import GetItemsRequest -from ..sdk.models.get_items_resource import GetItemsResource -from ..sdk.models.get_variations_request import GetVariationsRequest -from ..sdk.models.get_variations_resource import GetVariationsResource -from ..sdk.models.partner_type import PartnerType -from ..sdk.models.search_items_request import SearchItemsRequest -from ..sdk.models.search_items_resource import SearchItemsResource -from ..sdk.rest import ApiException +from amazon_paapi.models.browse_nodes_result import BrowseNode +from amazon_paapi.models.item_result import Item +from amazon_paapi.models.search_result import SearchResult +from amazon_paapi.models.variations_result import VariationsResult +from amazon_paapi.sdk.models.get_browse_nodes_request import GetBrowseNodesRequest +from amazon_paapi.sdk.models.get_browse_nodes_resource import GetBrowseNodesResource +from amazon_paapi.sdk.models.get_items_request import GetItemsRequest +from amazon_paapi.sdk.models.get_items_resource import GetItemsResource +from amazon_paapi.sdk.models.get_variations_request import GetVariationsRequest +from amazon_paapi.sdk.models.get_variations_resource import GetVariationsResource +from amazon_paapi.sdk.models.partner_type import PartnerType +from amazon_paapi.sdk.models.search_items_request import SearchItemsRequest +from amazon_paapi.sdk.models.search_items_resource import SearchItemsResource +from amazon_paapi.sdk.rest import ApiException def get_items_request(amazon_api, asin_chunk: List[str], **kwargs) -> GetItemsRequest: @@ -39,9 +38,8 @@ def get_items_request(amazon_api, asin_chunk: List[str], **kwargs) -> GetItemsRe **kwargs, ) except TypeError as exc: - raise MalformedRequest( - f"Parameters for get_items request are not correct: {exc}" - ) from exc + msg = f"Parameters for get_items request are not correct: {exc}" + raise MalformedRequest(msg) from exc def get_items_response(amazon_api, request: GetItemsRequest) -> List[Item]: @@ -51,7 +49,8 @@ def get_items_response(amazon_api, request: GetItemsRequest) -> List[Item]: _manage_response_exceptions(exc) if response.items_result is None: - raise ItemsNotFound("No items have been found") + msg = "No items have been found" + raise ItemsNotFound(msg) return response.items_result.items @@ -66,9 +65,8 @@ def get_search_items_request(amazon_api, **kwargs) -> SearchItemsRequest: **kwargs, ) except TypeError as exc: - raise MalformedRequest( - f"Parameters for search_items request are not correct: {exc}" - ) from exc + msg = f"Parameters for search_items request are not correct: {exc}" + raise MalformedRequest(msg) from exc def get_search_items_response(amazon_api, request: SearchItemsRequest) -> SearchResult: @@ -78,7 +76,8 @@ def get_search_items_response(amazon_api, request: SearchItemsRequest) -> Search _manage_response_exceptions(exc) if response.search_result is None: - raise ItemsNotFound("No items have been found") + msg = "No items have been found" + raise ItemsNotFound(msg) return response.search_result @@ -93,9 +92,8 @@ def get_variations_request(amazon_api, **kwargs) -> GetVariationsRequest: **kwargs, ) except TypeError as exc: - raise MalformedRequest( - f"Parameters for get_variations request are not correct: {exc}" - ) from exc + msg = f"Parameters for get_variations request are not correct: {exc}" + raise MalformedRequest(msg) from exc def get_variations_response( @@ -107,7 +105,8 @@ def get_variations_response( _manage_response_exceptions(exc) if response.variations_result is None: - raise ItemsNotFound("No variation items have been found") + msg = "No variation items have been found" + raise ItemsNotFound(msg) return response.variations_result @@ -122,9 +121,8 @@ def get_browse_nodes_request(amazon_api, **kwargs) -> GetBrowseNodesRequest: **kwargs, ) except TypeError as exc: - raise MalformedRequest( - f"Parameters for get_browse_nodes request are not correct: {exc}" - ) from exc + msg = f"Parameters for get_browse_nodes request are not correct: {exc}" + raise MalformedRequest(msg) from exc def get_browse_nodes_response( @@ -136,17 +134,15 @@ def get_browse_nodes_response( _manage_response_exceptions(exc) if response.browse_nodes_result is None: - raise ItemsNotFound("No browse nodes have been found") + msg = "No browse nodes have been found" + raise ItemsNotFound(msg) return response.browse_nodes_result.browse_nodes def _get_request_resources(resources) -> List[str]: resources = inspect.getmembers(resources, lambda a: not inspect.isroutine(a)) - resources = [ - x[-1] for x in resources if isinstance(x[-1], str) and x[0][0:2] != "__" - ] - return resources + return [x[-1] for x in resources if isinstance(x[-1], str) and x[0][0:2] != "__"] def _manage_response_exceptions(error) -> None: @@ -154,22 +150,22 @@ def _manage_response_exceptions(error) -> None: error_body = getattr(error, "body", "") or "" if error_status == 429: - raise TooManyRequests( + msg = ( "Requests limit reached, try increasing throttling or wait before" " trying again" ) + raise TooManyRequests(msg) if "InvalidParameterValue" in error_body: - raise InvalidArgument( - "The value provided in the request for atleast one parameter is invalid." - ) + msg = "The value provided in the request for atleast one parameter is invalid." + raise InvalidArgument(msg) if "InvalidPartnerTag" in error_body: - raise InvalidArgument("The partner tag is invalid or not present.") + msg = "The partner tag is invalid or not present." + raise InvalidArgument(msg) if "InvalidAssociate" in error_body: - raise AssociateValidationError( - "Used credentials are not valid for the selected country." - ) + msg = "Used credentials are not valid for the selected country." + raise AssociateValidationError(msg) raise RequestError("Request failed: " + str(error.reason)) diff --git a/amazon_paapi/models/__init__.py b/amazon_paapi/models/__init__.py index 415cda1..b4b8308 100644 --- a/amazon_paapi/models/__init__.py +++ b/amazon_paapi/models/__init__.py @@ -1,4 +1,5 @@ -from ..sdk.models import Availability, Condition, Merchant, SortBy +from amazon_paapi.sdk.models import Availability, Condition, Merchant, SortBy + from .browse_nodes_result import BrowseNode from .item_result import Item from .regions import Country diff --git a/amazon_paapi/models/browse_nodes_result.py b/amazon_paapi/models/browse_nodes_result.py index 14dcf2f..79e53b3 100644 --- a/amazon_paapi/models/browse_nodes_result.py +++ b/amazon_paapi/models/browse_nodes_result.py @@ -1,6 +1,6 @@ from typing import List -from ..sdk import models as sdk_models +from amazon_paapi.sdk import models as sdk_models class BrowseNodeChild(sdk_models.BrowseNodeChild): diff --git a/amazon_paapi/models/item_result.py b/amazon_paapi/models/item_result.py index 616a1d6..8c969cb 100644 --- a/amazon_paapi/models/item_result.py +++ b/amazon_paapi/models/item_result.py @@ -1,6 +1,6 @@ from typing import List, Optional -from ..sdk import models as sdk_models +from amazon_paapi.sdk import models as sdk_models class ApiLabelLocale: diff --git a/amazon_paapi/models/search_result.py b/amazon_paapi/models/search_result.py index 7e27143..a715acc 100644 --- a/amazon_paapi/models/search_result.py +++ b/amazon_paapi/models/search_result.py @@ -1,6 +1,7 @@ from typing import List -from ..sdk import models as sdk_models +from amazon_paapi.sdk import models as sdk_models + from .item_result import Item diff --git a/amazon_paapi/models/variations_result.py b/amazon_paapi/models/variations_result.py index a15db36..1189e5d 100644 --- a/amazon_paapi/models/variations_result.py +++ b/amazon_paapi/models/variations_result.py @@ -1,6 +1,7 @@ from typing import List -from ..sdk import models as sdk_models +from amazon_paapi.sdk import models as sdk_models + from .item_result import Item diff --git a/amazon_paapi/sdk/NOTICE.txt b/amazon_paapi/sdk/NOTICE.txt index 526e5d9..93040d3 100644 --- a/amazon_paapi/sdk/NOTICE.txt +++ b/amazon_paapi/sdk/NOTICE.txt @@ -1,2 +1,2 @@ Product Advertising API 5.0 SDK for Python -Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file +Copyright 2019-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/amazon_paapi/tools/asin.py b/amazon_paapi/tools/asin.py index 46a585c..1f08e8d 100644 --- a/amazon_paapi/tools/asin.py +++ b/amazon_paapi/tools/asin.py @@ -2,7 +2,7 @@ import re -from ..errors import AsinNotFound +from amazon_paapi.errors import AsinNotFound def get_asin(text: str) -> str: diff --git a/examples/example.py b/examples/example.py deleted file mode 100644 index dde03ad..0000000 --- a/examples/example.py +++ /dev/null @@ -1,35 +0,0 @@ -import secrets - -from amazon_paapi import AmazonApi - -# pylint: disable=no-member -amazon = AmazonApi( - secrets.KEY, secrets.SECRET, secrets.TAG, secrets.COUNTRY, throttling=2 -) - - -print("\nGet items") -print("=========================================================") -product = amazon.get_items("B01N5IB20Q") -print(product[0].item_info.title.display_value) - - -print("Search items") -print("=========================================================") -items = amazon.search_items(keywords="nintendo", item_count=3) -for item in items.items: - print(item.item_info.title.display_value) - - -print("\nGet variations") -print("=========================================================") -items = amazon.get_variations("B08F63PPNV", variation_count=3) -for item in items.items: - print(item.item_info.title.display_value) - - -print("\nGet nodes") -print("=========================================================") -items = amazon.get_browse_nodes(["667049031"]) # Only available in spanish marketplace -for item in items: - print(item.display_name) diff --git a/pyproject.toml b/pyproject.toml index a833497..3a9eee8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,79 @@ -[tool.black] -preview = true -exclude = ".*/sdk/.*" - -[tool.isort] -profile = "black" -skip_glob = "*/sdk/*" - -[tool.pylint] - [tool.pylint.master] - ignore = ["test.py"] - ignore-paths = [".*/sdk/", ".*docs/"] - [tool.pylint.message_control] - disable = [ - "no-self-use", - "protected-access", - "too-many-arguments", - "too-many-instance-attributes", - "too-many-locals", - "too-many-public-methods", - ] - ignored-argument-names = "args|kwargs" - [tool.pylint.similarities] - ignore-imports = true +[tool.ruff] +target-version = "py37" +cache-dir = ".cache/ruff" +unsafe-fixes = true + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN003", + "D104", + "D100", + "D203", + "D213", + "COM812", + "ISC001", +] +exclude = [ + "amazon_paapi/sdk/*", + "docs/*", +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "ANN201", + "ANN206", + "D101", + "D102", + "D103", + "D107", + "PT009", + "PT027", + "SLF001", +] + +[tool.ruff.format] +exclude = [ + "amazon_paapi/sdk/*", + "docs/*", + ".github", +] + +[tool.mypy] +python_version = "3.7" +ignore_missing_imports = true +no_implicit_optional = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true +cache_dir = '.cache/mypy' +exclude = ["amazon_paapi/sdk/*"] + +[[tool.mypy.overrides]] +module = [ + "amazon_paapi/sdk/*", +] +follow_imports = "skip" + +[tool.coverage.run] +branch = true +relative_files = true +source = ["amazon_paapi"] +omit = ["amazon_paapi/sdk/*"] + +[tool.coverage.report] +precision = 2 +skip_covered = true +skip_empty = true +fail_under = 95 + +[tool.coverage.html] +directory = "coverage_html_report" +skip_covered = false + +[tool.pytest.ini_options] +testpaths = "tests" +cache_dir = ".cache/pytest" diff --git a/scripts/check_black b/scripts/check_black deleted file mode 100755 index bdebf44..0000000 --- a/scripts/check_black +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -header "Checking code format with black" - -if [ -n "$(check_if_installed docker)" ]; then - docker run -v "${PWD}:/code" sergioteula/pytools black --check --diff --color . -elif [ -n "$(check_if_installed black)" ]; then - black --check --diff --color . -else - error "black is not installed" - exit 1 -fi - -EXIT_CODE="$?" -if [ "$EXIT_CODE" = "0" ]; then - success "Code is correctly formatted" -else - error "Code should be formatted using black" - exit 1 -fi diff --git a/scripts/check_flake8 b/scripts/check_flake8 deleted file mode 100755 index 1a117a1..0000000 --- a/scripts/check_flake8 +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -header "Checking code errors with flake8" - -if [ -n "$(check_if_installed docker)" ]; then - docker run -v "${PWD}:/code" sergioteula/pytools flake8 --color always . -elif [ -n "$(check_if_installed flake8)" ]; then - flake8 --color always . -else - error "flake8 is not installed" - exit 1 -fi - -EXIT_CODE="$?" -if [ "$EXIT_CODE" = "0" ]; then - success "Code analysis with flake8 is correct" -else - error "There are errors detected by flake8" - exit 1 -fi diff --git a/scripts/check_isort b/scripts/check_isort deleted file mode 100755 index 046205e..0000000 --- a/scripts/check_isort +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -header "Checking imports order with isort" - -if [ -n "$(check_if_installed docker)" ]; then - docker run -v "${PWD}:/code" sergioteula/pytools isort -c --color . -elif [ -n "$(check_if_installed isort)" ]; then - isort -c --color . -else - error "isort is not installed" - exit 1 -fi - -EXIT_CODE="$?" -if [ "$EXIT_CODE" = "0" ]; then - success "Imports are correctly ordered" -else - error "Imports order is not correct" - exit 1 -fi diff --git a/scripts/check_pylint b/scripts/check_pylint deleted file mode 100755 index 8cdb438..0000000 --- a/scripts/check_pylint +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -header "Checking code errors with pylint" - -if [ -n "$(check_if_installed docker)" ]; then - docker run -v "${PWD}:/code" sergioteula/pytools find . -type f -name '*.py' | xargs pylint --disable=missing-docstring --disable=too-few-public-methods -elif [ -n "$(check_if_installed pylint)" ]; then - find . -type f -name '*.py' | xargs pylint --disable=missing-docstring --disable=too-few-public-methods -else - error "pylint is not installed" - exit 1 -fi - -EXIT_CODE="$?" -if [ "$EXIT_CODE" = "0" ]; then - success "Code analysis with pylint is correct" -else - error "There are errors detected by pylint" - exit 1 -fi diff --git a/scripts/helpers b/scripts/helpers deleted file mode 100755 index 6c44dde..0000000 --- a/scripts/helpers +++ /dev/null @@ -1,30 +0,0 @@ -#! /bin/bash -e - -BLUE="\033[0;34m" -GREEN="\033[0;32m" -RED="\033[0;31m" -YELLOW="\033[0;33m" -NC="\033[0m" -LINE="----------------------------------------------------------------------" - -check_if_installed() { - if [ -x "$(command -v "${1}")" ]; then - echo "${1} is installed" - fi -} - -header(){ - echo -e "\n${BLUE}${*}\n${BLUE}${LINE}${NC}" -} - -warning(){ - echo -e "${YELLOW}WARNING: ${*}${NC}" -} - -error(){ - echo -e "${RED}ERROR: ${*}${NC}" -} - -success(){ - echo -e "${GREEN}${*}${NC}" -} diff --git a/scripts/run_tests b/scripts/run_tests deleted file mode 100755 index 23eb2fd..0000000 --- a/scripts/run_tests +++ /dev/null @@ -1,24 +0,0 @@ -#! /bin/bash - -ROOT_DIR="$(git rev-parse --show-toplevel)" -source "${ROOT_DIR}/scripts/helpers" - -header "Running tests" - -if [ -n "$(check_if_installed docker)" ]; then - docker run -v "${PWD}:/code" -u "$(id -u):$(id -g)" sergioteula/pytools bash -c \ - "coverage run -m unittest && coverage xml && coverage html && echo && coverage report" -elif [ -n "$(check_if_installed coverage)" ]; then - coverage run -m unittest && coverage xml && coverage html && echo && coverage report -else - error "coverage is not installed" - exit 1 -fi - -EXIT_CODE="$?" -if [ "$EXIT_CODE" = "0" ]; then - success "Tests passed" -else - error "Tests failed" - exit 1 -fi diff --git a/setup.py b/setup.py index f570722..8d02080 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import setuptools -with open("README.md", "r", encoding="utf8") as fh: +with open("README.md", encoding="utf8") as fh: long_description = fh.read() setuptools.setup( @@ -20,5 +20,5 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires=">=3.6", + python_requires=">=3.7", ) diff --git a/sonar-project.properties b/sonar-project.properties index fade800..0b516c0 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -7,4 +7,4 @@ sonar.tests=tests sonar.python.coverage.reportPaths=coverage.xml sonar.qualitygate.wait=true -sonar.python.version=3.6,3.7,3.8,3.9,3.10,3.11,3.12 +sonar.python.version=3.7,3.8,3.9,3.10,3.11,3.12 diff --git a/tests/integration_test.py b/tests/integration_test.py new file mode 100644 index 0000000..58dcfb3 --- /dev/null +++ b/tests/integration_test.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import os +from unittest import TestCase, skipUnless + +from amazon_paapi.api import AmazonApi + + +def get_api_credentials() -> tuple[str]: + api_key = os.environ.get("API_KEY") + api_secret = os.environ.get("API_SECRET") + affiliate_tag = os.environ.get("AFFILIATE_TAG") + country_code = os.environ.get("COUNTRY_CODE") + + return api_key, api_secret, affiliate_tag, country_code + + +@skipUnless(all(get_api_credentials()), "Needs Amazon API credentials") +class IntegrationTest(TestCase): + @classmethod + def setUpClass(cls): + api_key, api_secret, affiliate_tag, country_code = get_api_credentials() + cls.api = AmazonApi(api_key, api_secret, affiliate_tag, country_code) + cls.affiliate_tag = affiliate_tag + + def test_search_items_and_get_information_for_the_first_one(self): + search_result = self.api.search_items(keywords="zapatillas") + searched_item = search_result.items[0] + + self.assertEqual(10, len(search_result.items)) + self.assertIn(self.affiliate_tag, searched_item.detail_page_url) + + get_results = self.api.get_items(searched_item.asin) + + self.assertEqual(1, len(get_results)) + self.assertIn(self.affiliate_tag, get_results[0].detail_page_url) diff --git a/tests/test_helpers_items.py b/tests/test_helpers_items.py index 2f0dea6..7979c91 100644 --- a/tests/test_helpers_items.py +++ b/tests/test_helpers_items.py @@ -5,7 +5,7 @@ class MockedItem(mock.MagicMock): - def __init__(self, asin): + def __init__(self, asin) -> None: super().__init__() self.asin = asin