diff --git a/.flake8 b/.flake8 index ef45e16..58e6de0 100644 --- a/.flake8 +++ b/.flake8 @@ -2,5 +2,5 @@ max-line-length = 120 max-complexity = 12 select = B,C,E,F,W,T4,B9 -ignore = E501, E731, W503, F401, F403 +ignore = E501, E731, W503, F401, F403, E121, E127 exclude = docs \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 411c820..b06b82a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ repos: - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 24.4.0 hooks: - id: black exclude: ^docs/ language_version: python3 - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 exclude: ^docs/ @@ -19,7 +19,7 @@ repos: - flake8-comprehensions - flake8-pytest - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.9.0 hooks: - id: mypy exclude: ^docs/ @@ -29,7 +29,7 @@ repos: - id: darglint exclude: ^docs/ - repo: https://github.com/commitizen-tools/commitizen - rev: v3.3.0 + rev: v3.22.0 hooks: - id: commitizen stages: [commit-msg] \ No newline at end of file diff --git a/README.md b/README.md index 1bd27b8..7cc38df 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Fully async Inpost library using Python 3.10. +# WARNING +Please be aware, that version 0.2.x is not compatible with 0.1.x builds! Thank inpost for making their api upside down >:( + ## Documentation diff --git a/inpost/api.py b/inpost/api.py index f385a38..831e18b 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -58,35 +58,62 @@ validate_friendship_url, validate_sent_url, ) +from inpost.static.headers import useragent class Inpost: """Python representation of an Inpost app. Essentially implements methods to manage all incoming parcels""" - def __init__(self, phone_number): + def __init__( + self, + prefix: str, + phone_number: str, + sms_code=None, + auth_token=None, + refr_token=None, + ): """Constructor method - + :param prefix: country code + :type prefix: str :param phone_number: phone number :type phone_number: str + :param sms_code: sms code from inpost + :type sms_code: str | None + :param auth_token: authorization token from inpost + :type auth_token: str + :param refr_token: refresh token from inpost + :type refr_token: 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 + if not (len(phone_number) == 9 and phone_number.isdigit()) or not ( + prefix.startswith("+") and prefix[1:].isdigit() and 2 <= len(prefix) <= 3 + ): + raise PhoneNumberError( + f"Wrong phone number format: {phone_number} " f"(should be +xxx123123123 or +xx123123123)" + ) + self.prefix: str = prefix + self.phone_number: str = phone_number[-9:] + self.sms_code: str | None = sms_code + self.auth_token: str | None = auth_token + self.refr_token: str | None = refr_token self.sess: ClientSession = ClientSession() 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}") + @property + def combined_phone_number(self) -> str: + return self.prefix + self.phone_number + + @property + def login_auth_data(self) -> dict: + return {"phoneNumber": {"prefix": self.prefix, "value": self.phone_number}} + def __repr__(self): return f"{self.__class__.__name__}(phone_number={self.phone_number})" @@ -142,6 +169,9 @@ async def request( headers_ = {} if headers is None else headers if auth: + if self.auth_token is None: + raise UnauthorizedError("Missing authorization token") + headers_.update({"Authorization": self.auth_token}) resp = await self.sess.request(method, url, headers=headers_, params=params, json=data, **kwargs) @@ -166,25 +196,6 @@ async def request( raise UnidentifiedAPIError(reason=resp) - @classmethod - def from_dict(cls, data: dict) -> "Inpost": - """`Classmethod` to initialize :class:`Inpost` object with dict. - Should be used when retrieving configuration from database. - - :param data: User's Inpost data (e.g. phone_number, sms_code, auth_token, refr_token) - :type data: dict - :return: Inpost object from provided dict - :rtype: Inpost - """ - - inp = cls(phone_number=data["phone_number"]) - inp.sms_code = data["sms_code"] - inp.auth_token = data["auth_token"] - inp.refr_token = data["refr_token"] - - inp._log.info("initialized by from_dict") - return inp - async def send_sms_code(self) -> bool: """Sends sms code to `Inpost.phone_number` @@ -203,8 +214,8 @@ async def send_sms_code(self) -> bool: action="send sms code", url=send_sms_code_url, auth=False, - headers=None, - data={"phoneNumber": f"{self.phone_number}"}, + headers=useragent | appjson, + data=self.login_auth_data, autorefresh=False, ) @@ -231,14 +242,13 @@ async def confirm_sms_code(self, sms_code: str | int) -> bool: 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"}, + data={"smsCode": sms_code, "devicePlatform": "Android"} | self.login_auth_data, autorefresh=False, ) @@ -386,7 +396,7 @@ async def get_parcel( resp = await self.request( method="get", action=f"parcel with shipment number {shipment_number}", - url=f"{url}{shipment_number}", + url=f"{url}/{shipment_number}", auth=True, headers=None, data=None, @@ -511,7 +521,7 @@ async def get_multi_compartment(self, multi_uuid: str | int, parse: bool = False resp = await self.request( method="get", action=f"parcel with multi-compartment uuid {multi_uuid}", - url=f"{multi_url}{multi_uuid}", + url=f"{multi_url}/{multi_uuid}", auth=True, headers=None, data=None, @@ -569,17 +579,22 @@ async def collect_compartment_properties( 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, - }, + # fmt: off + data={"parcel": parcel_obj_.compartment_open_data | {"receiverPhoneNumber": + { + "prefix": self.prefix, + "value": self.phone_number + } + }, + "geoPoint": location if location is not None else parcel_obj_.mocked_location, + }, + # fmt: on autorefresh=True, ) @@ -665,7 +680,8 @@ async def check_compartment_status( if resp.status == 200: 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 + # return CompartmentExpectedStatus[(await resp.json())["status"]] == expected_status + return True raise UnidentifiedAPIError(reason=resp) diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index ac5b80e..ea374f5 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -1,19 +1,19 @@ # 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 +send_sms_code_url: str = "https://api-inmobile-pl.easypack24.net/v1/account" # post +confirm_sms_code_url: str = "https://api-inmobile-pl.easypack24.net/v1/account/verification" # post logout_url: str = "https://api-inmobile-pl.easypack24.net/v1/logout" # post refresh_token_url: str = "https://api-inmobile-pl.easypack24.net/v1/authenticate" # post # INCOMING PARCELS # -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 +tracked_url: str = "https://api-inmobile-pl.easypack24.net/v4/parcels/tracked" # get +multi_url: str = "https://api-inmobile-pl.easypack24.net/v4/parcels/multi" # get +collect_url: str = "https://api-inmobile-pl.easypack24.net/v2/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 +shared_url: str = "https://api-inmobile-pl.easypack24.net/v4/parcels/shared" # post # CREATING PARCEL # create_url: str = "https://api-inmobile-pl.easypack24.net/v1/parcels" diff --git a/inpost/static/headers.py b/inpost/static/headers.py index 9e06775..ba04f8c 100644 --- a/inpost/static/headers.py +++ b/inpost/static/headers.py @@ -1 +1,2 @@ -appjson = {"Content-Type": "application/json"} +appjson = {"Content-Type": "application/json; charset=UTF-8"} +useragent = {"User-Agent": "InPost-Mobile/3.23.0(32300001) (Android 9; unknown; unknown unknown; en)"} diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index a80a303..efbb373 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -266,7 +266,7 @@ def compartment_status(self, status) -> None: self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") @property - def compartment_open_data(self) -> dict | None: + def compartment_open_data(self) -> dict: """Returns a compartment open data for :class:`Parcel` :return: dict containing compartment open data for :class:`Parcel` @@ -275,18 +275,17 @@ def compartment_open_data(self) -> dict | None: self._log.debug("getting compartment open data") if self.receiver is None: - return None + return {} if self.shipment_type == ParcelShipmentType.parcel: self._log.debug("got compartment open data") return { "shipmentNumber": self.shipment_number, "openCode": self._open_code, - "receiverPhoneNumber": self.receiver.phone_number, } self._log.warning(f"wrong ParcelShipmentType: {repr(self.shipment_type)}") - return None + return {} @property def mocked_location(self) -> dict | None: diff --git a/pyproject.toml b/pyproject.toml index 14ece0d..aac9b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.1.5" +version = "0.2.0a2" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] @@ -23,16 +23,15 @@ classifiers = [ ] [tool.poetry.dev-dependencies] -pre-commit = "^3.3.3" -pytest = "^7.4.0" +pre-commit = "^3.7.0" +pytest = "^8.1.1" [tool.poetry.dependencies] python = "^3.10" -aiohttp = "^3.8.1" +aiohttp = "^3.9.4" arrow = "^1.2.3" qrcode = "^7.3.1" Pillow = "^9.4.0" -pytest = "^7.4.0" [build-system]