diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..931b44e --- /dev/null +++ b/.darglint @@ -0,0 +1,5 @@ +[darglint] +docstring_style=sphinx +ignore=DAR003,DAR402 +strictness=full +exclude=docs \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..ef45e16 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +max-line-length = 120 +max-complexity = 12 +select = B,C,E,F,W,T4,B9 +ignore = E501, E731, W503, F401, F403 +exclude = docs \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e5bb3a2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,17 @@ +name: Code Quality + +on: + pull_request: + branches: [dev] + + push: + branches: [dev] + +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/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..4dc99f7 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,89 @@ +name: Pytest + +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 --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/.github/workflows/todos.yml b/.github/workflows/todos.yml new file mode 100644 index 0000000..322ce7c --- /dev/null +++ b/.github/workflows/todos.yml @@ -0,0 +1,11 @@ +name: Create issues from todos + +on: ["push"] + +jobs: + build: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v3" + - name: "TODO to Issue" + uses: "alstr/todo-to-issue-action@v4" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5fb698 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/log +/otwarcie paczkomatu.xml +/otwarcie_paczkomatu.json +/poetry.lock +/.mypy_cache/ +/dist/ +/venv/ +/.idea/ +/tests/.pytest_cache +/tests/__pycache__/ +/tests/api_tests.py +/tests/data.json +/tests/data.py +/tests/data_responses.py +/inpost/static/__pycache__/ +/inpost/__pycache__/ +/otwarcie paczkomatu.xml.html \ No newline at end of file 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 + + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..411c820 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +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 + 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 + - flake8-pytest +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.3.0 + hooks: + - id: mypy + 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: + - id: commitizen + stages: [commit-msg] \ No newline at end of file diff --git a/README.md b/README.md index 6ce9f50..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/) @@ -17,13 +18,21 @@ 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 + +![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 +``` ## Authors @@ -37,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 diff --git a/inpost/api.py b/inpost/api.py index f0d3e7a..f385a38 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -1,171 +1,214 @@ 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_url, + collect_url, + compartment_open_url, + compartment_reopen_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, +) 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._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 async def __aexit__(self, exc_type, exc_val, exc_tb): - return self.logout() + return self.disconnect() 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 - async def from_phone_number(cls, phone_number: str | int): - """`Classmethod` to initialize :class:`Inpost` object with phone number + 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 + """ - :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() - await inp.set_phone_number(phone_number=phone_number) - inp._log.info(f'initialized by from_phone_number') - return inp + 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"] - @classmethod - async def from_dict(cls, data: dict): - inp = cls() - await 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') + inp._log.info("initialized by from_dict") return inp - async 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_url, + 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 +217,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_url, + 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 +258,39 @@ 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_url, + 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 @@ -260,54 +300,58 @@ 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""" - self._log.info(f'logging out') + :raises UnidentifiedAPIError: Unexpected thing happened + """ + + 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_url, 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') + :raises NotAuthenticatedError: User not authenticated in inpost service + """ + + 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: """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 +359,63 @@ 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 = tracked_url + case ParcelType.SENT: + self._log.debug(f"getting parcel type {parcel_type}") + url = sent_url + case ParcelType.RETURNS: + self._log.debug(f"getting parcel type {parcel_type}") + url = returns_url + case _: + self._log.error(f"unexpected parcel type {parcel_type}") + raise ParcelTypeError(reason=f"Unexpected 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 +426,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 @@ -361,98 +434,103 @@ async def get_parcels(self, :raises ParcelTypeError: Unknown parcel type selected :raises UnauthorizedError: Unauthorized access to inpost services, :raises NotFoundError: Phone number not found - :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info('getting parcels') + :raises UnidentifiedAPIError: Unexpected thing happened + """ - if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.info("getting parcels") - if not isinstance(parcel_type, ParcelType): - self._log.error(f'wrong parcel type {parcel_type}') - raise ParcelTypeError(reason=f'Unknown parcel type: {parcel_type}') + if not self.auth_token: + 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}') - url = parcels + self._log.debug(f"getting parcel type {parcel_type}") + url = tracked_url case ParcelType.SENT: - self._log.debug(f'getting parcel type {parcel_type}') - url = sent + self._log.debug(f"getting parcel type {parcel_type}") + url = sent_url case ParcelType.RETURNS: - self._log.debug(f'getting parcel type {parcel_type}') - url = returns + self._log.debug(f"getting parcel type {parcel_type}") + 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"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]: - if not self.auth_token: - self._log.error(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + """Fetches all available parcels for set `Inpost.phone_number` and optionally filters them - 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) + :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") + + resp = await self.request( + method="get", + action=f"parcel with multi-compartment uuid {multi_uuid}", + url=f"{multi_url}{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"]] + ) - async def collect_compartment_properties(self, shipment_number: str | int | None = None, - parcel_obj: Parcel | None = None, location: dict | None = None) -> bool: + 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 + ) -> Parcel: """Validates sent data and fetches required compartment properties for opening :param shipment_number: Parcel's shipment number @@ -462,77 +540,99 @@ 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}') + .. warning:: you must fill in only one parameter - shipment_number or parcel_obj! + """ - 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_url, + 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_ + + raise UnidentifiedAPIError(reason=resp) - async def open_compartment(self) -> bool: + async def open_compartment(self, parcel_obj: Parcel) -> Parcel: """Opens compartment for `Inpost.parcel` object - :return: True if compartment gets opened - :rtype: bool + :param parcel_obj: Parcel object + :type parcel_obj: Parcel + :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 - :raises UnidentifiedAPIError: Unexpected thing happened""" - self._log.info(f'opening compartment for {self.parcel.shipment_number}') + :raises UnidentifiedAPIError: Unexpected thing happened + """ - if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + self._log.info(f"opening compartment for {parcel_obj.shipment_number}") - 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) + 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 compartment for {parcel_obj.shipment_number}", + url=compartment_open_url, + 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}') - return True + self._log.debug(f"opened compartment for {parcel_obj.shipment_number}") + parcel_obj.compartment_location = await resp.json() + return parcel_obj + + raise UnidentifiedAPIError(reason=resp) - async def check_compartment_status(self, - expected_status: CompartmentExpectedStatus = CompartmentExpectedStatus.OPENED) -> bool: + 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 @@ -540,63 +640,72 @@ async def check_compartment_status(self, :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 {self.parcel.shipment_number}') + :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(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_url, + 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 - async def terminate_collect_session(self) -> bool: + raise UnidentifiedAPIError(reason=resp) + + 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}') + :raises UnidentifiedAPIError: Unexpected thing happened + """ + + 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_url, + 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 + ) -> Parcel | None: """Simplified method to open compartment :param shipment_number: Parcel's shipment number @@ -609,73 +718,477 @@ 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 - .. 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(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!") - if await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): - if await self.open_compartment(): - if await self.check_compartment_status(): - return True + self._log.info(f"collecting parcel with shipment number {parcel_obj.shipment_number}") - return False + if parcel_obj_ := await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): + if parcel_obj__ := await self.open_compartment(parcel_obj=parcel_obj_): + if await self.check_compartment_status(parcel_obj=parcel_obj__): + return parcel_obj__ - async def close_compartment(self) -> bool: + 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 :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}') + :rtype: bool + """ - if await self.check_compartment_status(expected_status=CompartmentExpectedStatus.CLOSED): - if await self.terminate_collect_session(): + 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): + 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}') + :raises UnidentifiedAPIError: Unexpected thing happened + """ + + 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_reopen_url, + auth=True, + headers=None, + data={"sessionUuid": parcel_obj.compartment_properties.session_uuid}, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"reopened 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 parcel points 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!") + + _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_url, + auth=True, + headers=None, + params=_params, + data=None, + autorefresh=True, + ) + if resp.status == 200: + 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: + """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") + + self._log.info("checking if user has opened blik session") + + resp = await self.request( + 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"]: + 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, + ) -> 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") + + self._log.info("creating new parcel") + + resp = await self.request( + method="post", + action="create parcel", + url=create_url, + 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: + """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") + + self._log.info(f"creating blik session for {shipment_number}") + + resp = await self.request( + method="post", + action="create blik session", + url=create_blik_url, + 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, + drop_off_point: str, + shipment_number: str | None = None, + parcel_obj: SentParcel | None = None, + location: dict | None = None, + ) -> SentParcel: + """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") + + 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"validating send for {parcel_obj_.shipment_number}") + + resp = await self.request( + method="post", + action="validate send parcel data", + url=validate_sent_url, + auth=True, + headers=None, + data={ + "parcel": { + "shipmentNumber": parcel_obj_.shipment_number, + "quickSendCode": parcel_obj_.quick_send_code, + }, + "geoPoint": location, + "boxMachineName": drop_off_point, + }, + autorefresh=True, + ) + + if resp.status == 200: + self._log.debug(f"validated send for 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_url, + 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 - 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) + 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_reopen_url, + 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"reopened 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_url, + 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 @@ -684,24 +1197,30 @@ 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""" - self._log.info(f'getting parcel prices') + :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') - - 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_url, + 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 @@ -712,51 +1231,75 @@ 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""" - self._log.info(f'getting friends') + :raises UnidentifiedAPIError: Unexpected thing happened + """ + + 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_url, 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"]] + ) + + raise UnidentifiedAPIError(reason=resp) async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: - self._log.info(f'getting parcel friends') + """Fetches parcel friends - if not self.auth_token: - self._log.debug(f'authorization token missing') - raise NotAuthenticatedError(reason='Not logged in') + :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 + """ - resp = await self.request(method='get', - action='get parcel friends', - url=f"{friendship}{shipment_number}", - auth=True, - headers=None, - data=None, - autorefresh=True) + self._log.info("getting parcel friends") + + 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_url}{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 @@ -775,60 +1318,67 @@ 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(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_url, + 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_url, + 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 +1395,49 @@ 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'adding 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_url}{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 @@ -895,40 +1452,45 @@ 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(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"{friendship_url}{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 +1500,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_url, + 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..c42d0ed 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -1,15 +1,168 @@ -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_url, + blik_status_url, + collect_url, + compartment_open_url, + compartment_reopen_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, + 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_url", + "blik_status_url", + "collect_url", + "compartment_open_url", + "compartment_reopen_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_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", + "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..ac5b80e 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -1,25 +1,42 @@ -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_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 -# \/ Secured by JWT \/ +# INCOMING PARCELS # +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 +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 +shared_url: 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_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_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_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_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 diff --git a/inpost/static/exceptions.py b/inpost/static/exceptions.py index 7319c79..77a78c3 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,95 @@ 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 UnknownStatusError(BaseInpostError): + """Is raised when no status matches""" + 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 +108,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..65c0f9e 100644 --- a/inpost/static/friends.py +++ b/inpost/static/friends.py @@ -1,34 +1,64 @@ 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 + """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"] + 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) + 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"], + "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..47944c4 100644 --- a/inpost/static/notifications.py +++ b/inpost/static/notifications.py @@ -1,15 +1,36 @@ -from arrow import get, Arrow +import logging + +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'] + """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 + 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) + + self._log.debug(f"created notification with id {self.id}") diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index a396766..a80a303 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -4,94 +4,146 @@ 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.exceptions import UnknownStatusError +from inpost.static.statuses import ( + CompartmentActualStatus, + ParcelCarrierSize, + ParcelDeliveryType, + ParcelLockerSize, + ParcelOwnership, + ParcelShipmentType, + ParcelStatus, + PaymentStatus, + PaymentType, + PointType, + ReturnsStatus, +) 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: 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 :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""" + """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: - 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') - 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: """Returns an open code for :class:`Parcel` :return: Open code for :class:`Parcel` - :rtype: str""" - self._log.debug('getting open code') + :rtype: str + """ + + 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.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -99,13 +151,15 @@ 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: - self._log.debug('got qr image') + :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)}') + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @property @@ -113,13 +167,15 @@ 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') + :rtype: CompartmentProperties + """ + + 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.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") return None @compartment_properties.setter @@ -127,258 +183,656 @@ 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 + ) + 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): """Returns a compartment location for :class:`Parcel` :return: Compartment location for :class:`Parcel` - :rtype: CompartmentLocation""" - self._log.debug('getting compartment location') + :rtype: CompartmentLocation + """ + + 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.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` + :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 + 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: """Returns a compartment status for :class:`Parcel` :return: Compartment status for :class:`Parcel` - :rtype: CompartmentActualStatus""" - self._log.debug('getting compartment status') + :rtype: CompartmentActualStatus + """ + + 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.warning(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: + """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") + return + if self.shipment_type == ParcelShipmentType.parcel: - self._log.debug('compartment status set') + self._log.debug("compartment status set") self._compartment_properties.status = status + return - 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): + 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') + :rtype: dict + """ + + self._log.debug("getting compartment open data") + if self.receiver is None: + return None + 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.warning(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 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.warning(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""" + :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: + :rtype: bool + """ + + if self.is_multicompartment and self.multi_compartment is not None: return self.multi_compartment.shipment_numbers is not None return None - # @property - # def get_from_multicompartment(self): - # return + @property + 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 class ReturnParcel(BaseParcel): + # TODO: Prepare properties required to ease up access + """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"] + 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: Recheck properties + """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.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")] + self._compartment_properties: CompartmentProperties | None = None + + @property + def compartment_properties(self): + """Returns a compartment properties for :class:`SentParcel` + + :return: Compartment properties for :class:`SentParcel` + :rtype: CompartmentProperties + """ + + self._log.debug("getting compartment properties") + if self.shipment_type == ParcelShipmentType.parcel: + self._log.debug("got compartment properties") + return self._compartment_properties + + 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:`SentParcel` + + :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: + self._log.debug("compartment properties set") + self._compartment_properties = CompartmentProperties( + compartmentproperties_data=compartmentproperties_data, logger=self._log + ) + return + + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def compartment_location(self): + """Returns a compartment location for :class:`SentParcel` + + :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.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:`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 + return + + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def compartment_status(self) -> CompartmentActualStatus | None: + """Returns a compartment status for :class:`SentParcel` + + :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") + return self._compartment_properties.status if self._compartment_properties else None + + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None + + @compartment_status.setter + 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") + self._compartment_properties.status = status + return + + self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + + @property + def mocked_location(self): + """Returns a mocked location for :class:`SentParcel` + + :return: dict containing mocked location for :class:`SentParcel` + :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.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") + return None 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""" - 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 + + :param receiver_data: dict containing receiver data + :type receiver_data: dict + :param logger: logger instance + :type logger: logging.Logger + """ - self._log.debug('created') + 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: - """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""" - 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._log.debug('created') + 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") 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:`BaseParcel` point and it's subclasses - :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:`BaseParcel` and it's subclasses + :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') + :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:`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"] + 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.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') - 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 @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:`PickupPoint` - :rtype: tuple""" - self._log.debug('getting location') + :return: tuple containing location for :class:`Point` + :rtype: tuple + """ + + self._log.debug("getting location") return self.latitude, self.longitude +class PickupPoint(Point): + """Object representation of :class:`BaseParcel` and it's subclasses pick up point + + :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 + """ + + 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:`BaseParcel` and it's subclasses drop off point + + :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 + """ + + 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:`BaseParcel` and it's subclasses delivery point + + :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 + """ + + 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 + 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:`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 + """ + + 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") + 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` - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + :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(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: @@ -387,31 +841,41 @@ 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: @@ -420,24 +884,42 @@ 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 + :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": + self.name = ReturnsStatus[eventlog_data.get("name")] + elif self.type == "PAYMENT": + self.name = PaymentStatus[eventlog_data.get("name")] + else: + self._log.warning(f'Unknown status type {eventlog_data.get("name")}!') + raise UnknownStatusError(reason=eventlog_data.get("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') - 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: @@ -446,20 +928,28 @@ 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 - self._log: logging.Logger = logger.getChild(__class__.__name__) - self._log.debug('created') + :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(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: @@ -468,18 +958,26 @@ 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: @@ -487,23 +985,20 @@ 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 @@ -513,24 +1008,32 @@ 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: @@ -542,18 +1045,25 @@ 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): @@ -561,7 +1071,8 @@ 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 @@ -569,8 +1080,10 @@ def location(self): """Returns a compartment location for :class:`CompartmentProperties` :return: compartment location for :class:`CompartmentProperties` - :rtype: str""" - self._log.debug('getting location') + :rtype: str + """ + + self._log.debug("getting location") return self._location @location.setter @@ -578,8 +1091,10 @@ 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""" - self._log.debug('setting location') + :type location_data: dict + """ + + self._log.debug("setting location") self._location = CompartmentLocation(location_data, self._log) @property @@ -587,18 +1102,21 @@ def status(self): """Returns a compartment status for :class:`CompartmentProperties` :return: compartment location for :class:`CompartmentProperties` - :rtype: CompartmentActualStatus""" - self._log.debug('getting status') + :rtype: CompartmentActualStatus + """ + + 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.debug(f'unexpected compartment actual status: {status_data}') + self._log.warning(f"unexpected compartment actual status: {status_data}") class AirSensorData: @@ -607,17 +1125,27 @@ 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..1d173b5 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): try: - return super().__getitem__(item) - except KeyError as error: + return super().__getitem__(item) if item is not None else None + except KeyError: return cls.UNKNOWN def __getattribute__(cls, item): try: - return super().__getattribute__(item) - except KeyError as error: + return super().__getattribute__(item) if item is not None else None + except KeyError: return cls.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,135 @@ 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""" + + # TODO: get known what does superpop stand for + UNKNOWN = "UNNKOWN DATA" + PL = "Paczkomat" + parcel_locker_superpop = "some paczkomat or pok stuff" + POK = "Mobilny punkt obs艂ugi klienta" + POP = "Punkt odbioru paczki" + + +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" 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 - - -class ReturnsStatus(ParcelBase): # TODO: translate from app and fill missing ones - ACCEPTED = 'Zaakceptowano' - USED = 'Nadano' - DELIVERED = 'Dostarczono' - UNKNOWN = 'UNKNOWN DATA' + + 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" 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 55d577e..0273675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.2" +version = "0.1.5" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] @@ -21,14 +21,44 @@ 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" arrow = "^1.2.3" qrcode = "^7.3.1" Pillow = "^9.4.0" +pytest = "^7.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 + | docs +)/ +''' + +[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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 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..ca1f785 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,386 @@ +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" +) + +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