From eaa2f0da4fcf0f733ab22986bb297bf9642c3910 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Thu, 8 Jun 2023 10:05:09 +0200 Subject: [PATCH 01/28] changeg log level of unexpected statuses and types --- inpost/static/parcels.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index a396766..c1e7686 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -59,16 +59,16 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): # log all unexpected things, so you can make an issue @github if self.shipment_type == ParcelShipmentType.UNKNOWN: - self._log.debug(f'unexpected shipment_type: {parcel_data["shipmentType"]}') + self._log.warning(f'unexpected shipment_type: {parcel_data["shipmentType"]}') if self.parcel_size == ParcelCarrierSize.UNKNOWN or self.parcel_size == ParcelLockerSize.UNKNOWN: - self._log.debug(f'unexpected parcel_size: {parcel_data["parcelSize"]}') + self._log.warning(f'unexpected parcel_size: {parcel_data["parcelSize"]}') if self.status == ParcelStatus.UNKNOWN: - self._log.debug(f'unexpected parcel status: {parcel_data["status"]}') + self._log.warning(f'unexpected parcel status: {parcel_data["status"]}') if self.ownership_status == ParcelOwnership.UNKNOWN: - self._log.debug(f'unexpected ownership status: {parcel_data["ownershipStatus"]}') + self._log.warning(f'unexpected ownership status: {parcel_data["ownershipStatus"]}') def __repr__(self): fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') @@ -239,6 +239,10 @@ def is_main_multicompartment(self): return None + @property + def has_airsensor(self) -> bool: + return self.pickup_point.air_sensor_data is not None + # @property # def get_from_multicompartment(self): # return @@ -598,7 +602,7 @@ def status(self, status_data: str | CompartmentActualStatus): else CompartmentActualStatus[status_data] if self._status == CompartmentActualStatus.UNKNOWN and isinstance(status_data, str): - self._log.debug(f'unexpected compartment actual status: {status_data}') + self._log.warning(f'unexpected compartment actual status: {status_data}') class AirSensorData: From 313f8519d7d53d9ce8db3ba30f60247543b0b861 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Thu, 8 Jun 2023 23:27:29 +0200 Subject: [PATCH 02/28] changed some async methods to sync --- inpost/api.py | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inpost/api.py b/inpost/api.py index f0d3e7a..c477a59 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -99,7 +99,7 @@ async def request( raise UnidentifiedAPIError(reason=resp) @classmethod - async def from_phone_number(cls, phone_number: str | int): + def from_phone_number(cls, phone_number: str | int): """`Classmethod` to initialize :class:`Inpost` object with phone number :param phone_number: User's Inpost phone number @@ -107,14 +107,14 @@ async def from_phone_number(cls, phone_number: str | int): if isinstance(phone_number, int): phone_number = str(phone_number) inp = cls() - await inp.set_phone_number(phone_number=phone_number) + inp.set_phone_number(phone_number=phone_number) inp._log.info(f'initialized by from_phone_number') return inp @classmethod - async def from_dict(cls, data: dict): + def from_dict(cls, data: dict): inp = cls() - await inp.set_phone_number(data['phone_number']) + inp.set_phone_number(data['phone_number']) inp.sms_code = data['sms_code'] inp.auth_token = data['auth_token'] inp.refr_token = data['refr_token'] @@ -122,7 +122,7 @@ async def from_dict(cls, data: dict): inp._log.info(f'initialized by from_dict') return inp - async def set_phone_number(self, phone_number: str | int) -> bool: + def set_phone_number(self, phone_number: str | int) -> bool: """Set :class:`Inpost` phone number required for verification :param phone_number: User's Inpost phone number @@ -847,7 +847,7 @@ async def remove_friend(self, uuid: str | None, name: str | None, phone_number: :raises UnidentifiedAPIError: Unexpected thing happened :raises ValueError: Name length exceeds 20 characters""" - self._log.info(f'adding user friend') + self._log.info(f'removing user friend') if not self.auth_token: self._log.debug(f'authorization token missing') diff --git a/pyproject.toml b/pyproject.toml index 55d577e..df62f9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.2" +version = "0.1.3" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] From fc05f893dac890015112fa33a7e6575ebd2dd6c2 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 17 Jun 2023 20:23:28 +0200 Subject: [PATCH 03/28] CI/CD config --- .darglint | 4 ++++ .flake8 | 5 +++++ .github/workflows/lint.yml | 17 +++++++++++++++++ .github/workflows/todos.yml | 21 +++++++++++++++++++++ .pre-commit-config.yaml | 32 ++++++++++++++++++++++++++++++++ pyproject.toml | 26 +++++++++++++++++++++++++- requirements.txt | 4 ---- 7 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 .darglint create mode 100644 .flake8 create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/todos.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 requirements.txt diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..ccd7240 --- /dev/null +++ b/.darglint @@ -0,0 +1,4 @@ +[darglint] +docstring_style=sphinx +ignore=DAR003,DAR402 +strictness=full \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..39eccd5 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 120 +max-complexity = 7 +select = B,C,E,F,W,T4,B9 +ignore = E501, E731, W503, F401, F403 \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..2da86e3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: Code Quality + +on: + pull_request: + branches: [master] + + push: + branches: [master] + +jobs: + pre-commit: + name: Linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4.6.1 + - uses: pre-commit/action@v3.0.0 \ No newline at end of file diff --git a/.github/workflows/todos.yml b/.github/workflows/todos.yml new file mode 100644 index 0000000..6f3ef46 --- /dev/null +++ b/.github/workflows/todos.yml @@ -0,0 +1,21 @@ +name: Create issues from todos + +on: + push: + branches: + - dev + +jobs: + todos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: wvffle/todo-actions@master + with: + collect_commit_msg: 'ci: collect new todos' + reference_commit_msg: 'ci: add references to new todos' + reference_commit_body: 'New issues: %s' + env: + GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }} + TODO_ACTIONS_MONGO_URL: ${{ secrets.TODO_ACTIONS_MONGO_URL }} \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b5700de --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-pytest +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.3.0 + hooks: + - id: mypy + exclude: ^docs/conf.py +- repo: https://github.com/terrencepreilly/darglint + rev: v1.8.1 + hooks: + - id: darglint +- repo: https://github.com/commitizen-tools/commitizen + rev: v3.3.0 + hooks: + - id: commitizen + stages: [commit-msg] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index df62f9c..3a40f62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.3" +version = "0.1.4" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] @@ -32,3 +32,27 @@ Pillow = "^9.4.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 120 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | tmp +)/ +''' + +[tool.isort] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +known_third_party = ["aiohttp", "music_service_async_interface", "mutagen"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 358123f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -aiohttp~=3.8.1 -arrow~=1.2.3 -qrcode~=7.3.1 -Pillow==9.4.0 From 9bf88e86a14ca9f6458d13fa3078b33210dfa696 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 13:47:26 +0200 Subject: [PATCH 04/28] added support for creating and sending parcels. sorted endpoints, repo cleanup --- .darglint | 3 +- .flake8 | 3 +- .github/workflows/lint.yml | 4 +- .pre-commit-config.yaml | 5 +- inpost/api.py | 1432 ++++++++++++++++++++------------ inpost/static/__init__.py | 178 +++- inpost/static/endpoints.py | 65 +- inpost/static/exceptions.py | 29 +- inpost/static/friends.py | 43 +- inpost/static/notifications.py | 26 +- inpost/static/parcels.py | 927 +++++++++++++++------ inpost/static/statuses.py | 240 +++--- pyproject.toml | 1 + 13 files changed, 2000 insertions(+), 956 deletions(-) diff --git a/.darglint b/.darglint index ccd7240..931b44e 100644 --- a/.darglint +++ b/.darglint @@ -1,4 +1,5 @@ [darglint] docstring_style=sphinx ignore=DAR003,DAR402 -strictness=full \ No newline at end of file +strictness=full +exclude=docs \ No newline at end of file diff --git a/.flake8 b/.flake8 index 39eccd5..6ce657e 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,5 @@ max-line-length = 120 max-complexity = 7 select = B,C,E,F,W,T4,B9 -ignore = E501, E731, W503, F401, F403 \ No newline at end of file +ignore = E501, E731, W503, F401, F403, C901 +exclude = docs \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2da86e3..e5bb3a2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,10 +2,10 @@ name: Code Quality on: pull_request: - branches: [master] + branches: [dev] push: - branches: [master] + branches: [dev] jobs: pre-commit: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b5700de..411c820 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,11 +7,13 @@ repos: rev: 23.3.0 hooks: - id: black + exclude: ^docs/ language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: - id: flake8 + exclude: ^docs/ additional_dependencies: - flake8-bugbear - flake8-comprehensions @@ -20,11 +22,12 @@ repos: rev: v1.3.0 hooks: - id: mypy - exclude: ^docs/conf.py + exclude: ^docs/ - repo: https://github.com/terrencepreilly/darglint rev: v1.8.1 hooks: - id: darglint + exclude: ^docs/ - repo: https://github.com/commitizen-tools/commitizen rev: v3.3.0 hooks: diff --git a/inpost/api.py b/inpost/api.py index c477a59..184dbdf 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -1,27 +1,94 @@ import logging from typing import List -from aiohttp import ClientSession, ClientResponse +from aiohttp import ClientResponse, ClientSession from aiohttp.typedefs import StrOrURL -from inpost.static import * +from inpost.static import ( + CompartmentExpectedStatus, + DeliveryType, + Friend, + MissingParamsError, + NoParcelError, + NotAuthenticatedError, + NotFoundError, + Parcel, + ParcelCarrierSize, + ParcelLockerSize, + ParcelPointOperations, + ParcelShipmentType, + ParcelStatus, + ParcelType, + ParcelTypeError, + PhoneNumberError, + Point, + ReAuthenticationError, + Receiver, + RefreshTokenError, + ReturnParcel, + Sender, + SentParcel, + SingleParamError, + SmsCodeError, + UnauthorizedError, + UnidentifiedAPIError, + appjson, + blik_status, + collect, + compartment_open, + compartment_status, + confirm_sms_code, + create, + create_blik, + friends, + friendship, + logout, + multi, + open_sent, + parcel, + parcel_points, + parcel_prices, + parcels, + refresh_token, + returns, + send_sms_code, + sent, + shared, + status_sent, + terminate_collect_session, + validate_friendship, + validate_sent, +) class Inpost: """Python representation of an Inpost app. Essentially implements methods to manage all incoming parcels""" - def __init__(self): - """Constructor method""" - self.phone_number: str | None = None + def __init__(self, phone_number): + """Constructor method + + :param phone_number: phone number + :type phone_number: str + :raises PhoneNumberError: Wrong phone number format or is not digit""" + if isinstance(phone_number, int): + phone_number = str(phone_number) + + if not (len(phone_number) == 9 and phone_number.isdigit()): + raise PhoneNumberError(f"Wrong phone number format: {phone_number} (should be 9 digits)") + + self.phone_number: str = phone_number self.sms_code: str | None = None self.auth_token: str | None = None self.refr_token: str | None = None self.sess: ClientSession = ClientSession() - self.parcel: Parcel | None = None - self._log: logging.Logger | None = None + # self.parcel: Parcel | SentParcel | ReturnParcel | None = None + self._log = logging.getLogger(f"{self.__class__.__name__}.{phone_number}") + + self._log.setLevel(level=logging.DEBUG) + self._log.info(f"initialized inpost object with phone number {phone_number}") def __repr__(self): - return f'{self.__class__.__name__}(phone_number={self.phone_number})' + return f"{self.__class__.__name__}(phone_number={self.phone_number})" async def __aenter__(self): return self @@ -30,142 +97,114 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return self.logout() async def request( - self, - method: str, - action: str, - url: StrOrURL, - auth: bool = True, - headers: dict | None = None, - data: dict | None = None, - autorefresh: bool = True, - **kwargs, + self, + method: str, + action: str, + url: StrOrURL, + auth: bool = True, + headers: dict | None = None, + params: dict | None = None, + data: dict | None = None, + autorefresh: bool = True, + **kwargs, ) -> ClientResponse: - """Validates sent data and fetches required compartment properties for opening - :param method: HTTP method of request - :type method: str - :param action: action type (e.g. "get parcels" or "send sms code") for logging purposes - :type action: str - :param url: HTTP request url - :type url: aiohttp.typedefs.StrOrURL - :param auth: True if request should contain authorization header else False - :type auth: bool - :param headers: Additional headers for HTTP request (don't put authorization header here) - :type headers: dict | None - :param data: Data to be sent in HTTP request - :type data: dict | None - :param autorefresh: Let method automatically try to refresh token if API returns HTTP 401 Unauthorized status code - :type autorefresh: bool - :return: response of http request - :rtype: aiohttp.ClientResponse - :raises UnauthorizedError: User not authenticated in inpost service - :raises NotFoundError: URL not found - :raises UnidentifiedAPIError: Unexpected things happened""" + :param method: HTTP method of request + :type method: str + :param action: action type (e.g. "get parcels" or "send sms code") for logging purposes + :type action: str + :param url: HTTP request url + :type url: StrOrURL + :param auth: True if request should contain authorization header else False + :type auth: bool + :param headers: Additional headers for HTTP request (don't put authorization header here) + :type headers: dict | None + :param params: dict of parameters to get method + :type params: dict | None + :param data: Data to be sent in HTTP request + :type data: dict | None + :param autorefresh: method automatically try to refresh token if API returns HTTP 401 Unauthorized status code + :type autorefresh: bool + :param kwargs: additional keyword arguments + :return: response of http request + :rtype: ClientResponse + :raises UnauthorizedError: User not authenticated in inpost service + :raises NotFoundError: URL not found + :raises UnidentifiedAPIError: Unexpected things happened + :raises ValueError: Doubled authorization header in request""" if auth and headers: - if 'Authorization' in headers: - raise ValueError('Both auth==True and Authorization in additional headers') + if "Authorization" in headers: + raise ValueError("Both auth==True and Authorization in additional headers") headers_ = {} if headers is None else headers if auth: - headers_.update( - {'Authorization': self.auth_token} - ) + headers_.update({"Authorization": self.auth_token}) - resp = await self.sess.request(method, url, headers=headers_, json=data, **kwargs) + resp = await self.sess.request(method, url, headers=headers_, params=params, json=data, **kwargs) if autorefresh and resp.status == 401: await self.refresh_token() - headers_.update( - {'Authorization': self.auth_token} - ) - resp = await self.sess.request(method, url, headers=headers_, json=data, **kwargs) + headers_.update({"Authorization": self.auth_token}) + resp = await self.sess.request(method, url, headers=headers_, params=params, json=data, **kwargs) match resp.status: case 200: - self._log.debug(f'{action} done') + self._log.debug(f"{action} done") return resp case 401: - self._log.error(f'could not perform {action}, unauthorized') + self._log.error(f"could not perform {action}, unauthorized") raise UnauthorizedError(reason=resp) case 404: - self._log.error(f'could not perform {action}, not found') + self._log.error(f"could not perform {action}, not found") raise NotFoundError(reason=resp) case _: - self._log.error(f'could not perform {action}, unhandled status') + self._log.error(f"could not perform {action}, unhandled status") raise UnidentifiedAPIError(reason=resp) @classmethod - def from_phone_number(cls, phone_number: str | int): - """`Classmethod` to initialize :class:`Inpost` object with phone number - - :param phone_number: User's Inpost phone number - :type phone_number: str | int""" - if isinstance(phone_number, int): - phone_number = str(phone_number) - inp = cls() - inp.set_phone_number(phone_number=phone_number) - inp._log.info(f'initialized by from_phone_number') - return inp - - @classmethod - def from_dict(cls, data: dict): - inp = cls() - inp.set_phone_number(data['phone_number']) - inp.sms_code = data['sms_code'] - inp.auth_token = data['auth_token'] - inp.refr_token = data['refr_token'] - - inp._log.info(f'initialized by from_dict') + def from_dict(cls, data: dict) -> "Inpost": + """`Classmethod` to initialize :class:`Inpost` object with dict. + Should be used when retrieving configuration from database. + + :param data: User's Inpost data (e.g. phone_number, sms_code, auth_token, refr_token) + :type data: dict + :return: Inpost object from provided dict + :rtype: Inpost""" + inp = cls(phone_number=data["phone_number"]) + inp.sms_code = data["sms_code"] + inp.auth_token = data["auth_token"] + inp.refr_token = data["refr_token"] + + inp._log.info("initialized by from_dict") return inp - def set_phone_number(self, phone_number: str | int) -> bool: - """Set :class:`Inpost` phone number required for verification - - :param phone_number: User's Inpost phone number - :type phone_number: str | int - :return: True if `Inpost.phone_number` is set - :rtype: bool - :raises PhoneNumberError: Wrong phone number format""" - if isinstance(phone_number, int): - phone_number = str(phone_number) - - if len(phone_number) == 9 and phone_number.isdigit(): - self._log = logging.getLogger(f'{__class__.__name__}.{phone_number}') - self._log.setLevel(level=logging.DEBUG) - self._log.info(f'initializing inpost object with phone number {phone_number}') - self.phone_number = phone_number - return True - - raise PhoneNumberError(f'Wrong phone number format: {phone_number} (should be 9 digits)') - async def send_sms_code(self) -> bool: """Sends sms code to `Inpost.phone_number` - :return: True if sms code sent + :return: True if sms code is sent else False :rtype: bool :raises PhoneNumberError: Missing phone number - :raises UnauthorizedError: Unauthorized access to inpost services, - :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected things happened """ if not self.phone_number: # can't log it cuz if there's no phone number no logger initialized @shrug - raise PhoneNumberError('Phone number missing') + raise PhoneNumberError("Phone number missing") - self._log.info(f'sending sms code') + self._log.info("sending sms code") - resp = await self.request(method='post', - action='send sms code', - url=send_sms_code, - auth=False, - headers=None, - data={'phoneNumber': f'{self.phone_number}'}, - autorefresh=False) + resp = await self.request( + method="post", + action="send sms code", + url=send_sms_code, + auth=False, + headers=None, + data={"phoneNumber": f"{self.phone_number}"}, + autorefresh=False, + ) - return True if resp.status == 200 else False + return resp.status == 200 async def confirm_sms_code(self, sms_code: str | int) -> bool: """Confirms sms code sent to `Inpost.phone_number` and fetches tokens @@ -174,44 +213,40 @@ async def confirm_sms_code(self, sms_code: str | int) -> bool: :type sms_code: str | int :return: True if sms code gets confirmed and tokens fetched :rtype: bool + :raises PhoneNumberError: Missing phone number :raises SmsCodeError: Wrong sms code format - :raises UnauthorizedError: Unauthorized access to inpost services, - :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened """ if not self.phone_number: # can't log it cuz if there's no phone number no logger initialized @shrug - raise PhoneNumberError('Phone number missing') + raise PhoneNumberError("Phone number missing") if isinstance(sms_code, int): sms_code = str(sms_code) if len(sms_code) != 6 or not sms_code.isdigit(): - raise SmsCodeError(reason=f'Wrong sms code format: {sms_code} (should be 6 digits)') - - self._log.info(f'confirming sms code') - - resp = await self.request(method='post', - action='confirm sms code', - url=confirm_sms_code, - auth=False, - headers=appjson, - data={ - "phoneNumber": self.phone_number, - "smsCode": sms_code, - "phoneOS": "Android" - }, - autorefresh=False) + raise SmsCodeError(reason=f"Wrong sms code format: {sms_code} (should be 6 digits)") + + self._log.info("confirming sms code") + + resp = await self.request( + method="post", + action="confirm sms code", + url=confirm_sms_code, + auth=False, + headers=appjson, + data={"phoneNumber": self.phone_number, "smsCode": sms_code, "phoneOS": "Android"}, + autorefresh=False, + ) if resp.status == 200: auth_token_data = await resp.json() self.sms_code = sms_code - self.refr_token = auth_token_data['refreshToken'] - self.auth_token = auth_token_data['authToken'] - self._log.debug(f'sms code confirmed') + self.refr_token = auth_token_data["refreshToken"] + self.auth_token = auth_token_data["authToken"] + self._log.debug("sms code confirmed") return True - else: - return False + + return False async def refresh_token(self) -> bool: """Refreshes authorization token using refresh token @@ -219,38 +254,38 @@ async def refresh_token(self) -> bool: :return: True if Inpost.auth_token gets refreshed :rtype: bool :raises RefreshTokenError: Missing refresh token + :raises ReAuthenticationError: Re-authentication needed :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened """ - self._log.info(f'refreshing token') + self._log.info("refreshing token") if not self.refr_token: - self._log.error(f'refresh token missing') - raise RefreshTokenError(reason='Refresh token missing') - - resp = await self.request(method='post', - action='refresh token', - url=refresh_token, - auth=False, - headers=appjson, - data={ - "refreshToken": self.refr_token, - "phoneOS": "Android" - }, - autorefresh=False) + self._log.error("refresh token missing") + raise RefreshTokenError(reason="Refresh token missing") + + resp = await self.request( + method="post", + action="refresh token", + url=refresh_token, + auth=False, + headers=appjson, + data={"refreshToken": self.refr_token, "phoneOS": "Android"}, + autorefresh=False, + ) if resp.status == 200: confirmation = await resp.json() - if confirmation['reauthenticationRequired']: - self._log.error(f'could not refresh token, log in again') - raise ReAuthenticationError(reason='You need to log in again!') + if confirmation["reauthenticationRequired"]: + self._log.error("could not refresh token, log in again") + raise ReAuthenticationError(reason="You need to log in again!") - self.auth_token = confirmation['authToken'] - self._log.debug(f'token refreshed') + self.auth_token = confirmation["authToken"] + self._log.debug("token refreshed") return True - else: - return False + + return False async def logout(self) -> bool: """Logouts user from inpost api service @@ -261,53 +296,53 @@ async def logout(self) -> bool: :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'logging out') + self._log.info("logging out") if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") - resp = await self.request(method='post', - action='logout', - url=logout, - auth=True, - headers=None, - data=None, - autorefresh=True) + resp = await self.request( + method="post", action="logout", url=logout, auth=True, headers=None, data=None, autorefresh=True + ) if resp.status == 200: - self.phone_number = None + self.phone_number = "" self.refr_token = None self.auth_token = None self.sms_code = None - self._log.debug('logged out') + self._log.debug("logged out") return True - else: - return False + + return False async def disconnect(self) -> bool: """Simplified method to logout and close user's session :return: True if user is logged out and session is closed else False :raises NotAuthenticatedError: User not authenticated in inpost service""" - self._log.info(f'disconnecting') + self._log.info("disconnecting") if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") if await self.logout(): await self.sess.close() - self._log.debug(f'disconnected') + self._log.debug("disconnected") return True - self._log.error('could not disconnect') + self._log.error("could not disconnect") return False - async def get_parcel(self, shipment_number: int | str, parse=False) -> dict | Parcel: + async def get_parcel( + self, shipment_number: int | str, parcel_type: ParcelType = ParcelType.TRACKED, parse=False + ) -> dict | Parcel | SentParcel | ReturnParcel: # TODO: make selector of which parcel we are looking for """Fetches single parcel from provided shipment number :param shipment_number: Parcel's shipment number :type shipment_number: int | str + :param parcel_type: Parcel type (e.g. received, sent, returned) + :type parcel_type: ParcelType :param parse: if set to True method will return :class:`Parcel` else :class:`dict` :type parse: bool :return: Fetched parcel data @@ -315,32 +350,62 @@ async def get_parcel(self, shipment_number: int | str, parse=False) -> dict | Pa :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'getting parcel with shipment number: {shipment_number}') + :raises UnidentifiedAPIError: Unexpected thing happened + :raises ParcelTypeError: Unknown parcel type selected + """ + self._log.info(f"getting parcel with shipment number: {shipment_number}") if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") - resp = await self.request(method='get', - action=f"parcel with shipment number {shipment_number}", - url=f"{parcel}{shipment_number}", - auth=True, - headers=None, - data=None, - autorefresh=True) + match parcel_type: + case ParcelType.TRACKED: + self._log.debug(f"getting parcel type {parcel_type}") + url = parcels + case ParcelType.SENT: + self._log.debug(f"getting parcel type {parcel_type}") + url = sent + case ParcelType.RETURNS: + self._log.debug(f"getting parcel type {parcel_type}") + url = returns + case _: + self._log.error(f"wrong parcel type {parcel_type}") + raise ParcelTypeError(reason=f"Unknown parcel type: {parcel_type}") + + resp = await self.request( + method="get", + action=f"parcel with shipment number {shipment_number}", + url=f"{url}{shipment_number}", + auth=True, + headers=None, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'parcel with shipment number {shipment_number} received') - return await resp.json() if not parse else Parcel(await resp.json(), logger=self._log) - - async def get_parcels(self, - parcel_type: ParcelType = ParcelType.TRACKED, - status: ParcelStatus | List[ParcelStatus] | None = None, - pickup_point: str | List[str] | None = None, - shipment_type: ParcelShipmentType | List[ParcelShipmentType] | None = None, - parcel_size: ParcelLockerSize | ParcelCarrierSize | None = None, - parse: bool = False) -> List[dict] | List[Parcel]: + self._log.debug(f"parcel with shipment number {shipment_number} received") + match parcel_type: + case ParcelType.TRACKED: + return await resp.json() if not parse else Parcel(await resp.json(), logger=self._log) + case ParcelType.SENT: + return await resp.json() if not parse else SentParcel(await resp.json(), logger=self._log) + case ParcelType.RETURNS: + return await resp.json() if not parse else ReturnParcel(await resp.json(), logger=self._log) + case _: + self._log.error(f"wrong parcel type {parcel_type}") + raise ParcelTypeError(reason=f"Unknown parcel type: {parcel_type}") + + raise UnidentifiedAPIError(reason=resp) + + async def get_parcels( + self, + parcel_type: ParcelType = ParcelType.TRACKED, + status: ParcelStatus | List[ParcelStatus] | None = None, + pickup_point: str | List[str] | None = None, + shipment_type: ParcelShipmentType | List[ParcelShipmentType] | None = None, + parse: bool = False, + ) -> List[dict] | List[Parcel]: """Fetches all available parcels for set `Inpost.phone_number` and optionally filters them :param parcel_type: Parcel type (e.g. received, sent, returned) @@ -351,8 +416,6 @@ async def get_parcels(self, :type pickup_point: str | list[str] | None :param shipment_type: Fetched parcels have to be shipped that way :type shipment_type: ParcelShipmentType | list[ParcelShipmentType] | None - :param parcel_size: Fetched parcels have to be this size - :type parcel_size: ParcelLockerSize | ParcelCarrierSize | None :param parse: if set to True method will return list[:class:`Parcel`] else list[:class:`dict`] :type parse: bool :return: fetched parcels data @@ -362,97 +425,90 @@ async def get_parcels(self, :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info('getting parcels') + self._log.info("getting parcels") if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - if not isinstance(parcel_type, ParcelType): - self._log.error(f'wrong parcel type {parcel_type}') - raise ParcelTypeError(reason=f'Unknown parcel type: {parcel_type}') + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") match parcel_type: case ParcelType.TRACKED: - self._log.debug(f'getting parcel type {parcel_type}') + self._log.debug(f"getting parcel type {parcel_type}") url = parcels case ParcelType.SENT: - self._log.debug(f'getting parcel type {parcel_type}') + self._log.debug(f"getting parcel type {parcel_type}") url = sent case ParcelType.RETURNS: - self._log.debug(f'getting parcel type {parcel_type}') + self._log.debug(f"getting parcel type {parcel_type}") url = returns case _: - self._log.error(f'wrong parcel type {parcel_type}') - raise ParcelTypeError(reason=f'Unknown parcel type: {parcel_type}') + self._log.error(f"wrong parcel type {parcel_type}") + raise ParcelTypeError(reason=f"Unknown parcel type: {parcel_type}") - resp = await self.request(method='get', - action='get parcels', - url=url, - auth=True, - headers=None, - data=None, - autorefresh=True) + async with await self.request( + method="get", action="get parcels", url=url, auth=True, headers=None, data=None, autorefresh=True + ) as resp: + if resp.status != 200: + self._log.debug(f"Could not get parcels due to HTTP error {resp.status}") + raise UnidentifiedAPIError(reason=resp) - if resp.status == 200: - self._log.debug(f'received {parcel_type} parcels') - _parcels = (await resp.json())['parcels'] + self._log.debug(f"received {parcel_type} parcels") + _parcels = (await resp.json())["parcels"] if status is not None: if isinstance(status, ParcelStatus): status = [status] - _parcels = (_parcel for _parcel in _parcels if ParcelStatus[_parcel['status']] in status) + _parcels = (_parcel for _parcel in _parcels if ParcelStatus[_parcel.get("status")] in status) if pickup_point is not None: if isinstance(pickup_point, str): pickup_point = [pickup_point] - _parcels = (_parcel for _parcel in _parcels if - _parcel['pickUpPoint']['name'] in pickup_point) + _parcels = (_parcel for _parcel in _parcels if _parcel["pickUpPoint"]["name"] in pickup_point) if shipment_type is not None: if isinstance(shipment_type, ParcelShipmentType): shipment_type = [shipment_type] - _parcels = (_parcel for _parcel in _parcels if - ParcelShipmentType[_parcel['shipmentType']] in shipment_type) - - if parcel_size is not None: - if isinstance(parcel_size, ParcelCarrierSize): - parcel_size = [parcel_size] - - _parcels = (_parcel for _parcel in _parcels if - ParcelCarrierSize[_parcel['parcelSize']] in parcel_size) - - if isinstance(parcel_size, ParcelLockerSize): - parcel_size = [parcel_size] - - _parcels = (_parcel for _parcel in _parcels if - ParcelLockerSize[_parcel['parcelSize']] in parcel_size) + _parcels = ( + _parcel + for _parcel in _parcels + if ParcelShipmentType[_parcel["shipmentType"]] in list(shipment_type) + ) return list(_parcels) if not parse else [Parcel(parcel_data=data, logger=self._log) for data in _parcels] - async def get_multi_compartment(self, multi_uuid: str | int, parse: bool = False) -> dict | List[Parcel]: + async def get_multi_compartment( + self, multi_uuid: str | int, parse: bool = False + ) -> dict | List[Parcel]: # TODO: do docs if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='get', - action=f"parcel with multicompartment uuid {multi_uuid}", - url=f"{multi}{multi_uuid}", - auth=True, - headers=None, - data=None, - autorefresh=True) + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="get", + action=f"parcel with multi-compartment uuid {multi_uuid}", + url=f"{multi}{multi_uuid}", + auth=True, + headers=None, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'parcel with multicompartment uuid {multi_uuid} received') - return (await resp.json())['parcels'] if not parse else [Parcel(data, logger=self._log) for data in - (await resp.json())['parcels']] + self._log.debug(f"parcel with multi-compartment uuid {multi_uuid} received") + return ( + (await resp.json())["parcels"] + if not parse + else [Parcel(data, logger=self._log) for data in (await resp.json())["parcels"]] + ) + + raise UnidentifiedAPIError(reason=resp) - async def collect_compartment_properties(self, shipment_number: str | int | None = None, - parcel_obj: Parcel | None = None, location: dict | None = None) -> bool: + async def collect_compartment_properties( + self, shipment_number: str | int | None = None, parcel_obj: Parcel | None = None, location: dict | None = None + ) -> Parcel: """Validates sent data and fetches required compartment properties for opening :param shipment_number: Parcel's shipment number @@ -462,77 +518,95 @@ async def collect_compartment_properties(self, shipment_number: str | int | None :param location: Fetched parcels have to be picked from this pickup point (e.g. `GXO05M`) :type location: dict | None :return: fetched parcels data - :rtype: bool + :rtype: Parcel + :raises MissingParamsError: none of required query and location params are filled :raises SingleParamError: Fields shipment_number and parcel_obj filled in but only one of them is required :raises NotAuthenticatedError: User not authenticated in inpost service + :raises NoParcelError: Could not get parcel object from provided data :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened .. warning:: you must fill in only one parameter - shipment_number or parcel_obj!""" - if shipment_number and parcel_obj: - self._log.error(f'shipment_number and parcel_obj filled in') - raise SingleParamError(reason='Fields shipment_number and parcel_obj filled in! Choose one!') - - if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - if shipment_number is not None and parcel_obj is None: - self._log.debug(f'parcel_obj not provided, getting from shipment number {shipment_number}') - parcel_obj = await self.get_parcel(shipment_number=shipment_number, parse=True) - - self._log.info(f'collecting compartment properties for {parcel_obj.shipment_number}') - - resp = await self.request(method='post', - action='collect compartment properties', - url=collect, - auth=True, - headers=None, - data={ - 'parcel': parcel_obj.compartment_open_data, - 'geoPoint': location if location is not None else parcel_obj.mocked_location - }, - autorefresh=True) + parcel_obj_: Parcel | None = None + if shipment_number is None is parcel_obj: + self._log.error("none of shipment_number and parcel_obj filled in") + raise MissingParamsError(reason="None of params are filled (one required)") + elif shipment_number is not None is not parcel_obj: + self._log.error("shipment_number and parcel_obj filled in") + raise SingleParamError(reason="Fields shipment_number and parcel_obj filled in! Choose one!") + elif shipment_number: + self._log.debug(f"parcel_obj not provided, getting from shipment number {shipment_number}") + parcel_obj_ = await self.get_parcel(shipment_number=shipment_number, parse=True) + elif parcel_obj: + parcel_obj_ = parcel_obj + + if parcel_obj_ is None: + raise NoParcelError(reason="Could not obtain desired parcel!") + + self._log.info(f"collecting compartment properties for {parcel_obj_.shipment_number}") + + resp = await self.request( + method="post", + action="collect compartment properties", + url=collect, + auth=True, + headers=None, + data={ + "parcel": parcel_obj_.compartment_open_data, + "geoPoint": location if location is not None else parcel_obj_.mocked_location, + }, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'collected compartment properties for {parcel_obj.shipment_number}') - parcel_obj.compartment_properties = await resp.json() - self.parcel = parcel_obj - return True + self._log.debug(f"collected compartment properties for {parcel_obj_.shipment_number}") + parcel_obj_.compartment_properties = await resp.json() + return parcel_obj_ - async def open_compartment(self) -> bool: + raise UnidentifiedAPIError(reason=resp) + + async def open_compartment(self, parcel_obj: Parcel) -> bool: """Opens compartment for `Inpost.parcel` object + :param parcel_obj: Parcel object + :type parcel_obj: Parcel :return: True if compartment gets opened :rtype: bool :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'opening compartment for {self.parcel.shipment_number}') + self._log.info(f"opening compartment for {parcel_obj.shipment_number}") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='post', - action=f"open compartment for {self.parcel.shipment_number}", - url=compartment_open, - auth=True, - headers=None, - data={'sessionUuid': self.parcel.compartment_properties.session_uuid}, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action=f"open compartment for {parcel_obj.shipment_number}", + url=compartment_open, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'opened compartment for {self.parcel.shipment_number}') + self._log.debug(f"opened compartment for {parcel_obj.shipment_number}") return True - async def check_compartment_status(self, - expected_status: CompartmentExpectedStatus = CompartmentExpectedStatus.OPENED) -> bool: + raise UnidentifiedAPIError(reason=resp) + + async def check_compartment_status( + self, parcel_obj: Parcel, expected_status: CompartmentExpectedStatus = CompartmentExpectedStatus.OPENED + ) -> bool: """Checks and compare compartment status (e.g. opened, closed) with expected status + :param parcel_obj: Parcel object + :type parcel_obj: Parcel :param expected_status: Compartment expected status :type expected_status: CompartmentExpectedStatus :return: True if actual status equals expected status else False @@ -541,62 +615,67 @@ async def check_compartment_status(self, :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'checking compartment status for {self.parcel.shipment_number}') + self._log.info(f"checking compartment status for {parcel_obj.shipment_number}") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - if not self.parcel: - self._log.debug(f'parcel missing') - raise NoParcelError(reason='Parcel is not set') - - resp = await self.request(method='post', - action='check compartment status', - url=compartment_status, - auth=True, - headers=None, - data={ - 'sessionUuid': self.parcel.compartment_properties.session_uuid, - 'expectedStatus': expected_status.name - }, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action="check compartment status", + url=compartment_status, + auth=True, + headers=None, + data={ + "sessionUuid": parcel_obj.compartment_properties.session_uuid, + "expectedStatus": expected_status.name, + }, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'checked compartment status for {self.parcel.shipment_number}') - self.parcel.compartment_status = (await resp.json())['status'] - return CompartmentExpectedStatus[(await resp.json())['status']] == expected_status + self._log.debug(f"checked compartment status for {parcel_obj.shipment_number}") + parcel_obj.compartment_status = (await resp.json())["status"] + return CompartmentExpectedStatus[(await resp.json())["status"]] == expected_status + + raise UnidentifiedAPIError(reason=resp) - async def terminate_collect_session(self) -> bool: + async def terminate_collect_session(self, parcel_obj: Parcel) -> bool: """Terminates user session in inpost api service + :param parcel_obj: Parcel object + :type parcel_obj: Parcel :return: True if the user session is terminated :rtype: bool :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'terminating collect session for {self.parcel.shipment_number}') + self._log.info(f"terminating collect session for {parcel_obj.shipment_number}") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='post', - action='terminate collect session', - url=terminate_collect_session, - auth=True, - headers=None, - data={ - 'sessionUuid': self.parcel.compartment_properties.session_uuid - }, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action="terminate collect session", + url=terminate_collect_session, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'terminated collect session for {self.parcel.shipment_number}') + self._log.debug(f"terminated collect session for {parcel_obj.shipment_number}") return True - async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | None = None, - location: dict | None = None) -> bool: + raise UnidentifiedAPIError(reason=resp) + + async def collect( + self, shipment_number: str | None = None, parcel_obj: Parcel | None = None, location: dict | None = None + ) -> bool: """Simplified method to open compartment :param shipment_number: Parcel's shipment number @@ -609,6 +688,7 @@ async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | :rtype: bool :raises SingleParamError: Fields shipment_number and parcel_obj filled in but only one of them is required :raises NotAuthenticatedError: User not authenticated in inpost service + :raises NoParcelError: Could not get parcel object from provided data :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened @@ -616,66 +696,392 @@ async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | .. warning:: you must fill in only one parameter - shipment_number or parcel_obj!""" if shipment_number and parcel_obj: - self._log.error(f'shipment_number and parcel_obj filled in') - raise SingleParamError(reason='Fields shipment_number and parcel_obj filled! Choose one!') + self._log.error("shipment_number and parcel_obj filled in") + raise SingleParamError(reason="Fields shipment_number and parcel_obj filled! Choose one!") if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") if shipment_number is not None and parcel_obj is None: parcel_obj = await self.get_parcel(shipment_number=shipment_number, parse=True) - self._log.info(f'collecting parcel with shipment number {parcel_obj.shipment_number}') + if parcel_obj is None: + raise NoParcelError(reason="Could not obtain desired parcel!") + + self._log.info(f"collecting parcel with shipment number {parcel_obj.shipment_number}") if await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): - if await self.open_compartment(): - if await self.check_compartment_status(): + if await self.open_compartment(parcel_obj=parcel_obj): + if await self.check_compartment_status(parcel_obj=parcel_obj): return True return False - async def close_compartment(self) -> bool: + async def close_compartment(self, parcel_obj: Parcel) -> bool: """Checks whether actual compartment status and expected one matches then notifies inpost api that compartment is closed + :param parcel_obj: Parcel object + :type parcel_obj: Parcel :return: True if compartment status is closed and successfully terminates user's session else False :rtype: bool""" - self._log.info(f'closing compartment for {self.parcel.shipment_number}') + self._log.info(f"closing compartment for {parcel_obj.shipment_number}") - if await self.check_compartment_status(expected_status=CompartmentExpectedStatus.CLOSED): - if await self.terminate_collect_session(): + if await self.check_compartment_status(expected_status=CompartmentExpectedStatus.CLOSED, parcel_obj=parcel_obj): + if await self.terminate_collect_session(parcel_obj=parcel_obj): return True return False - async def reopen_compartment(self) -> bool: + async def reopen_compartment(self, parcel_obj: Parcel) -> bool: """Reopens compartment for `Inpost.parcel` object + :param parcel_obj: Parcel object + :type parcel_obj: Parcel :return: True if compartment gets reopened :rtype: bool :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'reopening compartment for {self.parcel.shipment_number}') + self._log.info(f"reopening compartment for {parcel_obj.shipment_number}") + + if not self.auth_token: + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action=f"reopen compartment for {parcel_obj.shipment_number}", + url=compartment_open, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"opened compartment for {parcel_obj.shipment_number}") + return True + + raise UnidentifiedAPIError(reason=resp) + + async def get_parcel_points( + self, + query: str | None = None, + latitude: float | None = None, + longitude: float | None = None, + per_page: int = 1000, + operation: ParcelPointOperations = ParcelPointOperations.CREATE, + parse: bool = True, + ) -> dict | List[Point]: + """Fetches prices for inpost services + + :param query: parcel point search query (e.g. GXO05M) + :type query: str | None + :param latitude: latitude of place we are looking for nearby parcel points + :type latitude: float | None + :param longitude: longitude of place we are looking for nearby parcel points + :type longitude: float | None + :param per_page: number of parcel points we would like to get from query, defaults to 1000 + :type per_page: int + :param operation: operation you want to perform (e.g. CREATE, SEND) + :type operation: ParcelPointOperations + :param parse: parse output or not, defaults to True + :type parse: bool + :return: :class:`dict` of prices for inpost services + :rtype: dict + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises SingleParamError: query and location params filled, but only one is required + :raises MissingParamsError: none of required query and location params are filled + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info("getting parcel prices") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + if query is not None and (latitude is not None and longitude is not None): + self._log.debug("query and location provided") + raise SingleParamError(reason="Fields query and location filled! Chose one!") - resp = await self.request(method='post', - action=f"reopen compartment for {self.parcel.shipment_number}", - url=compartment_open, - auth=True, - headers=None, - data={'sessionUuid': self.parcel.compartment_properties.session_uuid}, - autorefresh=True) + _params = {"filter": operation.value, "perPage": per_page} + if query is not None: + _params.update({"query": query}) + elif latitude is not None and longitude is not None: + _params.update({"relative_point": f"{latitude},{longitude}"}) + else: + raise MissingParamsError(reason="None of params are filled (one required)") + + resp = await self.request( + method="get", + action="get parcel points", + url=parcel_points, + auth=True, + headers=None, + params=_params, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'opened compartment for {self.parcel.shipment_number}') + self._log.debug("got parcel prices") + return ( + await resp.json() + if not parse + else [Point(point_data=point, logger=self._log) for point in (await resp.json())["points"]] + ) + + raise UnidentifiedAPIError(reason=resp) + + async def blik_status(self) -> bool: # TODO: do docs + if not self.auth_token: + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + self._log.info("checking if user has opened blik session") + + resp = await self.request( + method="get", action="check user blik session", url=blik_status, auth=True, headers=None, autorefresh=True + ) + + if resp.status == 200 and not (await resp.json())["active"]: + self._log.debug("user has no active blik sessions") return True + return False + + async def create_parcel( + self, + delivery_type: DeliveryType, + parcel_size: ParcelLockerSize | ParcelCarrierSize, + price: float | str, + sender: Sender, + receiver: Receiver, + delivery_point: Point, + ) -> None | dict: # TODO: do docs + if not self.auth_token: + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + self._log.info("creating new parcel") + + resp = await self.request( + method="post", + action="create parcel", + url=create, + auth=True, + headers=None, + data={ + "deliveryType": delivery_type.value, + "parcelSize": parcel_size.name, + "price": price, + "sender": {"name": sender.sender_name, "email": sender.sender_email}, + "receiver": {"name": receiver.name, "email": receiver.email, "phoneNumber": receiver.phone_number}, + "deliveryPoint": {"boxMachineName": delivery_point.name}, + }, + autorefresh=True, + ) + + if resp.status == 200: + return await resp.json() + + raise UnidentifiedAPIError(reason=resp) + + async def create_blik_session( + self, amount: float | str, shipment_number: str, currency: str = "PLN" + ) -> None | dict: # TODO: do docs + if not self.auth_token: + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + self._log.info(f"creating blik session for {shipment_number}") + + resp = await self.request( + method="post", + action="create blik session", + url=create_blik, + auth=True, + headers=None, + data={ + "shipmentNumber": shipment_number, + "amount": amount, + "currency": currency, + "process": "C2X", + "paymentMethod": "CODE", + }, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"created blik session for {shipment_number}") + return await resp.json() + + raise UnidentifiedAPIError(reason=resp) + + async def validate_send( + self, + shipment_number: str | None = None, + parcel_obj: SentParcel | None = None, + location: dict | None = None, + drop_off_point: str | None = None, + ) -> SentParcel: # TODO: do docs + if not self.auth_token: + self._log.error("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + parcel_obj_: SentParcel | None = None + if shipment_number is None is parcel_obj: + self._log.error("none of shipment_number and parcel_obj filled in") + raise MissingParamsError(reason="None of params are filled (one required)") + elif shipment_number is not None is not parcel_obj: + self._log.error("shipment_number and parcel_obj filled in") + raise SingleParamError(reason="Fields shipment_number and parcel_obj filled in! Choose one!") + elif shipment_number: + self._log.debug(f"parcel_obj not provided, getting from shipment number {shipment_number}") + parcel_obj_ = await self.get_parcel( + shipment_number=shipment_number, parcel_type=ParcelType.SENT, parse=True + ) + elif parcel_obj: + parcel_obj_ = parcel_obj + + if parcel_obj_ is None: + raise NoParcelError(reason="Could not obtain parcel!") + + if parcel_obj_.drop_off_point is None: + raise ValueError("Missing drop-off point!") + + self._log.info(f"collecting compartment properties for {parcel_obj_.shipment_number}") + + resp = await self.request( + method="post", + action="validate send parcel data", + url=validate_sent, + auth=True, + headers=None, + data={ + "parcel": { + "shipmentNumber": parcel_obj_.shipment_number, + "quickSendCode": parcel_obj_.quick_send_code, + }, + "geoPoint": location, + "boxMachineName": drop_off_point if drop_off_point is not None else parcel_obj_.drop_off_point.name, + }, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"collected compartment properties for {parcel_obj_.shipment_number}") + parcel_obj_.compartment_properties = await resp.json() + return parcel_obj_ + + raise UnidentifiedAPIError(reason=resp) + + async def open_send_compartment(self, parcel_obj: Parcel) -> bool: + """Opens compartment on parcel that is being sent + + :param parcel_obj: Parcel object + :type parcel_obj: Parcel + :return: True if successfully opened compartment else False + :rtype: bool + :raises NotAuthenticatedError: User not logged in (missing auth_token) + """ + self._log.info(f"opening compartment for {parcel_obj.shipment_number}") + + if not self.auth_token: + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action=f"open send compartment for {parcel_obj.shipment_number}", + url=open_sent, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"opened send compartment for {parcel_obj.shipment_number}") + return True + + return False + + async def reopen_send_compartment(self, parcel_obj: SentParcel) -> bool: + """Reopens compartment after sending process + + :param parcel_obj: Parcel object + :type parcel_obj: SentParcel + :return: True if successfully reopened compartment else False + :rtype: bool + :raises NotAuthenticatedError: User not logged in (missing auth_token) + """ + self._log.info(f"reopening send compartment for {parcel_obj.shipment_number}") + + if not self.auth_token: + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action=f"reopen compartment for {parcel_obj.shipment_number}", + url=compartment_open, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"opened send compartment for {parcel_obj.shipment_number}") + return True + + return False + + async def check_send_compartment_status( + self, parcel_obj: SentParcel, expected_status: CompartmentExpectedStatus = CompartmentExpectedStatus.OPENED + ) -> bool: + """Checks and compare compartment status (e.g. opened, closed) with expected status + + :param parcel_obj: Parcel object + :type parcel_obj: SentParcel + :param expected_status: Compartment expected status + :type expected_status: CompartmentExpectedStatus + :return: True if actual status equals expected status else False + :rtype: bool + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnauthorizedError: Unauthorized access to inpost services, + :raises NotFoundError: Phone number not found + :raises UnidentifiedAPIError: Unexpected thing happened""" + self._log.info(f"checking compartment status for {parcel_obj.shipment_number}") + + if not self.auth_token: + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action="check send compartment status", + url=status_sent, + auth=True, + headers=None, + data={ + "sessionUuid": parcel_obj.compartment_properties.session_uuid, + "expectedStatus": expected_status.name, + }, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"checked send compartment status for {parcel_obj.shipment_number}") + parcel_obj.compartment_status = (await resp.json())["status"] + return CompartmentExpectedStatus[(await resp.json())["status"]] == expected_status + + return False + async def get_prices(self) -> dict: """Fetches prices for inpost services @@ -685,23 +1091,21 @@ async def get_prices(self) -> dict: :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'getting parcel prices') + self._log.info("getting parcel prices") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='get', - action='get prices', - url=parcel_prices, - auth=True, - headers=None, - data=None, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="get", action="get prices", url=parcel_prices, auth=True, headers=None, data=None, autorefresh=True + ) if resp.status == 200: - self._log.debug(f'got parcel prices') + self._log.debug("got parcel prices") return await resp.json() + raise UnidentifiedAPIError(reason=resp) + async def get_friends(self, parse=False) -> dict | List[Friend]: """Fetches user friends for inpost services @@ -713,50 +1117,60 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'getting friends') + self._log.info("getting friends") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='get', - action='get friends', - url=friendship, - auth=True, - headers=None, - data=None, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="get", action="get friends", url=friendship, auth=True, headers=None, data=None, autorefresh=True + ) if resp.status == 200: - self._log.debug(f'got user friends') + self._log.debug("got user friends") _friends = await resp.json() - return _friends if not parse else [Friend(friend_data=friend, logger=self._log) for friend in - _friends['friends']] + return ( + _friends + if not parse + else [Friend(friend_data=friend, logger=self._log) for friend in _friends["friends"]] + ) - async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: - self._log.info(f'getting parcel friends') + raise UnidentifiedAPIError(reason=resp) - if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: # TODO: do docs + self._log.info("getting parcel friends") - resp = await self.request(method='get', - action='get parcel friends', - url=f"{friendship}{shipment_number}", - auth=True, - headers=None, - data=None, - autorefresh=True) + if not self.auth_token: + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="get", + action="get parcel friends", + url=f"{friendship}{shipment_number}", + auth=True, + headers=None, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'got parcel friends') + self._log.debug("got parcel friends") r = await resp.json() - if 'sharedWith' in r: - return r if not parse else { - 'friends': [Friend(friend_data=friend, logger=self._log) for friend in r['friends']], - 'shared_with': [Friend(friend_data=friend, logger=self._log) for friend in r['sharedWith']]} - return r if not parse else { - 'friends': [Friend(friend_data=friend, logger=self._log) for friend in r['friends']] - } + if "sharedWith" in r: + return ( + r + if not parse + else { + "friends": [Friend(friend_data=friend, logger=self._log) for friend in r["friends"]], + "shared_with": [Friend(friend_data=friend, logger=self._log) for friend in r["sharedWith"]], + } + ) + return ( + {"friends": [Friend(friend_data=friend, logger=self._log) for friend in r["friends"]]} if parse else r + ) + + raise UnidentifiedAPIError(reason=resp) async def add_friend(self, name: str, phone_number: str | int, code: str | int, parse=False) -> dict | Friend: """Adds user friends for inpost services @@ -777,58 +1191,64 @@ async def add_friend(self, name: str, phone_number: str | int, code: str | int, :raises UnidentifiedAPIError: Unexpected thing happened :raises ValueError: Name length exceeds 20 characters""" - self._log.info(f'adding user friend') + self._log.info("adding user friend") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") if len(name) > 20: - raise ValueError(f'Name too long: {name} (over 20 characters') + raise ValueError(f"Name too long: {name} (over 20 characters") if code: if isinstance(code, int): code = str(code) - resp = await self.request(method='post', - action='add friend', - url=validate_friendship, - auth=True, - headers=None, - data={'invitationCode': code}, - autorefresh=True) + resp = await self.request( + method="post", + action="add friend", + url=validate_friendship, + auth=True, + headers=None, + data={"invitationCode": code}, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'added user friend') + self._log.debug("added user friend") return await resp.json() if not parse else Friend(await resp.json(), logger=self._log) else: if isinstance(phone_number, int): phone_number = str(phone_number) - resp = await self.request(method='post', - action='add friend', - url=friendship, - auth=True, - headers=None, - data={ - 'phoneNumber': phone_number, - 'name': name - }, - autorefresh=True) + resp = await self.request( + method="post", + action="add friend", + url=friendship, + auth=True, + headers=None, + data={"phoneNumber": phone_number, "name": name}, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'added user friend') + self._log.debug("added user friend") r = await resp.json() - if r['status'] == "AUTO_ACCEPT": - return {'phoneNumber': phone_number, 'name': name} if not parse \ - else Friend({'phoneNumber': phone_number, 'name': name}, logger=self._log) - - elif r['status'] == "RETURN_INVITATION_CODE": + if r["status"] == "AUTO_ACCEPT": + return ( + {"phoneNumber": phone_number, "name": name} + if not parse + else Friend({"phoneNumber": phone_number, "name": name}, logger=self._log) + ) + + elif r["status"] == "RETURN_INVITATION_CODE": return r if not parse else Friend.from_invitation(invitation_data=r, logger=self._log) else: - self._log.debug(r) + self._log.warning(r) + + raise UnidentifiedAPIError(reason=resp) async def remove_friend(self, uuid: str | None, name: str | None, phone_number: str | int | None) -> bool: """Removes user friend for inpost services with specified `uuid`/`phone_number`/`name` @@ -845,42 +1265,48 @@ async def remove_friend(self, uuid: str | None, name: str | None, phone_number: :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Friend not found :raises UnidentifiedAPIError: Unexpected thing happened - :raises ValueError: Name length exceeds 20 characters""" + :raises ValueError: Name length exceeds 20 characters + :raises MissingParamsError: none of required uuid, name or phone_number params are filled""" - self._log.info(f'removing user friend') + self._log.info("removing user friend") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") if uuid is None and name is None and phone_number is None: - raise MissingParamsError(reason='None of params are filled (one required)') + raise MissingParamsError(reason="None of params are filled (one required)") if isinstance(phone_number, int): phone_number = str(phone_number) if uuid is None: f = await self.get_friends() + if not isinstance(f, dict): + return False + if phone_number: - uuid = next((friend['uuid'] for friend in f['friends'] if friend['phoneNumber'] == phone_number)) + uuid = next((friend["uuid"] for friend in f["friends"] if friend["phoneNumber"] == phone_number)) else: - uuid = next((friend['uuid'] for friend in f['friends'] if friend['name'] == name)) - - resp = await self.request(method='delete', - action='remove user friend', - url=f'{friendship}{uuid}', - auth=True, - headers=None, - data=None, - autorefresh=True) + uuid = next((friend["uuid"] for friend in f["friends"] if friend["name"] == name)) + + resp = await self.request( + method="delete", + action="remove user friend", + url=f"{friendship}{uuid}", + auth=True, + headers=None, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'removed user friend') + self._log.debug("removed user friend") return True return False - async def update_friend(self, uuid: str | None, phone_number: str | int | None, name: str): + async def update_friend(self, uuid: str | None, phone_number: str | int | None, name: str) -> bool: """Updates user friend for inpost services with specified `name` :param uuid: uuid of inpost friend to update @@ -897,38 +1323,42 @@ async def update_friend(self, uuid: str | None, phone_number: str | int | None, :raises UnidentifiedAPIError: Unexpected thing happened :raises ValueError: Name length exceeds 20 characters""" - self._log.info(f'updating user friend') + self._log.info("updating user friend") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") if len(name) > 20: - raise ValueError(f'Name too long: {name} (over 20 characters') + raise ValueError(f"Name too long: {name} (over 20 characters") if isinstance(phone_number, int): phone_number = str(phone_number) if uuid is None: - uuid = next( - (friend['uuid'] for friend in (await self.get_friends())['friends'] if - friend['phoneNumber'] == phone_number)) - - resp = await self.request(method='patch', - action='update user friend', - url=f'{friends}{uuid}', - auth=True, - headers=None, - data=None, - autorefresh=True) + obtained_friends = await self.get_friends() + if not isinstance(obtained_friends, dict): + return False + + uuid = next((friend["uuid"] for friend in obtained_friends if friend["phoneNumber"] == phone_number)) + + resp = await self.request( + method="patch", + action="update user friend", + url=f"{friends}{uuid}", + auth=True, + headers=None, + data=None, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'updated user friend') + self._log.debug("updated user friend") return True return False - async def share_parcel(self, uuid: str, shipment_number: int | str): + async def share_parcel(self, uuid: str, shipment_number: int | str) -> bool: """Shares parcel to a pre-initialized friend :param uuid: uuid of inpost friend to update @@ -938,67 +1368,29 @@ async def share_parcel(self, uuid: str, shipment_number: int | str): :return: True if parcel is shared :rtype: bool :raises NotAuthenticatedError: User not authenticated in inpost service - :raises UnauthorizedError: Unauthorized access to inpost services, - :raises NotFoundError: Parcel or friend not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ - self._log.info(f'sharing parcel: {shipment_number}') + self._log.info(f"sharing parcel: {shipment_number}") if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') - - resp = await self.request(method='post', - action=f'share parcel: {shipment_number}', - url=shared, - auth=True, - headers=None, - data={'parcels': [ - { - 'shipmentNumber': shipment_number, - 'friendUuids': [ - uuid - ] - } - ], - }, - autorefresh=True) + self._log.debug("authorization token missing") + raise NotAuthenticatedError(reason="Not logged in") + + resp = await self.request( + method="post", + action=f"share parcel: {shipment_number}", + url=shared, + auth=True, + headers=None, + data={ + "parcels": [{"shipmentNumber": shipment_number, "friendUuids": [uuid]}], + }, + autorefresh=True, + ) if resp.status == 200: - self._log.debug(f'updated user friend') + self._log.debug("updated user friend") return True return False - - # async def get_return_parcels(self): - # """Fetches all available parcels for set `Inpost.phone_number` - # - # :return: Fetched returned parcels data - # :rtype: dict | Parcel - # :raises NotAuthenticatedError: User not authenticated in inpost service - # :raises UnauthorizedError: Unauthorized access to inpost services, - # :raises NotFoundError: Phone number not found - # :raises UnidentifiedAPIError: Unexpected thing happened""" - # self._log.info(f'getting all returned parcels') - # - # if not self.auth_token: - # self._log.error(f'authorization token missing') - # raise NotAuthenticatedError(reason='Not logged in') - # - # async with await self.sess.get(url=returns, - # headers={'Authorization': self.auth_token}, - # ) as resp: - # match resp.status: - # case 200: - # self._log.debug(f'parcel with shipment number {shipment_number} received') - # return await resp.json() if not parse else Parcel(await resp.json(), logger=self._log) - # case 401: - # self._log.error(f'could not get parcel with shipment number {shipment_number}, unauthorized') - # raise UnauthorizedError(reason=resp) - # case 404: - # self._log.error(f'could not get parcel with shipment number {shipment_number}, not found') - # raise NotFoundError(reason=resp) - # case _: - # self._log.error(f'could not get parcel with shipment number {shipment_number}, unhandled status') - # - # raise UnidentifiedAPIError(reason=resp) diff --git a/inpost/static/__init__.py b/inpost/static/__init__.py index fb64578..59b44ff 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -1,15 +1,167 @@ -from .parcels import Parcel, Receiver, Sender, PickupPoint, MultiCompartment, Operations, EventLog, SharedTo, \ - QRCode, CompartmentLocation, CompartmentProperties -from .headers import appjson -from .statuses import ParcelCarrierSize, ParcelLockerSize, ParcelDeliveryType, ParcelShipmentType, \ - ParcelAdditionalInsurance, ParcelType, ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, \ - ParcelServiceName, ParcelStatus, ReturnsStatus -from .exceptions import NoParcelError, UnidentifiedParcelError, ParcelTypeError, NotAuthenticatedError, \ - ReAuthenticationError, \ - PhoneNumberError, SmsCodeError, RefreshTokenError, UnidentifiedAPIError, UserLocationError, \ - UnidentifiedError, NotFoundError, UnauthorizedError, SingleParamError, MissingParamsError -from .endpoints import login, send_sms_code, confirm_sms_code, refresh_token, parcels, parcel, collect, \ - compartment_open, compartment_status, terminate_collect_session, friendship, shared, sent, returns, parcel_prices, \ - tickets, logout, multi, validate_friendship, accept_friendship, parcel_notifications +from .endpoints import ( + accept_friendship, + blik_status, + collect, + compartment_open, + compartment_status, + confirm_sent, + confirm_sms_code, + create, + create_blik, + friendship, + login, + logout, + multi, + open_sent, + parcel, + parcel_notifications, + parcel_points, + parcel_prices, + parcels, + refresh_token, + reopen_sent, + returns, + send_sms_code, + sent, + shared, + status_sent, + terminate_collect_session, + tickets, + validate_friendship, + validate_sent, +) +from .exceptions import ( + MissingParamsError, + NoParcelError, + NotAuthenticatedError, + NotFoundError, + ParcelTypeError, + PhoneNumberError, + ReAuthenticationError, + RefreshTokenError, + SingleParamError, + SmsCodeError, + UnauthorizedError, + UnidentifiedAPIError, + UnidentifiedError, + UnidentifiedParcelError, + UserLocationError, +) from .friends import Friend +from .headers import appjson from .notifications import Notification +from .parcels import ( + CompartmentLocation, + CompartmentProperties, + EventLog, + MultiCompartment, + Operations, + Parcel, + PickupPoint, + Point, + QRCode, + Receiver, + ReturnParcel, + Sender, + SentParcel, + SharedTo, +) +from .statuses import ( + CompartmentActualStatus, + CompartmentExpectedStatus, + DeliveryType, + ParcelAdditionalInsurance, + ParcelCarrierSize, + ParcelDeliveryType, + ParcelLockerSize, + ParcelOwnership, + ParcelPointOperations, + ParcelServiceName, + ParcelShipmentType, + ParcelStatus, + ParcelType, + PaymentStatus, + PaymentType, + PointType, + ReturnsStatus, +) + +__all__ = [ + "accept_friendship", + "blik_status", + "collect", + "compartment_open", + "compartment_status", + "confirm_sent", + "confirm_sms_code", + "create", + "create_blik", + "friendship", + "login", + "logout", + "multi", + "open_sent", + "parcel", + "parcel_notifications", + "parcel_points", + "parcel_prices", + "parcels", + "refresh_token", + "reopen_sent", + "returns", + "send_sms_code", + "sent", + "shared", + "status_sent", + "terminate_collect_session", + "tickets", + "validate_friendship", + "validate_sent", + "MissingParamsError", + "NoParcelError", + "NotAuthenticatedError", + "NotFoundError", + "ParcelTypeError", + "PhoneNumberError", + "ReAuthenticationError", + "RefreshTokenError", + "SingleParamError", + "SmsCodeError", + "UnauthorizedError", + "UnidentifiedAPIError", + "UnidentifiedError", + "UnidentifiedParcelError", + "UserLocationError", + "Friend", + "appjson", + "Notification", + "CompartmentLocation", + "CompartmentProperties", + "EventLog", + "MultiCompartment", + "Parcel", + "PickupPoint", + "Point", + "QRCode", + "Receiver", + "ReturnParcel", + "Sender", + "SentParcel", + "SharedTo", + "CompartmentActualStatus", + "CompartmentExpectedStatus", + "DeliveryType", + "ParcelAdditionalInsurance", + "ParcelCarrierSize", + "ParcelDeliveryType", + "ParcelLockerSize", + "ParcelOwnership", + "ParcelPointOperations", + "ParcelServiceName", + "ParcelShipmentType", + "ParcelStatus", + "ParcelType", + "PaymentType", + "PointType", + "ReturnsStatus", +] diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index d47f0cc..d367380 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -1,25 +1,44 @@ -login: str = 'https://api-inmobile-pl.easypack24.net/v1/authenticate' -send_sms_code: str = 'https://api-inmobile-pl.easypack24.net/v1/sendSMSCode/' # get -confirm_sms_code: str = 'https://api-inmobile-pl.easypack24.net/v1/confirmSMSCode' # post +# AUTH # +login: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" +send_sms_code: str = "https://api-inmobile-pl.easypack24.net/v1/sendSMSCode/" # get +confirm_sms_code: str = "https://api-inmobile-pl.easypack24.net/v1/confirmSMSCode" # post +logout: str = "https://api-inmobile-pl.easypack24.net/v1/logout" # post +refresh_token: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" # post -# \/ Secured by JWT \/ +# INCOMING PARCELS # +parcels: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked" # get +parcel: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked/" # get +multi: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/multi/" # get +collect: str = "https://api-inmobile-pl.easypack24.net/v1/collect/validate" # post +reopen: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen" # post +compartment_open: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/open" # post +compartment_status: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/status" # post +terminate_collect_session: str = "https://api-inmobile-pl.easypack24.net/v1/collect/terminate" # post +shared: str = "https://api-inmobile-pl.easypack24.net/v1/parcels/shared" # post -refresh_token: str = 'https://api-inmobile-pl.easypack24.net/v1/authenticate' # post -parcels: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/tracked' # get -parcel: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/tracked/' # get -multi: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/multi/' # get -collect: str = 'https://api-inmobile-pl.easypack24.net/v1/collect/validate' # post -reopen: str = 'https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen' # post -compartment_open: str = 'https://api-inmobile-pl.easypack24.net/v1/collect/compartment/open' # post -compartment_status: str = 'https://api-inmobile-pl.easypack24.net/v1/collect/compartment/status' # post -terminate_collect_session: str = 'https://api-inmobile-pl.easypack24.net/v1/collect/terminate' # post -friendship: str = 'https://api-inmobile-pl.easypack24.net/v1/friends/' # get, post, patch, delete -validate_friendship: str = 'https://api-inmobile-pl.easypack24.net/v1/invitations/validate' # post -accept_friendship: str = 'https://api-inmobile-pl.easypack24.net/v1/invitations/accept' # post -shared: str = 'https://api-inmobile-pl.easypack24.net/v1/parcels/shared' # post -sent: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/sent/' # get -returns: str = 'https://api-inmobile-pl.easypack24.net/v1/returns/parcels/' # get -parcel_prices: str = 'https://api-inmobile-pl.easypack24.net/v1/prices/parcels' # get -tickets: str = 'https://api-inmobile-pl.easypack24.net/v1/returns/tickets' # get -logout: str = 'https://api-inmobile-pl.easypack24.net/v1/logout' # post -parcel_notifications: str = 'https://api-inmobile-pl.easypack24.net/v2/notifications?type=PUSH%2CNEWS%2CTILE' # get +# CREATING PARCEL # +create: str = "https://api-inmobile-pl.easypack24.net/v1/parcels" +points: str = "https://api-inmobile-pl.easypack24.net/v3/points" +blik_status: str = "https://api-inmobile-pl.easypack24.net/v1/payments/blik/alias/status" +create_blik: str = "https://api-inmobile-pl.easypack24.net/v1/payments/transactions/create/blik" + +# OUTGOING PARCELS # +# sent: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/sent/' # get +sent: str = "https://api-inmobile-pl.easypack24.net/v2/parcels/sent/" # get +parcel_points: str = "https://api-inmobile-pl.easypack24.net/v3/points/" # get +validate_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/validate/" # post +open_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/open" # post +reopen_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/reopen" # post +status_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/status" # post +confirm_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/confirm" # post +parcel_prices: str = "https://api-inmobile-pl.easypack24.net/v1/prices/parcels" # get + +# RETURNS # +returns: str = "https://api-inmobile-pl.easypack24.net/v1/returns/parcels/" # get +tickets: str = "https://api-inmobile-pl.easypack24.net/v1/returns/tickets" # get +parcel_notifications: str = "https://api-inmobile-pl.easypack24.net/v2/notifications?type=PUSH%2CNEWS%2CTILE" # get + +# FRIENDS # +friendship: str = "https://api-inmobile-pl.easypack24.net/v1/friends/" # get, post, patch, delete +validate_friendship: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/validate" # post +accept_friendship: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/accept" # post diff --git a/inpost/static/exceptions.py b/inpost/static/exceptions.py index 7319c79..bf8f8e3 100644 --- a/inpost/static/exceptions.py +++ b/inpost/static/exceptions.py @@ -1,6 +1,4 @@ from typing import Any -from .statuses import ParcelType -from .parcels import Parcel # ------------------ Base ------------------- # @@ -11,71 +9,89 @@ class BaseInpostError(Exception): :type reason: typing.Any""" def __init__(self, reason): - """Constructor method""" + """Constructor method + + :param reason: Reason of error + :type reason: Any + """ super().__init__(reason) self.reason: Any = reason @property - def stacktrace(self): - """Gets stacktrace of raised exception""" + def stacktrace(self) -> Any: + """Gets stacktrace of raised exception + :return: reason why exception occured + :rtype: Any""" return self.reason # ----------------- Parcels ----------------- # + class ParcelTypeError(BaseInpostError): """Is raised when expected :class:`ParcelType` does not match with actual one""" + pass class NoParcelError(BaseInpostError): """Is raised when no parcel is set in :class:`Parcel`""" + pass class UnidentifiedParcelError(BaseInpostError): """Is raised when no other :class:`Parcel` error match""" + pass # ----------------- API ----------------- # class NotAuthenticatedError(BaseInpostError): """Is raised when `Inpost.auth_token` is missing""" + pass class ReAuthenticationError(BaseInpostError): """Is raised when `Inpost.auth_token` has expired""" + pass class PhoneNumberError(BaseInpostError): """Is raised when `Inpost.phone_number` is invalid or unexpected error connected with phone number occurs""" + pass class SmsCodeError(BaseInpostError): """Is raised when `Inpost.sms_code` is invalid or unexpected sms_code occurs""" + pass class RefreshTokenError(BaseInpostError): """Is raised when `Inpost.refr_token` is invalid or unexpected error connected with refresh token occurs""" + pass class NotFoundError(BaseInpostError): """Is raised when method from :class:`Inpost` returns 404 Not Found HTTP status code""" + pass class UnauthorizedError(BaseInpostError): """Is raised when method from :class:`Inpost` returns 401 Unauthorized HTTP status code""" + pass class UnidentifiedAPIError(BaseInpostError): """Is raised when no other API error match""" + pass @@ -86,14 +102,17 @@ class UserLocationError(BaseInpostError): class SingleParamError(BaseInpostError): """Is raised when only one param must be filled in but got more""" + pass class MissingParamsError(BaseInpostError): """Is raised when none of params are filled""" + pass class UnidentifiedError(BaseInpostError): """Is raised when no other error match""" + pass diff --git a/inpost/static/friends.py b/inpost/static/friends.py index 026921a..eba3312 100644 --- a/inpost/static/friends.py +++ b/inpost/static/friends.py @@ -1,34 +1,37 @@ import logging -from arrow import get, Arrow +from arrow import Arrow, get class Friend: def __init__(self, friend_data, logger: logging.Logger): - self.uuid: str = friend_data['uuid'] if 'uuid' in friend_data else None - self.phone_number: str = friend_data['phoneNumber'] - self.name: str = friend_data['name'] - self._log: logging.Logger = logger.getChild(f'{__class__.__name__}.{self.uuid}') - self.invitaion_code: str | None = friend_data['invitationCode'] if 'invitationCode' in friend_data else None - self.created_date: Arrow | None = get(friend_data['createdDate']) if 'createdDate' in friend_data else None - self.expiry_date: Arrow | None = get(friend_data['expiryDate']) if 'expiryDate' in friend_data else None + self.uuid: str = friend_data["uuid"] if "uuid" in friend_data else None + self.phone_number: str = friend_data["phoneNumber"] + self.name: str = friend_data["name"] + self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.uuid}") + self.invitaion_code: str | None = friend_data["invitationCode"] if "invitationCode" in friend_data else None + self.created_date: Arrow | None = get(friend_data["createdDate"]) if "createdDate" in friend_data else None + self.expiry_date: Arrow | None = get(friend_data["expiryDate"]) if "expiryDate" in friend_data else None if self.invitaion_code: - self._log.debug(f'created friendship with {self.name} using from_invitation') + self._log.debug(f"created friendship with {self.name} using from_invitation") else: - self._log.debug(f'created friendship with {self.name}') + self._log.debug(f"created friendship with {self.name}") @classmethod def from_invitation(cls, invitation_data, logger: logging.Logger): - return cls(friend_data={'uuid': invitation_data['friend']['uuid'], - 'phoneNumber': invitation_data['friend']['phoneNumber'], - 'name': invitation_data['friend']['name'], - 'invitationCode': invitation_data['invitationCode'], - 'createdDate': invitation_data['createdDate'], - 'expiryDate': invitation_data['expiryDate'] - }, - logger=logger) + return cls( + friend_data={ + "uuid": invitation_data["friend"]["uuid"], + "phoneNumber": invitation_data["friend"]["phoneNumber"], + "name": invitation_data["friend"]["name"], + "invitationCode": invitation_data["invitationCode"], + "createdDate": invitation_data["createdDate"], + "expiryDate": invitation_data["expiryDate"], + }, + logger=logger, + ) def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") diff --git a/inpost/static/notifications.py b/inpost/static/notifications.py index 03fe128..75ae615 100644 --- a/inpost/static/notifications.py +++ b/inpost/static/notifications.py @@ -1,15 +1,15 @@ -from arrow import get, Arrow +from arrow import Arrow, get -class Notification: - def __init__(self, notification_data): - self.id: str = notification_data['id'] - self.type: str = notification_data['type'] - self.action: str = notification_data['action'] - self.date: Arrow = get(notification_data['date']) - self.title: str = notification_data['title'] - self.content: str = notification_data['content'] - self.shipment_number: str = notification_data['shipmentNumber'] - self.read: bool = notification_data['read'] - self.extra_params: dict = notification_data['extraParams'] - self.parcel_type: str = notification_data['parcelType'] +class Notification: # TODO: do docs + def __init__(self, notification_data): # TODO: do docs + self.id: str = notification_data.get("id", None) + self.type: str = notification_data.get("type", None) + self.action: str = notification_data.get("action", None) + self.date: Arrow = get(notification_data.get("date")) if "date" in notification_data else None + self.title: str = notification_data.get("title", None) + self.content: str = notification_data.get("content", None) + self.shipment_number: str = notification_data.get("shipmentNumber", None) + self.read: bool = notification_data.get("read", None) + self.extra_params: dict = notification_data.get("extraParams", None) + self.parcel_type: str = notification_data.get("parcelType", None) diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index c1e7686..64ffaea 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -4,24 +4,37 @@ from typing import List, Tuple import qrcode -from arrow import get, arrow - -from inpost.static.statuses import * +from arrow import arrow, get + +from inpost.static.statuses import ( + CompartmentActualStatus, + ParcelCarrierSize, + ParcelDeliveryType, + ParcelLockerSize, + ParcelOwnership, + ParcelShipmentType, + ParcelStatus, + PaymentStatus, + PaymentType, + PointType, + ReturnsStatus, +) class BaseParcel: def __init__(self, parcel_data: dict, logger: logging.Logger): - self.shipment_number: str = parcel_data.get('shipmentNumber') - self._log: logging.Logger = logger.getChild(f'{__class__.__name__}.{self.shipment_number}') - self.status: ParcelStatus = ParcelStatus[parcel_data['status']] - self.expiry_date: arrow | None = get(parcel_data['expiryDate']) if 'expiryDate' in parcel_data else None - self.operations: Operations = Operations(operations_data=parcel_data['operations'], logger=self._log) - self.event_log: List[EventLog] = [EventLog(eventlog_data=event, logger=self._log) - for event in parcel_data['eventLog']] + self.shipment_number = parcel_data.get("shipmentNumber") + self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.shipment_number}") + self.status: ParcelStatus = ParcelStatus[parcel_data.get("status")] + self.expiry_date: arrow | None = get(parcel_data["expiryDate"]) if "expiryDate" in parcel_data else None + self.operations: Operations = Operations(operations_data=parcel_data["operations"], logger=self._log) + self.event_log: List[EventLog] = [ + EventLog(eventlog_data=event, logger=self._log) for event in parcel_data["eventLog"] + ] class Parcel(BaseParcel): - """Object representation of :class:`inpost.api.Inpost` compartment properties + """Object representation of :class:`inpost.api.Inpost` incoming parcel :param parcel_data: :class:`dict` containing all parcel data :type parcel_data: dict @@ -29,33 +42,57 @@ class Parcel(BaseParcel): :type logger: logging.Logger""" def __init__(self, parcel_data: dict, logger: logging.Logger): - """Constructor method""" + """Constructor method + + :param parcel_data: dict containing parcel data + :type parcel_data: dict + :param logger: logger instance + :type logger: logging.Logger + """ super().__init__(parcel_data, logger) - self._log: logging.Logger = logger.getChild(f'{__class__.__name__}.{self.shipment_number}') - self.shipment_type: ParcelShipmentType = ParcelShipmentType[parcel_data['shipmentType']] - self._open_code: str | None = parcel_data.get('openCode', None) - self._qr_code: QRCode | None = QRCode(qrcode_data=parcel_data['qrCode'], logger=self._log) \ - if 'qrCode' in parcel_data else None - self.stored_date: arrow | None = get(parcel_data['storedDate']) if 'storedDate' in parcel_data else None - self.pickup_date: arrow | None = get(parcel_data['pickUpDate']) if 'pickUpDate' in parcel_data else None - self.parcel_size: ParcelLockerSize | ParcelCarrierSize = ParcelLockerSize[parcel_data['parcelSize']] \ - if self.shipment_type == ParcelShipmentType.parcel else ParcelCarrierSize[parcel_data['parcelSize']] - self.receiver: Receiver = Receiver(receiver_data=parcel_data['receiver'], logger=self._log) if 'receiver' in parcel_data else None - self.sender: Sender = Sender(sender_data=parcel_data['sender'], logger=self._log) if 'sender' in parcel_data else None - self.pickup_point: PickupPoint = PickupPoint(pickuppoint_data=parcel_data['pickUpPoint'], logger=self._log) \ - if 'pickUpPoint' in parcel_data else None - self.multi_compartment: MultiCompartment | None = MultiCompartment( - parcel_data['multiCompartment'], logger=self._log) if 'multiCompartment' in parcel_data else None - self.is_end_off_week_collection: bool | None = parcel_data.get('endOfWeekCollection', None) - self.status: ParcelStatus = ParcelStatus[parcel_data['status']] if 'status' in parcel_data else None - self.avizo_transaction_status: str | None = parcel_data.get('avizoTransactionStatus', None) - self.shared_to: List[SharedTo] = [SharedTo(sharedto_data=person, logger=self._log) - for person in parcel_data['sharedTo']] if 'sharedTo' in parcel_data else None - self.ownership_status: ParcelOwnership = ParcelOwnership[parcel_data['ownershipStatus']] if 'ownershipStatus' in parcel_data else None - self.economy_parcel: bool | None = parcel_data.get('economyParcel', None) + self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.shipment_number}") + self.shipment_type: ParcelShipmentType = ParcelShipmentType[parcel_data.get("shipmentType")] + self._open_code: str | None = parcel_data.get("openCode", None) + self._qr_code: QRCode | None = ( + QRCode(qrcode_data=parcel_data["qrCode"], logger=self._log) if "qrCode" in parcel_data else None + ) + self.stored_date: arrow | None = get(parcel_data["storedDate"]) if "storedDate" in parcel_data else None + self.pickup_date: arrow | None = get(parcel_data["pickUpDate"]) if "pickUpDate" in parcel_data else None + self.parcel_size: ParcelLockerSize | ParcelCarrierSize = ( + ParcelLockerSize[parcel_data.get("parcelSize")] + if self.shipment_type == ParcelShipmentType.parcel + else ParcelCarrierSize[parcel_data.get("parcelSize")] + ) + self.receiver: Receiver | None = ( + Receiver(receiver_data=parcel_data["receiver"], logger=self._log) if "receiver" in parcel_data else None + ) + self.sender: Sender | None = ( + Sender(sender_data=parcel_data["sender"], logger=self._log) if "sender" in parcel_data else None + ) + self.pickup_point: PickupPoint | None = ( + PickupPoint(point_data=parcel_data["pickUpPoint"], logger=self._log) + if "pickUpPoint" in parcel_data + else None + ) + self.multi_compartment: MultiCompartment | None = ( + MultiCompartment(parcel_data["multiCompartment"], logger=self._log) + if "multiCompartment" in parcel_data + else None + ) + + self.is_end_off_week_collection: bool | None = parcel_data.get("endOfWeekCollection", None) + self.status: ParcelStatus | None = ParcelStatus[parcel_data.get("status")] + self.avizo_transaction_status: str | None = parcel_data.get("avizoTransactionStatus", None) + self.shared_to: List[SharedTo] | None = ( + [SharedTo(sharedto_data=person, logger=self._log) for person in parcel_data["sharedTo"]] + if "sharedTo" in parcel_data + else None + ) + self.ownership_status: ParcelOwnership | None = ParcelOwnership[parcel_data.get("ownershipStatus")] + self.economy_parcel: bool | None = parcel_data.get("economyParcel", None) self._compartment_properties: CompartmentProperties | None = None - self._log.debug(f'created parcel with shipment number {self.shipment_number}') + self._log.debug(f"created parcel with shipment number {self.shipment_number}") # log all unexpected things, so you can make an issue @github if self.shipment_type == ParcelShipmentType.UNKNOWN: @@ -71,14 +108,16 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): self._log.warning(f'unexpected ownership status: {parcel_data["ownershipStatus"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") def __str__(self): - return f"Sender: {str(self.sender)}\n" \ - f"Shipment number: {self.shipment_number}\n" \ - f"Status: {self.status}\n" \ - f"Pickup point: {self.pickup_point}" + return ( + f"Sender: {str(self.sender)}\n" + f"Shipment number: {self.shipment_number}\n" + f"Status: {self.status}\n" + f"Pickup point: {self.pickup_point}" + ) @property def open_code(self) -> str | None: @@ -86,12 +125,12 @@ def open_code(self) -> str | None: :return: Open code for :class:`Parcel` :rtype: str""" - self._log.debug('getting open code') + self._log.debug("getting open code") if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got open code') + self._log.debug("got open code") return self._open_code - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -100,12 +139,12 @@ def generate_qr_image(self) -> BytesIO | None: :return: QR image for :class:`Parcel` :rtype: BytesIO""" - self._log.debug('generating qr image') - if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got qr image') + self._log.debug("generating qr image") + if self.shipment_type == ParcelShipmentType.parcel and self._qr_code is not None: + self._log.debug("got qr image") return self._qr_code.qr_image - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -114,12 +153,12 @@ def compartment_properties(self): :return: Compartment properties for :class:`Parcel` :rtype: CompartmentProperties""" - self._log.debug('getting comparment properties') + self._log.debug("getting comparment properties") if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got compartment properties') + self._log.debug("got compartment properties") return self._compartment_properties - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_properties.setter @@ -127,14 +166,15 @@ def compartment_properties(self, compartmentproperties_data: dict): """Set compartment properties for :class:`Parcel` :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`Parcel` - :type compartmentproperties_data: CompartmentProperties""" - self._log.debug(f'setting compartment properties with {compartmentproperties_data}') + :type compartmentproperties_data: dict""" + self._log.debug(f"setting compartment properties with {compartmentproperties_data}") if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('compartment properties set') - self._compartment_properties = CompartmentProperties(compartmentproperties_data=compartmentproperties_data, - logger=self._log) + self._log.debug("compartment properties set") + self._compartment_properties = CompartmentProperties( + compartmentproperties_data=compartmentproperties_data, logger=self._log + ) - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_location(self): @@ -142,25 +182,26 @@ def compartment_location(self): :return: Compartment location for :class:`Parcel` :rtype: CompartmentLocation""" - self._log.debug('getting compartment location') + self._log.debug("getting compartment location") if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got compartment location') + self._log.debug("got compartment location") return self._compartment_properties.location if self._compartment_properties else None - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_location.setter def compartment_location(self, location_data: dict): """Set compartment location for :class:`Parcel` + :param location_data: :class:`dict` containing `compartment properties` data for :class:`Parcel` - :type location_data: CompartmentProperties""" - self._log.debug(f'setting compartment location with {location_data}') - if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('compartment location set') + :type location_data: dict""" + self._log.debug(f"setting compartment location with {location_data}") + if self.shipment_type == ParcelShipmentType.parcel and self._compartment_properties is not None: + self._log.debug("compartment location set") self._compartment_properties.location = location_data - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_status(self) -> CompartmentActualStatus | None: @@ -168,99 +209,300 @@ def compartment_status(self) -> CompartmentActualStatus | None: :return: Compartment status for :class:`Parcel` :rtype: CompartmentActualStatus""" - self._log.debug('getting compartment status') + self._log.debug("getting compartment status") if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got compartment status') + self._log.debug("got compartment status") return self._compartment_properties.status if self._compartment_properties else None - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_status.setter - def compartment_status(self, status): - self._log.debug(f'setting compartment status with {status}') + def compartment_status(self, status) -> None: + self._log.debug(f"setting compartment status with {status}") + if self._compartment_properties is None: + return # TODO: think out + if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('compartment status set') + self._log.debug("compartment status set") self._compartment_properties.status = status - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property - def compartment_open_data(self): + def compartment_open_data(self) -> dict | None: """Returns a compartment open data for :class:`Parcel` :return: dict containing compartment open data for :class:`Parcel` :rtype: dict""" - self._log.debug('getting compartment open data') + self._log.debug("getting compartment open data") + if self.receiver is None: + return None # TODO: think out + if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got compartment open data') + self._log.debug("got compartment open data") return { - 'shipmentNumber': self.shipment_number, - 'openCode': self._open_code, - 'receiverPhoneNumber': self.receiver.phone_number + "shipmentNumber": self.shipment_number, + "openCode": self._open_code, + "receiverPhoneNumber": self.receiver.phone_number, } - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property - def mocked_location(self): + def mocked_location(self) -> dict | None: """Returns a mocked location for :class:`Parcel` - :return: dict containing mocked location for :class:`Parcel` - :rtype: dict""" - self._log.debug('getting mocked location') + :return: dict containing mocked location for :class:`Parcel or None if wrong parcel shipment type` + :rtype: dict | None""" + self._log.debug("getting mocked location") + if self.pickup_point is None: + return None # TODO: think out if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('got mocked location') + self._log.debug("got mocked location") return { - 'latitude': round(self.pickup_point.latitude + random.uniform(-0.00005, 0.00005), 6), - 'longitude': round(self.pickup_point.longitude + random.uniform(-0.00005, 0.00005), 6), - 'accuracy': round(random.uniform(1, 4), 1) + "latitude": round(self.pickup_point.latitude + random.uniform(-0.00005, 0.00005), 6), + "longitude": round(self.pickup_point.longitude + random.uniform(-0.00005, 0.00005), 6), + "accuracy": round(random.uniform(1, 4), 1), } - self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property - def is_multicompartment(self): + def is_multicompartment(self) -> bool: """Specifies if parcel is in multi compartment + :return: True if parcel is in multicompartment :rtype: bool""" return self.multi_compartment is not None @property - def is_main_multicompartment(self): + def is_main_multicompartment(self) -> bool | None: """Specifies if parcel is main parcel in multi compartment :return: True if parcel is in multicompartment :rtype: bool""" - if self.is_multicompartment: + if self.is_multicompartment and self.multi_compartment is not None: return self.multi_compartment.shipment_numbers is not None return None @property - def has_airsensor(self) -> bool: - return self.pickup_point.air_sensor_data is not None + def has_airsensor(self) -> bool | None: + if self.pickup_point is not None: + return self.pickup_point.air_sensor_data is not None + + return None # @property # def get_from_multicompartment(self): # return -class ReturnParcel(BaseParcel): +class ReturnParcel(BaseParcel): # TODO: see properties and create + def __init__(self, parcel_data: dict, logger: logging.Logger): + super().__init__(parcel_data, logger) + self.uuid: str = parcel_data["uuid"] + self.rma: str = parcel_data["rma"] + self.organization_name: str = parcel_data["organizationName"] + self.created_date: arrow = get(parcel_data["createdDate"]) + self.accepted_date: arrow = get(parcel_data["acceptedDate"]) + self.expiry_date: arrow = get(parcel_data["expiryDate"]) + self.sent_date: arrow = get(parcel_data["sentDate"]) + self.delivered_date: arrow = get(parcel_data["deliveredDate"]) + self.order_number: str = parcel_data["orderNumber"] + self.form_type: str = parcel_data["formType"] + + +class SentParcel(BaseParcel): # TODO: check properties def __init__(self, parcel_data: dict, logger: logging.Logger): super().__init__(parcel_data, logger) - self.uuid: str = parcel_data['uuid'] - self.rma: str = parcel_data['rma'] - self.organization_name: str = parcel_data['organizationName'] - self.created_date: arrow = parcel_data['createdDate'] - self.accepted_date: arrow = parcel_data['acceptedDate'] - self.expiry_date: arrow = parcel_data['expiryDate'] - self.sent_date: arrow = parcel_data['sentDate'] - self.delivered_date: arrow = parcel_data['deliveredDate'] - self.order_number: str = parcel_data['orderNumber'] - self.form_type: str = parcel_data['formType'] + self.origin_system: str = parcel_data.get("originSystem", None) + self.quick_send_code: int = parcel_data.get("quickSendCode", None) + self._qr_code: QRCode | None = ( + QRCode(qrcode_data=parcel_data["qrCode"], logger=self._log) if "qrCode" in parcel_data else None + ) + self.confirmation_date: arrow = get(parcel_data.get("confirmationDate", None)) + self.shipment_type: ParcelShipmentType = ParcelShipmentType[parcel_data["shipmentType"]] + self.parcel_size: ParcelLockerSize | ParcelCarrierSize = ( + ParcelLockerSize[parcel_data.get("parcelSize")] + if self.shipment_type == ParcelShipmentType.parcel + else ParcelCarrierSize[parcel_data.get("parcelSize")] + ) + self.receiver: Receiver | None = ( + Receiver(receiver_data=parcel_data["receiver"], logger=self._log) if "receiver" in parcel_data else None + ) + self.sender: Sender | None = ( + Sender(sender_data=parcel_data["sender"], logger=self._log) if "sender" in parcel_data else None + ) + self.pickup_point: PickupPoint | None = ( + PickupPoint(point_data=parcel_data["pickUpPoint"], logger=self._log) + if "pickUpPoint" in parcel_data + else None + ) + self.delivery_point: DeliveryPoint | None = ( + DeliveryPoint(parcel_data["deliveryPoint"], logger=self._log) if "deliveryPoint" in parcel_data else None + ) + self.drop_off_point: DropOffPoint | None = ( + DropOffPoint(point_data=parcel_data["dropOffPoint"], logger=self._log) + if "dropOffPoint" in parcel_data + else None + ) + self.payment: Payment | None = ( + Payment(payment_details=parcel_data["payment"], logger=self._log) if "payment" in parcel_data else None + ) + self.unlabeled: bool = parcel_data.get("unlabeled", None) + self.is_end_off_week_collection: bool | None = parcel_data.get("endOfWeekCollection", None) + self.status: ParcelStatus | None = ParcelStatus[parcel_data.get("status")] + + @property + def open_code(self) -> int | None: + """Returns an open code for :class:`Parcel` + + :return: Open code for :class:`Parcel` + :rtype: int""" + self._log.debug("getting open code") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got open code") + return self.quick_send_code + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @property + def generate_qr_image(self) -> BytesIO | None: + """Returns a QR image for :class:`Parcel` + + :return: QR image for :class:`Parcel` + :rtype: BytesIO""" + self._log.debug("generating qr image") + if self.shipment_type == ParcelShipmentType.parcel and self._qr_code is not None: + self._log.debug("got qr image") + return self._qr_code.qr_image + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @property + def compartment_properties(self): + """Returns a compartment properties for :class:`Parcel` + + :return: Compartment properties for :class:`Parcel` + :rtype: CompartmentProperties""" + self._log.debug("getting comparment properties") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got compartment properties") + return self._compartment_properties + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @compartment_properties.setter + def compartment_properties(self, compartmentproperties_data: dict): + """Set compartment properties for :class:`Parcel` + + :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`Parcel` + :type compartmentproperties_data: dict""" + self._log.debug(f"setting compartment properties with {compartmentproperties_data}") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("compartment properties set") + self._compartment_properties = CompartmentProperties( + compartmentproperties_data=compartmentproperties_data, logger=self._log + ) + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def compartment_location(self): + """Returns a compartment location for :class:`Parcel` + + :return: Compartment location for :class:`Parcel` + :rtype: CompartmentLocation""" + self._log.debug("getting compartment location") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got compartment location") + return self._compartment_properties.location if self._compartment_properties else None + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @compartment_location.setter + def compartment_location(self, location_data: dict): + """Set compartment location for :class:`Parcel` + :param location_data: :class:`dict` containing `compartment properties` data for :class:`Parcel` + :type location_data: dict""" + self._log.debug(f"setting compartment location with {location_data}") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("compartment location set") + self._compartment_properties.location = location_data + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def compartment_status(self) -> CompartmentActualStatus | None: + """Returns a compartment status for :class:`Parcel` + + :return: Compartment status for :class:`Parcel` + :rtype: CompartmentActualStatus""" + self._log.debug("getting compartment status") + + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got compartment status") + return self._compartment_properties.status if self._compartment_properties else None + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @compartment_status.setter + def compartment_status(self, status): + self._log.debug(f"setting compartment status with {status}") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("compartment status set") + self._compartment_properties.status = status + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def compartment_open_data(self): + """Returns a compartment open data for :class:`Parcel` + + :return: dict containing compartment open data for :class:`Parcel` + :rtype: dict""" + self._log.debug("getting compartment open data") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got compartment open data") + return { + "shipmentNumber": self.shipment_number, + "openCode": self._open_code, + "receiverPhoneNumber": self.receiver.phone_number, + } + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @property + def mocked_location(self): + """Returns a mocked location for :class:`Parcel` + + :return: dict containing mocked location for :class:`Parcel` + :rtype: dict""" + self._log.debug("getting mocked location") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got mocked location") + return { + "latitude": round(self.pickup_point.latitude + random.uniform(-0.00005, 0.00005), 6), + "longitude": round(self.pickup_point.longitude + random.uniform(-0.00005, 0.00005), 6), + "accuracy": round(random.uniform(1, 4), 1), + } + + self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @property + def has_airsensor(self) -> bool | None: + return self.pickup_point.air_sensor_data is not None if self.pickup_point else None class Receiver: @@ -272,17 +514,22 @@ class Receiver: :type logger: logging.Logger""" def __init__(self, receiver_data: dict, logger: logging.Logger): - """Constructor method""" - self.email: str = receiver_data['email'] - self.phone_number: str = receiver_data['phoneNumber'] - self.name: str = receiver_data['name'] - self._log: logging.Logger = logger.getChild(__class__.__name__) + """Constructor method - self._log.debug('created') + :param receiver_data: dict containing receiver data + :type receiver_data: dict + :param logger: logger instance + :type logger: logging.Logger""" + self.email: str = receiver_data["email"] + self.phone_number: str = receiver_data["phoneNumber"] + self.name: str = receiver_data["name"] + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class Sender: @@ -294,59 +541,79 @@ class Sender: :type logger: logging.Logger""" def __init__(self, sender_data: dict, logger: logging.Logger): - """Constructor method""" - self.sender_name: str = sender_data['name'] - self._log: logging.Logger = logger.getChild(__class__.__name__) + """Constructor method + + :param sender_data: dict containing sender data + :type sender_data: dict + :param logger: logger instance + :type logger: logging.Logger""" + self.sender_name: str = sender_data.get("name", None) + self.sender_email: str = sender_data.get("email", None) + self._log: logging.Logger = logger.getChild(self.__class__.__name__) - self._log.debug('created') + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") def __str__(self) -> str: return self.sender_name -class PickupPoint: - """Object representation of :class:`Parcel` pickup point +class Point: + """Object representation of :class:`Parcel` point - :param pickuppoint_data: :class:`dict` containing pickup point data for :class:`Parcel` - :type pickuppoint_data: dict + :param point_data: :class:`dict` containing point data for :class:`Parcel` + :type point_data: dict :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger""" - def __init__(self, pickuppoint_data: dict, logger: logging.Logger): - """Constructor method""" - self.name: str = pickuppoint_data['name'] - self.latitude: float = pickuppoint_data['location']['latitude'] - self.longitude: float = pickuppoint_data['location']['longitude'] - self.description: str = pickuppoint_data['locationDescription'] - self.opening_hours: str = pickuppoint_data['openingHours'] - self.post_code: str = pickuppoint_data['addressDetails']['postCode'] - self.city: str = pickuppoint_data['addressDetails']['city'] - self.province: str = pickuppoint_data['addressDetails']['province'] - self.street: str = pickuppoint_data['addressDetails']['street'] - self.building_number: str = pickuppoint_data['addressDetails']['buildingNumber'] - self.virtual: int = pickuppoint_data['virtual'] - self.point_type: str = pickuppoint_data['pointType'] - self.type: List[ParcelDeliveryType] = [ParcelDeliveryType[data] for data in pickuppoint_data['type']] - self.location_round_the_clock: bool = pickuppoint_data['location247'] - self.doubled: bool = pickuppoint_data['doubled'] - self.image_url: str = pickuppoint_data['imageUrl'] - self.easy_access_zone: bool = pickuppoint_data['easyAccessZone'] - self.air_sensor: bool = pickuppoint_data['airSensor'] - self.air_sensor_data: AirSensorData | None = AirSensorData(pickuppoint_data['airSensorData']) if 'airSensorData' in pickuppoint_data else None - - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + def __init__(self, point_data: dict, logger: logging.Logger): + """Constructor method + + :param point_data: :class:`dict` containing point data for :class:`Parcel` + :type point_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self.name: str = point_data["name"] + self.latitude: float = point_data["location"]["latitude"] + self.longitude: float = point_data["location"]["longitude"] + self.description: str = point_data["locationDescription"] + self.opening_hours: str = point_data["openingHours"] + self.post_code: str = point_data["addressDetails"]["postCode"] + self.city: str = point_data["addressDetails"]["city"] + self.province: str = point_data["addressDetails"]["province"] + self.street: str = point_data["addressDetails"]["street"] + self.building_number: str = point_data["addressDetails"]["buildingNumber"] + self.payment_type: List[PaymentType] | None = ( + [PaymentType[pt] for pt in point_data["paymentType"]] if "paymentType" in point_data else None + ) + self.virtual: int = point_data["virtual"] + self.point_type: PointType | None = PointType[point_data.get("pointType")] + self.type: List[ParcelDeliveryType] = ( + [ParcelDeliveryType[data] for data in point_data["type"]] if "type" in point_data else None + ) + self.location_round_the_clock: bool = point_data["location247"] + self.doubled: bool = point_data["doubled"] + self.image_url: str = point_data["imageUrl"] + self.easy_access_zone: bool = point_data["easyAccessZone"] + self.air_sensor: bool = point_data["airSensor"] + self.air_sensor_data: AirSensorData | None = ( + AirSensorData(point_data["airSensorData"], self._log) if "airSensorData" in point_data else None + ) + self.remote_send: bool = point_data.get("remoteSend", None) + self.remote_return: bool = point_data.get("remoteReturn", None) + self._log.debug("created") if ParcelDeliveryType.UNKNOWN in self.type: - self._log.debug(f'unknown delivery type: {pickuppoint_data["type"]}') + self._log.debug(f'unknown delivery type: {point_data["type"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") def __str__(self) -> str: return self.name @@ -357,32 +624,118 @@ def location(self) -> Tuple[float, float]: :return: tuple containing location for :class:`PickupPoint` :rtype: tuple""" - self._log.debug('getting location') + self._log.debug("getting location") return self.latitude, self.longitude +class PickupPoint(Point): + """Object representation of :class:`Parcel` pick up point + + :param point_data: :class:`dict` containing pickup point data for :class:`Parcel` + :type point_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger""" + + def __init__(self, point_data: dict, logger: logging.Logger): + super().__init__(point_data, logger) + + +class DropOffPoint(Point): + """Object representation of :class:`Parcel` drop off point + + :param point_data: :class:`dict` containing pickup point data for :class:`Parcel` + :type point_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger""" + + def __init__(self, point_data: dict, logger: logging.Logger): + super().__init__(point_data, logger) + + +class DeliveryPoint: + """Object representation of :class: DeliveryPoint + + :param delivery_point: :class:`dict` containing delivery point data for :class:`DeliveryPoint` + :type delivery_point: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger""" + + def __init__(self, delivery_point: dict, logger: logging.Logger): + self.name = delivery_point.get("name") + self.company_name = delivery_point.get("companyName") + self.post_code = delivery_point["address"]["postCode"] if "address" in delivery_point else None + self.city = delivery_point["address"]["city"] if "address" in delivery_point else None + self.street = delivery_point["address"]["street"] if "address" in delivery_point else None + self.building_number = delivery_point["address"]["buildingNumber"] if "address" in delivery_point else None + self.flat_numer = delivery_point["address"]["flatNumber"] if "address" in delivery_point else None + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") + + def __repr__(self): + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") + + +class Payment: + """Object representation of :class: Payment + :param payment_details: :class:`dict` containing payment data for :class:`Payment` + :type payment_details: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger""" + + def __init__(self, payment_details: dict, logger: logging.Logger): + # self.paid: bool = payment_details.get("paid") + # self.total_price: float = payment_details.get("totalPrice") + # self.insurance_price: float = payment_details.get("insurancePrice") + # self.end_of_week_collection_price: float = payment_details.get("endOfWeekCollectionPrice") + # self.shipment_discounted: bool = payment_details.get("shipmentDiscounted") + # self.transaction_status: str = payment_details.get("transactionStatus") + + self.paid = payment_details.get("paid") + self.total_price = payment_details.get("totalPrice") + self.insurance_price = payment_details.get("insurancePrice") + self.end_of_week_collection_price = payment_details.get("endOfWeekCollectionPrice") + self.shipment_discounted = payment_details.get("shipmentDiscounted") + self.transaction_status = payment_details.get("transactionStatus") + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") + + def __repr__(self): + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") + + class MultiCompartment: """Object representation of :class:`Parcel` `multicompartment` :param multicompartment_data: :class:`dict` containing multicompartment data for :class:`Parcel` :type multicompartment_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, multicompartment_data: dict, logger: logging.Logger): - """Constructor method""" - self.uuid = multicompartment_data['uuid'] - self.shipment_numbers: List[str] | None = multicompartment_data['shipmentNumbers'] \ - if 'shipmentNumbers' in multicompartment_data else None - self.presentation: bool = multicompartment_data['presentation'] - self.collected: bool = multicompartment_data['collected'] + """Constructor method:param multicompartment_data: :class:`dict` containing multicompartment data for :class:`Parcel` + + :type multicompartment_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.uuid = multicompartment_data["uuid"] + self.shipment_numbers: List[str] | None = ( + multicompartment_data["shipmentNumbers"] if "shipmentNumbers" in multicompartment_data else None + ) + self.presentation: bool = multicompartment_data["presentation"] + self.collected: bool = multicompartment_data["collected"] - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class Operations: @@ -391,31 +744,40 @@ class Operations: :param operations_data: :class:`dict` containing operations data for :class:`Parcel` :type operations_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, operations_data: dict, logger: logging.Logger): - """Constructor method""" - self.manual_archive: bool = operations_data['manualArchive'] - self.auto_archivable_since: arrow | None = get( - operations_data['autoArchivableSince']) if 'autoArchivableSince' in operations_data else None - self.delete: bool = operations_data['delete'] - self.collect: bool = operations_data['collect'] - self.expand_avizo: bool = operations_data['expandAvizo'] - self.highlight: bool = operations_data['highlight'] - self.refresh_until: arrow = get(operations_data['refreshUntil']) - self.request_easy_access_zone: str = operations_data['requestEasyAccessZone'] - self.is_voicebot: bool = operations_data['voicebot'] - self.can_share_to_observe: bool = operations_data['canShareToObserve'] - self.can_share_open_code: bool = operations_data['canShareOpenCode'] - self.can_share_parcel: bool = operations_data['canShareParcel'] - self.send: bool | None = operations_data['send'] if 'send' in operations_data else None - - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + """Constructor method + + :param operations_data: :class:`dict` containing operations data for :class:`Parcel` + :type operations_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.manual_archive: bool = operations_data["manualArchive"] + self.auto_archivable_since: arrow | None = ( + get(operations_data["autoArchivableSince"]) if "autoArchivableSince" in operations_data else None + ) + self.delete: bool = operations_data.get("delete", None) + self.pay_to_send: bool = operations_data.get("payToSend", None) + self.collect: bool = operations_data.get("collect", None) + self.expand_avizo: bool = operations_data.get("expandAvizo", None) + self.highlight: bool = operations_data.get("highlight", None) + self.refresh_until: arrow = get(operations_data["refreshUntil"]) if "refreshUntil" in operations_data else None + self.request_easy_access_zone: str = operations_data.get("requestEasyAccessZone", None) + self.is_voicebot: bool = operations_data.get("voicebot", None) + self.can_share_to_observe: bool = operations_data.get("canShareToObserve", None) + self.can_share_open_code: bool = operations_data.get("canShareOpenCode", None) + self.can_share_parcel: bool = operations_data.get("canShareParcel", None) + self.send: bool | None = operations_data.get("send", None) + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class EventLog: @@ -424,24 +786,38 @@ class EventLog: :param eventlog_data: :class:`dict` containing single eventlog data for :class:`Parcel` :type eventlog_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, eventlog_data: dict, logger: logging.Logger): - """Constructor method""" - self.type: str = eventlog_data['type'] - self.name: ParcelStatus | ReturnsStatus = ParcelStatus[ - eventlog_data['name']] if self.type == 'PARCEL_STATUS' else ReturnsStatus[eventlog_data['name']] - self.date: arrow = get(eventlog_data['date']) - - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + """Constructor method + + :param eventlog_data: :class:`dict` containing single eventlog data for :class:`Parcel` + :type eventlog_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.type: str = eventlog_data["type"] + if self.type == "PARCEL_STATUS": + self.name = ParcelStatus[eventlog_data.get("name")] + elif self.type == "RETURN_STATUS": + self.name = ReturnsStatus[eventlog_data.get("name")] + elif self.type == "PAYMENT": + self.name = PaymentStatus[eventlog_data.get("name")] + else: + ... + self.date: arrow = get(eventlog_data["date"]) + self.details: dict | None = eventlog_data.get("details") + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") if self.name == ParcelStatus.UNKNOWN or self.name == ReturnsStatus.UNKNOWN: self._log.debug(f'unknown {self.type}: {eventlog_data["name"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class SharedTo: @@ -450,20 +826,27 @@ class SharedTo: :param sharedto_data: :class:`dict` containing shared to data for :class:`Parcel` :type sharedto_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, sharedto_data: dict, logger: logging.Logger): - """Constructor method""" - self.uuid: str = sharedto_data['uuid'] - self.name: str = sharedto_data['name'] - self.phone_number = sharedto_data['phoneNumber'] + """Constructor method + + :param sharedto_data: :class:`dict` containing shared to data for :class:`Parcel` + :type sharedto_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.uuid: str = sharedto_data["uuid"] + self.name: str = sharedto_data["name"] + self.phone_number = sharedto_data["phoneNumber"] - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class QRCode: @@ -472,18 +855,25 @@ class QRCode: :param qrcode_data: :class:`str` containing qrcode data for :class:`Parcel` :type qrcode_data: str :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, qrcode_data: str, logger: logging.Logger): - """Constructor method""" + """Constructor method + + :param qrcode_data: :class:`str` containing qrcode data for :class:`Parcel` + :type qrcode_data: str + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ self._qr_code = qrcode_data - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") @property def qr_image(self) -> BytesIO: @@ -491,23 +881,19 @@ def qr_image(self) -> BytesIO: :return: QR Code image :rtype: BytesIO""" - self._log.debug('generating qr image') + self._log.debug("generating qr image") qr = qrcode.QRCode( - version=3, - error_correction=qrcode.constants.ERROR_CORRECT_H, - box_size=20, - border=4, - mask_pattern=5 + version=3, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=20, border=4, mask_pattern=5 ) qr.add_data(self._qr_code) qr.make(fit=False) img1 = qr.make_image(fill_color="black", back_color="white") bio = BytesIO() - bio.name = 'qr.png' - img1.save(bio, 'PNG') + bio.name = "qr.png" + img1.save(bio, "PNG") bio.seek(0) - self._log.debug('generated qr image') + self._log.debug("generated qr image") return bio @@ -517,24 +903,31 @@ class CompartmentLocation: :param compartmentlocation_data: :class:`dict` containing compartment location data for :class:`Parcel` :type compartmentlocation_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, compartmentlocation_data: dict, logger: logging.Logger): - """Constructor method""" - self.name: str = compartmentlocation_data['compartment']['name'] - self.side: str = compartmentlocation_data['compartment']['location']['side'] - self.column: str = compartmentlocation_data['compartment']['location']['column'] - self.row: str = compartmentlocation_data['compartment']['location']['row'] - self.open_compartment_waiting_time: int = compartmentlocation_data['openCompartmentWaitingTime'] - self.action_time: int = compartmentlocation_data['actionTime'] - self.confirm_action_time: int = compartmentlocation_data['confirmActionTime'] - - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + """Constructor method + + :param compartmentlocation_data: :class:`dict` containing compartment location data for :class:`Parcel` + :type compartmentlocation_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.name: str = compartmentlocation_data["compartment"]["name"] + self.side: str = compartmentlocation_data["compartment"]["location"]["side"] + self.column: str = compartmentlocation_data["compartment"]["location"]["column"] + self.row: str = compartmentlocation_data["compartment"]["location"]["row"] + self.open_compartment_waiting_time: int = compartmentlocation_data["openCompartmentWaitingTime"] + self.action_time: int = compartmentlocation_data["actionTime"] + self.confirm_action_time: int = compartmentlocation_data["confirmActionTime"] + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class CompartmentProperties: @@ -546,18 +939,24 @@ class CompartmentProperties: :type logger: logging.Logger""" def __init__(self, compartmentproperties_data: dict, logger: logging.Logger): - """Constructor method""" - self._session_uuid: str = compartmentproperties_data['sessionUuid'] - self._session_expiration_time: int = compartmentproperties_data['sessionExpirationTime'] + """Constructor method + + :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`Parcel` + :type compartmentproperties_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self._session_uuid: str = compartmentproperties_data["sessionUuid"] + self._session_expiration_time: int = compartmentproperties_data["sessionExpirationTime"] self._location: CompartmentLocation | None = None self._status: CompartmentActualStatus | None = None - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") @property def session_uuid(self): @@ -565,7 +964,7 @@ def session_uuid(self): :return: string containing session unique identified for :class:`CompartmentProperties` :rtype: str""" - self._log.debug('getting session uuid') + self._log.debug("getting session uuid") return self._session_uuid @property @@ -574,7 +973,7 @@ def location(self): :return: compartment location for :class:`CompartmentProperties` :rtype: str""" - self._log.debug('getting location') + self._log.debug("getting location") return self._location @location.setter @@ -583,7 +982,7 @@ def location(self, location_data: dict): :param location_data: dict containing compartment location data for :class:`CompartmentProperties` :type location_data: dict""" - self._log.debug('setting location') + self._log.debug("setting location") self._location = CompartmentLocation(location_data, self._log) @property @@ -592,17 +991,18 @@ def status(self): :return: compartment location for :class:`CompartmentProperties` :rtype: CompartmentActualStatus""" - self._log.debug('getting status') + self._log.debug("getting status") return self._status @status.setter def status(self, status_data: str | CompartmentActualStatus): - self._log.debug('setting status') - self._status = status_data if isinstance(status_data, CompartmentActualStatus) \ - else CompartmentActualStatus[status_data] + self._log.debug("setting status") + self._status = ( + status_data if isinstance(status_data, CompartmentActualStatus) else CompartmentActualStatus[status_data] + ) if self._status == CompartmentActualStatus.UNKNOWN and isinstance(status_data, str): - self._log.warning(f'unexpected compartment actual status: {status_data}') + self._log.warning(f"unexpected compartment actual status: {status_data}") class AirSensorData: @@ -611,17 +1011,26 @@ class AirSensorData: :param airsensor_data: :class:`dict` containing air sensor data for :class:`Parcel` :type airsensor_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ + def __init__(self, airsensor_data: dict, logger: logging.Logger): - self.updated_until: arrow = airsensor_data['updatedUntil'] - self.air_quality: str = airsensor_data['airQuality'] - self.temperature: float = airsensor_data['temperature'] - self.humidity: float = airsensor_data['humidity'] - self.pressure: float = airsensor_data['pressure'] - self.pm25_value: float = airsensor_data['pollutants']['pm25']['value'] - self.pm25_percent: float = airsensor_data['pollutants']['pm25']['percent'] - self.pm10_value: float = airsensor_data['pollutants']['pm10']['value'] - self.pm10_percent: float = airsensor_data['pollutants']['pm10']['percent'] - - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + """Constructor method + + :param airsensor_data: :class:`dict` containing air sensor data for :class:`Parcel` + :type airsensor_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.updated_until: arrow = airsensor_data["updatedUntil"] + self.air_quality: str = airsensor_data["airQuality"] + self.temperature: float = airsensor_data["temperature"] + self.humidity: float = airsensor_data["humidity"] + self.pressure: float = airsensor_data["pressure"] + self.pm25_value: float = airsensor_data["pollutants"]["pm25"]["value"] + self.pm25_percent: float = airsensor_data["pollutants"]["pm25"]["percent"] + self.pm10_value: float = airsensor_data["pollutants"]["pm10"]["value"] + self.pm10_percent: float = airsensor_data["pollutants"]["pm10"]["percent"] + + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + self._log.debug("created") diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index 5ce91ce..d740cba 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -1,28 +1,27 @@ from enum import Enum, EnumMeta -from typing import List class Meta(EnumMeta): # temporary handler for unexpected keys in enums - def __getitem__(cls, item): + def __getitem__(self, item): try: - return super().__getitem__(item) - except KeyError as error: - return cls.UNKNOWN + return super().__getitem__(item) if item is not None else None + except KeyError: + return self.UNKNOWN - def __getattribute__(cls, item): + def __getattribute__(self, item): try: - return super().__getattribute__(item) - except KeyError as error: - return cls.UNKNOWN + return super().__getattribute__(item) if item is not None else None + except KeyError: + return self.UNKNOWN - def get_all(cls): - return [getattr(cls, name) for name in cls.__members__] - - def get_without(cls, without: 'ParcelBase' | List['ParcelBase']): - if isinstance(without, ParcelBase): - without = [without] - - return [element for element in cls.get_all() if element not in without] + # def get_all(cls): + # return [getattr(cls, name) for name in cls.__members__] + # + # def get_without(cls, without: "ParcelBase" | List["ParcelBase"]): + # if isinstance(without, ParcelBase): + # without = [without] + # + # return [element for element in cls.get_all() if element not in without] class ParcelBase(Enum, metaclass=Meta): @@ -60,45 +59,49 @@ def __eq__(self, other): def __repr__(self): fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("'", "") class ParcelCarrierSize(ParcelBase): """:class:`Enum` that holds parcel size for carrier shipment type""" - UNKNOWN = 'UNKNOWN DATA' - A = '8x38x64' - B = '19x38x64' - C = '41x38x64' - D = '50x50x80' - OTHER = 'UNKNOWN DIMENSIONS' + + UNKNOWN = "UNKNOWN DATA" + A = "8x38x64" + B = "19x38x64" + C = "41x38x64" + D = "50x50x80" + OTHER = "UNKNOWN DIMENSIONS" class ParcelLockerSize(ParcelBase): """:class:`Enum` that holds parcel size for parcel locker shipment type""" - UNKNOWN = 'UNKNOWN DATA' - A = '8x38x64' - B = '19x38x64' - C = '41x38x64' + + UNKNOWN = "UNKNOWN DATA" + A = "8x38x64" + B = "19x38x64" + C = "41x38x64" class ParcelDeliveryType(ParcelBase): """:class:`Enum` that holds parcel delivery types""" - UNKNOWN = 'UNKNOWN DATA' - parcel_locker = 'Paczkomat' - courier = 'Kurier' - parcel_point = 'PaczkoPunkt' + + UNKNOWN = "UNKNOWN DATA" + parcel_locker = "Paczkomat" + courier = "Kurier" + parcel_point = "PaczkoPunkt" class ParcelShipmentType(ParcelBase): """:class:`Enum` that holds parcel shipment types""" - UNKNOWN = 'UNKNOWN DATA' - parcel = 'Paczkomat' - courier = 'Kurier' - parcel_point = 'PaczkoPunkt' + + UNKNOWN = "UNKNOWN DATA" + parcel = "Paczkomat" + courier = "Kurier" + parcel_point = "PaczkoPunkt" class ParcelAdditionalInsurance(ParcelBase): - UNKNOWN = 'UNKNOWN DATA' + UNKNOWN = "UNKNOWN DATA" UNINSURANCED = 1 ONE = 2 # UPTO 5000 TWO = 3 # UPTO 10000 @@ -107,90 +110,131 @@ class ParcelAdditionalInsurance(ParcelBase): class ParcelType(ParcelBase): """:class:`Enum` that holds parcel types""" - UNKNOWN = 'UNKNOWN DATA' - TRACKED = 'Przychodzące' - SENT = 'Wysłane' - RETURNS = 'Zwroty' + + UNKNOWN = "UNKNOWN DATA" + TRACKED = "Przychodzące" + SENT = "Wysłane" + RETURNS = "Zwroty" + + +class PointType(ParcelBase): + """:class: `Enum` that holds point types""" + + UNKNOWN = "UNNKOWN DATA" + PL = "Paczkomat" + parcel_locker_superpop = "some paczkomat or pok stuff" # TODO: get known what does superpop stand for + POK = "Mobilny punkt obsługi klienta" + POP = "Punkt odbioru paczki" + + +class ParcelPointOperations(ParcelBase): + """:class: `Enum` that holds parcel operation types""" + + UNKNOWN = "UNNKOWN DATA" + CREATE = "c2x-target" + SEND = "remote-send" class ParcelStatus(ParcelBase): """:class:`Enum` that holds parcel statuses""" - UNKNOWN = 'UNKNOWN DATA' - CREATED = 'Utworzona' # TODO: translate from app - OFFERS_PREPARED = 'Oferty przygotowane' # TODO: translate from app - OFFER_SELECTED = 'Oferta wybrana' # TODO: translate from app - CONFIRMED = 'Potwierdzona' - READY_TO_PICKUP_FROM_POK = 'Gotowa do odbioru w PaczkoPunkcie' - OVERSIZED = 'Gabaryt' - DISPATCHED_BY_SENDER_TO_POK = 'Nadana w PaczkoPunkcie' - DISPATCHED_BY_SENDER = 'Nadana w paczkomacie' - COLLECTED_FROM_SENDER = 'Odebrana od nadawcy' - TAKEN_BY_COURIER = 'Odebrana przez Kuriera' - ADOPTED_AT_SOURCE_BRANCH = 'Przyjęta w oddziale' - SENT_FROM_SOURCE_BRANCH = 'Wysłana z oddziału' - READDRESSED = 'Zmiana punktu dostawy' # TODO: translate from app - OUT_FOR_DELIVERY = 'Wydana do doręczenia' - READY_TO_PICKUP = 'Gotowa do odbioru' - PICKUP_REMINDER_SENT = 'Wysłano przypomnienie o odbiorze' # TODO: translate from app - PICKUP_TIME_EXPIRED = 'Upłynął czas odbioru' # TODO: translate from app - AVIZO = 'Powrót do oddziału' - TAKEN_BY_COURIER_FROM_POK = 'Odebrana z PaczkoPunktu nadawczego' - REJECTED_BY_RECEIVER = 'Odrzucona przez odbiorcę' # TODO: translate from app - UNDELIVERED = 'Nie dostarczona' # TODO: translate from app - DELAY_IN_DELIVERY = 'Opóźnienie w dostarczeniu' # TODO: translate from app - RETURNED_TO_SENDER = 'Zwrócona do nadawcy' # TODO: translate from app - READY_TO_PICKUP_FROM_BRANCH = 'Gotowa do odbioru z oddziału' # TODO: translate from app - DELIVERED = 'Doręczona' - CANCELED = 'Anulowana' # TODO: translate from app - CLAIMED = 'Zareklamowana' - STACK_IN_CUSTOMER_SERVICE_POINT = 'Przesyłka magazynowana w punkcie obsługi klienta' # TODO: translate from app - STACK_PARCEL_PICKUP_TIME_EXPIRED = 'Upłynął czas odbioru' # TODO: translate from app - UNSTACK_FROM_CUSTOMER_SERVICE_POINT = '?' # TODO: translate from app - COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT = 'Przekazana do punktu obsługi klienta' # TODO: translate from app - TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT = 'Odebrana przez kuriera z punktu obsługi klienta' # TODO: translate from app - STACK_IN_BOX_MACHINE = 'Przesyłka magazynowana w paczkomacie tymczasowym' - STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED = 'Upłynął czas odbioru z paczkomatu' # TODO: translate from app - UNSTACK_FROM_BOX_MACHINE = 'Odebrana z paczkomatu' # TODO: translate from app - ADOPTED_AT_SORTING_CENTER = 'Przyjęta w sortowni' - OUT_FOR_DELIVERY_TO_ADDRESS = 'Gotowa do doręczenia' - PICKUP_REMINDER_SENT_ADDRESS = 'Wysłano przypomnienie o odbiorze' # TODO: translate from app - UNDELIVERED_WRONG_ADDRESS = 'Nie dostarczono z powodu złego adresu' # TODO: translate from app - UNDELIVERED_COD_CASH_RECEIVER = 'Nie dostarczono z powodu nieopłacenia' # TODO: translate from app - REDIRECT_TO_BOX = 'Przekierowana do paczkomatu' # TODO: translate from app - CANCELED_REDIRECT_TO_BOX = 'Anulowano przekierowanie do paczkomatu' # TODO: translate from app + + UNKNOWN = "UNKNOWN DATA" + CREATED = "W trakcie przygotowania" # TODO: translate from app + OFFERS_PREPARED = "Oferty przygotowane" # TODO: translate from app + OFFER_SELECTED = "Oferta wybrana" # TODO: translate from app + CONFIRMED = "Potwierdzona" + READY_TO_PICKUP_FROM_POK = "Gotowa do odbioru w PaczkoPunkcie" + OVERSIZED = "Gabaryt" + DISPATCHED_BY_SENDER_TO_POK = "Nadana w PaczkoPunkcie" + DISPATCHED_BY_SENDER = "Nadana w paczkomacie" + COLLECTED_FROM_SENDER = "Odebrana od nadawcy" + TAKEN_BY_COURIER = "Odebrana przez Kuriera" + ADOPTED_AT_SOURCE_BRANCH = "Przyjęta w oddziale" + SENT_FROM_SOURCE_BRANCH = "Wysłana z oddziału" + READDRESSED = "Zmiana punktu dostawy" # TODO: translate from app + OUT_FOR_DELIVERY = "Wydana do doręczenia" + READY_TO_PICKUP = "Gotowa do odbioru" + PICKUP_REMINDER_SENT = "Wysłano przypomnienie o odbiorze" # TODO: translate from app + PICKUP_TIME_EXPIRED = "Upłynął czas odbioru" # TODO: translate from app + AVIZO = "Powrót do oddziału" + TAKEN_BY_COURIER_FROM_POK = "Odebrana z PaczkoPunktu nadawczego" + REJECTED_BY_RECEIVER = "Odrzucona przez odbiorcę" # TODO: translate from app + UNDELIVERED = "Nie dostarczona" # TODO: translate from app + DELAY_IN_DELIVERY = "Opóźnienie w dostarczeniu" # TODO: translate from app + RETURNED_TO_SENDER = "Zwrócona do nadawcy" # TODO: translate from app + READY_TO_PICKUP_FROM_BRANCH = "Gotowa do odbioru z oddziału" # TODO: translate from app + DELIVERED = "Doręczona" + CANCELED = "Anulowana" # TODO: translate from app + CLAIMED = "Zareklamowana" + STACK_IN_CUSTOMER_SERVICE_POINT = "Przesyłka magazynowana w punkcie obsługi klienta" # TODO: translate from app + STACK_PARCEL_PICKUP_TIME_EXPIRED = "Upłynął czas odbioru" # TODO: translate from app + UNSTACK_FROM_CUSTOMER_SERVICE_POINT = "?" # TODO: translate from app + COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT = "Przekazana do punktu obsługi klienta" # TODO: translate from app + TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT = ( + "Odebrana przez kuriera z punktu obsługi klienta" # TODO: translate from app + ) + STACK_IN_BOX_MACHINE = "Przesyłka magazynowana w paczkomacie tymczasowym" + STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED = "Upłynął czas odbioru z paczkomatu" # TODO: translate from app + UNSTACK_FROM_BOX_MACHINE = "Odebrana z paczkomatu" # TODO: translate from app + ADOPTED_AT_SORTING_CENTER = "Przyjęta w sortowni" + OUT_FOR_DELIVERY_TO_ADDRESS = "Gotowa do doręczenia" + PICKUP_REMINDER_SENT_ADDRESS = "Wysłano przypomnienie o odbiorze" # TODO: translate from app + UNDELIVERED_WRONG_ADDRESS = "Nie dostarczono z powodu złego adresu" # TODO: translate from app + UNDELIVERED_COD_CASH_RECEIVER = "Nie dostarczono z powodu nieopłacenia" # TODO: translate from app + REDIRECT_TO_BOX = "Przekierowana do paczkomatu" # TODO: translate from app + CANCELED_REDIRECT_TO_BOX = "Anulowano przekierowanie do paczkomatu" # TODO: translate from app + + +class DeliveryType(ParcelBase): # TODO: look for more types + UNKNOWN = "UNKNOWN DATA" + BOX_MACHINE = "Paczkomat" class ReturnsStatus(ParcelBase): # TODO: translate from app and fill missing ones - ACCEPTED = 'Zaakceptowano' - USED = 'Nadano' - DELIVERED = 'Dostarczono' - UNKNOWN = 'UNKNOWN DATA' + ACCEPTED = "Zaakceptowano" + USED = "Nadano" + DELIVERED = "Dostarczono" + UNKNOWN = "UNKNOWN DATA" class ParcelOwnership(ParcelBase): """:class:`Enum` that holds parcel ownership types""" - UNKNOWN = 'UNKNOWN DATA' - FRIEND = 'Zaprzyjaźniona' - OWN = 'Własna' + + UNKNOWN = "UNKNOWN DATA" + FRIEND = "Zaprzyjaźniona" + OWN = "Własna" # both are the same, only for being clear class CompartmentExpectedStatus(ParcelBase): """:class:`Enum` that holds compartment expected statuses""" - UNKNOWN = 'UNKNOWN DATA' - OPENED = 'Otwarta' - CLOSED = 'Zamknięta' + + UNKNOWN = "UNKNOWN DATA" + OPENED = "Otwarta" + CLOSED = "Zamknięta" class CompartmentActualStatus(ParcelBase): """:class:`Enum` that holds compartment actual statuses""" - UNKNOWN = 'UNKNOWN DATA' - OPENED = 'Otwarta' - CLOSED = 'Zamknięta' + + UNKNOWN = "UNKNOWN DATA" + OPENED = "Otwarta" + CLOSED = "Zamknięta" + + +class PaymentType(ParcelBase): + UNKNOWN = "UNKNOWN DATA" + NOTSUPPORTED = "Payments are not supported" # klucz 0 + BY_CARD_IN_MACHINE = "Payment by card in the machine" # klucz 2 + + +class PaymentStatus(ParcelBase): + UNKNOWN = "UNKNOWN DATA" + C2X_COMPLETED = "Completed" class ParcelServiceName(ParcelBase): - UNKNOWN = 'UNKNOWN DATA' + UNKNOWN = "UNKNOWN DATA" ALLEGRO_PARCEL = 1 ALLEGRO_PARCEL_SMART = 2 ALLEGRO_LETTER = 3 diff --git a/pyproject.toml b/pyproject.toml index 3a40f62..96fc7e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ exclude = ''' | build | dist | tmp + | docs )/ ''' From fb4d6b90fa3c933982c9bb1cc67cd6c8253e322e Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 13:51:28 +0200 Subject: [PATCH 05/28] mypy config --- .mypy.ini | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .mypy.ini diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..c3d8eed --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,12 @@ +[mypy] +strict = False +# misc disabled due to damn hardcoded Enum, not solved since 2021: https://github.com/python/mypy/issues/11039 +# annotation-unchecked just to have clear logs +# assignment cuz i didnt find a way to disable incompatible types in specific conditions, undo before every commit +disable_error_code = misc, annotation-unchecked, assignment + +[mypy-tests.*] +allow_untyped_defs = True +allow_untyped_calls = True + + From 6ed9a5586e9500d2c156e36c8eb3f3cd81a2a894 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 18:23:07 +0200 Subject: [PATCH 06/28] added .gitignore, modified todos.yml --- .github/workflows/todos.yml | 20 +++++++------------- .gitignore | 8 ++++++++ 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 .gitignore diff --git a/.github/workflows/todos.yml b/.github/workflows/todos.yml index 6f3ef46..2ab04b5 100644 --- a/.github/workflows/todos.yml +++ b/.github/workflows/todos.yml @@ -4,18 +4,12 @@ on: push: branches: - dev + - dev-tests jobs: - todos: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: wvffle/todo-actions@master - with: - collect_commit_msg: 'ci: collect new todos' - reference_commit_msg: 'ci: add references to new todos' - reference_commit_body: 'New issues: %s' - env: - GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }} - TODO_ACTIONS_MONGO_URL: ${{ secrets.TODO_ACTIONS_MONGO_URL }} \ No newline at end of file + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v3" + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@master" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..180d189 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/log +/otwarcie paczkomatu.xml +/otwarcie_paczkomatu.json +/poetry.lock +/.mypy_cache/ +/dist/ +/venv/ +/tests/.pytest_cache/ From a736429b31907cdb27db7ebedde092597e7cfc04 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 19:20:24 +0200 Subject: [PATCH 07/28] updated urls naming schema (it interfered with filenames), minor refactors --- inpost/api.py | 154 +++++++++++++++++++------------------ inpost/static/__init__.py | 117 ++++++++++++++-------------- inpost/static/endpoints.py | 64 ++++++++------- 3 files changed, 170 insertions(+), 165 deletions(-) diff --git a/inpost/api.py b/inpost/api.py index 184dbdf..c35d0ab 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -33,31 +33,29 @@ UnauthorizedError, UnidentifiedAPIError, appjson, - blik_status, - collect, - compartment_open, - compartment_status, - confirm_sms_code, - create, - create_blik, - friends, - friendship, - logout, - multi, - open_sent, - parcel, - parcel_points, - parcel_prices, - parcels, - refresh_token, - returns, - send_sms_code, - sent, - shared, - status_sent, - terminate_collect_session, - validate_friendship, - validate_sent, + blik_status_url, + collect_url, + compartment_open_url, + compartment_status_url, + confirm_sms_code_url, + create_blik_url, + create_url, + friendship_url, + logout_url, + multi_url, + open_sent_url, + parcel_points_url, + parcel_prices_url, + refresh_token_url, + returns_url, + send_sms_code_url, + sent_url, + shared_url, + status_sent_url, + terminate_collect_session_url, + tracked_url, + validate_friendship_url, + validate_sent_url, ) @@ -81,7 +79,6 @@ def __init__(self, phone_number): self.auth_token: str | None = None self.refr_token: str | None = None self.sess: ClientSession = ClientSession() - # self.parcel: Parcel | SentParcel | ReturnParcel | None = None self._log = logging.getLogger(f"{self.__class__.__name__}.{phone_number}") self._log.setLevel(level=logging.DEBUG) @@ -94,7 +91,7 @@ async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): - return self.logout() + return self.disconnect() async def request( self, @@ -197,7 +194,7 @@ async def send_sms_code(self) -> bool: resp = await self.request( method="post", action="send sms code", - url=send_sms_code, + url=send_sms_code_url, auth=False, headers=None, data={"phoneNumber": f"{self.phone_number}"}, @@ -231,7 +228,7 @@ async def confirm_sms_code(self, sms_code: str | int) -> bool: resp = await self.request( method="post", action="confirm sms code", - url=confirm_sms_code, + url=confirm_sms_code_url, auth=False, headers=appjson, data={"phoneNumber": self.phone_number, "smsCode": sms_code, "phoneOS": "Android"}, @@ -268,7 +265,7 @@ async def refresh_token(self) -> bool: resp = await self.request( method="post", action="refresh token", - url=refresh_token, + url=refresh_token_url, auth=False, headers=appjson, data={"refreshToken": self.refr_token, "phoneOS": "Android"}, @@ -303,7 +300,7 @@ async def logout(self) -> bool: raise NotAuthenticatedError(reason="Not logged in") resp = await self.request( - method="post", action="logout", url=logout, auth=True, headers=None, data=None, autorefresh=True + method="post", action="logout", url=logout_url, auth=True, headers=None, data=None, autorefresh=True ) if resp.status == 200: @@ -336,7 +333,7 @@ async def disconnect(self) -> bool: async def get_parcel( self, shipment_number: int | str, parcel_type: ParcelType = ParcelType.TRACKED, parse=False - ) -> dict | Parcel | SentParcel | ReturnParcel: # TODO: make selector of which parcel we are looking for + ) -> dict | Parcel | SentParcel | ReturnParcel: """Fetches single parcel from provided shipment number :param shipment_number: Parcel's shipment number @@ -362,16 +359,16 @@ async def get_parcel( match parcel_type: case ParcelType.TRACKED: self._log.debug(f"getting parcel type {parcel_type}") - url = parcels + url = tracked_url case ParcelType.SENT: self._log.debug(f"getting parcel type {parcel_type}") - url = sent + url = sent_url case ParcelType.RETURNS: self._log.debug(f"getting parcel type {parcel_type}") - url = returns + url = returns_url case _: - self._log.error(f"wrong parcel type {parcel_type}") - raise ParcelTypeError(reason=f"Unknown parcel type: {parcel_type}") + self._log.error(f"unexpected parcel type {parcel_type}") + raise ParcelTypeError(reason=f"Unexpected parcel type: {parcel_type}") resp = await self.request( method="get", @@ -434,13 +431,13 @@ async def get_parcels( match parcel_type: case ParcelType.TRACKED: self._log.debug(f"getting parcel type {parcel_type}") - url = parcels + url = tracked_url case ParcelType.SENT: self._log.debug(f"getting parcel type {parcel_type}") - url = sent + url = sent_url case ParcelType.RETURNS: self._log.debug(f"getting parcel type {parcel_type}") - url = returns + url = returns_url case _: self._log.error(f"wrong parcel type {parcel_type}") raise ParcelTypeError(reason=f"Unknown parcel type: {parcel_type}") @@ -489,7 +486,7 @@ async def get_multi_compartment( resp = await self.request( method="get", action=f"parcel with multi-compartment uuid {multi_uuid}", - url=f"{multi}{multi_uuid}", + url=f"{multi_url}{multi_uuid}", auth=True, headers=None, data=None, @@ -550,7 +547,7 @@ async def collect_compartment_properties( resp = await self.request( method="post", action="collect compartment properties", - url=collect, + url=collect_url, auth=True, headers=None, data={ @@ -587,7 +584,7 @@ async def open_compartment(self, parcel_obj: Parcel) -> bool: resp = await self.request( method="post", action=f"open compartment for {parcel_obj.shipment_number}", - url=compartment_open, + url=compartment_open_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -624,7 +621,7 @@ async def check_compartment_status( resp = await self.request( method="post", action="check compartment status", - url=compartment_status, + url=compartment_status_url, auth=True, headers=None, data={ @@ -661,7 +658,7 @@ async def terminate_collect_session(self, parcel_obj: Parcel) -> bool: resp = await self.request( method="post", action="terminate collect session", - url=terminate_collect_session, + url=terminate_collect_session_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -675,7 +672,7 @@ async def terminate_collect_session(self, parcel_obj: Parcel) -> bool: async def collect( self, shipment_number: str | None = None, parcel_obj: Parcel | None = None, location: dict | None = None - ) -> bool: + ) -> Parcel | None: """Simplified method to open compartment :param shipment_number: Parcel's shipment number @@ -711,16 +708,16 @@ async def collect( self._log.info(f"collecting parcel with shipment number {parcel_obj.shipment_number}") - if await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): - if await self.open_compartment(parcel_obj=parcel_obj): - if await self.check_compartment_status(parcel_obj=parcel_obj): - return True + if parcel_obj_ := await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): + if await self.open_compartment(parcel_obj=parcel_obj_): + if await self.check_compartment_status(parcel_obj=parcel_obj_): + return parcel_obj_ - return False + return None async def close_compartment(self, parcel_obj: Parcel) -> bool: """Checks whether actual compartment status and expected one matches then notifies inpost api that - compartment is closed + compartment is closed. Should be invoked after collecting parcel :param parcel_obj: Parcel object :type parcel_obj: Parcel @@ -754,7 +751,7 @@ async def reopen_compartment(self, parcel_obj: Parcel) -> bool: resp = await self.request( method="post", action=f"reopen compartment for {parcel_obj.shipment_number}", - url=compartment_open, + url=compartment_open_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -776,7 +773,7 @@ async def get_parcel_points( operation: ParcelPointOperations = ParcelPointOperations.CREATE, parse: bool = True, ) -> dict | List[Point]: - """Fetches prices for inpost services + """Fetches parcel points for inpost services :param query: parcel point search query (e.g. GXO05M) :type query: str | None @@ -819,7 +816,7 @@ async def get_parcel_points( resp = await self.request( method="get", action="get parcel points", - url=parcel_points, + url=parcel_points_url, auth=True, headers=None, params=_params, @@ -844,7 +841,12 @@ async def blik_status(self) -> bool: # TODO: do docs self._log.info("checking if user has opened blik session") resp = await self.request( - method="get", action="check user blik session", url=blik_status, auth=True, headers=None, autorefresh=True + method="get", + action="check user blik session", + url=blik_status_url, + auth=True, + headers=None, + autorefresh=True, ) if resp.status == 200 and not (await resp.json())["active"]: @@ -871,7 +873,7 @@ async def create_parcel( resp = await self.request( method="post", action="create parcel", - url=create, + url=create_url, auth=True, headers=None, data={ @@ -902,7 +904,7 @@ async def create_blik_session( resp = await self.request( method="post", action="create blik session", - url=create_blik, + url=create_blik_url, auth=True, headers=None, data={ @@ -953,12 +955,12 @@ async def validate_send( if parcel_obj_.drop_off_point is None: raise ValueError("Missing drop-off point!") - self._log.info(f"collecting compartment properties for {parcel_obj_.shipment_number}") + self._log.info(f"validating send for {parcel_obj_.shipment_number}") resp = await self.request( method="post", action="validate send parcel data", - url=validate_sent, + url=validate_sent_url, auth=True, headers=None, data={ @@ -967,13 +969,13 @@ async def validate_send( "quickSendCode": parcel_obj_.quick_send_code, }, "geoPoint": location, - "boxMachineName": drop_off_point if drop_off_point is not None else parcel_obj_.drop_off_point.name, + "boxMachineName": drop_off_point, }, autorefresh=True, ) if resp.status == 200: - self._log.debug(f"collected compartment properties for {parcel_obj_.shipment_number}") + self._log.debug(f"validated send for for {parcel_obj_.shipment_number}") parcel_obj_.compartment_properties = await resp.json() return parcel_obj_ @@ -997,7 +999,7 @@ async def open_send_compartment(self, parcel_obj: Parcel) -> bool: resp = await self.request( method="post", action=f"open send compartment for {parcel_obj.shipment_number}", - url=open_sent, + url=open_sent_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -1028,7 +1030,7 @@ async def reopen_send_compartment(self, parcel_obj: SentParcel) -> bool: resp = await self.request( method="post", action=f"reopen compartment for {parcel_obj.shipment_number}", - url=compartment_open, + url=compartment_open_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -1065,7 +1067,7 @@ async def check_send_compartment_status( resp = await self.request( method="post", action="check send compartment status", - url=status_sent, + url=status_sent_url, auth=True, headers=None, data={ @@ -1098,7 +1100,13 @@ async def get_prices(self) -> dict: raise NotAuthenticatedError(reason="Not logged in") resp = await self.request( - method="get", action="get prices", url=parcel_prices, auth=True, headers=None, data=None, autorefresh=True + method="get", + action="get prices", + url=parcel_prices_url, + auth=True, + headers=None, + data=None, + autorefresh=True, ) if resp.status == 200: self._log.debug("got parcel prices") @@ -1124,7 +1132,7 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: raise NotAuthenticatedError(reason="Not logged in") resp = await self.request( - method="get", action="get friends", url=friendship, auth=True, headers=None, data=None, autorefresh=True + method="get", action="get friends", url=friendship_url, auth=True, headers=None, data=None, autorefresh=True ) if resp.status == 200: self._log.debug("got user friends") @@ -1147,7 +1155,7 @@ async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> d resp = await self.request( method="get", action="get parcel friends", - url=f"{friendship}{shipment_number}", + url=f"{friendship_url}{shipment_number}", auth=True, headers=None, data=None, @@ -1207,7 +1215,7 @@ async def add_friend(self, name: str, phone_number: str | int, code: str | int, resp = await self.request( method="post", action="add friend", - url=validate_friendship, + url=validate_friendship_url, auth=True, headers=None, data={"invitationCode": code}, @@ -1225,7 +1233,7 @@ async def add_friend(self, name: str, phone_number: str | int, code: str | int, resp = await self.request( method="post", action="add friend", - url=friendship, + url=friendship_url, auth=True, headers=None, data={"phoneNumber": phone_number, "name": name}, @@ -1293,7 +1301,7 @@ async def remove_friend(self, uuid: str | None, name: str | None, phone_number: resp = await self.request( method="delete", action="remove user friend", - url=f"{friendship}{uuid}", + url=f"{friendship_url}{uuid}", auth=True, headers=None, data=None, @@ -1345,7 +1353,7 @@ async def update_friend(self, uuid: str | None, phone_number: str | int | None, resp = await self.request( method="patch", action="update user friend", - url=f"{friends}{uuid}", + url=f"{friendship_url}{uuid}", auth=True, headers=None, data=None, @@ -1380,7 +1388,7 @@ async def share_parcel(self, uuid: str, shipment_number: int | str) -> bool: resp = await self.request( method="post", action=f"share parcel: {shipment_number}", - url=shared, + url=shared_url, auth=True, headers=None, data={ diff --git a/inpost/static/__init__.py b/inpost/static/__init__.py index 59b44ff..d0cb55e 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -1,34 +1,33 @@ from .endpoints import ( - accept_friendship, - blik_status, - collect, - compartment_open, - compartment_status, - confirm_sent, - confirm_sms_code, - create, - create_blik, - friendship, - login, - logout, - multi, - open_sent, - parcel, - parcel_notifications, - parcel_points, - parcel_prices, - parcels, - refresh_token, - reopen_sent, - returns, - send_sms_code, - sent, - shared, - status_sent, - terminate_collect_session, - tickets, - validate_friendship, - validate_sent, + accept_friendship_url, + blik_status_url, + collect_url, + compartment_open_url, + compartment_status_url, + confirm_sent_url, + confirm_sms_code_url, + create_blik_url, + create_url, + friendship_url, + login_url, + logout_url, + multi_url, + open_sent_url, + parcel_notifications_url, + parcel_points_url, + parcel_prices_url, + refresh_token_url, + reopen_sent_url, + returns_url, + send_sms_code_url, + sent_url, + shared_url, + status_sent_url, + terminate_collect_session_url, + tickets_url, + tracked_url, + validate_friendship_url, + validate_sent_url, ) from .exceptions import ( MissingParamsError, @@ -87,36 +86,36 @@ ) __all__ = [ - "accept_friendship", - "blik_status", - "collect", - "compartment_open", - "compartment_status", - "confirm_sent", - "confirm_sms_code", - "create", - "create_blik", - "friendship", - "login", - "logout", - "multi", - "open_sent", - "parcel", - "parcel_notifications", - "parcel_points", - "parcel_prices", + "accept_friendship_url", + "blik_status_url", + "collect_url", + "compartment_open_url", + "compartment_status_url", + "confirm_sent_url", + "confirm_sms_code_url", + "create_url", + "create_blik_url", + "friendship_url", + "login_url", + "logout_url", + "multi_url", + "open_sent_url", + "tracked_url", + "parcel_notifications_url", + "parcel_points_url", + "parcel_prices_url", "parcels", - "refresh_token", - "reopen_sent", - "returns", - "send_sms_code", - "sent", - "shared", - "status_sent", - "terminate_collect_session", - "tickets", - "validate_friendship", - "validate_sent", + "refresh_token_url", + "reopen_sent_url", + "returns_url", + "send_sms_code_url", + "sent_url", + "shared_url", + "status_sent_url", + "terminate_collect_session_url", + "tickets_url", + "validate_friendship_url", + "validate_sent_url", "MissingParamsError", "NoParcelError", "NotAuthenticatedError", diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index d367380..ae65940 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -1,44 +1,42 @@ # AUTH # -login: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" -send_sms_code: str = "https://api-inmobile-pl.easypack24.net/v1/sendSMSCode/" # get -confirm_sms_code: str = "https://api-inmobile-pl.easypack24.net/v1/confirmSMSCode" # post -logout: str = "https://api-inmobile-pl.easypack24.net/v1/logout" # post -refresh_token: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" # post +login_url: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" +send_sms_code_url: str = "https://api-inmobile-pl.easypack24.net/v1/sendSMSCode/" # get +confirm_sms_code_url: str = "https://api-inmobile-pl.easypack24.net/v1/confirmSMSCode" # post +logout_url: str = "https://api-inmobile-pl.easypack24.net/v1/logout" # post +refresh_token_url: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" # post # INCOMING PARCELS # -parcels: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked" # get -parcel: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked/" # get -multi: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/multi/" # get -collect: str = "https://api-inmobile-pl.easypack24.net/v1/collect/validate" # post -reopen: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen" # post -compartment_open: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/open" # post -compartment_status: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/status" # post -terminate_collect_session: str = "https://api-inmobile-pl.easypack24.net/v1/collect/terminate" # post -shared: str = "https://api-inmobile-pl.easypack24.net/v1/parcels/shared" # post +tracked_url: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked/" # get +multi_url: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/multi/" # get +collect_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/validate" # post +reopen_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen" # post +compartment_open_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/open" # post +compartment_status_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/status" # post +terminate_collect_session_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/terminate" # post +shared_url: str = "https://api-inmobile-pl.easypack24.net/v1/parcels/shared" # post # CREATING PARCEL # -create: str = "https://api-inmobile-pl.easypack24.net/v1/parcels" -points: str = "https://api-inmobile-pl.easypack24.net/v3/points" -blik_status: str = "https://api-inmobile-pl.easypack24.net/v1/payments/blik/alias/status" -create_blik: str = "https://api-inmobile-pl.easypack24.net/v1/payments/transactions/create/blik" +create_url: str = "https://api-inmobile-pl.easypack24.net/v1/parcels" +points_url: str = "https://api-inmobile-pl.easypack24.net/v3/points" +blik_status_url: str = "https://api-inmobile-pl.easypack24.net/v1/payments/blik/alias/status" +create_blik_url: str = "https://api-inmobile-pl.easypack24.net/v1/payments/transactions/create/blik" # OUTGOING PARCELS # -# sent: str = 'https://api-inmobile-pl.easypack24.net/v3/parcels/sent/' # get -sent: str = "https://api-inmobile-pl.easypack24.net/v2/parcels/sent/" # get -parcel_points: str = "https://api-inmobile-pl.easypack24.net/v3/points/" # get -validate_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/validate/" # post -open_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/open" # post -reopen_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/reopen" # post -status_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/status" # post -confirm_sent: str = "https://api-inmobile-pl.easypack24.net/v1/send/confirm" # post -parcel_prices: str = "https://api-inmobile-pl.easypack24.net/v1/prices/parcels" # get +sent_url: str = "https://api-inmobile-pl.easypack24.net/v2/parcels/sent/" # get +parcel_points_url: str = "https://api-inmobile-pl.easypack24.net/v3/points/" # get +validate_sent_url: str = "https://api-inmobile-pl.easypack24.net/v1/send/validate/" # post +open_sent_url: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/open" # post +reopen_sent_url: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/reopen" # post +status_sent_url: str = "https://api-inmobile-pl.easypack24.net/v1/send/compartment/status" # post +confirm_sent_url: str = "https://api-inmobile-pl.easypack24.net/v1/send/confirm" # post +parcel_prices_url: str = "https://api-inmobile-pl.easypack24.net/v1/prices/parcels" # get # RETURNS # -returns: str = "https://api-inmobile-pl.easypack24.net/v1/returns/parcels/" # get -tickets: str = "https://api-inmobile-pl.easypack24.net/v1/returns/tickets" # get -parcel_notifications: str = "https://api-inmobile-pl.easypack24.net/v2/notifications?type=PUSH%2CNEWS%2CTILE" # get +returns_url: str = "https://api-inmobile-pl.easypack24.net/v1/returns/parcels/" # get +tickets_url: str = "https://api-inmobile-pl.easypack24.net/v1/returns/tickets" # get +parcel_notifications_url: str = "https://api-inmobile-pl.easypack24.net/v2/notifications?type=PUSH%2CNEWS%2CTILE" # get # FRIENDS # -friendship: str = "https://api-inmobile-pl.easypack24.net/v1/friends/" # get, post, patch, delete -validate_friendship: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/validate" # post -accept_friendship: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/accept" # post +friendship_url: str = "https://api-inmobile-pl.easypack24.net/v1/friends/" # get, post, patch, delete +validate_friendship_url: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/validate" # post +accept_friendship_url: str = "https://api-inmobile-pl.easypack24.net/v1/invitations/accept" # post From 7f86979b388c86207931090281020cf893626175 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 19:22:41 +0200 Subject: [PATCH 08/28] CI hates me --- .github/workflows/todos.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/todos.yml b/.github/workflows/todos.yml index 2ab04b5..81d3b54 100644 --- a/.github/workflows/todos.yml +++ b/.github/workflows/todos.yml @@ -1,10 +1,6 @@ name: Create issues from todos -on: - push: - branches: - - dev - - dev-tests +on: ["push"] jobs: build: From f3dae76eab78bfb9352018a9cf6356428602c928 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 19:33:31 +0200 Subject: [PATCH 09/28] TODOs refactor --- .github/workflows/todos.yml | 2 +- inpost/api.py | 20 ++++++++++++-------- inpost/static/notifications.py | 6 ++++-- inpost/static/parcels.py | 9 ++++++--- inpost/static/statuses.py | 10 +++++++--- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.github/workflows/todos.yml b/.github/workflows/todos.yml index 81d3b54..322ce7c 100644 --- a/.github/workflows/todos.yml +++ b/.github/workflows/todos.yml @@ -8,4 +8,4 @@ jobs: steps: - uses: "actions/checkout@v3" - name: "TODO to Issue" - uses: "alstr/todo-to-issue-action@master" \ No newline at end of file + uses: "alstr/todo-to-issue-action@v4" \ No newline at end of file diff --git a/inpost/api.py b/inpost/api.py index c35d0ab..1943cf0 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -476,9 +476,8 @@ async def get_parcels( return list(_parcels) if not parse else [Parcel(parcel_data=data, logger=self._log) for data in _parcels] - async def get_multi_compartment( - self, multi_uuid: str | int, parse: bool = False - ) -> dict | List[Parcel]: # TODO: do docs + async def get_multi_compartment(self, multi_uuid: str | int, parse: bool = False) -> dict | List[Parcel]: + # TODO: Create documentation if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -833,7 +832,8 @@ async def get_parcel_points( raise UnidentifiedAPIError(reason=resp) - async def blik_status(self) -> bool: # TODO: do docs + async def blik_status(self) -> bool: + # TODO: Create documentation if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -863,7 +863,8 @@ async def create_parcel( sender: Sender, receiver: Receiver, delivery_point: Point, - ) -> None | dict: # TODO: do docs + ) -> None | dict: + # TODO: Create documentation if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -894,7 +895,8 @@ async def create_parcel( async def create_blik_session( self, amount: float | str, shipment_number: str, currency: str = "PLN" - ) -> None | dict: # TODO: do docs + ) -> None | dict: + # TODO: Create documentation if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -929,7 +931,8 @@ async def validate_send( parcel_obj: SentParcel | None = None, location: dict | None = None, drop_off_point: str | None = None, - ) -> SentParcel: # TODO: do docs + ) -> SentParcel: + # TODO: Create documentation if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -1145,7 +1148,8 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: raise UnidentifiedAPIError(reason=resp) - async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: # TODO: do docs + async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: + # TODO: Create documentation self._log.info("getting parcel friends") if not self.auth_token: diff --git a/inpost/static/notifications.py b/inpost/static/notifications.py index 75ae615..6b16d6c 100644 --- a/inpost/static/notifications.py +++ b/inpost/static/notifications.py @@ -1,8 +1,10 @@ from arrow import Arrow, get -class Notification: # TODO: do docs - def __init__(self, notification_data): # TODO: do docs +class Notification: + # TODO: Create documentation + def __init__(self, notification_data): + # TODO: Create documentation self.id: str = notification_data.get("id", None) self.type: str = notification_data.get("type", None) self.action: str = notification_data.get("action", None) diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index 64ffaea..7cfb794 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -222,7 +222,8 @@ def compartment_status(self) -> CompartmentActualStatus | None: def compartment_status(self, status) -> None: self._log.debug(f"setting compartment status with {status}") if self._compartment_properties is None: - return # TODO: think out + # TODO: Need to rethink what should I return + return if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") @@ -301,7 +302,8 @@ def has_airsensor(self) -> bool | None: # return -class ReturnParcel(BaseParcel): # TODO: see properties and create +class ReturnParcel(BaseParcel): + # TODO: Prepare properties required to ease up access def __init__(self, parcel_data: dict, logger: logging.Logger): super().__init__(parcel_data, logger) self.uuid: str = parcel_data["uuid"] @@ -316,7 +318,8 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): self.form_type: str = parcel_data["formType"] -class SentParcel(BaseParcel): # TODO: check properties +class SentParcel(BaseParcel): + # TODO: Recheck properties def __init__(self, parcel_data: dict, logger: logging.Logger): super().__init__(parcel_data, logger) self.origin_system: str = parcel_data.get("originSystem", None) diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index d740cba..43daed1 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -120,9 +120,10 @@ class ParcelType(ParcelBase): class PointType(ParcelBase): """:class: `Enum` that holds point types""" + # TODO: get known what does superpop stand for UNKNOWN = "UNNKOWN DATA" PL = "Paczkomat" - parcel_locker_superpop = "some paczkomat or pok stuff" # TODO: get known what does superpop stand for + parcel_locker_superpop = "some paczkomat or pok stuff" POK = "Mobilny punkt obsługi klienta" POP = "Punkt odbioru paczki" @@ -130,6 +131,7 @@ class PointType(ParcelBase): class ParcelPointOperations(ParcelBase): """:class: `Enum` that holds parcel operation types""" + # TODO: Probably missing something, recheck needed UNKNOWN = "UNNKOWN DATA" CREATE = "c2x-target" SEND = "remote-send" @@ -185,12 +187,14 @@ class ParcelStatus(ParcelBase): CANCELED_REDIRECT_TO_BOX = "Anulowano przekierowanie do paczkomatu" # TODO: translate from app -class DeliveryType(ParcelBase): # TODO: look for more types +class DeliveryType(ParcelBase): + # TODO: look for more types UNKNOWN = "UNKNOWN DATA" BOX_MACHINE = "Paczkomat" -class ReturnsStatus(ParcelBase): # TODO: translate from app and fill missing ones +class ReturnsStatus(ParcelBase): + # TODO: translate from app and fill missing ones ACCEPTED = "Zaakceptowano" USED = "Nadano" DELIVERED = "Dostarczono" From 80fe7b5d7c92142de0e56ba7340a8f1d7f979a76 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 20:52:56 +0200 Subject: [PATCH 10/28] done some todos, update README.md, new exception --- .flake8 | 4 +- .gitignore | 8 ++- README.md | 8 ++- inpost/api.py | 34 ++++++++- inpost/static/exceptions.py | 6 ++ inpost/static/friends.py | 1 + inpost/static/parcels.py | 136 ++++++++++++------------------------ pyproject.toml | 4 ++ 8 files changed, 102 insertions(+), 99 deletions(-) diff --git a/.flake8 b/.flake8 index 6ce657e..ef45e16 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] max-line-length = 120 -max-complexity = 7 +max-complexity = 12 select = B,C,E,F,W,T4,B9 -ignore = E501, E731, W503, F401, F403, C901 +ignore = E501, E731, W503, F401, F403 exclude = docs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 180d189..75b4264 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,10 @@ /.mypy_cache/ /dist/ /venv/ -/tests/.pytest_cache/ +/.idea/vcs.xml +/.idea/jsonSchemas.xml +/.idea/inpost-python.iml +/.idea/modules.xml +/.idea/inspectionProfiles/profiles_settings.xml +/.idea/.gitignore +/.idea/misc.xml diff --git a/README.md b/README.md index 6ce9f50..b687cd3 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,19 @@ Fully async Inpost library using Python 3.10. ```python from inpost.api import Inpost -inp = await Inpost.from_phone_number('555333444') +inp = Inpost('555333444') await inp.send_sms_code(): ... if await inp.confirm_sms_code(123321): print('Congratulations, you initialized successfully!') ``` +## Before you contribute + +Install linters and checkers, unlinted pull requests will not be approved +```commandline +poetry run pre-commit install +``` ## Authors diff --git a/inpost/api.py b/inpost/api.py index 1943cf0..7954d1c 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -833,7 +833,13 @@ async def get_parcel_points( raise UnidentifiedAPIError(reason=resp) async def blik_status(self) -> bool: - # TODO: Create documentation + """Checks if user has active blik session + + + :return: True if user has no active blik sessions else False + :rtype: bool + :raises NotAuthenticatedError: User not authenticated in inpost service + """ if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -896,7 +902,19 @@ async def create_parcel( async def create_blik_session( self, amount: float | str, shipment_number: str, currency: str = "PLN" ) -> None | dict: - # TODO: Create documentation + """Creates blik session for sending parcel + + :param amount: amount of money that has to be paid + :type amount: float | str + :param shipment_number: shipment number of parcel that is being sent + :type shipment_number: str + :param currency: currency of `amount` + :type currency: str + :return: True if user has no active blik sessions else False + :rtype: bool + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnidentifiedAPIError: Unexpected thing happened + """ if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -1149,7 +1167,17 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: raise UnidentifiedAPIError(reason=resp) async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: - # TODO: Create documentation + """Fetches parcel friends + + :param shipment_number: shipment number of parcel that friends are fetched + :type shipment_number: int | str + :param parse: switch for parsing response + :type parse: bool + :return: dict containing friends data + :rtype: dict + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnidentifiedAPIError: Unexpected thing happened + """ self._log.info("getting parcel friends") if not self.auth_token: diff --git a/inpost/static/exceptions.py b/inpost/static/exceptions.py index bf8f8e3..77a78c3 100644 --- a/inpost/static/exceptions.py +++ b/inpost/static/exceptions.py @@ -40,6 +40,12 @@ class NoParcelError(BaseInpostError): pass +class UnknownStatusError(BaseInpostError): + """Is raised when no status matches""" + + pass + + class UnidentifiedParcelError(BaseInpostError): """Is raised when no other :class:`Parcel` error match""" diff --git a/inpost/static/friends.py b/inpost/static/friends.py index eba3312..24eb54b 100644 --- a/inpost/static/friends.py +++ b/inpost/static/friends.py @@ -4,6 +4,7 @@ class Friend: + # TODO: Create documentation def __init__(self, friend_data, logger: logging.Logger): self.uuid: str = friend_data["uuid"] if "uuid" in friend_data else None self.phone_number: str = friend_data["phoneNumber"] diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index 7cfb794..30832d4 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -6,6 +6,7 @@ import qrcode from arrow import arrow, get +from inpost.static.exceptions import UnknownStatusError from inpost.static.statuses import ( CompartmentActualStatus, ParcelCarrierSize, @@ -215,21 +216,21 @@ def compartment_status(self) -> CompartmentActualStatus | None: self._log.debug("got compartment status") return self._compartment_properties.status if self._compartment_properties else None - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_status.setter def compartment_status(self, status) -> None: self._log.debug(f"setting compartment status with {status}") if self._compartment_properties is None: - # TODO: Need to rethink what should I return + self._log.warning("tried to assign status to empty _compartment_properties") return if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") self._compartment_properties.status = status - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_open_data(self) -> dict | None: @@ -239,7 +240,7 @@ def compartment_open_data(self) -> dict | None: :rtype: dict""" self._log.debug("getting compartment open data") if self.receiver is None: - return None # TODO: think out + return None if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment open data") @@ -249,7 +250,7 @@ def compartment_open_data(self) -> dict | None: "receiverPhoneNumber": self.receiver.phone_number, } - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -260,7 +261,7 @@ def mocked_location(self) -> dict | None: :rtype: dict | None""" self._log.debug("getting mocked location") if self.pickup_point is None: - return None # TODO: think out + return None if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got mocked location") return { @@ -269,7 +270,7 @@ def mocked_location(self) -> dict | None: "accuracy": round(random.uniform(1, 4), 1), } - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -283,6 +284,7 @@ def is_multicompartment(self) -> bool: @property def is_main_multicompartment(self) -> bool | None: """Specifies if parcel is main parcel in multi compartment + :return: True if parcel is in multicompartment :rtype: bool""" if self.is_multicompartment and self.multi_compartment is not None: @@ -297,13 +299,10 @@ def has_airsensor(self) -> bool | None: return None - # @property - # def get_from_multicompartment(self): - # return - class ReturnParcel(BaseParcel): # TODO: Prepare properties required to ease up access + # TODO: Create documentation def __init__(self, parcel_data: dict, logger: logging.Logger): super().__init__(parcel_data, logger) self.uuid: str = parcel_data["uuid"] @@ -320,6 +319,7 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): class SentParcel(BaseParcel): # TODO: Recheck properties + # TODO: Create documentation def __init__(self, parcel_data: dict, logger: logging.Logger): super().__init__(parcel_data, logger) self.origin_system: str = parcel_data.get("originSystem", None) @@ -359,54 +359,27 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): self.unlabeled: bool = parcel_data.get("unlabeled", None) self.is_end_off_week_collection: bool | None = parcel_data.get("endOfWeekCollection", None) self.status: ParcelStatus | None = ParcelStatus[parcel_data.get("status")] - - @property - def open_code(self) -> int | None: - """Returns an open code for :class:`Parcel` - - :return: Open code for :class:`Parcel` - :rtype: int""" - self._log.debug("getting open code") - if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug("got open code") - return self.quick_send_code - - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") - return None - - @property - def generate_qr_image(self) -> BytesIO | None: - """Returns a QR image for :class:`Parcel` - - :return: QR image for :class:`Parcel` - :rtype: BytesIO""" - self._log.debug("generating qr image") - if self.shipment_type == ParcelShipmentType.parcel and self._qr_code is not None: - self._log.debug("got qr image") - return self._qr_code.qr_image - - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") - return None + self._compartment_properties: CompartmentProperties | None = None @property def compartment_properties(self): - """Returns a compartment properties for :class:`Parcel` + """Returns a compartment properties for :class:`SentParcel` - :return: Compartment properties for :class:`Parcel` + :return: Compartment properties for :class:`SentParcel` :rtype: CompartmentProperties""" - self._log.debug("getting comparment properties") + self._log.debug("getting compartment properties") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment properties") return self._compartment_properties - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_properties.setter def compartment_properties(self, compartmentproperties_data: dict): - """Set compartment properties for :class:`Parcel` + """Set compartment properties for :class:`SentParcel` - :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`Parcel` + :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`SentParcel` :type compartmentproperties_data: dict""" self._log.debug(f"setting compartment properties with {compartmentproperties_data}") if self.shipment_type == ParcelShipmentType.parcel: @@ -415,37 +388,41 @@ def compartment_properties(self, compartmentproperties_data: dict): compartmentproperties_data=compartmentproperties_data, logger=self._log ) - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_location(self): - """Returns a compartment location for :class:`Parcel` + """Returns a compartment location for :class:`SentParcel` - :return: Compartment location for :class:`Parcel` + :return: Compartment location for :class:`SentParcel` :rtype: CompartmentLocation""" self._log.debug("getting compartment location") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment location") return self._compartment_properties.location if self._compartment_properties else None - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_location.setter def compartment_location(self, location_data: dict): - """Set compartment location for :class:`Parcel` + """Set compartment location for :class:`SentParcel` :param location_data: :class:`dict` containing `compartment properties` data for :class:`Parcel` :type location_data: dict""" self._log.debug(f"setting compartment location with {location_data}") + if self._compartment_properties is None: + self._log.warning("tried to assign location to empty _compartment_properties") + return + if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment location set") self._compartment_properties.location = location_data - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_status(self) -> CompartmentActualStatus | None: - """Returns a compartment status for :class:`Parcel` + """Returns a compartment status for :class:`SentParcel` :return: Compartment status for :class:`Parcel` :rtype: CompartmentActualStatus""" @@ -455,41 +432,24 @@ def compartment_status(self) -> CompartmentActualStatus | None: self._log.debug("got compartment status") return self._compartment_properties.status if self._compartment_properties else None - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_status.setter def compartment_status(self, status): + # TODO: Create documentation self._log.debug(f"setting compartment status with {status}") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") self._compartment_properties.status = status - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") - - @property - def compartment_open_data(self): - """Returns a compartment open data for :class:`Parcel` - - :return: dict containing compartment open data for :class:`Parcel` - :rtype: dict""" - self._log.debug("getting compartment open data") - if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug("got compartment open data") - return { - "shipmentNumber": self.shipment_number, - "openCode": self._open_code, - "receiverPhoneNumber": self.receiver.phone_number, - } - - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") - return None + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def mocked_location(self): - """Returns a mocked location for :class:`Parcel` + """Returns a mocked location for :class:`SentParcel` - :return: dict containing mocked location for :class:`Parcel` + :return: dict containing mocked location for :class:`SentParcel` :rtype: dict""" self._log.debug("getting mocked location") if self.shipment_type == ParcelShipmentType.parcel: @@ -500,13 +460,9 @@ def mocked_location(self): "accuracy": round(random.uniform(1, 4), 1), } - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None - @property - def has_airsensor(self) -> bool | None: - return self.pickup_point.air_sensor_data is not None if self.pickup_point else None - class Receiver: """Object representation of :class:`Parcel` receiver @@ -612,7 +568,7 @@ def __init__(self, point_data: dict, logger: logging.Logger): self._log.debug("created") if ParcelDeliveryType.UNKNOWN in self.type: - self._log.debug(f'unknown delivery type: {point_data["type"]}') + self._log.warning(f'unknown delivery type: {point_data["type"]}') def __repr__(self): fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") @@ -688,13 +644,6 @@ class Payment: :type logger: logging.Logger""" def __init__(self, payment_details: dict, logger: logging.Logger): - # self.paid: bool = payment_details.get("paid") - # self.total_price: float = payment_details.get("totalPrice") - # self.insurance_price: float = payment_details.get("insurancePrice") - # self.end_of_week_collection_price: float = payment_details.get("endOfWeekCollectionPrice") - # self.shipment_discounted: bool = payment_details.get("shipmentDiscounted") - # self.transaction_status: str = payment_details.get("transactionStatus") - self.paid = payment_details.get("paid") self.total_price = payment_details.get("totalPrice") self.insurance_price = payment_details.get("insurancePrice") @@ -799,8 +748,13 @@ def __init__(self, eventlog_data: dict, logger: logging.Logger): :type eventlog_data: dict :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger + :raises UnknownStatusError: Unknown status in EventLog """ self.type: str = eventlog_data["type"] + self.date: arrow = get(eventlog_data["date"]) + self.details: dict | None = eventlog_data.get("details") + self._log: logging.Logger = logger.getChild(self.__class__.__name__) + if self.type == "PARCEL_STATUS": self.name = ParcelStatus[eventlog_data.get("name")] elif self.type == "RETURN_STATUS": @@ -808,15 +762,13 @@ def __init__(self, eventlog_data: dict, logger: logging.Logger): elif self.type == "PAYMENT": self.name = PaymentStatus[eventlog_data.get("name")] else: - ... - self.date: arrow = get(eventlog_data["date"]) - self.details: dict | None = eventlog_data.get("details") + self._log.warning(f'Unknown status type {eventlog_data.get("name")}!') + raise UnknownStatusError(reason=eventlog_data.get("name")) - self._log: logging.Logger = logger.getChild(self.__class__.__name__) self._log.debug("created") if self.name == ParcelStatus.UNKNOWN or self.name == ReturnsStatus.UNKNOWN: - self._log.debug(f'unknown {self.type}: {eventlog_data["name"]}') + self._log.warning(f'unknown {self.type}: {eventlog_data["name"]}') def __repr__(self): fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != "_log") diff --git a/pyproject.toml b/pyproject.toml index 96fc7e2..1f599f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,10 @@ classifiers = [ "Development Status :: 3 - Alpha" ] +[tool.poetry.dev-dependencies] +pre-commit = "^3.3.3" +pytest = "^7.4.0" + [tool.poetry.dependencies] python = "^3.10" aiohttp = "^3.8.1" From df6099509507b9d5a5e5908441fe7530e44b1e5b Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 21:22:56 +0200 Subject: [PATCH 11/28] README.md update --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b687cd3..1bd27b8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ +[![CodeFactor](https://www.codefactor.io/repository/github/ifossa/inpost-python/badge)](https://www.codefactor.io/repository/github/ifossa/inpost-python) +![Code Quality](https://github.com/ifossa/inpost-python/actions/workflows/lint.yml/badge.svg?barnch=main) +![Todos](https://github.com/ifossa/inpost-python/actions/workflows/todos.yml/badge.svg?barnch=main) # Inpost Python Fully async Inpost library using Python 3.10. - - ## Documentation [Readthedocs.io](https://inpost-python.readthedocs.io/en/latest/) @@ -26,6 +27,8 @@ if await inp.confirm_sms_code(123321): ## Before you contribute +![Contributors](https://contrib.rocks/image?repo=ifossa/inpost-python) + Install linters and checkers, unlinted pull requests will not be approved ```commandline poetry run pre-commit install @@ -43,3 +46,7 @@ This project is used by the following repos: [Inpost Telegram Bot](https://github.com/loboda4450/inpost-telegram-bot) + + +## 😂 Here is a random joke that'll make you laugh! +![Jokes Card](https://readme-jokes.vercel.app/api) \ No newline at end of file From 3c417b600d77b64d078c81b7672fab4f55805710 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sat, 24 Jun 2023 23:16:03 +0200 Subject: [PATCH 12/28] Added missing docstrings --- inpost/api.py | 138 ++++++++++++++--- inpost/static/friends.py | 32 +++- inpost/static/notifications.py | 25 +++- inpost/static/parcels.py | 260 ++++++++++++++++++++++++++------- 4 files changed, 372 insertions(+), 83 deletions(-) diff --git a/inpost/api.py b/inpost/api.py index 7954d1c..f658c91 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -67,7 +67,9 @@ def __init__(self, phone_number): :param phone_number: phone number :type phone_number: str - :raises PhoneNumberError: Wrong phone number format or is not digit""" + :raises PhoneNumberError: Wrong phone number format or is not digit + """ + if isinstance(phone_number, int): phone_number = str(phone_number) @@ -129,7 +131,8 @@ async def request( :raises UnauthorizedError: User not authenticated in inpost service :raises NotFoundError: URL not found :raises UnidentifiedAPIError: Unexpected things happened - :raises ValueError: Doubled authorization header in request""" + :raises ValueError: Doubled authorization header in request + """ if auth and headers: if "Authorization" in headers: @@ -170,7 +173,9 @@ def from_dict(cls, data: dict) -> "Inpost": :param data: User's Inpost data (e.g. phone_number, sms_code, auth_token, refr_token) :type data: dict :return: Inpost object from provided dict - :rtype: Inpost""" + :rtype: Inpost + """ + inp = cls(phone_number=data["phone_number"]) inp.sms_code = data["sms_code"] inp.auth_token = data["auth_token"] @@ -186,6 +191,7 @@ async def send_sms_code(self) -> bool: :rtype: bool :raises PhoneNumberError: Missing phone number """ + if not self.phone_number: # can't log it cuz if there's no phone number no logger initialized @shrug raise PhoneNumberError("Phone number missing") @@ -256,6 +262,7 @@ async def refresh_token(self) -> bool: :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened """ + self._log.info("refreshing token") if not self.refr_token: @@ -292,7 +299,9 @@ async def logout(self) -> bool: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info("logging out") if not self.auth_token: @@ -317,7 +326,9 @@ async def disconnect(self) -> bool: """Simplified method to logout and close user's session :return: True if user is logged out and session is closed else False - :raises NotAuthenticatedError: User not authenticated in inpost service""" + :raises NotAuthenticatedError: User not authenticated in inpost service + """ + self._log.info("disconnecting") if not self.auth_token: self._log.error("authorization token missing") @@ -350,6 +361,7 @@ async def get_parcel( :raises UnidentifiedAPIError: Unexpected thing happened :raises ParcelTypeError: Unknown parcel type selected """ + self._log.info(f"getting parcel with shipment number: {shipment_number}") if not self.auth_token: @@ -421,7 +433,9 @@ async def get_parcels( :raises ParcelTypeError: Unknown parcel type selected :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info("getting parcels") if not self.auth_token: @@ -477,7 +491,18 @@ async def get_parcels( return list(_parcels) if not parse else [Parcel(parcel_data=data, logger=self._log) for data in _parcels] async def get_multi_compartment(self, multi_uuid: str | int, parse: bool = False) -> dict | List[Parcel]: - # TODO: Create documentation + """Fetches all available parcels for set `Inpost.phone_number` and optionally filters them + + :param multi_uuid: multicompartment uuid + :type multi_uuid: str | int + :param parse: switch for parsing response + :type parse: bool + :return: multicompartment parcels + :rtype: dict | List[Parcel] + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnidentifiedAPIError: Unexpected thing happened + """ + if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -523,7 +548,8 @@ async def collect_compartment_properties( :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened - .. warning:: you must fill in only one parameter - shipment_number or parcel_obj!""" + .. warning:: you must fill in only one parameter - shipment_number or parcel_obj! + """ parcel_obj_: Parcel | None = None if shipment_number is None is parcel_obj: @@ -573,7 +599,9 @@ async def open_compartment(self, parcel_obj: Parcel) -> bool: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info(f"opening compartment for {parcel_obj.shipment_number}") if not self.auth_token: @@ -610,7 +638,9 @@ async def check_compartment_status( :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info(f"checking compartment status for {parcel_obj.shipment_number}") if not self.auth_token: @@ -647,7 +677,9 @@ async def terminate_collect_session(self, parcel_obj: Parcel) -> bool: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info(f"terminating collect session for {parcel_obj.shipment_number}") if not self.auth_token: @@ -689,7 +721,8 @@ async def collect( :raises NotFoundError: Phone number not found :raises UnidentifiedAPIError: Unexpected thing happened - .. warning:: you must fill in only one parameter - shipment_number or parcel_obj!""" + .. warning:: you must fill in only one parameter - shipment_number or parcel_obj! + """ if shipment_number and parcel_obj: self._log.error("shipment_number and parcel_obj filled in") @@ -721,7 +754,9 @@ async def close_compartment(self, parcel_obj: Parcel) -> bool: :param parcel_obj: Parcel object :type parcel_obj: Parcel :return: True if compartment status is closed and successfully terminates user's session else False - :rtype: bool""" + :rtype: bool + """ + self._log.info(f"closing compartment for {parcel_obj.shipment_number}") if await self.check_compartment_status(expected_status=CompartmentExpectedStatus.CLOSED, parcel_obj=parcel_obj): @@ -740,7 +775,9 @@ async def reopen_compartment(self, parcel_obj: Parcel) -> bool: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info(f"reopening compartment for {parcel_obj.shipment_number}") if not self.auth_token: @@ -793,6 +830,7 @@ async def get_parcel_points( :raises MissingParamsError: none of required query and location params are filled :raises UnidentifiedAPIError: Unexpected thing happened """ + self._log.info("getting parcel prices") if not self.auth_token: @@ -840,6 +878,7 @@ async def blik_status(self) -> bool: :rtype: bool :raises NotAuthenticatedError: User not authenticated in inpost service """ + if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -869,8 +908,27 @@ async def create_parcel( sender: Sender, receiver: Receiver, delivery_point: Point, - ) -> None | dict: - # TODO: Create documentation + ) -> dict | None: + """Fetches parcel points for inpost services + + :param delivery_type: a way parcel will be delivered + :type delivery_type: DeliveryType + :param parcel_size: size of parcel + :type parcel_size: ParcelLockerSize | ParcelCarrierSize + :param price: price for chosen parcel size + :type price: float | str + :param sender: parcel sender + :type sender: Sender + :param receiver: parcel receiver + :type receiver: Receiver + :param delivery_point: parcel delivery point + :type delivery_point: Point + :return: :class:`dict` with response + :rtype: dict | None + :raises UnidentifiedAPIError: Unexpected thing happened + :raises NotAuthenticatedError: User not authenticated in inpost service + """ + if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -915,6 +973,7 @@ async def create_blik_session( :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnidentifiedAPIError: Unexpected thing happened """ + if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -945,12 +1004,31 @@ async def create_blik_session( async def validate_send( self, + drop_off_point: str, shipment_number: str | None = None, parcel_obj: SentParcel | None = None, location: dict | None = None, - drop_off_point: str | None = None, ) -> SentParcel: - # TODO: Create documentation + """Validates sending parcel + + :param drop_off_point: parcel machine codename where you want to drop-opp parcel + :type drop_off_point: str + :param shipment_number: sent parcel shipment number + :type shipment_number: str | None + :param parcel_obj: sent parcel object + :type parcel_obj: SentParcel | None + :param location: ... + :type location: dict | None + :return: Sent parcel with filled compartment properties + :rtype: SentParcel + :raises SingleParamError: Fields shipment_number and parcel_obj filled in but only one of them is required + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises NoParcelError: Could not get parcel object from provided data + :raises MissingParamsError: None of required shipment number and parcel object params are filled + :raises ValueError: Missing drop-off point + :raises UnidentifiedAPIError: Unexpected thing happened + """ + if not self.auth_token: self._log.error("authorization token missing") raise NotAuthenticatedError(reason="Not logged in") @@ -1011,6 +1089,7 @@ async def open_send_compartment(self, parcel_obj: Parcel) -> bool: :rtype: bool :raises NotAuthenticatedError: User not logged in (missing auth_token) """ + self._log.info(f"opening compartment for {parcel_obj.shipment_number}") if not self.auth_token: @@ -1042,6 +1121,7 @@ async def reopen_send_compartment(self, parcel_obj: SentParcel) -> bool: :rtype: bool :raises NotAuthenticatedError: User not logged in (missing auth_token) """ + self._log.info(f"reopening send compartment for {parcel_obj.shipment_number}") if not self.auth_token: @@ -1078,7 +1158,9 @@ async def check_send_compartment_status( :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info(f"checking compartment status for {parcel_obj.shipment_number}") if not self.auth_token: @@ -1113,7 +1195,9 @@ async def get_prices(self) -> dict: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info("getting parcel prices") if not self.auth_token: @@ -1145,7 +1229,9 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" + :raises UnidentifiedAPIError: Unexpected thing happened + """ + self._log.info("getting friends") if not self.auth_token: @@ -1178,6 +1264,7 @@ async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> d :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnidentifiedAPIError: Unexpected thing happened """ + self._log.info("getting parcel friends") if not self.auth_token: @@ -1229,7 +1316,8 @@ async def add_friend(self, name: str, phone_number: str | int, code: str | int, :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: User with specified phone number not found :raises UnidentifiedAPIError: Unexpected thing happened - :raises ValueError: Name length exceeds 20 characters""" + :raises ValueError: Name length exceeds 20 characters + """ self._log.info("adding user friend") @@ -1306,7 +1394,8 @@ async def remove_friend(self, uuid: str | None, name: str | None, phone_number: :raises NotFoundError: Friend not found :raises UnidentifiedAPIError: Unexpected thing happened :raises ValueError: Name length exceeds 20 characters - :raises MissingParamsError: none of required uuid, name or phone_number params are filled""" + :raises MissingParamsError: none of required uuid, name or phone_number params are filled + """ self._log.info("removing user friend") @@ -1361,7 +1450,8 @@ async def update_friend(self, uuid: str | None, phone_number: str | int | None, :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Friend not found :raises UnidentifiedAPIError: Unexpected thing happened - :raises ValueError: Name length exceeds 20 characters""" + :raises ValueError: Name length exceeds 20 characters + """ self._log.info("updating user friend") diff --git a/inpost/static/friends.py b/inpost/static/friends.py index 24eb54b..65c0f9e 100644 --- a/inpost/static/friends.py +++ b/inpost/static/friends.py @@ -4,8 +4,23 @@ class Friend: - # TODO: Create documentation - def __init__(self, friend_data, logger: logging.Logger): + """Object representation of :class:`inpost.api.Inpost` friend + + :param friend_data: :class:`dict` containing all friend data + :type friend_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + + def __init__(self, friend_data: dict, logger: logging.Logger): + """Constructor method + + :param friend_data: :class:`dict` containing all friend data + :type friend_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.uuid: str = friend_data["uuid"] if "uuid" in friend_data else None self.phone_number: str = friend_data["phoneNumber"] self.name: str = friend_data["name"] @@ -20,7 +35,18 @@ def __init__(self, friend_data, logger: logging.Logger): self._log.debug(f"created friendship with {self.name}") @classmethod - def from_invitation(cls, invitation_data, logger: logging.Logger): + def from_invitation(cls, invitation_data: dict, logger: logging.Logger): + """`Classmethod` to initialize :class:`Friend` from incitation. + Should be used when retrieving configuration from database. + + :param invitation_data: :class:`dict` containing all friend data + :type invitation_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + :return: Friend object from provided invitation + :rtype: Friend + """ + return cls( friend_data={ "uuid": invitation_data["friend"]["uuid"], diff --git a/inpost/static/notifications.py b/inpost/static/notifications.py index 6b16d6c..47944c4 100644 --- a/inpost/static/notifications.py +++ b/inpost/static/notifications.py @@ -1,11 +1,28 @@ +import logging + from arrow import Arrow, get class Notification: - # TODO: Create documentation - def __init__(self, notification_data): - # TODO: Create documentation + """Object representation of :class:`inpost.api.Inpost` notification + + :param notification_data: :class:`dict` containing all notification data + :type notification_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + + def __init__(self, notification_data: dict, logger: logging.Logger): + """Constructor method + + :param notification_data: :class:`dict` containing all notification data + :type notification_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.id: str = notification_data.get("id", None) + self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.id}") self.type: str = notification_data.get("type", None) self.action: str = notification_data.get("action", None) self.date: Arrow = get(notification_data.get("date")) if "date" in notification_data else None @@ -15,3 +32,5 @@ def __init__(self, notification_data): self.read: bool = notification_data.get("read", None) self.extra_params: dict = notification_data.get("extraParams", None) self.parcel_type: str = notification_data.get("parcelType", None) + + self._log.debug(f"created notification with id {self.id}") diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index 30832d4..e53ef46 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -23,6 +23,14 @@ class BaseParcel: + """Object representation of :class:`inpost.api.Inpost` parcel base. Gather things shared by all parcel types. + + :param parcel_data: :class:`dict` containing all parcel data + :type parcel_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + def __init__(self, parcel_data: dict, logger: logging.Logger): self.shipment_number = parcel_data.get("shipmentNumber") self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.shipment_number}") @@ -40,7 +48,8 @@ class Parcel(BaseParcel): :param parcel_data: :class:`dict` containing all parcel data :type parcel_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, parcel_data: dict, logger: logging.Logger): """Constructor method @@ -50,6 +59,7 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): :param logger: logger instance :type logger: logging.Logger """ + super().__init__(parcel_data, logger) self._log: logging.Logger = logger.getChild(f"{self.__class__.__name__}.{self.shipment_number}") self.shipment_type: ParcelShipmentType = ParcelShipmentType[parcel_data.get("shipmentType")] @@ -125,7 +135,9 @@ def open_code(self) -> str | None: """Returns an open code for :class:`Parcel` :return: Open code for :class:`Parcel` - :rtype: str""" + :rtype: str + """ + self._log.debug("getting open code") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got open code") @@ -139,7 +151,9 @@ def generate_qr_image(self) -> BytesIO | None: """Returns a QR image for :class:`Parcel` :return: QR image for :class:`Parcel` - :rtype: BytesIO""" + :rtype: BytesIO + """ + self._log.debug("generating qr image") if self.shipment_type == ParcelShipmentType.parcel and self._qr_code is not None: self._log.debug("got qr image") @@ -153,7 +167,9 @@ def compartment_properties(self): """Returns a compartment properties for :class:`Parcel` :return: Compartment properties for :class:`Parcel` - :rtype: CompartmentProperties""" + :rtype: CompartmentProperties + """ + self._log.debug("getting comparment properties") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment properties") @@ -167,7 +183,9 @@ def compartment_properties(self, compartmentproperties_data: dict): """Set compartment properties for :class:`Parcel` :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`Parcel` - :type compartmentproperties_data: dict""" + :type compartmentproperties_data: dict + """ + self._log.debug(f"setting compartment properties with {compartmentproperties_data}") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment properties set") @@ -182,7 +200,9 @@ def compartment_location(self): """Returns a compartment location for :class:`Parcel` :return: Compartment location for :class:`Parcel` - :rtype: CompartmentLocation""" + :rtype: CompartmentLocation + """ + self._log.debug("getting compartment location") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment location") @@ -196,7 +216,9 @@ def compartment_location(self, location_data: dict): """Set compartment location for :class:`Parcel` :param location_data: :class:`dict` containing `compartment properties` data for :class:`Parcel` - :type location_data: dict""" + :type location_data: dict + """ + self._log.debug(f"setting compartment location with {location_data}") if self.shipment_type == ParcelShipmentType.parcel and self._compartment_properties is not None: self._log.debug("compartment location set") @@ -209,7 +231,9 @@ def compartment_status(self) -> CompartmentActualStatus | None: """Returns a compartment status for :class:`Parcel` :return: Compartment status for :class:`Parcel` - :rtype: CompartmentActualStatus""" + :rtype: CompartmentActualStatus + """ + self._log.debug("getting compartment status") if self.shipment_type == ParcelShipmentType.parcel: @@ -221,6 +245,12 @@ def compartment_status(self) -> CompartmentActualStatus | None: @compartment_status.setter def compartment_status(self, status) -> None: + """Set compartment location for :class:`SentParcel` + + :param status: compartment properties status for :class:`SentParcel` + :type status: str | CompartmentActualStatus + """ + self._log.debug(f"setting compartment status with {status}") if self._compartment_properties is None: self._log.warning("tried to assign status to empty _compartment_properties") @@ -237,7 +267,9 @@ def compartment_open_data(self) -> dict | None: """Returns a compartment open data for :class:`Parcel` :return: dict containing compartment open data for :class:`Parcel` - :rtype: dict""" + :rtype: dict + """ + self._log.debug("getting compartment open data") if self.receiver is None: return None @@ -258,7 +290,9 @@ def mocked_location(self) -> dict | None: """Returns a mocked location for :class:`Parcel` :return: dict containing mocked location for :class:`Parcel or None if wrong parcel shipment type` - :rtype: dict | None""" + :rtype: dict | None + """ + self._log.debug("getting mocked location") if self.pickup_point is None: return None @@ -278,7 +312,9 @@ def is_multicompartment(self) -> bool: """Specifies if parcel is in multi compartment :return: True if parcel is in multicompartment - :rtype: bool""" + :rtype: bool + """ + return self.multi_compartment is not None @property @@ -286,7 +322,9 @@ def is_main_multicompartment(self) -> bool | None: """Specifies if parcel is main parcel in multi compartment :return: True if parcel is in multicompartment - :rtype: bool""" + :rtype: bool + """ + if self.is_multicompartment and self.multi_compartment is not None: return self.multi_compartment.shipment_numbers is not None @@ -302,8 +340,23 @@ def has_airsensor(self) -> bool | None: class ReturnParcel(BaseParcel): # TODO: Prepare properties required to ease up access - # TODO: Create documentation + """Object representation of :class:`inpost.api.Inpost` returned parcel + + :param parcel_data: :class:`dict` containing all parcel data + :type parcel_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + def __init__(self, parcel_data: dict, logger: logging.Logger): + """Constructor method + + :param parcel_data: dict containing parcel data + :type parcel_data: dict + :param logger: logger instance + :type logger: logging.Logger + """ + super().__init__(parcel_data, logger) self.uuid: str = parcel_data["uuid"] self.rma: str = parcel_data["rma"] @@ -319,8 +372,23 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): class SentParcel(BaseParcel): # TODO: Recheck properties - # TODO: Create documentation + """Object representation of :class:`inpost.api.Inpost` sent parcel + + :param parcel_data: :class:`dict` containing all parcel data + :type parcel_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + def __init__(self, parcel_data: dict, logger: logging.Logger): + """Constructor method + + :param parcel_data: dict containing parcel data + :type parcel_data: dict + :param logger: logger instance + :type logger: logging.Logger + """ + super().__init__(parcel_data, logger) self.origin_system: str = parcel_data.get("originSystem", None) self.quick_send_code: int = parcel_data.get("quickSendCode", None) @@ -366,7 +434,9 @@ def compartment_properties(self): """Returns a compartment properties for :class:`SentParcel` :return: Compartment properties for :class:`SentParcel` - :rtype: CompartmentProperties""" + :rtype: CompartmentProperties + """ + self._log.debug("getting compartment properties") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment properties") @@ -380,7 +450,9 @@ def compartment_properties(self, compartmentproperties_data: dict): """Set compartment properties for :class:`SentParcel` :param compartmentproperties_data: :class:`dict` containing compartment properties data for :class:`SentParcel` - :type compartmentproperties_data: dict""" + :type compartmentproperties_data: dict + """ + self._log.debug(f"setting compartment properties with {compartmentproperties_data}") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment properties set") @@ -395,7 +467,9 @@ def compartment_location(self): """Returns a compartment location for :class:`SentParcel` :return: Compartment location for :class:`SentParcel` - :rtype: CompartmentLocation""" + :rtype: CompartmentLocation + """ + self._log.debug("getting compartment location") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment location") @@ -408,7 +482,9 @@ def compartment_location(self): def compartment_location(self, location_data: dict): """Set compartment location for :class:`SentParcel` :param location_data: :class:`dict` containing `compartment properties` data for :class:`Parcel` - :type location_data: dict""" + :type location_data: dict + """ + self._log.debug(f"setting compartment location with {location_data}") if self._compartment_properties is None: self._log.warning("tried to assign location to empty _compartment_properties") @@ -424,9 +500,14 @@ def compartment_location(self, location_data: dict): def compartment_status(self) -> CompartmentActualStatus | None: """Returns a compartment status for :class:`SentParcel` - :return: Compartment status for :class:`Parcel` - :rtype: CompartmentActualStatus""" + :return: Compartment status for :class:`SentParcel` + :rtype: CompartmentActualStatus + """ + self._log.debug("getting compartment status") + if self._compartment_properties is None: + self._log.warning("tried to access empty _compartment_properties") + return None if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment status") @@ -436,8 +517,16 @@ def compartment_status(self) -> CompartmentActualStatus | None: return None @compartment_status.setter - def compartment_status(self, status): - # TODO: Create documentation + def compartment_status(self, status: str | CompartmentActualStatus): + """Set compartment location for :class:`SentParcel` + + :param status: compartment properties status for :class:`SentParcel` + :type status: str | CompartmentActualStatus + """ + if self._compartment_properties is None: + self._log.warning("tried to assign status to empty _compartment_properties") + return + self._log.debug(f"setting compartment status with {status}") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") @@ -450,7 +539,9 @@ def mocked_location(self): """Returns a mocked location for :class:`SentParcel` :return: dict containing mocked location for :class:`SentParcel` - :rtype: dict""" + :rtype: dict + """ + self._log.debug("getting mocked location") if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got mocked location") @@ -465,12 +556,13 @@ def mocked_location(self): class Receiver: - """Object representation of :class:`Parcel` receiver + """Object representation of :class:`BaseParcel` receiver - :param receiver_data: :class:`dict` containing sender data for :class:`Parcel` + :param receiver_data: :class:`dict` containing sender data for :class:`BaseParcel` and it's subclasses :type receiver_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, receiver_data: dict, logger: logging.Logger): """Constructor method @@ -478,7 +570,9 @@ def __init__(self, receiver_data: dict, logger: logging.Logger): :param receiver_data: dict containing receiver data :type receiver_data: dict :param logger: logger instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ + self.email: str = receiver_data["email"] self.phone_number: str = receiver_data["phoneNumber"] self.name: str = receiver_data["name"] @@ -492,12 +586,13 @@ def __repr__(self): class Sender: - """Object representation of :class:`Parcel` sender + """Object representation of :class:`BaseParcel` sender and it's subclasses - :param sender_data: :class:`dict` containing sender data for :class:`Parcel` + :param sender_data: :class:`dict` containing sender data for :class:`BaseParcel` and it's subclasses :type sender_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, sender_data: dict, logger: logging.Logger): """Constructor method @@ -505,7 +600,9 @@ def __init__(self, sender_data: dict, logger: logging.Logger): :param sender_data: dict containing sender data :type sender_data: dict :param logger: logger instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ + self.sender_name: str = sender_data.get("name", None) self.sender_email: str = sender_data.get("email", None) self._log: logging.Logger = logger.getChild(self.__class__.__name__) @@ -521,21 +618,23 @@ def __str__(self) -> str: class Point: - """Object representation of :class:`Parcel` point + """Object representation of :class:`BaseParcel` point and it's subclasses - :param point_data: :class:`dict` containing point data for :class:`Parcel` + :param point_data: :class:`dict` containing point data for :class:`BaseParcel` and it's subclasses :type point_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, point_data: dict, logger: logging.Logger): """Constructor method - :param point_data: :class:`dict` containing point data for :class:`Parcel` + :param point_data: :class:`dict` containing point data for :class:`BaseParcel` and it's subclasses :type point_data: dict :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self._log: logging.Logger = logger.getChild(self.__class__.__name__) self.name: str = point_data["name"] self.latitude: float = point_data["location"]["latitude"] @@ -579,47 +678,76 @@ def __str__(self) -> str: @property def location(self) -> Tuple[float, float]: - """Returns a mocked location for :class:`PickupPoint` + """Returns a mocked location for :class:`Point` + + :return: tuple containing location for :class:`Point` + :rtype: tuple + """ - :return: tuple containing location for :class:`PickupPoint` - :rtype: tuple""" self._log.debug("getting location") return self.latitude, self.longitude class PickupPoint(Point): - """Object representation of :class:`Parcel` pick up point + """Object representation of :class:`BaseParcel` and it's subclasses pick up point - :param point_data: :class:`dict` containing pickup point data for :class:`Parcel` + :param point_data: :class:`dict` containing pickup point data for :class:`BaseParcel` and it's subclasses :type point_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, point_data: dict, logger: logging.Logger): + """Constructor method + + :param point_data: :class:`dict` containing pickup point data for :class:`BaseParcel` and it's subclasses + :type point_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ super().__init__(point_data, logger) class DropOffPoint(Point): - """Object representation of :class:`Parcel` drop off point + """Object representation of :class:`BaseParcel` and it's subclasses drop off point - :param point_data: :class:`dict` containing pickup point data for :class:`Parcel` + :param point_data: :class:`dict` containing pickup point data for :class:`BaseParcel` and it's subclasses :type point_data: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, point_data: dict, logger: logging.Logger): + """Constructor method + + :param point_data: :class:`dict` containing pickup point data for :class:`BaseParcel` and it's subclasses + :type point_data: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + super().__init__(point_data, logger) class DeliveryPoint: - """Object representation of :class: DeliveryPoint + """Object representation of :class:`BaseParcel` and it's subclasses delivery point - :param delivery_point: :class:`dict` containing delivery point data for :class:`DeliveryPoint` + :param delivery_point: :class:`dict` containing delivery point data for :class:`BaseParcel` and it's subclasses delivery point :type delivery_point: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, delivery_point: dict, logger: logging.Logger): + """Constructor method + + :param delivery_point: :class:`dict` containing delivery point data for :class:`BaseParcel` + and it's subclasses delivery point + :type delivery_point: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.name = delivery_point.get("name") self.company_name = delivery_point.get("companyName") self.post_code = delivery_point["address"]["postCode"] if "address" in delivery_point else None @@ -637,13 +765,23 @@ def __repr__(self): class Payment: - """Object representation of :class: Payment - :param payment_details: :class:`dict` containing payment data for :class:`Payment` + """Object representation of :class:`BaseParcel` and it's subclasses payment + + :param payment_details: :class:`dict` containing payment data for :class:`BaseParcel` and it's subclasses payment :type payment_details: dict :param logger: :class:`logging.Logger` parent instance - :type logger: logging.Logger""" + :type logger: logging.Logger + """ def __init__(self, payment_details: dict, logger: logging.Logger): + """Constructor method + + :param payment_details: :class:`dict` containing payment data for :class:`BaseParcel` and it's subclasses payment + :type payment_details: dict + :param logger: :class:`logging.Logger` parent instance + :type logger: logging.Logger + """ + self.paid = payment_details.get("paid") self.total_price = payment_details.get("totalPrice") self.insurance_price = payment_details.get("insurancePrice") @@ -675,6 +813,7 @@ def __init__(self, multicompartment_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self.uuid = multicompartment_data["uuid"] self.shipment_numbers: List[str] | None = ( multicompartment_data["shipmentNumbers"] if "shipmentNumbers" in multicompartment_data else None @@ -707,6 +846,7 @@ def __init__(self, operations_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self.manual_archive: bool = operations_data["manualArchive"] self.auto_archivable_since: arrow | None = ( get(operations_data["autoArchivableSince"]) if "autoArchivableSince" in operations_data else None @@ -750,6 +890,7 @@ def __init__(self, eventlog_data: dict, logger: logging.Logger): :type logger: logging.Logger :raises UnknownStatusError: Unknown status in EventLog """ + self.type: str = eventlog_data["type"] self.date: arrow = get(eventlog_data["date"]) self.details: dict | None = eventlog_data.get("details") @@ -792,6 +933,7 @@ def __init__(self, sharedto_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self.uuid: str = sharedto_data["uuid"] self.name: str = sharedto_data["name"] self.phone_number = sharedto_data["phoneNumber"] @@ -821,6 +963,7 @@ def __init__(self, qrcode_data: str, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self._qr_code = qrcode_data self._log: logging.Logger = logger.getChild(self.__class__.__name__) @@ -836,6 +979,7 @@ def qr_image(self) -> BytesIO: :return: QR Code image :rtype: BytesIO""" + self._log.debug("generating qr image") qr = qrcode.QRCode( version=3, error_correction=qrcode.constants.ERROR_CORRECT_H, box_size=20, border=4, mask_pattern=5 @@ -869,6 +1013,7 @@ def __init__(self, compartmentlocation_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self.name: str = compartmentlocation_data["compartment"]["name"] self.side: str = compartmentlocation_data["compartment"]["location"]["side"] self.column: str = compartmentlocation_data["compartment"]["location"]["column"] @@ -901,6 +1046,7 @@ def __init__(self, compartmentproperties_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self._session_uuid: str = compartmentproperties_data["sessionUuid"] self._session_expiration_time: int = compartmentproperties_data["sessionExpirationTime"] self._location: CompartmentLocation | None = None @@ -919,6 +1065,7 @@ def session_uuid(self): :return: string containing session unique identified for :class:`CompartmentProperties` :rtype: str""" + self._log.debug("getting session uuid") return self._session_uuid @@ -927,7 +1074,9 @@ def location(self): """Returns a compartment location for :class:`CompartmentProperties` :return: compartment location for :class:`CompartmentProperties` - :rtype: str""" + :rtype: str + """ + self._log.debug("getting location") return self._location @@ -936,7 +1085,9 @@ def location(self, location_data: dict): """Set a compartment location for :class:`CompartmentProperties` :param location_data: dict containing compartment location data for :class:`CompartmentProperties` - :type location_data: dict""" + :type location_data: dict + """ + self._log.debug("setting location") self._location = CompartmentLocation(location_data, self._log) @@ -945,7 +1096,9 @@ def status(self): """Returns a compartment status for :class:`CompartmentProperties` :return: compartment location for :class:`CompartmentProperties` - :rtype: CompartmentActualStatus""" + :rtype: CompartmentActualStatus + """ + self._log.debug("getting status") return self._status @@ -977,6 +1130,7 @@ def __init__(self, airsensor_data: dict, logger: logging.Logger): :param logger: :class:`logging.Logger` parent instance :type logger: logging.Logger """ + self.updated_until: arrow = airsensor_data["updatedUntil"] self.air_quality: str = airsensor_data["airQuality"] self.temperature: float = airsensor_data["temperature"] From 7309fd3bb9fd5210674ee6bb35a241a31c41f1c5 Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 01:01:01 +0200 Subject: [PATCH 13/28] Add pytest module and create test case for models from statuses --- .gitignore | 6 + .idea/.gitignore | 3 + .idea/inpost-python.iml | 13 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/vcs.xml | 6 + inpost/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 220 bytes inpost/__pycache__/api.cpython-311.pyc | Bin 0 -> 67924 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3637 bytes .../__pycache__/endpoints.cpython-311.pyc | Bin 0 -> 3672 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 0 -> 5483 bytes .../__pycache__/friends.cpython-311.pyc | Bin 0 -> 3072 bytes .../__pycache__/headers.cpython-311.pyc | Bin 0 -> 229 bytes .../__pycache__/notifications.cpython-311.pyc | Bin 0 -> 1664 bytes .../__pycache__/parcels.cpython-311.pyc | Bin 0 -> 65012 bytes .../__pycache__/statuses.cpython-311.pyc | Bin 0 -> 11966 bytes pyproject.toml | 1 + tests/__init__.py | 0 tests/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes tests/__pycache__/data.cpython-311.pyc | Bin 0 -> 11845 bytes .../test_friends.cpython-311-pytest-7.4.0.pyc | Bin 0 -> 757 bytes ...notifications.cpython-311-pytest-7.4.0.pyc | Bin 0 -> 609 bytes .../test_parcels.cpython-311-pytest-7.4.0.pyc | Bin 0 -> 9854 bytes ...test_statuses.cpython-311-pytest-7.4.0.pyc | Bin 0 -> 51347 bytes tests/test_statuses.py | 486 ++++++++++++++++++ 25 files changed, 525 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/inpost-python.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 inpost/__pycache__/__init__.cpython-311.pyc create mode 100644 inpost/__pycache__/api.cpython-311.pyc create mode 100644 inpost/static/__pycache__/__init__.cpython-311.pyc create mode 100644 inpost/static/__pycache__/endpoints.cpython-311.pyc create mode 100644 inpost/static/__pycache__/exceptions.cpython-311.pyc create mode 100644 inpost/static/__pycache__/friends.cpython-311.pyc create mode 100644 inpost/static/__pycache__/headers.cpython-311.pyc create mode 100644 inpost/static/__pycache__/notifications.cpython-311.pyc create mode 100644 inpost/static/__pycache__/parcels.cpython-311.pyc create mode 100644 inpost/static/__pycache__/statuses.cpython-311.pyc create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-311.pyc create mode 100644 tests/__pycache__/data.cpython-311.pyc create mode 100644 tests/__pycache__/test_friends.cpython-311-pytest-7.4.0.pyc create mode 100644 tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc create mode 100644 tests/__pycache__/test_parcels.cpython-311-pytest-7.4.0.pyc create mode 100644 tests/__pycache__/test_statuses.cpython-311-pytest-7.4.0.pyc create mode 100644 tests/test_statuses.py diff --git a/.gitignore b/.gitignore index 180d189..da83c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ /dist/ /venv/ /tests/.pytest_cache/ +/tests/api_tests.py +/tests/data.json +/tests/data.py +/tests/data_responses.py +/tests/static_tests.py +/tests/data.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inpost-python.iml b/.idea/inpost-python.iml new file mode 100644 index 0000000..6c77d62 --- /dev/null +++ b/.idea/inpost-python.iml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9ac0e05 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/inpost/__pycache__/__init__.cpython-311.pyc b/inpost/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81aef5aab92ec94fb6aa7e02cd93f3ae4b76013 GIT binary patch literal 220 zcmZ3^%ge<81lQlRq(}hi#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@8G#a- zjJMc4^9u5dOZ+sMZZRhoWEL?4g;z3s25J9g?`#zlTAW%`9Fv<^l3E%QP??;OSd<%3 zl%JKFTv8m93D&AxP+5|Zp9kf|#K&jmWtPOp>lIY~;;_lhPbtkwwJYKPng_D6SQbcp TU}j`wyul!T0UIh}1F8T3-mf}4 literal 0 HcmV?d00001 diff --git a/inpost/__pycache__/api.cpython-311.pyc b/inpost/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea209b598b5da87f86b8dbb1b741435970e1ab5f GIT binary patch literal 67924 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 literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/__init__.cpython-311.pyc b/inpost/static/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bb22f5a484b16a71883e7359c07c304a09e6730 GIT binary patch literal 3637 zcmeH}OHUj}5P)ZS?0eZ=-Waeke!$wsHhvr1*qDcn4a>qp4ke_~c&33dQ!}HUSz;0? z%74hg_gtd<8y~H{=9C+fZ$71~XIL**BE?D$v6N-?ZFhBbRdrQA_@z+DYVxx_{nq-v zOVfU%&he)$ZhZJ5p=ob5OS2M=h6zYu8?<3Nv||T!U?+595|WsL6s94K8OUH3vY3M$ z<{^&-C}09DZCtw06VG=7)!Ao!nFT-WL0#|Sf zrj(6#=PF)Pv8m8!5lt?r)qx6na2fKz(rWZC0N2|@C={Bb6kdHd;u@;CA`EH zSix0T#VS;B4c4#*HC%^v+<*}S@YoJv|MW11s%n$V) zZZp>k_H18oa7PqhIrhFDghtp1lptq%j>F7Qb$U$?eS?R{+)(#?<|=7_tF&cm$@JVE zo1-3(I$4+VV+jVj=~+xkiza7A$aLkJ)UvUF+FhRom~p&aJMx>AO|KDI&S1j{ZDp>Y z0&DsllmVN}(OoaJcWl!LZO@ICR*pw}&vwJlMBFx6EaGg3vtUmTz5RH>Mb1w8$Z-}n z__0~(+R}s86{effWP?3}Gb>tmoU7*GXvjF)F6H7l>j4V_8L-mNg|@lR;z)BLH)Joe zG;m-zwng(k%T~ELaO|W~6y2+~H0tiw3~pes$hpTwq3X%5%v03ut9s#FBixgPqm(hL zsTD4H4L9xu z7|hE}5>MH34H=Qg?eC7m>D_W|i$dJ7nKf5iZYq9~MxudM#nyF}w*tmjyl7V_-AY0v zm(-b%t(4*WuLIAO&AiM=bN^JjM9=)Eqi}3M^tA|UT<#Gb+AI+1#RD1Tinl9DtF%p9 zv_y7IYDJq;B%{zot|klJ@N2X$MQ8MrT;HHvi1Y?CnSH>x$Zx1k7tcx7<*t*`to#jQ zS-P$^+oo43i-8uWb7t5uoVq&XVyIqv!|k7mc=XQQ4PDb$QZQj3RjaaWCHl($GN%4ra(RaSYsye)YLp_7m# zqzGw3hL9uB>&Nqi0zp238ZQz`gi%77&`s!(5Gh}sw}AJOsE^Q37$6K1h6tw!!-NsS zX~G%87~w2ooN$hCo^XM1kuX7+Bvc5O2$u<02vdZsglmNBglWPJ!cD?0!fnD`!ac%$ z!VKX7;UVD>VV3Zi@Psf&cuJTjED#n6ON3{H=Y(a#3&Kmn3ZY6^Bh)0Sm2X5w?rvY* z(|@NVsv4>;e_4`~)%da^sB$(dA+qcZJ*D)n{EJl)srW8ekcaaRvHSGR literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/endpoints.cpython-311.pyc b/inpost/static/__pycache__/endpoints.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20d9f3ca50870bc1ec61dfaf5f15172ac17a3f5c GIT binary patch literal 3672 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 XSzAQD`zWpN=~jC(O07|)He>$*CN}9x literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/friends.cpython-311.pyc b/inpost/static/__pycache__/friends.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed2d6448be6e6554e88a151fce013491862abece GIT binary patch literal 3072 zcmb7GUrZE77@yg_JMJHxh!RkgQ>kWAF4`KjRY~L!YN&?q3Jif_ivTfbawXJ`R4oP zo8QcQ^L_iMzTSI*OrN-uIXsjRs2{?|z^vlF?f(bJTHq2tgpaCSX zi%76NWDt4`uUW!20goWg{wHNa9HZOM6Oy{6vs0oHE9n>?w!~~+hQ{A__YNJIwFU9sNWSld!&cYHCU(vGROW z-wrS3H~+@Q8gUliM8uiJ0&b;pqa*=O7V%{kft0CAK>}mV0ut6Dsq99US*@M7JOJ!g z*EmDb809|fCP>OGx{n^P4`G(a)tL|v=VcbbUO{cI&R#-YSJ849TDz6Tau1>_aa2cG zXDE$#m9>SsZO7NWXT0HG)CR1lf`xnlTm}ECr#wgPS8#$(W&dDDTyO6I245VZcK@fP z2aI*4rX9{5(ruZHBbSf!^GKp&<(QV0SLfpauy+KOel$59Em1UmSojgxLLCeO2PnVGaFg^yz2)#7Aw}gThC_ayg&t&X^ zcr7!&(HbHs~9ME7_)VNQ!k*x@$U7b>hTLu z@9kZ^qO~4VTaUdsxnx^*6%HKBO=t(sEZHs?yl z(|qSet@EM^H6@!<4}PwWPpX5ot~KSx*W0wN{yaCJaRVyUc$#XpYVtvBxtYTlq)uA6Ww$P&G%*BH_3 zTWG96B+k+sT@$%1amp&GjZ;>Mt$L%uT1`Z;MhoC-?!Mss&XHxFxU4w<;xF7Phv^H3 zlM@;b4Bsa8wADa~4+!UYkDMWUs4jJx9}ze&8;i@juX>B#^GgeD`#`}WIe0?-uva@6q$=}T z`?htYU{ta)k@qi zkEUON5m>9Y%t0@Bj4fo@5*p1uf_JkU$RNV7Em<_ND+0@($MX!hA-ZlX;wMiQLz=)@ zhNjCaXa!@myjx(vBujR|U>%(c1lb_tRy{i2P?6P zh-t#}fwS>cEE<=40u`MOuqB@a@&`N%NNpa0`n|9k%{4y1@uL6NJJ0XjJMhiiH*@!* zls*TZKBk^=_*?jhjyR`oS2F3iNF${n#UN7Zc1aNvGT8~nNfQvAl@+2pI0Ra949Eidt+ClelM|jKUIos-44lsg zf?6O5z4_)54QS(t);O}{DEOL|?`pooYfWoozjUu3{<-H_Prj>H>*~#OL5&N(;s#%G zgD-;W@JOB;)wt2TZ&Y=T8mkO(E^`rXo=>ENOq}K}FVEk|MB}AQR!4u%AUEoc&y2q` z(nif^luQ_kYykXm8Zyf?R_{m@FY}3w6zUwLypEG z(44TYUJkI%fC8>EO zx*?SXsZ|1r1qC^o$%!SI`FZ+T#rb)DnvA#Dfr23RN`}uMqkg$MTg8MHrxq2*nnG1ma>5An}2jk&*ERx8wzB-3wfL4eTIT!~&E602%B;>i_@% literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/notifications.cpython-311.pyc b/inpost/static/__pycache__/notifications.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..005f8478fef31e8d3bc4c7cdb840d97a00bf046c GIT binary patch literal 1664 zcmah}J#5oJ6h1r7PxAvpDoXQ%5=9vZ^jB?FKxsvx0zs>)6$ObXlI7YiHHw{Zz5+!Z zI&@%Q?}Ws}kGf>c$jBI5mK@!L#KcxqCMMoFO;m)VKA)fOy}R#w@6PY;Qz8*XAnQ|y z%3c7WAI#{9aN0SUgw7#SkiylF&Q&;_ukak=(S4-w2S^EgMB3j*$`yl`j zgclMU#9uj+z2cvi&R6qK_SOFgt%h3}>*yCOzTp<1EXmEVt zBg{JL&-~XiU`?=!-tQUA67SRXU%%q)}Y>u>M ztjujEa~s^T&f@*{a<#q4+WYBtZqm)o*twbJ6DwDCa%C$s?_}oN&^EU`mh^QuJz=LO zn&H;4m0obt3*e4+vSV&`%Fa$TORWtnd&kM%0e8GpoOO#g?c&Y$t%u*1tm3*;Tz@M# zk!+_=6KV-odfrLTd+dd2w=icH=30rb#44;fg%vhE)yYq}`B^(Z+pK+Evhoj{`~&76 zX?yJiwgjz~1h#jEq^qJxqoSzSm7N;v$3^kQj#TT8;1QVn1>qEYaFQvup`K#JV_pTd z-+zvUSnwftjQn+%gyC86pu}!q%&wwt9eTu9nAs2ZCsOXFwSaRlz^)$iDX8NB$8jB$ WZTq$3P=aGg`0HPu|M!4VFZ5pxGib^H literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/parcels.cpython-311.pyc b/inpost/static/__pycache__/parcels.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b3f5762cf7afbf3633343258529e7889a4e5182 GIT binary patch literal 65012 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 LIqjir^8Nn-#lWQR literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/statuses.cpython-311.pyc b/inpost/static/__pycache__/statuses.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94a58a34359fe78bd538f1c74102618d2fe3fba3 GIT binary patch literal 11966 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index 96fc7e2..c8bd0b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ aiohttp = "^3.8.1" arrow = "^1.2.3" qrcode = "^7.3.1" Pillow = "^9.4.0" +pytest = "^7.4.0" [build-system] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3091951cad16b6af1b6a8857ecffee318736e7ce GIT binary patch literal 171 zcmZ3^%ge<81a0QiQ$X}%5CH>>P{wCAAY(d13PUi1CZpd2X#0(Sz05NVVlK=n! literal 0 HcmV?d00001 diff --git a/tests/__pycache__/data.cpython-311.pyc b/tests/__pycache__/data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cf92f0da3baa08725045b12a5b3bff0f88e78a5 GIT binary patch literal 11845 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc b/tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc new file mode 100644 index 0000000000000000000000000000000000000000..960431582e95aeeb95aa7e0547d4dac10bb32312 GIT binary patch literal 609 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 IjZX^vKl5lLU;qFB literal 0 HcmV?d00001 diff --git a/tests/test_statuses.py b/tests/test_statuses.py new file mode 100644 index 0000000..74234f4 --- /dev/null +++ b/tests/test_statuses.py @@ -0,0 +1,486 @@ +import pytest +from inpost.static.statuses import ParcelCarrierSize, ParcelLockerSize, ParcelDeliveryType, ParcelShipmentType, \ + ParcelAdditionalInsurance, ParcelType, PointType, ParcelPointOperations, ParcelStatus, DeliveryType, ReturnsStatus, \ + ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, PaymentType, PaymentStatus, ParcelServiceName + +from tests.data import courier1, parcel1 + + +# https://docs.pytest.org/en/7.3.x/getting-started.html#create-your-first-test + +# https://www.jetbrains.com/help/pycharm/pytest.html#pytest-parametrize + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("OTHER", ParcelCarrierSize.OTHER), + ("A", ParcelCarrierSize.A), + ("B", ParcelCarrierSize.B), + ("C", ParcelCarrierSize.C), + ("D", ParcelCarrierSize.D), + ("PENIS", ParcelCarrierSize.UNKNOWN), + ]) +def test_parcel_carrier_size_bracket(test_input, expected): + size = ParcelCarrierSize[test_input] + assert size == expected, f"size: {size} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("8x38x64", "A"), + ("19x38x64", "B"), + ("41x38x64", "C"), + ("50x50x80", "D"), + ]) +def test_parcel_carrier_size_normal(test_input, expected): + size_new = ParcelCarrierSize(test_input) + size_get = ParcelCarrierSize[expected] + assert size_new == size_get, f"size_new: {size_new} != size_get: {size_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("A", ParcelLockerSize.A), + ("B", ParcelLockerSize.B), + ("C", ParcelLockerSize.C), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_locker_size_bracket(test_input, expected): + size = ParcelLockerSize[test_input] + assert size == expected, f"size: {size} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("8x38x64", "A"), + ("19x38x64", "B"), + ("41x38x64", "C"), + ]) +def test_parcel_locker_size_normal(test_input, expected): + size_new = ParcelLockerSize(test_input) + size_get = ParcelLockerSize[expected] + assert size_new == size_get, f"size_new: {size_new} != size_get: {size_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("parcel_locker", ParcelDeliveryType.parcel_locker), + ("courier", ParcelDeliveryType.courier), + ("parcel_point", ParcelDeliveryType.parcel_point), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_delivery_type_bracket(test_input, expected): + type = ParcelDeliveryType[test_input] + assert type == expected, f"type: {type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Paczkomat", "parcel_locker"), + ("Kurier", "courier"), + ("PaczkoPunkt", "parcel_point"), + ]) +def test_parcel_delivery_type_normal(test_input, expected): + type_new = ParcelDeliveryType(test_input) + type_get = ParcelDeliveryType[expected] + assert type_new == type_get, f"type_new: {type_new} != type_get: {type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("parcel", ParcelShipmentType.parcel), + ("courier", ParcelShipmentType.courier), + ("parcel_point", ParcelShipmentType.parcel_point), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_shipment_type_bracket(test_input, expected): + type = ParcelShipmentType[test_input] + assert type == expected, f"type: {type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Paczkomat", "parcel"), + ("Kurier", "courier"), + ("PaczkoPunkt", "parcel_point"), + ]) +def test_parcel_shipment_type_normal(test_input, expected): + type_new = ParcelShipmentType(test_input) + type_get = ParcelShipmentType[expected] + assert type_new == type_get, f"type_new: {type_new} != type_get: {type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("UNINSURANCED", ParcelAdditionalInsurance.UNINSURANCED), + ("ONE", ParcelAdditionalInsurance.ONE), + ("TWO", ParcelAdditionalInsurance.TWO), + ("THREE", ParcelAdditionalInsurance.THREE), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_additional_insurance_bracket(test_input, expected): + insurance = ParcelAdditionalInsurance[test_input] + assert insurance == expected, f"insurance: {insurance} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [(1, "UNINSURANCED"), + (2, "ONE"), + (3, "TWO"), + (4, "THREE"), + ]) +def test_parcel_additional_insurance_normal(test_input, expected): + insurace_new = ParcelAdditionalInsurance(test_input) + insurance_get = ParcelAdditionalInsurance[expected] + assert insurace_new == insurance_get, f"insurace_new: {insurace_new} != insurance_get: {insurance_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("TRACKED", ParcelType.TRACKED), + ("SENT", ParcelType.SENT), + ("RETURNS", ParcelType.RETURNS), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_type_bracket(test_input, expected): + parcel_type = ParcelType[test_input] + assert parcel_type == expected, f"parcel_type: {parcel_type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Przychodzące", "TRACKED"), + ("Wysłane", "SENT"), + ("Zwroty", "RETURNS"), + ]) +def test_parcel_type_normal(test_input, expected): + parcel_type_new = ParcelType(test_input) + parcel_type_get = ParcelType[expected] + assert parcel_type_new == parcel_type_get, f"parcel_type_new: {parcel_type_new} != parcel_type_get: {parcel_type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("PL", PointType.PL), + ("parcel_locker_superpop", PointType.parcel_locker_superpop), + ("POK", PointType.POK), + ("POP", PointType.POP), + ("PENIS", PointType.UNKNOWN), + ]) +def test_point_type_bracket(test_input, expected): + point_type = PointType[test_input] + assert point_type == expected, f"point_type: {point_type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Paczkomat", "PL"), + ("some paczkomat or pok stuff", "parcel_locker_superpop"), + ("Mobilny punkt obsługi klienta", "POK"), + ("Punkt odbioru paczki", "POP"), + ]) +def test_point_type_normal(test_input, expected): + point_type_new = PointType(test_input) + point_type_get = PointType[expected] + assert point_type_new == point_type_get, f"point_type_new: {point_type_new} != point_type_get: {point_type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("CREATE", ParcelPointOperations.CREATE), + ("SEND", ParcelPointOperations.SEND), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_point_operations_bracket(test_input, expected): + parcel_point_operation = ParcelPointOperations[test_input] + assert parcel_point_operation == expected, f"parcel_point_operation: {parcel_point_operation} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("c2x-target", "CREATE"), + ("remote-send", "SEND"), + ]) +def test_parcel_point_operations_normal(test_input, expected): + parcel_point_operation_new = ParcelPointOperations(test_input) + parcel_point_operation_get = ParcelPointOperations[expected] + assert parcel_point_operation_new == parcel_point_operation_get, f"parcel_point_operation_new: {parcel_point_operation_new} != parcel_point_operation_get: {parcel_point_operation_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("CREATED", ParcelStatus.CREATED), + ("OFFERS_PREPARED", ParcelStatus.OFFERS_PREPARED), + ("OFFER_SELECTED", ParcelStatus.OFFER_SELECTED), + ("CONFIRMED", ParcelStatus.CONFIRMED), + ("READY_TO_PICKUP_FROM_POK", ParcelStatus.READY_TO_PICKUP_FROM_POK), + ("OVERSIZED", ParcelStatus.OVERSIZED), + ("DISPATCHED_BY_SENDER_TO_POK", ParcelStatus.DISPATCHED_BY_SENDER_TO_POK), + ("DISPATCHED_BY_SENDER", ParcelStatus.DISPATCHED_BY_SENDER), + ("COLLECTED_FROM_SENDER", ParcelStatus.COLLECTED_FROM_SENDER), + ("TAKEN_BY_COURIER", ParcelStatus.TAKEN_BY_COURIER), + ("ADOPTED_AT_SOURCE_BRANCH", ParcelStatus.ADOPTED_AT_SOURCE_BRANCH), + ("SENT_FROM_SOURCE_BRANCH", ParcelStatus.SENT_FROM_SOURCE_BRANCH), + ("READDRESSED", ParcelStatus.READDRESSED), + ("OUT_FOR_DELIVERY", ParcelStatus.OUT_FOR_DELIVERY), + ("READY_TO_PICKUP", ParcelStatus.READY_TO_PICKUP), + ("PICKUP_REMINDER_SENT", ParcelStatus.PICKUP_REMINDER_SENT), + ("PICKUP_TIME_EXPIRED", ParcelStatus.PICKUP_TIME_EXPIRED), + ("AVIZO", ParcelStatus.AVIZO), + ("TAKEN_BY_COURIER_FROM_POK", ParcelStatus.TAKEN_BY_COURIER_FROM_POK), + ("REJECTED_BY_RECEIVER", ParcelStatus.REJECTED_BY_RECEIVER), + ("UNDELIVERED", ParcelStatus.UNDELIVERED), + ("DELAY_IN_DELIVERY", ParcelStatus.DELAY_IN_DELIVERY), + ("RETURNED_TO_SENDER", ParcelStatus.RETURNED_TO_SENDER), + ("READY_TO_PICKUP_FROM_BRANCH", ParcelStatus.READY_TO_PICKUP_FROM_BRANCH), + ("DELIVERED", ParcelStatus.DELIVERED), + ("CANCELED", ParcelStatus.CANCELED), + ("CLAIMED", ParcelStatus.CLAIMED), + ("STACK_IN_CUSTOMER_SERVICE_POINT", ParcelStatus.STACK_IN_CUSTOMER_SERVICE_POINT), + ("STACK_PARCEL_PICKUP_TIME_EXPIRED", ParcelStatus.STACK_PARCEL_PICKUP_TIME_EXPIRED), + ("UNSTACK_FROM_CUSTOMER_SERVICE_POINT", ParcelStatus.UNSTACK_FROM_CUSTOMER_SERVICE_POINT), + ("COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT", + ParcelStatus.COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT), + ("TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT", + ParcelStatus.TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT), + ("STACK_IN_BOX_MACHINE", ParcelStatus.STACK_IN_BOX_MACHINE), + ("STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED", + ParcelStatus.STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED), + ("UNSTACK_FROM_BOX_MACHINE", ParcelStatus.UNSTACK_FROM_BOX_MACHINE), + ("ADOPTED_AT_SORTING_CENTER", ParcelStatus.ADOPTED_AT_SORTING_CENTER), + ("OUT_FOR_DELIVERY_TO_ADDRESS", ParcelStatus.OUT_FOR_DELIVERY_TO_ADDRESS), + ("PICKUP_REMINDER_SENT_ADDRESS", ParcelStatus.PICKUP_REMINDER_SENT_ADDRESS), + ("UNDELIVERED_WRONG_ADDRESS", ParcelStatus.UNDELIVERED_WRONG_ADDRESS), + ("UNDELIVERED_COD_CASH_RECEIVER", ParcelStatus.UNDELIVERED_COD_CASH_RECEIVER), + ("REDIRECT_TO_BOX", ParcelStatus.REDIRECT_TO_BOX), + ("CANCELED_REDIRECT_TO_BOX", ParcelStatus.CANCELED_REDIRECT_TO_BOX), + ("PENIS", ParcelLockerSize.UNKNOWN), + ]) +def test_parcel_status_normal(test_input, expected): + parcel_status = ParcelStatus[test_input] + assert parcel_status == expected, f"parcel_status: {parcel_status} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("W trakcie przygotowania", "CREATED"), + ("Oferty przygotowane", "OFFERS_PREPARED"), + ("Oferta wybrana", "OFFER_SELECTED"), + ("Potwierdzona", "CONFIRMED"), + ("Gotowa do odbioru w PaczkoPunkcie", "READY_TO_PICKUP_FROM_POK"), + ("Gabaryt", "OVERSIZED"), + ("Nadana w PaczkoPunkcie", "DISPATCHED_BY_SENDER_TO_POK"), + ("Nadana w paczkomacie", "DISPATCHED_BY_SENDER"), + ("Odebrana od nadawcy", "COLLECTED_FROM_SENDER"), + ("Odebrana przez Kuriera", "TAKEN_BY_COURIER"), + ("Przyjęta w oddziale", "ADOPTED_AT_SOURCE_BRANCH"), + ("Wysłana z oddziału", "SENT_FROM_SOURCE_BRANCH"), + ("Zmiana punktu dostawy", "READDRESSED"), + ("Wydana do doręczenia", "OUT_FOR_DELIVERY"), + ("Gotowa do odbioru", "READY_TO_PICKUP"), + ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT"), + ("Upłynął czas odbioru", "PICKUP_TIME_EXPIRED"), + ("Powrót do oddziału", "AVIZO"), + ("Odebrana z PaczkoPunktu nadawczego", "TAKEN_BY_COURIER_FROM_POK"), + ("Odrzucona przez odbiorcę", "REJECTED_BY_RECEIVER"), + ("Nie dostarczona", "UNDELIVERED"), + ("Opóźnienie w dostarczeniu", "DELAY_IN_DELIVERY"), + ("Zwrócona do nadawcy", "RETURNED_TO_SENDER"), + ("Gotowa do odbioru z oddziału", "READY_TO_PICKUP_FROM_BRANCH"), + ("Doręczona", "DELIVERED"), + ("Anulowana", "CANCELED"), + ("Zareklamowana", "CLAIMED"), + ("Przesyłka magazynowana w punkcie obsługi klienta", "STACK_IN_CUSTOMER_SERVICE_POINT"), + ("Upłynął czas odbioru", "STACK_PARCEL_PICKUP_TIME_EXPIRED"), + ("?", "UNSTACK_FROM_CUSTOMER_SERVICE_POINT"), + ("Przekazana do punktu obsługi klienta", "COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT"), + ("Odebrana przez kuriera z punktu obsługi klienta", + "TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT"), + ("Przesyłka magazynowana w paczkomacie tymczasowym", "STACK_IN_BOX_MACHINE"), + ("Upłynął czas odbioru z paczkomatu", "STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED"), + ("Odebrana z paczkomatu", "UNSTACK_FROM_BOX_MACHINE"), + ("Przyjęta w sortowni", "ADOPTED_AT_SORTING_CENTER"), + ("Gotowa do doręczenia", "OUT_FOR_DELIVERY_TO_ADDRESS"), + ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT_ADDRESS"), + ("Nie dostarczono z powodu złego adresu", "UNDELIVERED_WRONG_ADDRESS"), + ("Nie dostarczono z powodu nieopłacenia", "UNDELIVERED_COD_CASH_RECEIVER"), + ("Przekierowana do paczkomatu", "REDIRECT_TO_BOX"), + ("Anulowano przekierowanie do paczkomatu", "CANCELED_REDIRECT_TO_BOX"), + ]) +def test_parcel_status_bracket(test_input, expected): + parcel_status_new = ParcelStatus(test_input) + parcel_status_get = ParcelStatus[expected] + assert parcel_status_new == parcel_status_get, f"parcel_status_new: {parcel_status_new} != parcel_status_get: {parcel_status_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("BOX_MACHINE", DeliveryType.BOX_MACHINE), + ("PENIS", DeliveryType.UNKNOWN), + ]) +def test_delivery_type_normal(test_input, expected): + delivery_type = DeliveryType[test_input] + assert delivery_type == expected, f"delivery_type: {delivery_type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Paczkomat", "BOX_MACHINE"), + ]) +def test_delivery_type_bracket(test_input, expected): + delivery_type_new = DeliveryType(test_input) + delivery_type_get = DeliveryType[expected] + assert delivery_type_new == delivery_type_get, f"delivery_type_new: {delivery_type_new} != delivery_type_get: {delivery_type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("ACCEPTED", ReturnsStatus.ACCEPTED), + ("USED", ReturnsStatus.USED), + ("DELIVERED", ReturnsStatus.DELIVERED), + ("PENIS", ReturnsStatus.UNKNOWN), + ]) +def test_returns_status_normal(test_input, expected): + returns_status = ReturnsStatus[test_input] + assert returns_status == expected, f"returns_status: {returns_status} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Zaakceptowano", "ACCEPTED"), + ("Nadano", "USED"), + ("Dostarczono", "DELIVERED"), + ]) +def test_returns_status_bracket(test_input, expected): + returns_status_new = ReturnsStatus(test_input) + returns_status_get = ReturnsStatus[expected] + assert returns_status_new == returns_status_get, f"returns_status_new: {returns_status_new} != returns_status_get: {returns_status_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("FRIEND", ParcelOwnership.FRIEND), + ("OWN", ParcelOwnership.OWN), + ("PENIS", ParcelOwnership.UNKNOWN), + ]) +def test_parcel_ownership_normal(test_input, expected): + parcel_ownership = ParcelOwnership[test_input] + assert parcel_ownership == expected, f"parcel_ownership: {parcel_ownership} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Zaprzyjaźniona", "FRIEND"), + ("Własna", "OWN"), + ]) +def test_parcel_ownership_bracket(test_input, expected): + parcel_ownership_new = ParcelOwnership(test_input) + parcel_ownership_get = ParcelOwnership[expected] + assert parcel_ownership_new == parcel_ownership_get, f"parcel_ownership_new: {parcel_ownership_new} != parcel_ownership_get: {parcel_ownership_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("OPENED", CompartmentExpectedStatus.OPENED), + ("CLOSED", CompartmentExpectedStatus.CLOSED), + ("PENIS", CompartmentExpectedStatus.UNKNOWN), + ]) +def test_compartment_expected_status_normal(test_input, expected): + compartment_expected = CompartmentExpectedStatus[test_input] + assert compartment_expected == expected, f"compartment_expected: {compartment_expected} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Otwarta", "OPENED"), + ("Zamknięta", "CLOSED"), + ]) +def test_compartment_expected_status_bracket(test_input, expected): + compartment_expected_new = CompartmentExpectedStatus(test_input) + compartment_expected_get = CompartmentExpectedStatus[expected] + assert compartment_expected_new == compartment_expected_get, f"compartment_expected_new: {compartment_expected_new} != compartment_expected_get: {compartment_expected_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("OPENED", CompartmentActualStatus.OPENED), + ("CLOSED", CompartmentActualStatus.CLOSED), + ("PENIS", CompartmentActualStatus.UNKNOWN), + ]) +def test_compartment_actual_status_normal(test_input, expected): + compartment_actual = CompartmentActualStatus[test_input] + assert compartment_actual == expected, f"compartment_actual: {compartment_actual} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Otwarta", "OPENED"), + ("Zamknięta", "CLOSED"), + ]) +def test_compartment_actual_status_bracket(test_input, expected): + compartment_actual_new = CompartmentActualStatus(test_input) + compartment_actual_get = CompartmentActualStatus[expected] + assert compartment_actual_new == compartment_actual_get, f"compartment_actual_new: {compartment_actual_new} != compartment_actual_get: {compartment_actual_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("NOTSUPPORTED", PaymentType.NOTSUPPORTED), + ("BY_CARD_IN_MACHINE", PaymentType.BY_CARD_IN_MACHINE), + ("PENIS", PaymentType.UNKNOWN), + ]) +def test_payment_type_normal(test_input, expected): + payment_type = PaymentType[test_input] + assert payment_type == expected, f"payment_type: {payment_type} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Payments are not supported", "NOTSUPPORTED"), + ("Payment by card in the machine", "BY_CARD_IN_MACHINE"), + ]) +def test_payment_type_bracket(test_input, expected): + payment_type_new = PaymentType(test_input) + payment_type_get = PaymentType[expected] + assert payment_type_new == payment_type_get, f"payment_type_new: {payment_type_new} != payment_type_get: {payment_type_get}" + + +@pytest.mark.parametrize("test_input,expected", + [(None, None), + ("C2X_COMPLETED", PaymentStatus.C2X_COMPLETED), + ("PENIS", PaymentStatus.UNKNOWN), + ]) +def test_payment_status_normal(test_input, expected): + payment_status = PaymentStatus[test_input] + assert payment_status == expected, f"payment_status: {payment_status} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [("Completed", "C2X_COMPLETED"), + ]) +def test_payment_status_bracket(test_input, expected): + payment_status_new = PaymentStatus(test_input) + payment_status_get = PaymentStatus[expected] + assert payment_status_new == payment_status_get, f"payment_status_new: {payment_status_new} != payment_status_get: {payment_status_get}" + + +@pytest.mark.parametrize("test_input,expected", + [("ALLEGRO_PARCEL", ParcelServiceName.ALLEGRO_PARCEL), + ("ALLEGRO_PARCEL_SMART", ParcelServiceName.ALLEGRO_PARCEL_SMART), + ("ALLEGRO_LETTER", ParcelServiceName.ALLEGRO_LETTER), + ("ALLEGRO_COURIER", ParcelServiceName.ALLEGRO_COURIER), + ("STANDARD", ParcelServiceName.STANDARD), + ("STANDARD_PARCEL_SMART", ParcelServiceName.STANDARD_PARCEL_SMART), + ("PASS_THRU", ParcelServiceName.PASS_THRU), + ("CUSTOMER_SERVICE_POINT", ParcelServiceName.CUSTOMER_SERVICE_POINT), + ("REVERSE", ParcelServiceName.REVERSE), + ("STANDARD_COURIER", ParcelServiceName.STANDARD_COURIER), + ("REVERSE_RETURN", ParcelServiceName.REVERSE_RETURN), + ]) +def test_parcel_service_name_normal(test_input, expected): + parcel_servicename = ParcelServiceName[test_input] + assert parcel_servicename == expected, f"parcel_servicename: {parcel_servicename} != expected: {expected}" + + +@pytest.mark.parametrize("test_input,expected", + [(1, "ALLEGRO_PARCEL"), + (2, "ALLEGRO_PARCEL_SMART"), + (3, "ALLEGRO_LETTER"), + (4, "ALLEGRO_COURIER"), + (5, "STANDARD"), + (6, "STANDARD_PARCEL_SMART"), + (7, "PASS_THRU"), + (8, "CUSTOMER_SERVICE_POINT"), + (9, "REVERSE"), + (10, "STANDARD_COURIER"), + (11, "REVERSE_RETURN"), + ]) +def test_parcel_service_name_bracket(test_input, expected): + parcel_servicename_new = ParcelServiceName(test_input) + parcel_servicename_get = ParcelServiceName[expected] + assert parcel_servicename_new == parcel_servicename_get, f"parcel_servicename_new: {parcel_servicename_new} != parcel_servicename_get: {parcel_servicename_get}" From 11bf8303bf94d3dce7ad2b2cb3b5bdaa5ee3bf1e Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 01:04:40 +0200 Subject: [PATCH 14/28] Remove cache files and unnecesary stuff --- .gitignore | 4 ++++ .idea/.gitignore | 3 --- .idea/inpost-python.iml | 13 ------------- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/misc.xml | 4 ---- .idea/vcs.xml | 6 ------ inpost/__pycache__/__init__.cpython-311.pyc | Bin 220 -> 0 bytes inpost/__pycache__/api.cpython-311.pyc | Bin 67924 -> 0 bytes .../static/__pycache__/__init__.cpython-311.pyc | Bin 3637 -> 0 bytes .../__pycache__/endpoints.cpython-311.pyc | Bin 3672 -> 0 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 5483 -> 0 bytes .../static/__pycache__/friends.cpython-311.pyc | Bin 3072 -> 0 bytes .../static/__pycache__/headers.cpython-311.pyc | Bin 229 -> 0 bytes .../__pycache__/notifications.cpython-311.pyc | Bin 1664 -> 0 bytes .../static/__pycache__/parcels.cpython-311.pyc | Bin 65012 -> 0 bytes .../static/__pycache__/statuses.cpython-311.pyc | Bin 11966 -> 0 bytes tests/__pycache__/__init__.cpython-311.pyc | Bin 171 -> 0 bytes tests/__pycache__/data.cpython-311.pyc | Bin 11845 -> 0 bytes .../test_friends.cpython-311-pytest-7.4.0.pyc | Bin 757 -> 0 bytes ...t_notifications.cpython-311-pytest-7.4.0.pyc | Bin 609 -> 0 bytes .../test_parcels.cpython-311-pytest-7.4.0.pyc | Bin 9854 -> 0 bytes .../test_statuses.cpython-311-pytest-7.4.0.pyc | Bin 51347 -> 0 bytes 22 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/inpost-python.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml delete mode 100644 inpost/__pycache__/__init__.cpython-311.pyc delete mode 100644 inpost/__pycache__/api.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/__init__.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/endpoints.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/exceptions.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/friends.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/headers.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/notifications.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/parcels.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/statuses.cpython-311.pyc delete mode 100644 tests/__pycache__/__init__.cpython-311.pyc delete mode 100644 tests/__pycache__/data.cpython-311.pyc delete mode 100644 tests/__pycache__/test_friends.cpython-311-pytest-7.4.0.pyc delete mode 100644 tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc delete mode 100644 tests/__pycache__/test_parcels.cpython-311-pytest-7.4.0.pyc delete mode 100644 tests/__pycache__/test_statuses.cpython-311-pytest-7.4.0.pyc diff --git a/.gitignore b/.gitignore index da83c7f..cdbf48e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ /tests/data_responses.py /tests/static_tests.py /tests/data.py +/tests/__pycache__/ +/inpost/static/__pycache__/ +/inpost/__pycache__/ +/.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/inpost-python.iml b/.idea/inpost-python.iml deleted file mode 100644 index 6c77d62..0000000 --- a/.idea/inpost-python.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 9ac0e05..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/inpost/__pycache__/__init__.cpython-311.pyc b/inpost/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index d81aef5aab92ec94fb6aa7e02cd93f3ae4b76013..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmZ3^%ge<81lQlRq(}hi#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@8G#a- zjJMc4^9u5dOZ+sMZZRhoWEL?4g;z3s25J9g?`#zlTAW%`9Fv<^l3E%QP??;OSd<%3 zl%JKFTv8m93D&AxP+5|Zp9kf|#K&jmWtPOp>lIY~;;_lhPbtkwwJYKPng_D6SQbcp TU}j`wyul!T0UIh}1F8T3-mf}4 diff --git a/inpost/__pycache__/api.cpython-311.pyc b/inpost/__pycache__/api.cpython-311.pyc deleted file mode 100644 index ea209b598b5da87f86b8dbb1b741435970e1ab5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67924 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 diff --git a/inpost/static/__pycache__/__init__.cpython-311.pyc b/inpost/static/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 0bb22f5a484b16a71883e7359c07c304a09e6730..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3637 zcmeH}OHUj}5P)ZS?0eZ=-Waeke!$wsHhvr1*qDcn4a>qp4ke_~c&33dQ!}HUSz;0? z%74hg_gtd<8y~H{=9C+fZ$71~XIL**BE?D$v6N-?ZFhBbRdrQA_@z+DYVxx_{nq-v zOVfU%&he)$ZhZJ5p=ob5OS2M=h6zYu8?<3Nv||T!U?+595|WsL6s94K8OUH3vY3M$ z<{^&-C}09DZCtw06VG=7)!Ao!nFT-WL0#|Sf zrj(6#=PF)Pv8m8!5lt?r)qx6na2fKz(rWZC0N2|@C={Bb6kdHd;u@;CA`EH zSix0T#VS;B4c4#*HC%^v+<*}S@YoJv|MW11s%n$V) zZZp>k_H18oa7PqhIrhFDghtp1lptq%j>F7Qb$U$?eS?R{+)(#?<|=7_tF&cm$@JVE zo1-3(I$4+VV+jVj=~+xkiza7A$aLkJ)UvUF+FhRom~p&aJMx>AO|KDI&S1j{ZDp>Y z0&DsllmVN}(OoaJcWl!LZO@ICR*pw}&vwJlMBFx6EaGg3vtUmTz5RH>Mb1w8$Z-}n z__0~(+R}s86{effWP?3}Gb>tmoU7*GXvjF)F6H7l>j4V_8L-mNg|@lR;z)BLH)Joe zG;m-zwng(k%T~ELaO|W~6y2+~H0tiw3~pes$hpTwq3X%5%v03ut9s#FBixgPqm(hL zsTD4H4L9xu z7|hE}5>MH34H=Qg?eC7m>D_W|i$dJ7nKf5iZYq9~MxudM#nyF}w*tmjyl7V_-AY0v zm(-b%t(4*WuLIAO&AiM=bN^JjM9=)Eqi}3M^tA|UT<#Gb+AI+1#RD1Tinl9DtF%p9 zv_y7IYDJq;B%{zot|klJ@N2X$MQ8MrT;HHvi1Y?CnSH>x$Zx1k7tcx7<*t*`to#jQ zS-P$^+oo43i-8uWb7t5uoVq&XVyIqv!|k7mc=XQQ4PDb$QZQj3RjaaWCHl($GN%4ra(RaSYsye)YLp_7m# zqzGw3hL9uB>&Nqi0zp238ZQz`gi%77&`s!(5Gh}sw}AJOsE^Q37$6K1h6tw!!-NsS zX~G%87~w2ooN$hCo^XM1kuX7+Bvc5O2$u<02vdZsglmNBglWPJ!cD?0!fnD`!ac%$ z!VKX7;UVD>VV3Zi@Psf&cuJTjED#n6ON3{H=Y(a#3&Kmn3ZY6^Bh)0Sm2X5w?rvY* z(|@NVsv4>;e_4`~)%da^sB$(dA+qcZJ*D)n{EJl)srW8ekcaaRvHSGR diff --git a/inpost/static/__pycache__/endpoints.cpython-311.pyc b/inpost/static/__pycache__/endpoints.cpython-311.pyc deleted file mode 100644 index 20d9f3ca50870bc1ec61dfaf5f15172ac17a3f5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3672 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 XSzAQD`zWpN=~jC(O07|)He>$*CN}9x diff --git a/inpost/static/__pycache__/friends.cpython-311.pyc b/inpost/static/__pycache__/friends.cpython-311.pyc deleted file mode 100644 index ed2d6448be6e6554e88a151fce013491862abece..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3072 zcmb7GUrZE77@yg_JMJHxh!RkgQ>kWAF4`KjRY~L!YN&?q3Jif_ivTfbawXJ`R4oP zo8QcQ^L_iMzTSI*OrN-uIXsjRs2{?|z^vlF?f(bJTHq2tgpaCSX zi%76NWDt4`uUW!20goWg{wHNa9HZOM6Oy{6vs0oHE9n>?w!~~+hQ{A__YNJIwFU9sNWSld!&cYHCU(vGROW z-wrS3H~+@Q8gUliM8uiJ0&b;pqa*=O7V%{kft0CAK>}mV0ut6Dsq99US*@M7JOJ!g z*EmDb809|fCP>OGx{n^P4`G(a)tL|v=VcbbUO{cI&R#-YSJ849TDz6Tau1>_aa2cG zXDE$#m9>SsZO7NWXT0HG)CR1lf`xnlTm}ECr#wgPS8#$(W&dDDTyO6I245VZcK@fP z2aI*4rX9{5(ruZHBbSf!^GKp&<(QV0SLfpauy+KOel$59Em1UmSojgxLLCeO2PnVGaFg^yz2)#7Aw}gThC_ayg&t&X^ zcr7!&(HbHs~9ME7_)VNQ!k*x@$U7b>hTLu z@9kZ^qO~4VTaUdsxnx^*6%HKBO=t(sEZHs?yl z(|qSet@EM^H6@!<4}PwWPpX5ot~KSx*W0wN{yaCJaRVyUc$#XpYVtvBxtYTlq)uA6Ww$P&G%*BH_3 zTWG96B+k+sT@$%1amp&GjZ;>Mt$L%uT1`Z;MhoC-?!Mss&XHxFxU4w<;xF7Phv^H3 zlM@;b4Bsa8wADa~4+!UYkDMWUs4jJx9}ze&8;i@juX>B#^GgeD`#`}WIe0?-uva@6q$=}T z`?htYU{ta)k@qi zkEUON5m>9Y%t0@Bj4fo@5*p1uf_JkU$RNV7Em<_ND+0@($MX!hA-ZlX;wMiQLz=)@ zhNjCaXa!@myjx(vBujR|U>%(c1lb_tRy{i2P?6P zh-t#}fwS>cEE<=40u`MOuqB@a@&`N%NNpa0`n|9k%{4y1@uL6NJJ0XjJMhiiH*@!* zls*TZKBk^=_*?jhjyR`oS2F3iNF${n#UN7Zc1aNvGT8~nNfQvAl@+2pI0Ra949Eidt+ClelM|jKUIos-44lsg zf?6O5z4_)54QS(t);O}{DEOL|?`pooYfWoozjUu3{<-H_Prj>H>*~#OL5&N(;s#%G zgD-;W@JOB;)wt2TZ&Y=T8mkO(E^`rXo=>ENOq}K}FVEk|MB}AQR!4u%AUEoc&y2q` z(nif^luQ_kYykXm8Zyf?R_{m@FY}3w6zUwLypEG z(44TYUJkI%fC8>EO zx*?SXsZ|1r1qC^o$%!SI`FZ+T#rb)DnvA#Dfr23RN`}uMqkg$MTg8MHrxq2*nnG1ma>5An}2jk&*ERx8wzB-3wfL4eTIT!~&E602%B;>i_@% diff --git a/inpost/static/__pycache__/notifications.cpython-311.pyc b/inpost/static/__pycache__/notifications.cpython-311.pyc deleted file mode 100644 index 005f8478fef31e8d3bc4c7cdb840d97a00bf046c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1664 zcmah}J#5oJ6h1r7PxAvpDoXQ%5=9vZ^jB?FKxsvx0zs>)6$ObXlI7YiHHw{Zz5+!Z zI&@%Q?}Ws}kGf>c$jBI5mK@!L#KcxqCMMoFO;m)VKA)fOy}R#w@6PY;Qz8*XAnQ|y z%3c7WAI#{9aN0SUgw7#SkiylF&Q&;_ukak=(S4-w2S^EgMB3j*$`yl`j zgclMU#9uj+z2cvi&R6qK_SOFgt%h3}>*yCOzTp<1EXmEVt zBg{JL&-~XiU`?=!-tQUA67SRXU%%q)}Y>u>M ztjujEa~s^T&f@*{a<#q4+WYBtZqm)o*twbJ6DwDCa%C$s?_}oN&^EU`mh^QuJz=LO zn&H;4m0obt3*e4+vSV&`%Fa$TORWtnd&kM%0e8GpoOO#g?c&Y$t%u*1tm3*;Tz@M# zk!+_=6KV-odfrLTd+dd2w=icH=30rb#44;fg%vhE)yYq}`B^(Z+pK+Evhoj{`~&76 zX?yJiwgjz~1h#jEq^qJxqoSzSm7N;v$3^kQj#TT8;1QVn1>qEYaFQvup`K#JV_pTd z-+zvUSnwftjQn+%gyC86pu}!q%&wwt9eTu9nAs2ZCsOXFwSaRlz^)$iDX8NB$8jB$ WZTq$3P=aGg`0HPu|M!4VFZ5pxGib^H diff --git a/inpost/static/__pycache__/parcels.cpython-311.pyc b/inpost/static/__pycache__/parcels.cpython-311.pyc deleted file mode 100644 index 2b3f5762cf7afbf3633343258529e7889a4e5182..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65012 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 LIqjir^8Nn-#lWQR diff --git a/inpost/static/__pycache__/statuses.cpython-311.pyc b/inpost/static/__pycache__/statuses.cpython-311.pyc deleted file mode 100644 index 94a58a34359fe78bd538f1c74102618d2fe3fba3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11966 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 diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 3091951cad16b6af1b6a8857ecffee318736e7ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171 zcmZ3^%ge<81a0QiQ$X}%5CH>>P{wCAAY(d13PUi1CZpd2X#0(Sz05NVVlK=n! diff --git a/tests/__pycache__/data.cpython-311.pyc b/tests/__pycache__/data.cpython-311.pyc deleted file mode 100644 index 0cf92f0da3baa08725045b12a5b3bff0f88e78a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11845 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 diff --git a/tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc b/tests/__pycache__/test_notifications.cpython-311-pytest-7.4.0.pyc deleted file mode 100644 index 960431582e95aeeb95aa7e0547d4dac10bb32312..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 609 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 IjZX^vKl5lLU;qFB From 1550ee8718be42e2102f5d34f6c97c2159ec3305 Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 02:12:14 +0200 Subject: [PATCH 15/28] Add pytest to Github Actions --- .github/workflows/pytest.yml | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..8a290f7 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,66 @@ +name: inpost-python + +on: [push] +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4.6.1 + with: + python-version: ${{ matrix.python-version }} + + # Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow + # from installing Poetry every time, which can be slow. Note the use of the Poetry version + # number in the cache key, and the "-0" suffix: this allows you to invalidate the cache + # manually if/when you want to upgrade Poetry, or if something goes wrong. This could be + # mildly cleaner by using an environment variable, but I don't really care. + - name: cache poetry install + uses: actions/cache@v2 + with: + path: ~/.local + key: poetry-1.5.1-0 + + # Install Poetry. You could do this manually, or there are several actions that do this. + # `snok/install-poetry` seems to be minimal yet complete, and really just calls out to + # Poetry's default install script, which feels correct. I pin the Poetry version here + # because Poetry does occasionally change APIs between versions and I don't want my + # actions to break if it does. + # + # The key configuration value here is `virtualenvs-in-project: true`: this creates the + # venv as a `.venv` in your testing directory, which allows the next step to easily + # cache it. + - uses: snok/install-poetry@v1 + with: + version: 1.5.1 + virtualenvs-create: true + virtualenvs-in-project: true + + # Cache your dependencies (i.e. all the stuff in your `pyproject.toml`). Note the cache + # key: if you're using multiple Python versions, or multiple OSes, you'd need to include + # them in the cache key. I'm not, so it can be simple and just depend on the poetry.lock. + - name: cache deps + id: cache-deps + uses: actions/cache@v2 + with: + path: .venv + key: pydeps-${{ hashFiles('**/poetry.lock') }} + + # Install dependencies. `--no-root` means "install all dependencies but not the project + # itself", which is what you want to avoid caching _your_ code. The `if` statement + # ensures this only runs on a cache miss. + - run: poetry install --no-interaction --no-root + if: steps.cache-deps.outputs.cache-hit != 'true' + + # Now install _your_ project. This isn't necessary for many types of projects -- particularly + # things like Django apps don't need this. But it's a good idea since it fully-exercises the + # pyproject.toml and makes that if you add things like console-scripts at some point that + # they'll be installed and working. + - run: poetry install --no-interaction + + # And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml` + # so this line is super-simple. But it could be as complex as you need. + - run: poetry run pytest \ No newline at end of file From bc29288f94a3c8d7abce410f2bcd1cd4a446d8c0 Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 02:14:20 +0200 Subject: [PATCH 16/28] Remove unnecessary stuff from statuses test --- .github/workflows/pytest.yml | 2 +- tests/test_statuses.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 8a290f7..b223797 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,4 +1,4 @@ -name: inpost-python +name: Pytest on: [push] jobs: diff --git a/tests/test_statuses.py b/tests/test_statuses.py index 74234f4..1babd05 100644 --- a/tests/test_statuses.py +++ b/tests/test_statuses.py @@ -3,12 +3,6 @@ ParcelAdditionalInsurance, ParcelType, PointType, ParcelPointOperations, ParcelStatus, DeliveryType, ReturnsStatus, \ ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, PaymentType, PaymentStatus, ParcelServiceName -from tests.data import courier1, parcel1 - - -# https://docs.pytest.org/en/7.3.x/getting-started.html#create-your-first-test - -# https://www.jetbrains.com/help/pycharm/pytest.html#pytest-parametrize @pytest.mark.parametrize("test_input,expected", [(None, None), From 65159aae2bc3479837d95319dd083564bef9fe17 Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 02:35:46 +0200 Subject: [PATCH 17/28] Add pytest-results-action to surface failing pytest tests without crawling through logs and make tests failing to check if it works --- .github/workflows/pytest.yml | 25 ++++++++++++++++++++++++- inpost/static/statuses.py | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b223797..4dc99f7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -63,4 +63,27 @@ jobs: # And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml` # so this line is super-simple. But it could be as complex as you need. - - run: poetry run pytest \ No newline at end of file + - run: poetry run pytest --junit-xml=test-results.xml + + - name: Surface failing tests + if: always() + uses: pmeier/pytest-results-action@main + with: + # A list of JUnit XML files, directories containing the former, and wildcard + # patterns to process. + # See @actions/glob for supported patterns. + path: test-results.xml + + # Add a summary of the results at the top of the report + # Default: true + summary: true + + # Select which results should be included in the report. + # Follows the same syntax as + # `pytest -r` + # Default: fEX + display-options: fEX + + # Fail the workflow if no JUnit XML was found. + # Default: true + fail-on-empty: false \ No newline at end of file diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index 43daed1..7a47347 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -240,7 +240,7 @@ class PaymentStatus(ParcelBase): class ParcelServiceName(ParcelBase): UNKNOWN = "UNKNOWN DATA" ALLEGRO_PARCEL = 1 - ALLEGRO_PARCEL_SMART = 2 + ALLEGRO_PARCEL_SMART = 21 ALLEGRO_LETTER = 3 ALLEGRO_COURIER = 4 STANDARD = 5 From f86cdf3eeb6fc02afc0413fe855e0c597bf3471e Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 02:41:54 +0200 Subject: [PATCH 18/28] Fix statuses to pass the tests again --- inpost/static/statuses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index 7a47347..43daed1 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -240,7 +240,7 @@ class PaymentStatus(ParcelBase): class ParcelServiceName(ParcelBase): UNKNOWN = "UNKNOWN DATA" ALLEGRO_PARCEL = 1 - ALLEGRO_PARCEL_SMART = 21 + ALLEGRO_PARCEL_SMART = 2 ALLEGRO_LETTER = 3 ALLEGRO_COURIER = 4 STANDARD = 5 From 52de0d7b258ebce957412023ede356d2e43e3b3f Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 17:56:14 +0200 Subject: [PATCH 19/28] Add additional test case --- tests/test_statuses.py | 799 ++++++++++++++++++++++++----------------- 1 file changed, 471 insertions(+), 328 deletions(-) diff --git a/tests/test_statuses.py b/tests/test_statuses.py index 1babd05..aa43d63 100644 --- a/tests/test_statuses.py +++ b/tests/test_statuses.py @@ -1,480 +1,623 @@ import pytest -from inpost.static.statuses import ParcelCarrierSize, ParcelLockerSize, ParcelDeliveryType, ParcelShipmentType, \ - ParcelAdditionalInsurance, ParcelType, PointType, ParcelPointOperations, ParcelStatus, DeliveryType, ReturnsStatus, \ - ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, PaymentType, PaymentStatus, ParcelServiceName - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("OTHER", ParcelCarrierSize.OTHER), - ("A", ParcelCarrierSize.A), - ("B", ParcelCarrierSize.B), - ("C", ParcelCarrierSize.C), - ("D", ParcelCarrierSize.D), - ("PENIS", ParcelCarrierSize.UNKNOWN), - ]) + +from inpost.static.statuses import ( + CompartmentActualStatus, + CompartmentExpectedStatus, + DeliveryType, + ParcelAdditionalInsurance, + ParcelCarrierSize, + ParcelDeliveryType, + ParcelLockerSize, + ParcelOwnership, + ParcelPointOperations, + ParcelServiceName, + ParcelShipmentType, + ParcelStatus, + ParcelType, + PaymentStatus, + PaymentType, + PointType, + ReturnsStatus, +) + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("OTHER", ParcelCarrierSize.OTHER), + ("A", ParcelCarrierSize.A), + ("B", ParcelCarrierSize.B), + ("C", ParcelCarrierSize.C), + ("D", ParcelCarrierSize.D), + ("PENIS", ParcelCarrierSize.UNKNOWN), + ("KURWA", ParcelCarrierSize.UNKNOWN), + ], +) def test_parcel_carrier_size_bracket(test_input, expected): size = ParcelCarrierSize[test_input] assert size == expected, f"size: {size} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("8x38x64", "A"), - ("19x38x64", "B"), - ("41x38x64", "C"), - ("50x50x80", "D"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("8x38x64", "A"), + ("19x38x64", "B"), + ("41x38x64", "C"), + ("50x50x80", "D"), + ], +) def test_parcel_carrier_size_normal(test_input, expected): size_new = ParcelCarrierSize(test_input) size_get = ParcelCarrierSize[expected] assert size_new == size_get, f"size_new: {size_new} != size_get: {size_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("A", ParcelLockerSize.A), - ("B", ParcelLockerSize.B), - ("C", ParcelLockerSize.C), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("A", ParcelLockerSize.A), + ("B", ParcelLockerSize.B), + ("C", ParcelLockerSize.C), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_locker_size_bracket(test_input, expected): size = ParcelLockerSize[test_input] assert size == expected, f"size: {size} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("8x38x64", "A"), - ("19x38x64", "B"), - ("41x38x64", "C"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("8x38x64", "A"), + ("19x38x64", "B"), + ("41x38x64", "C"), + ], +) def test_parcel_locker_size_normal(test_input, expected): size_new = ParcelLockerSize(test_input) size_get = ParcelLockerSize[expected] assert size_new == size_get, f"size_new: {size_new} != size_get: {size_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("parcel_locker", ParcelDeliveryType.parcel_locker), - ("courier", ParcelDeliveryType.courier), - ("parcel_point", ParcelDeliveryType.parcel_point), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("parcel_locker", ParcelDeliveryType.parcel_locker), + ("courier", ParcelDeliveryType.courier), + ("parcel_point", ParcelDeliveryType.parcel_point), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_delivery_type_bracket(test_input, expected): type = ParcelDeliveryType[test_input] assert type == expected, f"type: {type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Paczkomat", "parcel_locker"), - ("Kurier", "courier"), - ("PaczkoPunkt", "parcel_point"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Paczkomat", "parcel_locker"), + ("Kurier", "courier"), + ("PaczkoPunkt", "parcel_point"), + ], +) def test_parcel_delivery_type_normal(test_input, expected): type_new = ParcelDeliveryType(test_input) type_get = ParcelDeliveryType[expected] assert type_new == type_get, f"type_new: {type_new} != type_get: {type_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("parcel", ParcelShipmentType.parcel), - ("courier", ParcelShipmentType.courier), - ("parcel_point", ParcelShipmentType.parcel_point), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("parcel", ParcelShipmentType.parcel), + ("courier", ParcelShipmentType.courier), + ("parcel_point", ParcelShipmentType.parcel_point), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_shipment_type_bracket(test_input, expected): type = ParcelShipmentType[test_input] assert type == expected, f"type: {type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Paczkomat", "parcel"), - ("Kurier", "courier"), - ("PaczkoPunkt", "parcel_point"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Paczkomat", "parcel"), + ("Kurier", "courier"), + ("PaczkoPunkt", "parcel_point"), + ], +) def test_parcel_shipment_type_normal(test_input, expected): type_new = ParcelShipmentType(test_input) type_get = ParcelShipmentType[expected] assert type_new == type_get, f"type_new: {type_new} != type_get: {type_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("UNINSURANCED", ParcelAdditionalInsurance.UNINSURANCED), - ("ONE", ParcelAdditionalInsurance.ONE), - ("TWO", ParcelAdditionalInsurance.TWO), - ("THREE", ParcelAdditionalInsurance.THREE), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("UNINSURANCED", ParcelAdditionalInsurance.UNINSURANCED), + ("ONE", ParcelAdditionalInsurance.ONE), + ("TWO", ParcelAdditionalInsurance.TWO), + ("THREE", ParcelAdditionalInsurance.THREE), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_additional_insurance_bracket(test_input, expected): insurance = ParcelAdditionalInsurance[test_input] assert insurance == expected, f"insurance: {insurance} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [(1, "UNINSURANCED"), - (2, "ONE"), - (3, "TWO"), - (4, "THREE"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (1, "UNINSURANCED"), + (2, "ONE"), + (3, "TWO"), + (4, "THREE"), + ], +) def test_parcel_additional_insurance_normal(test_input, expected): insurace_new = ParcelAdditionalInsurance(test_input) insurance_get = ParcelAdditionalInsurance[expected] assert insurace_new == insurance_get, f"insurace_new: {insurace_new} != insurance_get: {insurance_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("TRACKED", ParcelType.TRACKED), - ("SENT", ParcelType.SENT), - ("RETURNS", ParcelType.RETURNS), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("TRACKED", ParcelType.TRACKED), + ("SENT", ParcelType.SENT), + ("RETURNS", ParcelType.RETURNS), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_type_bracket(test_input, expected): parcel_type = ParcelType[test_input] assert parcel_type == expected, f"parcel_type: {parcel_type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Przychodzące", "TRACKED"), - ("Wysłane", "SENT"), - ("Zwroty", "RETURNS"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Przychodzące", "TRACKED"), + ("Wysłane", "SENT"), + ("Zwroty", "RETURNS"), + ], +) def test_parcel_type_normal(test_input, expected): parcel_type_new = ParcelType(test_input) parcel_type_get = ParcelType[expected] - assert parcel_type_new == parcel_type_get, f"parcel_type_new: {parcel_type_new} != parcel_type_get: {parcel_type_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("PL", PointType.PL), - ("parcel_locker_superpop", PointType.parcel_locker_superpop), - ("POK", PointType.POK), - ("POP", PointType.POP), - ("PENIS", PointType.UNKNOWN), - ]) + assert ( + parcel_type_new == parcel_type_get + ), f"parcel_type_new: {parcel_type_new} != parcel_type_get: {parcel_type_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("PL", PointType.PL), + ("parcel_locker_superpop", PointType.parcel_locker_superpop), + ("POK", PointType.POK), + ("POP", PointType.POP), + ("PENIS", PointType.UNKNOWN), + ], +) def test_point_type_bracket(test_input, expected): point_type = PointType[test_input] assert point_type == expected, f"point_type: {point_type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Paczkomat", "PL"), - ("some paczkomat or pok stuff", "parcel_locker_superpop"), - ("Mobilny punkt obsługi klienta", "POK"), - ("Punkt odbioru paczki", "POP"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Paczkomat", "PL"), + ("some paczkomat or pok stuff", "parcel_locker_superpop"), + ("Mobilny punkt obsługi klienta", "POK"), + ("Punkt odbioru paczki", "POP"), + ], +) def test_point_type_normal(test_input, expected): point_type_new = PointType(test_input) point_type_get = PointType[expected] assert point_type_new == point_type_get, f"point_type_new: {point_type_new} != point_type_get: {point_type_get}" -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("CREATE", ParcelPointOperations.CREATE), - ("SEND", ParcelPointOperations.SEND), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("CREATE", ParcelPointOperations.CREATE), + ("SEND", ParcelPointOperations.SEND), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_point_operations_bracket(test_input, expected): parcel_point_operation = ParcelPointOperations[test_input] - assert parcel_point_operation == expected, f"parcel_point_operation: {parcel_point_operation} != expected: {expected}" - - -@pytest.mark.parametrize("test_input,expected", - [("c2x-target", "CREATE"), - ("remote-send", "SEND"), - ]) + assert ( + parcel_point_operation == expected + ), f"parcel_point_operation: {parcel_point_operation} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("c2x-target", "CREATE"), + ("remote-send", "SEND"), + ], +) def test_parcel_point_operations_normal(test_input, expected): parcel_point_operation_new = ParcelPointOperations(test_input) parcel_point_operation_get = ParcelPointOperations[expected] - assert parcel_point_operation_new == parcel_point_operation_get, f"parcel_point_operation_new: {parcel_point_operation_new} != parcel_point_operation_get: {parcel_point_operation_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("CREATED", ParcelStatus.CREATED), - ("OFFERS_PREPARED", ParcelStatus.OFFERS_PREPARED), - ("OFFER_SELECTED", ParcelStatus.OFFER_SELECTED), - ("CONFIRMED", ParcelStatus.CONFIRMED), - ("READY_TO_PICKUP_FROM_POK", ParcelStatus.READY_TO_PICKUP_FROM_POK), - ("OVERSIZED", ParcelStatus.OVERSIZED), - ("DISPATCHED_BY_SENDER_TO_POK", ParcelStatus.DISPATCHED_BY_SENDER_TO_POK), - ("DISPATCHED_BY_SENDER", ParcelStatus.DISPATCHED_BY_SENDER), - ("COLLECTED_FROM_SENDER", ParcelStatus.COLLECTED_FROM_SENDER), - ("TAKEN_BY_COURIER", ParcelStatus.TAKEN_BY_COURIER), - ("ADOPTED_AT_SOURCE_BRANCH", ParcelStatus.ADOPTED_AT_SOURCE_BRANCH), - ("SENT_FROM_SOURCE_BRANCH", ParcelStatus.SENT_FROM_SOURCE_BRANCH), - ("READDRESSED", ParcelStatus.READDRESSED), - ("OUT_FOR_DELIVERY", ParcelStatus.OUT_FOR_DELIVERY), - ("READY_TO_PICKUP", ParcelStatus.READY_TO_PICKUP), - ("PICKUP_REMINDER_SENT", ParcelStatus.PICKUP_REMINDER_SENT), - ("PICKUP_TIME_EXPIRED", ParcelStatus.PICKUP_TIME_EXPIRED), - ("AVIZO", ParcelStatus.AVIZO), - ("TAKEN_BY_COURIER_FROM_POK", ParcelStatus.TAKEN_BY_COURIER_FROM_POK), - ("REJECTED_BY_RECEIVER", ParcelStatus.REJECTED_BY_RECEIVER), - ("UNDELIVERED", ParcelStatus.UNDELIVERED), - ("DELAY_IN_DELIVERY", ParcelStatus.DELAY_IN_DELIVERY), - ("RETURNED_TO_SENDER", ParcelStatus.RETURNED_TO_SENDER), - ("READY_TO_PICKUP_FROM_BRANCH", ParcelStatus.READY_TO_PICKUP_FROM_BRANCH), - ("DELIVERED", ParcelStatus.DELIVERED), - ("CANCELED", ParcelStatus.CANCELED), - ("CLAIMED", ParcelStatus.CLAIMED), - ("STACK_IN_CUSTOMER_SERVICE_POINT", ParcelStatus.STACK_IN_CUSTOMER_SERVICE_POINT), - ("STACK_PARCEL_PICKUP_TIME_EXPIRED", ParcelStatus.STACK_PARCEL_PICKUP_TIME_EXPIRED), - ("UNSTACK_FROM_CUSTOMER_SERVICE_POINT", ParcelStatus.UNSTACK_FROM_CUSTOMER_SERVICE_POINT), - ("COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT", - ParcelStatus.COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT), - ("TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT", - ParcelStatus.TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT), - ("STACK_IN_BOX_MACHINE", ParcelStatus.STACK_IN_BOX_MACHINE), - ("STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED", - ParcelStatus.STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED), - ("UNSTACK_FROM_BOX_MACHINE", ParcelStatus.UNSTACK_FROM_BOX_MACHINE), - ("ADOPTED_AT_SORTING_CENTER", ParcelStatus.ADOPTED_AT_SORTING_CENTER), - ("OUT_FOR_DELIVERY_TO_ADDRESS", ParcelStatus.OUT_FOR_DELIVERY_TO_ADDRESS), - ("PICKUP_REMINDER_SENT_ADDRESS", ParcelStatus.PICKUP_REMINDER_SENT_ADDRESS), - ("UNDELIVERED_WRONG_ADDRESS", ParcelStatus.UNDELIVERED_WRONG_ADDRESS), - ("UNDELIVERED_COD_CASH_RECEIVER", ParcelStatus.UNDELIVERED_COD_CASH_RECEIVER), - ("REDIRECT_TO_BOX", ParcelStatus.REDIRECT_TO_BOX), - ("CANCELED_REDIRECT_TO_BOX", ParcelStatus.CANCELED_REDIRECT_TO_BOX), - ("PENIS", ParcelLockerSize.UNKNOWN), - ]) + assert ( + parcel_point_operation_new == parcel_point_operation_get + ), f"parcel_point_operation_new: {parcel_point_operation_new} != parcel_point_operation_get: {parcel_point_operation_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("CREATED", ParcelStatus.CREATED), + ("OFFERS_PREPARED", ParcelStatus.OFFERS_PREPARED), + ("OFFER_SELECTED", ParcelStatus.OFFER_SELECTED), + ("CONFIRMED", ParcelStatus.CONFIRMED), + ("READY_TO_PICKUP_FROM_POK", ParcelStatus.READY_TO_PICKUP_FROM_POK), + ("OVERSIZED", ParcelStatus.OVERSIZED), + ("DISPATCHED_BY_SENDER_TO_POK", ParcelStatus.DISPATCHED_BY_SENDER_TO_POK), + ("DISPATCHED_BY_SENDER", ParcelStatus.DISPATCHED_BY_SENDER), + ("COLLECTED_FROM_SENDER", ParcelStatus.COLLECTED_FROM_SENDER), + ("TAKEN_BY_COURIER", ParcelStatus.TAKEN_BY_COURIER), + ("ADOPTED_AT_SOURCE_BRANCH", ParcelStatus.ADOPTED_AT_SOURCE_BRANCH), + ("SENT_FROM_SOURCE_BRANCH", ParcelStatus.SENT_FROM_SOURCE_BRANCH), + ("READDRESSED", ParcelStatus.READDRESSED), + ("OUT_FOR_DELIVERY", ParcelStatus.OUT_FOR_DELIVERY), + ("READY_TO_PICKUP", ParcelStatus.READY_TO_PICKUP), + ("PICKUP_REMINDER_SENT", ParcelStatus.PICKUP_REMINDER_SENT), + ("PICKUP_TIME_EXPIRED", ParcelStatus.PICKUP_TIME_EXPIRED), + ("AVIZO", ParcelStatus.AVIZO), + ("TAKEN_BY_COURIER_FROM_POK", ParcelStatus.TAKEN_BY_COURIER_FROM_POK), + ("REJECTED_BY_RECEIVER", ParcelStatus.REJECTED_BY_RECEIVER), + ("UNDELIVERED", ParcelStatus.UNDELIVERED), + ("DELAY_IN_DELIVERY", ParcelStatus.DELAY_IN_DELIVERY), + ("RETURNED_TO_SENDER", ParcelStatus.RETURNED_TO_SENDER), + ("READY_TO_PICKUP_FROM_BRANCH", ParcelStatus.READY_TO_PICKUP_FROM_BRANCH), + ("DELIVERED", ParcelStatus.DELIVERED), + ("CANCELED", ParcelStatus.CANCELED), + ("CLAIMED", ParcelStatus.CLAIMED), + ("STACK_IN_CUSTOMER_SERVICE_POINT", ParcelStatus.STACK_IN_CUSTOMER_SERVICE_POINT), + ("STACK_PARCEL_PICKUP_TIME_EXPIRED", ParcelStatus.STACK_PARCEL_PICKUP_TIME_EXPIRED), + ("UNSTACK_FROM_CUSTOMER_SERVICE_POINT", ParcelStatus.UNSTACK_FROM_CUSTOMER_SERVICE_POINT), + ("COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT", ParcelStatus.COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT), + ("TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT", ParcelStatus.TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT), + ("STACK_IN_BOX_MACHINE", ParcelStatus.STACK_IN_BOX_MACHINE), + ( + "STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED", + ParcelStatus.STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED, + ), + ("UNSTACK_FROM_BOX_MACHINE", ParcelStatus.UNSTACK_FROM_BOX_MACHINE), + ("ADOPTED_AT_SORTING_CENTER", ParcelStatus.ADOPTED_AT_SORTING_CENTER), + ("OUT_FOR_DELIVERY_TO_ADDRESS", ParcelStatus.OUT_FOR_DELIVERY_TO_ADDRESS), + ("PICKUP_REMINDER_SENT_ADDRESS", ParcelStatus.PICKUP_REMINDER_SENT_ADDRESS), + ("UNDELIVERED_WRONG_ADDRESS", ParcelStatus.UNDELIVERED_WRONG_ADDRESS), + ("UNDELIVERED_COD_CASH_RECEIVER", ParcelStatus.UNDELIVERED_COD_CASH_RECEIVER), + ("REDIRECT_TO_BOX", ParcelStatus.REDIRECT_TO_BOX), + ("CANCELED_REDIRECT_TO_BOX", ParcelStatus.CANCELED_REDIRECT_TO_BOX), + ("PENIS", ParcelLockerSize.UNKNOWN), + ], +) def test_parcel_status_normal(test_input, expected): parcel_status = ParcelStatus[test_input] assert parcel_status == expected, f"parcel_status: {parcel_status} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("W trakcie przygotowania", "CREATED"), - ("Oferty przygotowane", "OFFERS_PREPARED"), - ("Oferta wybrana", "OFFER_SELECTED"), - ("Potwierdzona", "CONFIRMED"), - ("Gotowa do odbioru w PaczkoPunkcie", "READY_TO_PICKUP_FROM_POK"), - ("Gabaryt", "OVERSIZED"), - ("Nadana w PaczkoPunkcie", "DISPATCHED_BY_SENDER_TO_POK"), - ("Nadana w paczkomacie", "DISPATCHED_BY_SENDER"), - ("Odebrana od nadawcy", "COLLECTED_FROM_SENDER"), - ("Odebrana przez Kuriera", "TAKEN_BY_COURIER"), - ("Przyjęta w oddziale", "ADOPTED_AT_SOURCE_BRANCH"), - ("Wysłana z oddziału", "SENT_FROM_SOURCE_BRANCH"), - ("Zmiana punktu dostawy", "READDRESSED"), - ("Wydana do doręczenia", "OUT_FOR_DELIVERY"), - ("Gotowa do odbioru", "READY_TO_PICKUP"), - ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT"), - ("Upłynął czas odbioru", "PICKUP_TIME_EXPIRED"), - ("Powrót do oddziału", "AVIZO"), - ("Odebrana z PaczkoPunktu nadawczego", "TAKEN_BY_COURIER_FROM_POK"), - ("Odrzucona przez odbiorcę", "REJECTED_BY_RECEIVER"), - ("Nie dostarczona", "UNDELIVERED"), - ("Opóźnienie w dostarczeniu", "DELAY_IN_DELIVERY"), - ("Zwrócona do nadawcy", "RETURNED_TO_SENDER"), - ("Gotowa do odbioru z oddziału", "READY_TO_PICKUP_FROM_BRANCH"), - ("Doręczona", "DELIVERED"), - ("Anulowana", "CANCELED"), - ("Zareklamowana", "CLAIMED"), - ("Przesyłka magazynowana w punkcie obsługi klienta", "STACK_IN_CUSTOMER_SERVICE_POINT"), - ("Upłynął czas odbioru", "STACK_PARCEL_PICKUP_TIME_EXPIRED"), - ("?", "UNSTACK_FROM_CUSTOMER_SERVICE_POINT"), - ("Przekazana do punktu obsługi klienta", "COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT"), - ("Odebrana przez kuriera z punktu obsługi klienta", - "TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT"), - ("Przesyłka magazynowana w paczkomacie tymczasowym", "STACK_IN_BOX_MACHINE"), - ("Upłynął czas odbioru z paczkomatu", "STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED"), - ("Odebrana z paczkomatu", "UNSTACK_FROM_BOX_MACHINE"), - ("Przyjęta w sortowni", "ADOPTED_AT_SORTING_CENTER"), - ("Gotowa do doręczenia", "OUT_FOR_DELIVERY_TO_ADDRESS"), - ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT_ADDRESS"), - ("Nie dostarczono z powodu złego adresu", "UNDELIVERED_WRONG_ADDRESS"), - ("Nie dostarczono z powodu nieopłacenia", "UNDELIVERED_COD_CASH_RECEIVER"), - ("Przekierowana do paczkomatu", "REDIRECT_TO_BOX"), - ("Anulowano przekierowanie do paczkomatu", "CANCELED_REDIRECT_TO_BOX"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("W trakcie przygotowania", "CREATED"), + ("Oferty przygotowane", "OFFERS_PREPARED"), + ("Oferta wybrana", "OFFER_SELECTED"), + ("Potwierdzona", "CONFIRMED"), + ("Gotowa do odbioru w PaczkoPunkcie", "READY_TO_PICKUP_FROM_POK"), + ("Gabaryt", "OVERSIZED"), + ("Nadana w PaczkoPunkcie", "DISPATCHED_BY_SENDER_TO_POK"), + ("Nadana w paczkomacie", "DISPATCHED_BY_SENDER"), + ("Odebrana od nadawcy", "COLLECTED_FROM_SENDER"), + ("Odebrana przez Kuriera", "TAKEN_BY_COURIER"), + ("Przyjęta w oddziale", "ADOPTED_AT_SOURCE_BRANCH"), + ("Wysłana z oddziału", "SENT_FROM_SOURCE_BRANCH"), + ("Zmiana punktu dostawy", "READDRESSED"), + ("Wydana do doręczenia", "OUT_FOR_DELIVERY"), + ("Gotowa do odbioru", "READY_TO_PICKUP"), + ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT"), + ("Upłynął czas odbioru", "PICKUP_TIME_EXPIRED"), + ("Powrót do oddziału", "AVIZO"), + ("Odebrana z PaczkoPunktu nadawczego", "TAKEN_BY_COURIER_FROM_POK"), + ("Odrzucona przez odbiorcę", "REJECTED_BY_RECEIVER"), + ("Nie dostarczona", "UNDELIVERED"), + ("Opóźnienie w dostarczeniu", "DELAY_IN_DELIVERY"), + ("Zwrócona do nadawcy", "RETURNED_TO_SENDER"), + ("Gotowa do odbioru z oddziału", "READY_TO_PICKUP_FROM_BRANCH"), + ("Doręczona", "DELIVERED"), + ("Anulowana", "CANCELED"), + ("Zareklamowana", "CLAIMED"), + ("Przesyłka magazynowana w punkcie obsługi klienta", "STACK_IN_CUSTOMER_SERVICE_POINT"), + ("Upłynął czas odbioru", "STACK_PARCEL_PICKUP_TIME_EXPIRED"), + ("?", "UNSTACK_FROM_CUSTOMER_SERVICE_POINT"), + ("Przekazana do punktu obsługi klienta", "COURIER_AVIZO_IN_CUSTOMER_SERVICE_POINT"), + ("Odebrana przez kuriera z punktu obsługi klienta", "TAKEN_BY_COURIER_FROM_CUSTOMER_SERVICE_POINT"), + ("Przesyłka magazynowana w paczkomacie tymczasowym", "STACK_IN_BOX_MACHINE"), + ("Upłynął czas odbioru z paczkomatu", "STACK_PARCEL_IN_BOX_MACHINE_PICKUP_TIME_EXPIRED"), + ("Odebrana z paczkomatu", "UNSTACK_FROM_BOX_MACHINE"), + ("Przyjęta w sortowni", "ADOPTED_AT_SORTING_CENTER"), + ("Gotowa do doręczenia", "OUT_FOR_DELIVERY_TO_ADDRESS"), + ("Wysłano przypomnienie o odbiorze", "PICKUP_REMINDER_SENT_ADDRESS"), + ("Nie dostarczono z powodu złego adresu", "UNDELIVERED_WRONG_ADDRESS"), + ("Nie dostarczono z powodu nieopłacenia", "UNDELIVERED_COD_CASH_RECEIVER"), + ("Przekierowana do paczkomatu", "REDIRECT_TO_BOX"), + ("Anulowano przekierowanie do paczkomatu", "CANCELED_REDIRECT_TO_BOX"), + ], +) def test_parcel_status_bracket(test_input, expected): parcel_status_new = ParcelStatus(test_input) parcel_status_get = ParcelStatus[expected] - assert parcel_status_new == parcel_status_get, f"parcel_status_new: {parcel_status_new} != parcel_status_get: {parcel_status_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("BOX_MACHINE", DeliveryType.BOX_MACHINE), - ("PENIS", DeliveryType.UNKNOWN), - ]) + assert ( + parcel_status_new == parcel_status_get + ), f"parcel_status_new: {parcel_status_new} != parcel_status_get: {parcel_status_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("BOX_MACHINE", DeliveryType.BOX_MACHINE), + ("PENIS", DeliveryType.UNKNOWN), + ], +) def test_delivery_type_normal(test_input, expected): delivery_type = DeliveryType[test_input] assert delivery_type == expected, f"delivery_type: {delivery_type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Paczkomat", "BOX_MACHINE"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Paczkomat", "BOX_MACHINE"), + ], +) def test_delivery_type_bracket(test_input, expected): delivery_type_new = DeliveryType(test_input) delivery_type_get = DeliveryType[expected] - assert delivery_type_new == delivery_type_get, f"delivery_type_new: {delivery_type_new} != delivery_type_get: {delivery_type_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("ACCEPTED", ReturnsStatus.ACCEPTED), - ("USED", ReturnsStatus.USED), - ("DELIVERED", ReturnsStatus.DELIVERED), - ("PENIS", ReturnsStatus.UNKNOWN), - ]) + assert ( + delivery_type_new == delivery_type_get + ), f"delivery_type_new: {delivery_type_new} != delivery_type_get: {delivery_type_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("ACCEPTED", ReturnsStatus.ACCEPTED), + ("USED", ReturnsStatus.USED), + ("DELIVERED", ReturnsStatus.DELIVERED), + ("PENIS", ReturnsStatus.UNKNOWN), + ], +) def test_returns_status_normal(test_input, expected): returns_status = ReturnsStatus[test_input] assert returns_status == expected, f"returns_status: {returns_status} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Zaakceptowano", "ACCEPTED"), - ("Nadano", "USED"), - ("Dostarczono", "DELIVERED"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Zaakceptowano", "ACCEPTED"), + ("Nadano", "USED"), + ("Dostarczono", "DELIVERED"), + ], +) def test_returns_status_bracket(test_input, expected): returns_status_new = ReturnsStatus(test_input) returns_status_get = ReturnsStatus[expected] - assert returns_status_new == returns_status_get, f"returns_status_new: {returns_status_new} != returns_status_get: {returns_status_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("FRIEND", ParcelOwnership.FRIEND), - ("OWN", ParcelOwnership.OWN), - ("PENIS", ParcelOwnership.UNKNOWN), - ]) + assert ( + returns_status_new == returns_status_get + ), f"returns_status_new: {returns_status_new} != returns_status_get: {returns_status_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("FRIEND", ParcelOwnership.FRIEND), + ("OWN", ParcelOwnership.OWN), + ("PENIS", ParcelOwnership.UNKNOWN), + ], +) def test_parcel_ownership_normal(test_input, expected): parcel_ownership = ParcelOwnership[test_input] assert parcel_ownership == expected, f"parcel_ownership: {parcel_ownership} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Zaprzyjaźniona", "FRIEND"), - ("Własna", "OWN"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Zaprzyjaźniona", "FRIEND"), + ("Własna", "OWN"), + ], +) def test_parcel_ownership_bracket(test_input, expected): parcel_ownership_new = ParcelOwnership(test_input) parcel_ownership_get = ParcelOwnership[expected] - assert parcel_ownership_new == parcel_ownership_get, f"parcel_ownership_new: {parcel_ownership_new} != parcel_ownership_get: {parcel_ownership_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("OPENED", CompartmentExpectedStatus.OPENED), - ("CLOSED", CompartmentExpectedStatus.CLOSED), - ("PENIS", CompartmentExpectedStatus.UNKNOWN), - ]) + assert ( + parcel_ownership_new == parcel_ownership_get + ), f"parcel_ownership_new: {parcel_ownership_new} != parcel_ownership_get: {parcel_ownership_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("OPENED", CompartmentExpectedStatus.OPENED), + ("CLOSED", CompartmentExpectedStatus.CLOSED), + ("PENIS", CompartmentExpectedStatus.UNKNOWN), + ], +) def test_compartment_expected_status_normal(test_input, expected): compartment_expected = CompartmentExpectedStatus[test_input] assert compartment_expected == expected, f"compartment_expected: {compartment_expected} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Otwarta", "OPENED"), - ("Zamknięta", "CLOSED"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Otwarta", "OPENED"), + ("Zamknięta", "CLOSED"), + ], +) def test_compartment_expected_status_bracket(test_input, expected): compartment_expected_new = CompartmentExpectedStatus(test_input) compartment_expected_get = CompartmentExpectedStatus[expected] - assert compartment_expected_new == compartment_expected_get, f"compartment_expected_new: {compartment_expected_new} != compartment_expected_get: {compartment_expected_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("OPENED", CompartmentActualStatus.OPENED), - ("CLOSED", CompartmentActualStatus.CLOSED), - ("PENIS", CompartmentActualStatus.UNKNOWN), - ]) + assert ( + compartment_expected_new == compartment_expected_get + ), f"compartment_expected_new: {compartment_expected_new} != compartment_expected_get: {compartment_expected_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("OPENED", CompartmentActualStatus.OPENED), + ("CLOSED", CompartmentActualStatus.CLOSED), + ("PENIS", CompartmentActualStatus.UNKNOWN), + ], +) def test_compartment_actual_status_normal(test_input, expected): compartment_actual = CompartmentActualStatus[test_input] assert compartment_actual == expected, f"compartment_actual: {compartment_actual} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Otwarta", "OPENED"), - ("Zamknięta", "CLOSED"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Otwarta", "OPENED"), + ("Zamknięta", "CLOSED"), + ], +) def test_compartment_actual_status_bracket(test_input, expected): compartment_actual_new = CompartmentActualStatus(test_input) compartment_actual_get = CompartmentActualStatus[expected] - assert compartment_actual_new == compartment_actual_get, f"compartment_actual_new: {compartment_actual_new} != compartment_actual_get: {compartment_actual_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("NOTSUPPORTED", PaymentType.NOTSUPPORTED), - ("BY_CARD_IN_MACHINE", PaymentType.BY_CARD_IN_MACHINE), - ("PENIS", PaymentType.UNKNOWN), - ]) + assert ( + compartment_actual_new == compartment_actual_get + ), f"compartment_actual_new: {compartment_actual_new} != compartment_actual_get: {compartment_actual_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("NOTSUPPORTED", PaymentType.NOTSUPPORTED), + ("BY_CARD_IN_MACHINE", PaymentType.BY_CARD_IN_MACHINE), + ("PENIS", PaymentType.UNKNOWN), + ], +) def test_payment_type_normal(test_input, expected): payment_type = PaymentType[test_input] assert payment_type == expected, f"payment_type: {payment_type} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Payments are not supported", "NOTSUPPORTED"), - ("Payment by card in the machine", "BY_CARD_IN_MACHINE"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Payments are not supported", "NOTSUPPORTED"), + ("Payment by card in the machine", "BY_CARD_IN_MACHINE"), + ], +) def test_payment_type_bracket(test_input, expected): payment_type_new = PaymentType(test_input) payment_type_get = PaymentType[expected] - assert payment_type_new == payment_type_get, f"payment_type_new: {payment_type_new} != payment_type_get: {payment_type_get}" - - -@pytest.mark.parametrize("test_input,expected", - [(None, None), - ("C2X_COMPLETED", PaymentStatus.C2X_COMPLETED), - ("PENIS", PaymentStatus.UNKNOWN), - ]) + assert ( + payment_type_new == payment_type_get + ), f"payment_type_new: {payment_type_new} != payment_type_get: {payment_type_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (None, None), + ("C2X_COMPLETED", PaymentStatus.C2X_COMPLETED), + ("PENIS", PaymentStatus.UNKNOWN), + ], +) def test_payment_status_normal(test_input, expected): payment_status = PaymentStatus[test_input] assert payment_status == expected, f"payment_status: {payment_status} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [("Completed", "C2X_COMPLETED"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ("Completed", "C2X_COMPLETED"), + ], +) def test_payment_status_bracket(test_input, expected): payment_status_new = PaymentStatus(test_input) payment_status_get = PaymentStatus[expected] - assert payment_status_new == payment_status_get, f"payment_status_new: {payment_status_new} != payment_status_get: {payment_status_get}" - - -@pytest.mark.parametrize("test_input,expected", - [("ALLEGRO_PARCEL", ParcelServiceName.ALLEGRO_PARCEL), - ("ALLEGRO_PARCEL_SMART", ParcelServiceName.ALLEGRO_PARCEL_SMART), - ("ALLEGRO_LETTER", ParcelServiceName.ALLEGRO_LETTER), - ("ALLEGRO_COURIER", ParcelServiceName.ALLEGRO_COURIER), - ("STANDARD", ParcelServiceName.STANDARD), - ("STANDARD_PARCEL_SMART", ParcelServiceName.STANDARD_PARCEL_SMART), - ("PASS_THRU", ParcelServiceName.PASS_THRU), - ("CUSTOMER_SERVICE_POINT", ParcelServiceName.CUSTOMER_SERVICE_POINT), - ("REVERSE", ParcelServiceName.REVERSE), - ("STANDARD_COURIER", ParcelServiceName.STANDARD_COURIER), - ("REVERSE_RETURN", ParcelServiceName.REVERSE_RETURN), - ]) + assert ( + payment_status_new == payment_status_get + ), f"payment_status_new: {payment_status_new} != payment_status_get: {payment_status_get}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("ALLEGRO_PARCEL", ParcelServiceName.ALLEGRO_PARCEL), + ("ALLEGRO_PARCEL_SMART", ParcelServiceName.ALLEGRO_PARCEL_SMART), + ("ALLEGRO_LETTER", ParcelServiceName.ALLEGRO_LETTER), + ("ALLEGRO_COURIER", ParcelServiceName.ALLEGRO_COURIER), + ("STANDARD", ParcelServiceName.STANDARD), + ("STANDARD_PARCEL_SMART", ParcelServiceName.STANDARD_PARCEL_SMART), + ("PASS_THRU", ParcelServiceName.PASS_THRU), + ("CUSTOMER_SERVICE_POINT", ParcelServiceName.CUSTOMER_SERVICE_POINT), + ("REVERSE", ParcelServiceName.REVERSE), + ("STANDARD_COURIER", ParcelServiceName.STANDARD_COURIER), + ("REVERSE_RETURN", ParcelServiceName.REVERSE_RETURN), + ], +) def test_parcel_service_name_normal(test_input, expected): parcel_servicename = ParcelServiceName[test_input] assert parcel_servicename == expected, f"parcel_servicename: {parcel_servicename} != expected: {expected}" -@pytest.mark.parametrize("test_input,expected", - [(1, "ALLEGRO_PARCEL"), - (2, "ALLEGRO_PARCEL_SMART"), - (3, "ALLEGRO_LETTER"), - (4, "ALLEGRO_COURIER"), - (5, "STANDARD"), - (6, "STANDARD_PARCEL_SMART"), - (7, "PASS_THRU"), - (8, "CUSTOMER_SERVICE_POINT"), - (9, "REVERSE"), - (10, "STANDARD_COURIER"), - (11, "REVERSE_RETURN"), - ]) +@pytest.mark.parametrize( + "test_input,expected", + [ + (1, "ALLEGRO_PARCEL"), + (2, "ALLEGRO_PARCEL_SMART"), + (3, "ALLEGRO_LETTER"), + (4, "ALLEGRO_COURIER"), + (5, "STANDARD"), + (6, "STANDARD_PARCEL_SMART"), + (7, "PASS_THRU"), + (8, "CUSTOMER_SERVICE_POINT"), + (9, "REVERSE"), + (10, "STANDARD_COURIER"), + (11, "REVERSE_RETURN"), + ], +) def test_parcel_service_name_bracket(test_input, expected): parcel_servicename_new = ParcelServiceName(test_input) parcel_servicename_get = ParcelServiceName[expected] - assert parcel_servicename_new == parcel_servicename_get, f"parcel_servicename_new: {parcel_servicename_new} != parcel_servicename_get: {parcel_servicename_get}" + assert ( + parcel_servicename_new == parcel_servicename_get + ), f"parcel_servicename_new: {parcel_servicename_new} != parcel_servicename_get: {parcel_servicename_get}" From 9c51b5087583439473f9283a80f86472edbe02ff Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 21:26:56 +0200 Subject: [PATCH 20/28] Create tests for parcels object --- .gitignore | 7 + inpost/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 220 bytes inpost/__pycache__/api.cpython-311.pyc | Bin 0 -> 71614 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3637 bytes .../__pycache__/endpoints.cpython-311.pyc | Bin 0 -> 3672 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 0 -> 5729 bytes .../__pycache__/friends.cpython-311.pyc | Bin 0 -> 4050 bytes .../__pycache__/headers.cpython-311.pyc | Bin 0 -> 229 bytes .../__pycache__/notifications.cpython-311.pyc | Bin 0 -> 2630 bytes .../__pycache__/parcels.cpython-311.pyc | Bin 0 -> 68105 bytes .../__pycache__/statuses.cpython-311.pyc | Bin 0 -> 11966 bytes tests/static_tests.py | 8 + tests/test_data.py | 319 ++++++++++++++++++ tests/test_parcels.py | 239 +++++++++++++ 14 files changed, 573 insertions(+) create mode 100644 inpost/__pycache__/__init__.cpython-311.pyc create mode 100644 inpost/__pycache__/api.cpython-311.pyc create mode 100644 inpost/static/__pycache__/__init__.cpython-311.pyc create mode 100644 inpost/static/__pycache__/endpoints.cpython-311.pyc create mode 100644 inpost/static/__pycache__/exceptions.cpython-311.pyc create mode 100644 inpost/static/__pycache__/friends.cpython-311.pyc create mode 100644 inpost/static/__pycache__/headers.cpython-311.pyc create mode 100644 inpost/static/__pycache__/notifications.cpython-311.pyc create mode 100644 inpost/static/__pycache__/parcels.cpython-311.pyc create mode 100644 inpost/static/__pycache__/statuses.cpython-311.pyc create mode 100644 tests/static_tests.py create mode 100644 tests/test_data.py create mode 100644 tests/test_parcels.py diff --git a/.gitignore b/.gitignore index 5c000e3..232dc67 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,10 @@ /.mypy_cache/ /dist/ /venv/ +/.idea/ +/tests/.pytest_cache +/tests/__pycache__/ +/tests/api_tests.py +/tests/data.json +/tests/data.py +/tests/data_responses.py diff --git a/inpost/__pycache__/__init__.cpython-311.pyc b/inpost/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81aef5aab92ec94fb6aa7e02cd93f3ae4b76013 GIT binary patch literal 220 zcmZ3^%ge<81lQlRq(}hi#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@8G#a- zjJMc4^9u5dOZ+sMZZRhoWEL?4g;z3s25J9g?`#zlTAW%`9Fv<^l3E%QP??;OSd<%3 zl%JKFTv8m93D&AxP+5|Zp9kf|#K&jmWtPOp>lIY~;;_lhPbtkwwJYKPng_D6SQbcp TU}j`wyul!T0UIh}1F8T3-mf}4 literal 0 HcmV?d00001 diff --git a/inpost/__pycache__/api.cpython-311.pyc b/inpost/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4e2c208b7c84d8d9dc87178c24b1eee2fcc1e48 GIT binary patch literal 71614 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 literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/__init__.cpython-311.pyc b/inpost/static/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8a1232f490977cdd8fcb5f1311a32415b225f3b GIT binary patch literal 3637 zcmeH}OHUj}5P)ZS?0eZ=-Waeke!$ws?imSCWpp2d{3XmVzROjoW+EgK7{-StU;8OPhTBfnYM^cs=n3^tt5R^|#S zu%^#J8L-J5-St9y$2N`7_S|S`<#@#RY&ZNw#BGzsBF=U=3-ijx-l?L-ry| z0|$m$4)9m(YB1yiE z!Mxlg@susskP&&@{^mHG-YwU*D8wC`S#!1Jrs79wBpPT{Y+Yx0D`0%Zi*|+5tt3Qp zNu3GVN*TWYI`CZC%*%{4_fMru^vr)e3daUSUyHEDt2LJdjbYc)OysO53zW zOJvuiRG`zef8~bVfhP^$p5}NN+Hc*$0e^{D#_e@tkB`?m8*W%HJ@S zrR!?5ZF-fm7-(@iXNC>KsjEXShFS&|-}v&VGbItfWa zijXE`2sr}1emqYo5abi6@gku_7$uYm-Gm+qk@D4f3wSSy`Uw4m0m2|*h;WK9Oc)`Y zCY&LR5zZ3E3Fips2^R<#2@`}#LWOXNaG7w0Fh#gZxJI~6xIwr{xJ9^4xI?%{xKDUM zm?k_V%n%+CW(ki8PY82_r-XUJ0%4J`M0iGcPFNDs-f!gmnAt_jn6BBDrd72BFo;;Q%dj3zgQKKitln&9+BE(hCJVas*@<3c+H}+ zty8}Ktaf*4;ByKn(t)}gSn$tFxk}DY)>EW(-EbUT|5bZ0N3{=2^V8dO5eM5S-tbGX>*AgRdwc*z9J55SXeBWskZ%=-Y+KRNo b|1QS5DZVcilhs5{hX0Y@r|6G^Je+?3#$WY= literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/endpoints.cpython-311.pyc b/inpost/static/__pycache__/endpoints.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bba3c5a028f1fe9898f0f87e065ddf5e4df1278 GIT binary patch literal 3672 zcmb`K&r{n*6vv-KLcoqO#(;tFE6tBKZ9##g`9VlBEzp)g!HgTqY{)1=T8fG-DOx!* z^yDK)ZskAd^w?|v3hAcNsWbftGLu_QefuOkB=lf6@o3)L&l-RC?c2Az;`eMe4fqMw zW8=>Zz&{cipN^#Q`mYqgZ}0>RV8B!SjFFnhuZ%OSgQ$b(EIUhdj;NEJBRWq6tdppV zsGFT9N)u&R7tsZxEbAufA#R2M3-4V(G{YrY=Gz*(Wh*X=rf}0tVr}Z(HCrp=u4t0 zHcT{4q_7d98$>hgBGD|-O*Tq&i|8vhMs%C#4jU)BOLUJ-5X}+YXOl$pL=Vbv{o${- z_dL%LbBdxl=9Fnw?H#kir<}?(*TjLN>HD*{rY-I%FJ~03=IwFIGj+}5QFq1u*|gM} zTWLo^aLXwFP+qVNu0(lBs;CN8M<%Jetw*EV)~@MR6Sb&6;dk|Jw62Hqv1?wGj=hW~ z%Da5m5prhAxOp~MrUp`u(eTN*?jPXNR#q&KpL$6K6z^+fjCC;)jj zHa9DZHTEqQR1(soR%bP%kl2ZWS7>`>X-hTrzWxBcz?xjcGfx{n^o|M18I=is8^ivt#kISn|(NI#*@BB3lHQVY? zGs>=OVr2^@>PcFArb8MJTh}nHmnNR#wDBAJA}Tgo>U+ZT|4&%cb?$i4K+gQww-sw{ z`LGqhRyg`R85I5V0G@~Ah7W_s9X_Bp0x-g>J3fpZ3m?$$1h5nKt;Q0oKCA|?icu84 zO~qYHJ}d>WbSm=VvwH5R;KMU~0(ce{jhK@$P$K_0Rqr~yd?Gz4Pw7d935Ri()PcSn zKsg-PtY;4~u130wEEZ)xumD*2J`a_A=sm(A(3b)zol^X-d=}Q?{H^)07Qk9K`ND^x zV-!LEB7hh8o8n5U-gAT*dyG#2kHZPohvG3#7rh#Q8jd~nVc-ZuMgKG)iw*Y**2up{ CE!L0# literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/exceptions.cpython-311.pyc b/inpost/static/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85ce0b47a365377934fa92aa8cb51dbbab47bb8b GIT binary patch literal 5729 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 literal 0 HcmV?d00001 diff --git a/inpost/static/__pycache__/notifications.cpython-311.pyc b/inpost/static/__pycache__/notifications.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebf522008ab71240985773928fafbd513c5ba0f9 GIT binary patch literal 2630 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

9D3I;B*tT3~_F*$uDIV;cwjPiC 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 ob3bZy3N^A>UI*HEJ3)=0^l_clV!Z literal 0 HcmV?d00001 diff --git a/tests/static_tests.py b/tests/static_tests.py new file mode 100644 index 0000000..f34fcd3 --- /dev/null +++ b/tests/static_tests.py @@ -0,0 +1,8 @@ +import asyncio + +import data +import data_responses +import pytest +import pytest_mock + +from inpost.static.parcels import Parcel diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..1c1386e --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,319 @@ +courier_parcel = { + "shipmentNumber": "954928772800409758129169", + "shipmentType": "courier", + "pickUpDate": "2022-12-13T11:20:59.000Z", + "parcelSize": "OTHER", + "receiver": {"email": "john@doe.com", "phoneNumber": "594244881", "name": "John Doe"}, + "sender": {"name": "Blueboat Trade Bulik J\\u00f3\\u017awiak sp.j."}, + "endOfWeekCollection": False, + "operations": { + "manualArchive": True, + "autoArchivableSince": "2022-12-13T11:20:59.000Z", + "delete": True, + "collect": False, + "expandAvizo": False, + "highlight": False, + "refreshUntil": "2023-01-23T15:16:38.395Z", + "requestEasyAccessZone": "DISALLOWED", + "voicebot": True, + "canShareToObserve": False, + "canShareOpenCode": False, + "canShareParcel": False, + }, + "status": "DELIVERED", + "eventLog": [ + {"type": "PARCEL_STATUS", "name": "DELIVERED", "date": "2022-12-13T11:20:59.000Z"}, + {"type": "PARCEL_STATUS", "name": "OUT_FOR_DELIVERY_TO_ADDRESS", "date": "2022-12-13T06:41:09.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-13T05:05:03.000Z"}, + {"type": "PARCEL_STATUS", "name": "SENT_FROM_SOURCE_BRANCH", "date": "2022-12-12T17:10:33.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-12T15:26:03.000Z"}, + {"type": "PARCEL_STATUS", "name": "COLLECTED_FROM_SENDER", "date": "2022-12-12T13:52:57.000Z"}, + {"type": "PARCEL_STATUS", "name": "CONFIRMED", "date": "2022-12-09T15:16:04.000Z"}, + ], + "avizoTransactionStatus": "NONE", + "sharedTo": [], + "ownershipStatus": "OWN", +} +parcel_locker = { + "shipmentNumber": "991798006092038618752844", + "shipmentType": "parcel", + "openCode": "465649", + "qrCode": "P|524507211|465649", + "storedDate": "2022-11-30T06:55:08.000Z", + "pickUpDate": "2022-11-30T13:00:46.000Z", + "parcelSize": "B", + "receiver": {"email": "john@doe.com", "phoneNumber": "524507211", "name": "john@doe.com"}, + "sender": {"name": "Wrapster Sp. z o.o."}, + "pickUpPoint": { + "name": "WRO23A", + "location": {"latitude": 51.0775, "longitude": 17.04745}, + "locationDescription": "Przy Centrum Handlowym Gaj", + "openingHours": "24/7", + "addressDetails": { + "postCode": "50-559", + "city": "Wroc\\u0142aw", + "province": "dolno\\u015bl\\u0105skie", + "street": "\\u015awieradowska", + "buildingNumber": "70", + }, + "virtual": 0, + "pointType": "PL", + "type": ["parcel_locker"], + "location247": True, + "doubled": False, + "imageUrl": "https://static.easypack24.net/points/pl/images/WRO23A.jpg", + "easyAccessZone": True, + "airSensor": True, + "airSensorData": { + "updatedUntil": "2022-11-30T06:55:08.000Z", + "airQuality": "good", + "temperature": 25.2, + "humidity": 35.3, + "pressure": 1020.5, + "pollutants": { + "pm25": { + "value": 25.2, + "percent": 30.5, + }, + "pm10": { + "value": 25.2, + "percent": 30.5, + }, + }, + }, + }, + "endOfWeekCollection": False, + "operations": { + "manualArchive": True, + "autoArchivableSince": "2022-11-30T13:00:46.000Z", + "delete": True, + "collect": False, + "expandAvizo": False, + "highlight": False, + "refreshUntil": "2023-01-13T12:51:55.329Z", + "requestEasyAccessZone": "DISALLOWED", + "voicebot": True, + "canShareToObserve": False, + "canShareOpenCode": False, + "canShareParcel": False, + }, + "status": "DELIVERED", + "eventLog": [ + {"type": "PARCEL_STATUS", "name": "DELIVERED", "date": "2022-11-30T13:00:46.000Z"}, + {"type": "PARCEL_STATUS", "name": "READY_TO_PICKUP", "date": "2022-11-30T06:55:08.000Z"}, + {"type": "PARCEL_STATUS", "name": "OUT_FOR_DELIVERY", "date": "2022-11-30T05:20:16.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-11-30T02:00:41.000Z"}, + {"type": "PARCEL_STATUS", "name": "SENT_FROM_SOURCE_BRANCH", "date": "2022-11-29T19:24:41.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-11-29T18:18:05.000Z"}, + {"type": "PARCEL_STATUS", "name": "COLLECTED_FROM_SENDER", "date": "2022-11-29T13:04:14.000Z"}, + {"type": "PARCEL_STATUS", "name": "CONFIRMED", "date": "2022-11-29T12:51:55.000Z"}, + ], + "avizoTransactionStatus": "NONE", + "sharedTo": [], + "ownershipStatus": "OWN", +} +parcel_locker_multi = { + "shipmentNumber": "687100956250559114549363", + "shipmentType": "parcel", + "openCode": "288432", + "qrCode": "P|794654933|288432", + "storedDate": "2022-12-08T09:13:15.000Z", + "pickUpDate": "2022-12-08T20:22:35.000Z", + "parcelSize": "A", + "receiver": { + "email": "ksvnlnbhmm+10a9590d7@allegromail.pl", + "phoneNumber": "794654933", + "name": "ksvnlnbhmm+10a9590d7@allegromail.pl", + }, + "sender": {"name": "RRMOTO.PL"}, + "pickUpPoint": { + "name": "WRO23A", + "location": {"latitude": 51.0775, "longitude": 17.04745}, + "locationDescription": "Przy Centrum Handlowym Gaj", + "openingHours": "24/7", + "addressDetails": { + "postCode": "50-559", + "city": "Wroc\\u0142aw", + "province": "dolno\\u015bl\\u0105skie", + "street": "\\u015awieradowska", + "buildingNumber": "70", + }, + "virtual": 0, + "pointType": "PL", + "type": ["parcel_locker"], + "location247": True, + "doubled": False, + "imageUrl": "https://static.easypack24.net/points/pl/images/WRO23A.jpg", + "easyAccessZone": True, + "airSensor": False, + }, + "multiCompartment": {"uuid": "ae863297-112a-4b6e-8c79-61aeda7ee540", "presentation": False, "collected": True}, + "endOfWeekCollection": False, + "operations": { + "manualArchive": True, + "autoArchivableSince": "2022-12-08T20:22:35.000Z", + "delete": True, + "collect": False, + "expandAvizo": False, + "highlight": False, + "refreshUntil": "2023-01-21T08:45:55.385Z", + "requestEasyAccessZone": "DISALLOWED", + "voicebot": True, + "canShareToObserve": False, + "canShareOpenCode": False, + "canShareParcel": False, + }, + "status": "DELIVERED", + "eventLog": [ + {"type": "PARCEL_STATUS", "name": "DELIVERED", "date": "2022-12-08T20:22:35.000Z"}, + {"type": "PARCEL_STATUS", "name": "READY_TO_PICKUP", "date": "2022-12-08T09:13:15.000Z"}, + {"type": "PARCEL_STATUS", "name": "OUT_FOR_DELIVERY", "date": "2022-12-08T05:43:59.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-08T03:24:30.000Z"}, + {"type": "PARCEL_STATUS", "name": "SENT_FROM_SOURCE_BRANCH", "date": "2022-12-07T18:38:23.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-07T16:31:53.000Z"}, + {"type": "PARCEL_STATUS", "name": "COLLECTED_FROM_SENDER", "date": "2022-12-07T14:52:42.000Z"}, + {"type": "PARCEL_STATUS", "name": "CONFIRMED", "date": "2022-12-07T08:45:55.000Z"}, + ], + "avizoTransactionStatus": "NONE", + "sharedTo": [], + "ownershipStatus": "OWN", +} +parcel_locker_multi_main = { + "shipmentNumber": "662025956250559113851741", + "shipmentType": "parcel", + "openCode": "250162", + "qrCode": "P|794654933|250162", + "storedDate": "2022-12-08T09:13:15.000Z", + "pickUpDate": "2022-12-08T20:22:35.000Z", + "parcelSize": "A", + "receiver": { + "email": "d38tp3wvmr+34f6eefb7@allegromail.pl", + "phoneNumber": "794654933", + "name": "d38tp3wvmr+34f6eefb7@allegromail.pl", + }, + "sender": {"name": "TM Products Tomasz Mali\\u0144ski"}, + "pickUpPoint": { + "name": "WRO23A", + "location": {"latitude": 51.0775, "longitude": 17.04745}, + "locationDescription": "Przy Centrum Handlowym Gaj", + "openingHours": "24/7", + "addressDetails": { + "postCode": "50-559", + "city": "Wroc\\u0142aw", + "province": "dolno\\u015bl\\u0105skie", + "street": "\\u015awieradowska", + "buildingNumber": "70", + }, + "virtual": 0, + "pointType": "PL", + "type": ["parcel_locker"], + "location247": True, + "doubled": False, + "imageUrl": "https://static.easypack24.net/points/pl/images/WRO23A.jpg", + "easyAccessZone": True, + "airSensor": True, + "airSensorData": { + "updatedUntil": "2022-11-30T06:55:08.000Z", + "airQuality": "good", + "temperature": 25.2, + "humidity": 35.3, + "pressure": 1020.5, + "pollutants": { + "pm25": { + "value": 25.2, + "percent": 30.5, + }, + "pm10": { + "value": 25.2, + "percent": 30.5, + }, + }, + }, + }, + "multiCompartment": { + "uuid": "ae863297-112a-4b6e-8c79-61aeda7ee540", + "shipmentNumbers": [ + "614500956250559117908538", + "690768956250559116536063", + "687100956250559114549363", + "662025956250559113851741", + ], + "presentation": False, + "collected": True, + }, + "endOfWeekCollection": False, + "operations": { + "manualArchive": True, + "autoArchivableSince": "2022-12-08T20:22:35.000Z", + "delete": True, + "collect": False, + "expandAvizo": False, + "highlight": False, + "refreshUntil": "2023-01-21T05:35:37.816Z", + "requestEasyAccessZone": "DISALLOWED", + "voicebot": True, + "canShareToObserve": False, + "canShareOpenCode": False, + "canShareParcel": False, + }, + "status": "DELIVERED", + "eventLog": [ + {"type": "PARCEL_STATUS", "name": "DELIVERED", "date": "2022-12-08T20:22:35.000Z"}, + {"type": "PARCEL_STATUS", "name": "READY_TO_PICKUP", "date": "2022-12-08T09:13:15.000Z"}, + {"type": "PARCEL_STATUS", "name": "OUT_FOR_DELIVERY", "date": "2022-12-08T05:43:59.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-08T02:00:13.000Z"}, + {"type": "PARCEL_STATUS", "name": "SENT_FROM_SOURCE_BRANCH", "date": "2022-12-07T16:11:23.000Z"}, + {"type": "PARCEL_STATUS", "name": "ADOPTED_AT_SOURCE_BRANCH", "date": "2022-12-07T12:57:17.000Z"}, + {"type": "PARCEL_STATUS", "name": "COLLECTED_FROM_SENDER", "date": "2022-12-07T12:53:51.000Z"}, + {"type": "PARCEL_STATUS", "name": "CONFIRMED", "date": "2022-12-07T05:35:37.000Z"}, + ], + "avizoTransactionStatus": "NONE", + "sharedTo": [], + "ownershipStatus": "OWN", +} + +parcel_properties = { + "sessionUuid": "426b2d3f-68ef-418e-b38b-ab1395bd0797", + "sessionExpirationTime": 40000, + "compartment": {"name": "3R1", "location": {"side": "R", "column": "2", "row": "1"}}, + "openCompartmentWaitingTime": 37000, + "actionTime": 25000, + "confirmActionTime": 50000, +} + +qr_result = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\xe4\x00\x00\x02\xe4\x01\x00\x00\x00\x00o\xdf\x1d\xc8" + b"\x00\x00\x03\x95IDATx\x9c\xed\xddK\x92\x9b0\x10\x00\xd0V*{\xb8A\xee\x7f\xac\xb9\x01\x9c@Y\xf0\xd1\x07<" + b"\x13\xc7\x9e\x94\x89_/\x98\x11\x86\xb7\xd0F%Zj\xa5\x1c\xdf\x17\xf3\x8fo\xc4#\xe8t:\x9dN\xa7\xd3\xe9t:" + b"\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\xfd_\xea\x91\xf78\xfc4\xac\xf7r\x9e\xeaf\xf7\xee\xd9kkLW\xee\x19" + b":\x9dN\xa7\xd3\x1f\xd4\xcb\x88\x98\xeb!5\xa5\xb1~\xea0\x9a\x1e_;\xd5\x9f\x1et:\x9dN\xa7\xbf\xba\x9e\x96" + b"\x18\xf7\xe6\xaf\x93\x19i\x1c\x06\xdc\xee\xb5[\xfas\x83N\xa7\xd3\xe9\xf4W\xd3\x7f~\xfes\x8e9E\x8e\x88Hk" + b"s\x8c\x88y\\\xee}\xb9G\xe7\xca=C\xa7\xd3\xe9t\xfa\xfd\xf1\xc5\xb8\x9ab\xc8\xcb\x90\x1a\xcb\x08;\xe4\x88" + b"\xe5\xf2'q\xe5\x9e\xa1\xd3\xe9t:\xfd\xfe\xe8\xc7\xd5\xdc7\xe7\xd4\xbf\x92b\x98\xb6\x01w\x98N_\xdb\xe2" + b"\xca=C\xa7\xd3\xe9t\xfa\x83\xfa\x9c\xaa\x88\x88=\xbf:\xac\x97c\x06\xf5\xfc\xb5S\xfd\xe9A\xa7\xd3\xe9t" + b"\xfa\xab\xe9\xd5|\xf5\xe6\xc7\xdd.\x97\x9aO.7\xe2\xca=C\xa7\xd3\xe9t\xfa\xd3\xf4y]\xdd\x9b\xf3\xc7>\x05" + b"\x9dS*\xcd\x94\xc6O._\xe8O\n:\x9dN\xa7\xd3_Mo\xf2\xab\xf3\xb8\xfc]\xe7\xa1s\x8a\xd8\xd3\xa8\xa9Z\xad" + b"\xb4\xad\x07n\xdeM\x11\xfb\xe2\xe1-\xae\xdc3t:\x9dN\xa7\xffE\xe46\xca\xbd\xa6p\xd2\xbaa\xb5~n\xd8\x1f" + b'\x99"\xeaf\x89\xe9\xca=C\xa7\xd3\xe9t\xfa\xfdq:_]g\xa4\xcb|uZ\xf6\xd9\xac\xf3\xd5f\xaa\x9a\xab\xbd7q' + b"\x96i\xbdr\xcf\xd0\xe9t:\x9d\xfe\x90\xbe&T\xa7\xb6\xf9\x91\xd2:A\x9d\xebLki\xd6\t\xd5--{\xa2\x7fC\xd0" + b"\xe9t:\x9d\xfe\xc2z\x19RK\xb3^\x8a4\xec_\x7f\x8f\x0f\xabcH\xa7\xd3\xe9tz\xc4!eZ'OK\x0cu\xe2\xf5\xfc\xe1" + b"-\xd3*\xbfJ\xa7\xd3\xe9\xf47\xd6O\xcf\x89+\x03d=|F\xdc8\xcafZ\xff;,^2\xae\xd2\xe9t:\xfd\xcd\xf4c}\xe0" + b"a\x8a\xa8\x8a\x146[gJ5\xa5\xee^\xa9wh\x9f\r\x9dN\xa7\xd3\xdfX\xbf]\x1f8\xb7Cj\xb7\xd8w\xdb\xd3:\xb5\xa7" + b"\xdd\xd8\xbfJ\xa7\xd3\xe9\xf4\xb7\xd6\xfb\xef\xc0\x87\xdd\xa8S\xfb\xf0t\xfe\xe5\xd8w`:\x9dN\xa7\xd3\xa3" + b"\x9b\xaf\x96sj\xe2\xd6Q6\xb1OU\xb77\xba2MM\\\xb9g\xe8t:\x9dN\xbf?\x8e\xdf\x81\xe7T\x950,\x85 \xd2\xe1" + b"\x911\xa2\x1az\xe5W\xe9t:\x9dN?9\xcf\xa69\xb9\xbc\x9e\xbeFT\x07\xae\xae\xb15\x0f3\xdb%\xae\xdc3t:\x9dN" + b"\xa7?\xa4\xa7>\xc6=\xa1:\xed\x0f\xcd\xa9\x1fBK5\x88.\x19\x1b\xffM\xcf\xd0\xe9t:\x9d\xfe\x87\xd1\xe7WK" + b"\xa4\xfd\xb2\xce]\x97\x9a\xc1cU.\xb8\xfeu\xdb\x9e\xd3\xc4\x95{\x86N\xa7\xd3\xe9\xf4\xfb\xa3\xcf\xaf\xd6" + b'\xeb\x91"\xaa\x814\xfa\x81t;\x18\xee\xb3\xb8r\xcf\xd0\xe9t:\x9d\xfe=\xfaZ)8\xa5\xd4U\xe5\xaf\xab\x17' + b"~\xd4\xb5\xf8\xef\xd1\xff>\xe8t:\x9dN\x7f5\xfdXo\xa9\x8bm\x19S\xaaO\x8c\x9b\x96\xdf\xb6\xb5L\xb9\xfa," + b"\xdc\xc4\x95{\x86N\xa7\xd3\xe9\xf4\xc7\xf5\xad\xa2\xc3~c^\x171u+\x98\xcai7\xa5\x191\xe4\xec\x9c8:\x9d" + b"N\xa7\xbf\xb1\xde\x9ek\xfei\xb6t\x1e\xa3$YS{\x9a\xf9Y\xe1\xe0\xb8v\xcf\xd0\xe9t:\x9d~\x7f\xa4c\x91\xa4" + b"\xe7\xc5|\xe5\x9e\xa1\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9\xf46~\x03\x0f" + b"\xb5\t\xa2\xca\nmu\x00\x00\x00\x00IEND\xaeB`\x82" +) + +open_data_result = { + "openCode": "465649", + "receiverPhoneNumber": "524507211", + "shipmentNumber": "991798006092038618752844", +} diff --git a/tests/test_parcels.py b/tests/test_parcels.py new file mode 100644 index 0000000..51df2af --- /dev/null +++ b/tests/test_parcels.py @@ -0,0 +1,239 @@ +import logging + +import pytest + +from inpost.static import CompartmentActualStatus +from inpost.static.parcels import CompartmentLocation, CompartmentProperties, Parcel +from tests.test_data import ( + courier_parcel, + open_data_result, + parcel_locker, + parcel_locker_multi, + parcel_locker_multi_main, + parcel_properties, + qr_result, +) + + +def build_dict(object, test_data, keys: list): + test_input_dict = {} + expected_dict = {} + for key in keys: + test_input_dict[key] = eval("object." + key) + expected_dict[key] = eval("test_data." + key) + return test_input_dict, expected_dict + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, "465649"), + (courier_parcel, None), + ], +) +def test_open_code(test_input, expected): + parcel_code = Parcel(test_input, logging.getLogger(__name__)).open_code + assert parcel_code == expected, f"parcel_code: {parcel_code} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, qr_result), + ], +) +def test_generate_qr_image(test_input, expected): + qr_image = Parcel(test_input, logging.getLogger(__name__)).generate_qr_image + assert qr_image == expected, f"qr_image: {qr_image} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, None), + (courier_parcel, None), + ], +) +def test_compartment_properties(test_input, expected): + compartment_properties = Parcel(test_input, logging.getLogger(__name__)).compartment_properties + assert ( + compartment_properties == expected + ), f"compartment_properties: {compartment_properties} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, parcel_properties), + ], +) +def test_compartment_properties_setter(test_input, expected): + parcel = Parcel(test_input, logging.getLogger(__name__)) + parcel.compartment_properties = expected + test_data = CompartmentProperties(expected, logging.getLogger(__name__)) + test_input_dict, expected_dict = build_dict( + parcel.compartment_properties, test_data, ["_status", "_session_uuid", "_session_expiration_time", "_location"] + ) + assert ( + test_input_dict == expected_dict + ), f"compartment_properties_setter: {test_input_dict} != expected: {expected_dict}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, None), + (courier_parcel, None), + ], +) +def test_compartment_location(test_input, expected): + compartment_location = Parcel(test_input, logging.getLogger(__name__)).compartment_location + assert compartment_location == expected, f"compartment_location: {compartment_location} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, parcel_properties), + ], +) +def test_compartment_location_setter(test_input, expected): + parcel = Parcel(test_input, logging.getLogger(__name__)) + parcel.compartment_properties = expected + parcel.compartment_location = expected + test_data = CompartmentLocation(expected, logging.getLogger(__name__)) + test_input_dict, expected_dict = build_dict( + parcel.compartment_location, + test_data, + ["name", "side", "column", "row", "open_compartment_waiting_time", "action_time", "confirm_action_time"], + ) + assert ( + test_input_dict == expected_dict + ), f"compartment_location_setter: {test_input_dict} != expected: {expected_dict}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, None), + (courier_parcel, None), + ], +) +def test_compartment_status(test_input, expected): + parcel_status = Parcel(test_input, logging.getLogger(__name__)).compartment_status + assert parcel_status == expected, f"parcel_status: {parcel_status} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ("CLOSED", CompartmentActualStatus.CLOSED), + ("OPENED", CompartmentActualStatus.OPENED), + ("PENIS", CompartmentActualStatus.UNKNOWN), + (CompartmentActualStatus.CLOSED, CompartmentActualStatus.CLOSED), + (CompartmentActualStatus.OPENED, CompartmentActualStatus.OPENED), + ], +) +def test_compartment_status_setter(test_input, expected): + parcel = Parcel(parcel_locker, logging.getLogger(__name__)) + parcel.compartment_properties = parcel_properties + parcel.compartment_status = test_input + assert ( + parcel.compartment_status == expected + ), f"compartment_status: {parcel.compartment_status} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, open_data_result), + (courier_parcel, None), + ], +) +def test_compartment_open_data(test_input, expected): + parcel_open_data = Parcel(test_input, logging.getLogger(__name__)).compartment_open_data + assert parcel_open_data == expected, f"parcel_open_data: {parcel_open_data} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, parcel_locker), + (parcel_locker, parcel_locker), + (parcel_locker, parcel_locker), + (parcel_locker, parcel_locker), + (parcel_locker, parcel_locker), + (parcel_locker_multi, parcel_locker_multi), + (parcel_locker_multi, parcel_locker_multi), + (parcel_locker_multi, parcel_locker_multi), + (parcel_locker_multi, parcel_locker_multi), + (parcel_locker_multi, parcel_locker_multi), + (parcel_locker_multi_main, parcel_locker_multi_main), + (parcel_locker_multi_main, parcel_locker_multi_main), + (parcel_locker_multi_main, parcel_locker_multi_main), + (parcel_locker_multi_main, parcel_locker_multi_main), + (parcel_locker_multi_main, parcel_locker_multi_main), + (courier_parcel, None), + ], +) +def test_mocked_location(test_input, expected): + mocked_location = Parcel(test_input, logging.getLogger(__name__)).mocked_location + expected = expected["pickUpPoint"]["location"] + is_in_range = ( + abs(mocked_location["latitude"] - expected["latitude"]) <= 0.00005 + and abs(mocked_location["longitude"] - expected["longitude"]) <= 0.00005 + ) + assert is_in_range, ( + f"mocked_location invalid threshold. Should be less or equal to 0.00005 but on of values is not meeting it" + f"mocked_latitude: {mocked_location['latitude']}, expected: {expected['latitude']}," + f" diff: {abs(mocked_location['latitude'] - expected['latitude'])}" + f"mocked_latitude: {mocked_location['longitude']}, expected: {expected['longitude']}," + f" diff: {abs(mocked_location['latitude'] - expected['longitude'])}" + ) + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, False), + (parcel_locker_multi, True), + (parcel_locker_multi_main, True), + (courier_parcel, False), + ], +) +def test_is_multicompartment(test_input, expected): + is_multicompartment = Parcel(test_input, logging.getLogger(__name__)).is_multicompartment + assert is_multicompartment is expected, f"is_multicompartment: {is_multicompartment} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, None), + (parcel_locker_multi, False), + (parcel_locker_multi_main, True), + (courier_parcel, None), + ], +) +def test_is_main_multicompartment(test_input, expected): + is_main_multicompartment = Parcel(test_input, logging.getLogger(__name__)).is_main_multicompartment + assert ( + is_main_multicompartment is expected + ), f"is_main_multicompartment: {is_main_multicompartment} != expected: {expected}" + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (parcel_locker, True), + (parcel_locker_multi, False), + (parcel_locker_multi_main, True), + (courier_parcel, None), + ], +) +def test_has_airsensor(test_input, expected): + has_airsensor = Parcel(test_input, logging.getLogger(__name__)).has_airsensor + assert has_airsensor is expected, f"has_airsensor: {has_airsensor} != expected: {expected}" + + +# TODO: Add rest tests! From 63ee57b2975d0fe48f5d78e6410f0af4b64e5e4c Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 21:28:57 +0200 Subject: [PATCH 21/28] Remove these stupid __pycache__ directories --- .gitignore | 2 ++ inpost/__pycache__/__init__.cpython-311.pyc | Bin 220 -> 0 bytes inpost/__pycache__/api.cpython-311.pyc | Bin 71614 -> 0 bytes .../static/__pycache__/__init__.cpython-311.pyc | Bin 3637 -> 0 bytes .../__pycache__/endpoints.cpython-311.pyc | Bin 3672 -> 0 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 5729 -> 0 bytes .../static/__pycache__/friends.cpython-311.pyc | Bin 4050 -> 0 bytes .../static/__pycache__/headers.cpython-311.pyc | Bin 229 -> 0 bytes .../__pycache__/notifications.cpython-311.pyc | Bin 2630 -> 0 bytes .../static/__pycache__/parcels.cpython-311.pyc | Bin 68105 -> 0 bytes .../static/__pycache__/statuses.cpython-311.pyc | Bin 11966 -> 0 bytes 11 files changed, 2 insertions(+) delete mode 100644 inpost/__pycache__/__init__.cpython-311.pyc delete mode 100644 inpost/__pycache__/api.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/__init__.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/endpoints.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/exceptions.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/friends.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/headers.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/notifications.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/parcels.cpython-311.pyc delete mode 100644 inpost/static/__pycache__/statuses.cpython-311.pyc diff --git a/.gitignore b/.gitignore index 232dc67..137472a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ /tests/data.json /tests/data.py /tests/data_responses.py +/inpost/static/__pycache__/ +/inpost/__pycache__/ diff --git a/inpost/__pycache__/__init__.cpython-311.pyc b/inpost/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index d81aef5aab92ec94fb6aa7e02cd93f3ae4b76013..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 220 zcmZ3^%ge<81lQlRq(}hi#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@8G#a- zjJMc4^9u5dOZ+sMZZRhoWEL?4g;z3s25J9g?`#zlTAW%`9Fv<^l3E%QP??;OSd<%3 zl%JKFTv8m93D&AxP+5|Zp9kf|#K&jmWtPOp>lIY~;;_lhPbtkwwJYKPng_D6SQbcp TU}j`wyul!T0UIh}1F8T3-mf}4 diff --git a/inpost/__pycache__/api.cpython-311.pyc b/inpost/__pycache__/api.cpython-311.pyc deleted file mode 100644 index a4e2c208b7c84d8d9dc87178c24b1eee2fcc1e48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71614 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 diff --git a/inpost/static/__pycache__/__init__.cpython-311.pyc b/inpost/static/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index d8a1232f490977cdd8fcb5f1311a32415b225f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3637 zcmeH}OHUj}5P)ZS?0eZ=-Waeke!$ws?imSCWpp2d{3XmVzROjoW+EgK7{-StU;8OPhTBfnYM^cs=n3^tt5R^|#S zu%^#J8L-J5-St9y$2N`7_S|S`<#@#RY&ZNw#BGzsBF=U=3-ijx-l?L-ry| z0|$m$4)9m(YB1yiE z!Mxlg@susskP&&@{^mHG-YwU*D8wC`S#!1Jrs79wBpPT{Y+Yx0D`0%Zi*|+5tt3Qp zNu3GVN*TWYI`CZC%*%{4_fMru^vr)e3daUSUyHEDt2LJdjbYc)OysO53zW zOJvuiRG`zef8~bVfhP^$p5}NN+Hc*$0e^{D#_e@tkB`?m8*W%HJ@S zrR!?5ZF-fm7-(@iXNC>KsjEXShFS&|-}v&VGbItfWa zijXE`2sr}1emqYo5abi6@gku_7$uYm-Gm+qk@D4f3wSSy`Uw4m0m2|*h;WK9Oc)`Y zCY&LR5zZ3E3Fips2^R<#2@`}#LWOXNaG7w0Fh#gZxJI~6xIwr{xJ9^4xI?%{xKDUM zm?k_V%n%+CW(ki8PY82_r-XUJ0%4J`M0iGcPFNDs-f!gmnAt_jn6BBDrd72BFo;;Q%dj3zgQKKitln&9+BE(hCJVas*@<3c+H}+ zty8}Ktaf*4;ByKn(t)}gSn$tFxk}DY)>EW(-EbUT|5bZ0N3{=2^V8dO5eM5S-tbGX>*AgRdwc*z9J55SXeBWskZ%=-Y+KRNo b|1QS5DZVcilhs5{hX0Y@r|6G^Je+?3#$WY= diff --git a/inpost/static/__pycache__/endpoints.cpython-311.pyc b/inpost/static/__pycache__/endpoints.cpython-311.pyc deleted file mode 100644 index 2bba3c5a028f1fe9898f0f87e065ddf5e4df1278..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3672 zcmb`K&r{n*6vv-KLcoqO#(;tFE6tBKZ9##g`9VlBEzp)g!HgTqY{)1=T8fG-DOx!* z^yDK)ZskAd^w?|v3hAcNsWbftGLu_QefuOkB=lf6@o3)L&l-RC?c2Az;`eMe4fqMw zW8=>Zz&{cipN^#Q`mYqgZ}0>RV8B!SjFFnhuZ%OSgQ$b(EIUhdj;NEJBRWq6tdppV zsGFT9N)u&R7tsZxEbAufA#R2M3-4V(G{YrY=Gz*(Wh*X=rf}0tVr}Z(HCrp=u4t0 zHcT{4q_7d98$>hgBGD|-O*Tq&i|8vhMs%C#4jU)BOLUJ-5X}+YXOl$pL=Vbv{o${- z_dL%LbBdxl=9Fnw?H#kir<}?(*TjLN>HD*{rY-I%FJ~03=IwFIGj+}5QFq1u*|gM} zTWLo^aLXwFP+qVNu0(lBs;CN8M<%Jetw*EV)~@MR6Sb&6;dk|Jw62Hqv1?wGj=hW~ z%Da5m5prhAxOp~MrUp`u(eTN*?jPXNR#q&KpL$6K6z^+fjCC;)jj zHa9DZHTEqQR1(soR%bP%kl2ZWS7>`>X-hTrzWxBcz?xjcGfx{n^o|M18I=is8^ivt#kISn|(NI#*@BB3lHQVY? zGs>=OVr2^@>PcFArb8MJTh}nHmnNR#wDBAJA}Tgo>U+ZT|4&%cb?$i4K+gQww-sw{ z`LGqhRyg`R85I5V0G@~Ah7W_s9X_Bp0x-g>J3fpZ3m?$$1h5nKt;Q0oKCA|?icu84 zO~qYHJ}d>WbSm=VvwH5R;KMU~0(ce{jhK@$P$K_0Rqr~yd?Gz4Pw7d935Ri()PcSn zKsg-PtY;4~u130wEEZ)xumD*2J`a_A=sm(A(3b)zol^X-d=}Q?{H^)07Qk9K`ND^x zV-!LEB7hh8o8n5U-gAT*dyG#2kHZPohvG3#7rh#Q8jd~nVc-ZuMgKG)iw*Y**2up{ CE!L0# diff --git a/inpost/static/__pycache__/exceptions.cpython-311.pyc b/inpost/static/__pycache__/exceptions.cpython-311.pyc deleted file mode 100644 index 85ce0b47a365377934fa92aa8cb51dbbab47bb8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5729 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 diff --git a/inpost/static/__pycache__/notifications.cpython-311.pyc b/inpost/static/__pycache__/notifications.cpython-311.pyc deleted file mode 100644 index ebf522008ab71240985773928fafbd513c5ba0f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2630 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

9D3I;B*tT3~_F*$uDIV;cwjPiC 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 ob3bZy3N^A>UI*HEJ3)=0^l_clV!Z From 2afc489f1df3b3402c15419374cc9a8c3a54f34a Mon Sep 17 00:00:00 2001 From: Mrkazik99 Date: Sun, 25 Jun 2023 21:38:18 +0200 Subject: [PATCH 22/28] Fix qr_image test and add two test cases for it --- tests/test_data.py | 67 +++++++++++++++++++++++++++++++++++++++++++ tests/test_parcels.py | 6 +++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/tests/test_data.py b/tests/test_data.py index 1c1386e..ca1f785 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -312,6 +312,73 @@ b"\xb5\t\xa2\xca\nmu\x00\x00\x00\x00IEND\xaeB`\x82" ) +qr_result_multi = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\xe4\x00\x00\x02\xe4\x01\x00\x00\x00\x00o\xdf" + b"\x1d\xc8\x00\x00\x03\x85IDATx\x9c\xed\xddM\x92\x9b0\x10\x06\xd0V*{|\xff\xdb\xe5\x06\xe6\x04\xca" + b"\x02a\xfd\x80+\xc1\x99IF\xf1\xfb\x16\x8e1\xf0*\xa5\x8dJ\xa0\xeeI9>/\xeb\xb7O\xc4#\xe8t:\x9dN\xa7" + b"\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\xfd\x1f\xe9i\xcc\xad\xfc\x16\xb1\x96\xc3\x92" + b"\xb5\\\xf0\xf4\xb6\xbf\xfe\x7f\xa7\xd3\xe9t:\xfd\xeb\xe9Kn\x12\x11\x119\xdf\xcb\xb9\x9c\x7f\x94" + b"\x19\xf6`\x1co;\xd5?ve\xe6\x91\xa1\xd3\xe9" + b"t:\xfdz\x8e\xfb\x81\xd7[D\xa9\xa4y<\xd4]\xee\xdbK\xd6\xdc\x7f\x0c\xa9k\xd8=3\x8f\x0c\x9dN\xa7\xd3" + b"\xe9\xd7\xd3\xcc\xab\xe91}F\xfb\xe0\xf76~K\xfd\x0e\xa6v\x9a]\x86\xe9v\xe6\x91\xa1\xd3\xe9t:\xfd" + b"\x85\x0cO\x7f\x0f\x1f]\xca\x1d\xdd\x89\xc3\xd3\xdf\xc6\x9byd\xe8t:\x9dN\x7f!\x87\x0e\x84K[\xab:" + b"|k\x8bX\xeb\xd9\xe6\x85\xeap8\xf3\xc8\xd0\xe9t:\x9d~='\xefWKo\xa5x<\xf8-\xa5\xabK\x8e\xe8;\x1a" + b"\xd6\x1c\x9b\x06G\xc4\xdc#C\xa7\xd3\xe9t\xfa\xf5<\xefcX\xbb\x02\x1f\x9aCl[\x9b\xee{s\x88\xee\x84" + b"}Kt:\x9dN\x7fc\xbd\x9bW\x87m\xbfk\x8a\xdc\xac\\\xb7\x99\xb3\xfcV\x17\xa8\xb5\xba5\xd9\xb7D\xa7" + b"\xd3\xe9\xf47\xd7\xbb\xbe\x10\xb5L\xa6\x16\xdbtg\xa39\xbb_\x9c\x9a\xb3\xeal\xe8t:\x9d\xfe\xdez_g" + b"\x93\xf7Y\xb2\x9bRS\xf3j\xb5\x96\xddD\xa9\xb8\xc9\xfd\xbd]f\x1e\x19:\x9dN\xa7\xd3_\xc8\xa1\x88" + b"\xa6\x96\xceD[S3\x94\xd3\x1c\x1a,\xdd#\xf4[\xa2\xd3\xe9t\xfa\xbb\xebc!\xea\xa1\x92\xb5\xabZ\x1d" + b"\xbeU`\x98p\xcd\xabt:\x9dN\x7fO}\xdc\x0f\xbc\xef\xee\xcd\xe3_\x82\xfbU\x89M\xed\xd0\xd4d\xe6\x91" + b"\xa1\xd3\xe9t:\xfd\x85\x9c\xb4IjR.\xea\x1e\xf7\x9e\xf5\x808\xf7\xacW\xe9t:\x9d\xfefz\xb7oi\xc8r" + b"\xdf\xba\x02\xe7~\xef\xefP\xa6:\xb4\x0b\xee2\xf3\xc8\xd0\xe9t:\x9d~=c\xfdjM:\xf9\x88\xfe\xb1p-v" + b"\x8d\xf3\xc9u\xe6\x91\xa1\xd3\xe9t:\xfdz\x9e\xbd_\xbdo\xff\xb6\x15\xaa\x11\x91\x0e+\xd7'\r\x0cKf" + b"\x1e\x19:\x9dN\xa7\xd3?G/\xefR\xd7\x94\x9e\xfc\xf9\xf2\x88\x9c\x7f\xa4T.\xb9\xa8\xbf\x1e:\x9dN" + b"\xa7\xd3\xbf\x9a\xfe\xbc?p\xcdaM:\xbcs\xdd7\x0f\x1f3\xf3\xc8\xd0\xe9t:\x9d~=\xe3\xbcz\x98\x1d\x87" + b"V\xfa]\x7f\xe0\xf4\xe8\x19\x1c\xe5\xb0\xaf\xcc\x99zd\xe8t:\x9dN\xbf\x9en^={[Z\xd7\xa1C\xfdj\xf7\n" + b"V\xfd*\x9dN\xa7\xd3\xe9\x11\x91\xce\x1e\xdf~T\xd6\x99G\x86N\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9dN" + b"\xa7\xd3\xe9t:\x9dN\xa7\xd3\xfb\xfc\x04@\xc6C\xc0\xb4\x1e\xf6\xfb\x00\x00\x00\x00IEND\xaeB`\x82" +) + +qr_result_multi_main = ( + b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\xe4\x00\x00\x02\xe4\x01\x00\x00\x00\x00o\xdf" + b"\x1d\xc8\x00\x00\x03\x8fIDATx\x9c\xed\xddA\x92\xa38\x10\x00\xc0\xd2\xc6\xde\xe1\x07\xf3\xffg" + b"\xcd\x0f\xe0\x05\xda\x83\x85\x11\x92\xd8i\xbb\xbbg\xcc8\xeb@t#\x94\xe1\xd0\x85\xa8B\x14)\xc7" + b"\xf7\xc5\xfa\xcf7\xe2\x11t:\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9dN\xa7\xd3\xe9t:\x9d\xfe" + b"\x87\xf4\xd4\xc6|?7\x1f'\xad\xe5\xf4\xe9\xb4\xdf\xfe\xdb\xe9t:\x9dN\x7f=}\xcaUDDD\xceK9\x94" + b"\x1b\xe9:\xb8\xcd\xf6\xd3\x86\xfa\x97\x07\x9dN\xa7\xd3\xe9\xaf\xae79\xe7\x9e\xb4\x96\x9b\xeb" + b'\x8f\x9c\xcb\x8dt)\xa3\xa3ig\xfa\xd7\x06\x9dN\xa7\xd3\xe9\xaf\xa6\xff\xfb\xff\xc39""\xdd\x0e' + b"S\xbe\xfd\x15\xb1\xa6\xdb\xc0/\xdf\xd1\xb9\xf2\xca\xd0\xe9t:\x9d\xfe\xe5zJsD\xa9\xf0\xfe,u" + b"\xe0\x88)\x7f\xf0\xad\xd7\xbfxe\xe8t:\x9dN\x1fD\x9b\xaf\xe6\xf6\xdf5\r\xe7\x95\xf4uZ\x86\xd3" + b"\xb6\xb8\xf2\xca\xd0\xe9t:\x9d\xfeI}\xad\xb7\xf5FD\xfd@\xb5>4\xd1O\x1b\xea_\x1et:\x9dN\xa7" + b"\xbf\x9a^\xe5\xab\xa7\xb5\xdd:i\xdd\x1f\xad~\xe4!\xeb\x95W\x86N\xa7\xd3\xe9\xf4'\"\x0fc\x89" + b"\xd8\xb7\xfd\x8e\xae\xebr\xd8\xc3aS\xae\xbc2t:\x9dN\xa7?\x1e\x87\xe7\xab\xeb\xbcm\x00\x9e" + b"\xea,4\xdfG\xe3\xfc\x81\xea\x9ab\xcff\xb7\xb8\xf2\xca\xd0\xe9t:\x9d\xfexT\xf7\xd5\xe6M\x9a" + b'\x88i\xd9^\xb1\xd9"\xdf\x07\xa2:w\x1b\x98\x07\x15\xe1+\xaf\x0c\x9dN\xa7\xd3\xe9ODW\xfd\x8d' + b'\xba\xcb\xd2^\xd5\xdd\xb7,\x95\xd1\xbe"\xdcW\x93\xaf\xbc2t:\x9dN\xa7?\x1e\x87|5"\xf6d4E\xd4' + b"\t\xea\xbeQ)\xd5\xa3\xf3\xed\xe2Q\xa98\xae\xbd2t:\x9dN\xa7\x7fJ/\t\xea\xd6\xf9aO_K\x1c\xf2" + b"\xd5&\xf41\xa4\xd3\xe9t:=b\xb0\xcf7\xceJ\xc0}\xa5\xb7\xdb\x05\xbc4u\xe5+\xaf\x0c\x9dN\xa7" + b"\xd3\xe9\x8f\xc7I\x7f\xe0)G\xd9\xdd\xbb\xce[1x{\x89\xf5\xb4%S\xb3\x8d8\xae\xbd2t:\x9dN\xa7?" + b"\x11M\xf6Y\x17y\xa7A\xfa\xba\xdc\xa7-en}I\xc8W\xe9t:\x9d\xfe\xd6\xfa\xe1\xbe::7uu\xe0rQsN" + b"\x1d\x98N\xa7\xd3\xe9\xf4A\x1dxZn\xfd\xf4\xf7\xfd\xc0\xcdV\xe0\xfd\xc5\xd6\xb8\x9f\xdb\xf6" + b'\r\xd7\xbd\xf8\xe3\xda+C\xa7\xd3\xe9t\xfa\x13\xd1\xd5\x81\xf3=\xf1\\"\xbaF\xfbM\xa98\xc6\x9b' + b"\x9c\xe4\xabt:\x9dN\x7fO\xbd\xcfWo\xef\xa0\xe6\xa8\xbfL\xb3\xa7\xa0\xfdg\xe3\xd2q@\x1fC:\x9d" + b"N\xa7\xbf\xb1\xde\x7f\xcff\xdb\xd3[\n\xbf\xfbf\xdf\xed\xdc\xb6Gx\x8e\xa6\x89D\x1fW^\x19:\x9d" + b"N\xa7\xd3?\xa5\xa76\xe6{\x95\xf8gJ\xa3n\x10e\xda\xfc\x11\xfd\x1b\x82N\xa7\xd3\xe9\xf4W\xd3" + b"\x0fu\xe0C?\xfd\xd2\xaepo\xbe\x7f\xdb\x94t\xaf\xf3n\xa5\xe2<\x98\xbb\xc5\x95W\x86N\xa7\xd3" + b"\xe9\xf4\xc7\xa3}\xbe\xda4\t\xce\xf7\xe6\x10%\xd6\x14\xb9\xfd\xee\xcdy\\ye\xe8t:\x9dN\x7f Date: Sun, 25 Jun 2023 21:56:52 +0200 Subject: [PATCH 23/28] Fix equation to avoid approximation error and remove test case that is failing --- tests/test_parcels.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_parcels.py b/tests/test_parcels.py index 21810ff..546df71 100644 --- a/tests/test_parcels.py +++ b/tests/test_parcels.py @@ -177,22 +177,21 @@ def test_compartment_open_data(test_input, expected): (parcel_locker_multi_main, parcel_locker_multi_main), (parcel_locker_multi_main, parcel_locker_multi_main), (parcel_locker_multi_main, parcel_locker_multi_main), - (courier_parcel, None), ], ) def test_mocked_location(test_input, expected): mocked_location = Parcel(test_input, logging.getLogger(__name__)).mocked_location expected = expected["pickUpPoint"]["location"] is_in_range = ( - abs(mocked_location["latitude"] - expected["latitude"]) <= 0.00005 - and abs(mocked_location["longitude"] - expected["longitude"]) <= 0.00005 + round(abs(mocked_location["latitude"] - expected["latitude"]), 6) <= 0.00005 + and round(abs(mocked_location["longitude"] - expected["longitude"]), 6) <= 0.00005 ) assert is_in_range, ( - f"mocked_location invalid threshold. Should be less or equal to 0.00005 but on of values is not meeting it" - f"mocked_latitude: {mocked_location['latitude']}, expected: {expected['latitude']}," - f" diff: {abs(mocked_location['latitude'] - expected['latitude'])}" - f"mocked_latitude: {mocked_location['longitude']}, expected: {expected['longitude']}," - f" diff: {abs(mocked_location['latitude'] - expected['longitude'])}" + f"mocked_location invalid threshold. Should be less or equal to 0.00005 but one of values is not meeting it. " + f"mocked_latitude: {mocked_location['latitude']}, expected: {expected['latitude']}, " + f" diff: {abs(mocked_location['latitude'] - expected['latitude'])}. " + f"mocked_longitude: {mocked_location['longitude']}, expected: {expected['longitude']}, " + f" diff: {abs(mocked_location['longitude'] - expected['longitude'])}" ) From 600220ae6537827858e808e4775d6323a86021f6 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sun, 2 Jul 2023 10:38:53 +0200 Subject: [PATCH 24/28] Fixed wrong ParcelShipmentType log, inpost version update --- inpost/static/parcels.py | 18 ++++++++++++------ pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index e53ef46..a80a303 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -143,7 +143,7 @@ def open_code(self) -> str | None: self._log.debug("got open code") return self._open_code - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -159,7 +159,7 @@ def generate_qr_image(self) -> BytesIO | None: self._log.debug("got qr image") return self._qr_code.qr_image - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -175,7 +175,7 @@ def compartment_properties(self): self._log.debug("got compartment properties") return self._compartment_properties - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_properties.setter @@ -192,8 +192,9 @@ def compartment_properties(self, compartmentproperties_data: dict): self._compartment_properties = CompartmentProperties( compartmentproperties_data=compartmentproperties_data, logger=self._log ) + return - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_location(self): @@ -208,7 +209,7 @@ def compartment_location(self): self._log.debug("got compartment location") return self._compartment_properties.location if self._compartment_properties else None - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_location.setter @@ -223,8 +224,9 @@ def compartment_location(self, location_data: dict): if self.shipment_type == ParcelShipmentType.parcel and self._compartment_properties is not None: self._log.debug("compartment location set") self._compartment_properties.location = location_data + return - self._log.debug(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property def compartment_status(self) -> CompartmentActualStatus | None: @@ -259,6 +261,7 @@ def compartment_status(self, status) -> None: if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") self._compartment_properties.status = status + return self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @@ -459,6 +462,7 @@ def compartment_properties(self, compartmentproperties_data: dict): self._compartment_properties = CompartmentProperties( compartmentproperties_data=compartmentproperties_data, logger=self._log ) + return self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @@ -493,6 +497,7 @@ def compartment_location(self, location_data: dict): if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment location set") self._compartment_properties.location = location_data + return self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @@ -531,6 +536,7 @@ def compartment_status(self, status: str | CompartmentActualStatus): if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("compartment status set") self._compartment_properties.status = status + return self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") diff --git a/pyproject.toml b/pyproject.toml index 7508d2c..16ccb46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.4" +version = "0.1.5a1" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] From 4405cc65bcd0446a7cb920b29e4429ac46adc7e9 Mon Sep 17 00:00:00 2001 From: loboda4450 Date: Sun, 23 Jul 2023 11:01:08 +0200 Subject: [PATCH 25/28] Fixed no compartment location --- inpost/api.py | 15 ++++++++------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/inpost/api.py b/inpost/api.py index f658c91..b4f326d 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -589,13 +589,13 @@ async def collect_compartment_properties( raise UnidentifiedAPIError(reason=resp) - async def open_compartment(self, parcel_obj: Parcel) -> bool: + async def open_compartment(self, parcel_obj: Parcel) -> Parcel: """Opens compartment for `Inpost.parcel` object :param parcel_obj: Parcel object :type parcel_obj: Parcel - :return: True if compartment gets opened - :rtype: bool + :return: Parcel with compartment location set if it gets opened + :rtype: Parcel :raises NotAuthenticatedError: User not authenticated in inpost service :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found @@ -620,7 +620,8 @@ async def open_compartment(self, parcel_obj: Parcel) -> bool: if resp.status == 200: self._log.debug(f"opened compartment for {parcel_obj.shipment_number}") - return True + parcel_obj.compartment_location = await resp.json() + return parcel_obj raise UnidentifiedAPIError(reason=resp) @@ -741,9 +742,9 @@ async def collect( self._log.info(f"collecting parcel with shipment number {parcel_obj.shipment_number}") if parcel_obj_ := await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): - if await self.open_compartment(parcel_obj=parcel_obj_): - if await self.check_compartment_status(parcel_obj=parcel_obj_): - return parcel_obj_ + if parcel_obj__ := await self.open_compartment(parcel_obj=parcel_obj_): + if await self.check_compartment_status(parcel_obj=parcel_obj__): + return parcel_obj__ return None diff --git a/pyproject.toml b/pyproject.toml index 16ccb46..527a9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.5a1" +version = "0.1.5a2" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] From e6c1a0aa28eb4f8a9288851a6f5b530b90941850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=81oboda?= Date: Sun, 17 Dec 2023 19:44:23 +0100 Subject: [PATCH 26/28] Fixed reopening url and log message --- .gitignore | 1 + inpost/api.py | 9 +++++---- inpost/static/__init__.py | 2 ++ inpost/static/endpoints.py | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 137472a..a5fb698 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /tests/data_responses.py /inpost/static/__pycache__/ /inpost/__pycache__/ +/otwarcie paczkomatu.xml.html \ No newline at end of file diff --git a/inpost/api.py b/inpost/api.py index b4f326d..f385a38 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -36,6 +36,7 @@ blik_status_url, collect_url, compartment_open_url, + compartment_reopen_url, compartment_status_url, confirm_sms_code_url, create_blik_url, @@ -788,7 +789,7 @@ async def reopen_compartment(self, parcel_obj: Parcel) -> bool: resp = await self.request( method="post", action=f"reopen compartment for {parcel_obj.shipment_number}", - url=compartment_open_url, + url=compartment_reopen_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -796,7 +797,7 @@ async def reopen_compartment(self, parcel_obj: Parcel) -> bool: ) if resp.status == 200: - self._log.debug(f"opened compartment for {parcel_obj.shipment_number}") + self._log.debug(f"reopened compartment for {parcel_obj.shipment_number}") return True raise UnidentifiedAPIError(reason=resp) @@ -1132,7 +1133,7 @@ async def reopen_send_compartment(self, parcel_obj: SentParcel) -> bool: resp = await self.request( method="post", action=f"reopen compartment for {parcel_obj.shipment_number}", - url=compartment_open_url, + url=compartment_reopen_url, auth=True, headers=None, data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, @@ -1140,7 +1141,7 @@ async def reopen_send_compartment(self, parcel_obj: SentParcel) -> bool: ) if resp.status == 200: - self._log.debug(f"opened send compartment for {parcel_obj.shipment_number}") + self._log.debug(f"reopened send compartment for {parcel_obj.shipment_number}") return True return False diff --git a/inpost/static/__init__.py b/inpost/static/__init__.py index d0cb55e..c42d0ed 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -3,6 +3,7 @@ blik_status_url, collect_url, compartment_open_url, + compartment_reopen_url, compartment_status_url, confirm_sent_url, confirm_sms_code_url, @@ -90,6 +91,7 @@ "blik_status_url", "collect_url", "compartment_open_url", + "compartment_reopen_url", "compartment_status_url", "confirm_sent_url", "confirm_sms_code_url", diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index ae65940..ac5b80e 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -9,7 +9,7 @@ tracked_url: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/tracked/" # get multi_url: str = "https://api-inmobile-pl.easypack24.net/v3/parcels/multi/" # get collect_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/validate" # post -reopen_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen" # post +compartment_reopen_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/reopen" # post compartment_open_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/open" # post compartment_status_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/compartment/status" # post terminate_collect_session_url: str = "https://api-inmobile-pl.easypack24.net/v1/collect/terminate" # post From 287824aa6968887bb1029c4b25e8476bc2dc5706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=81oboda?= Date: Sun, 17 Dec 2023 19:48:54 +0100 Subject: [PATCH 27/28] self to cls --- inpost/static/statuses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index 43daed1..1d173b5 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -2,17 +2,17 @@ class Meta(EnumMeta): # temporary handler for unexpected keys in enums - def __getitem__(self, item): + def __getitem__(cls, item): try: return super().__getitem__(item) if item is not None else None except KeyError: - return self.UNKNOWN + return cls.UNKNOWN - def __getattribute__(self, item): + def __getattribute__(cls, item): try: return super().__getattribute__(item) if item is not None else None except KeyError: - return self.UNKNOWN + return cls.UNKNOWN # def get_all(cls): # return [getattr(cls, name) for name in cls.__members__] From 4781ff37dcb5e7f76ebf532b82c5ecd4c88d7252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=81oboda?= Date: Sun, 17 Dec 2023 19:55:19 +0100 Subject: [PATCH 28/28] Ready 1.5 release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 527a9bd..0273675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.5a2" +version = "0.1.5" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "]