diff --git a/README.md b/README.md index f241f2e..6ce9f50 100644 --- a/README.md +++ b/README.md @@ -1 +1,39 @@ -Fully async Inpost API library + +# Inpost Python + +Fully async Inpost library using Python 3.10. + + + + +## Documentation + +[Readthedocs.io](https://inpost-python.readthedocs.io/en/latest/) + + +## Usage/Examples + + +```python +from inpost.api import Inpost + +inp = await Inpost.from_phone_number('555333444') +await inp.send_sms_code(): +... +if await inp.confirm_sms_code(123321): + print('Congratulations, you initialized successfully!') +``` + + +## Authors + +- [@loboda4450](https://www.github.com/loboda4450) +- [@mrkazik99](https://www.github.com/mrkazik99) + + +## Used By + +This project is used by the following repos: + +[Inpost Telegram Bot](https://github.com/loboda4450/inpost-telegram-bot) + diff --git a/inpost/api.py b/inpost/api.py index 6f321f2..0aac53a 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -1,7 +1,8 @@ -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientResponse +from aiohttp.typedefs import StrOrURL from typing import List import logging - +from arrow import utcnow from inpost.static import * @@ -27,6 +28,72 @@ async def __aenter__(self): async def __aexit__(self, exc_type, exc_val, exc_tb): return self.logout() + async def request( + self, + method: str, + action: str, + url: StrOrURL, + auth: bool = True, + headers: dict | None = None, + data: dict | None = None, + autorefresh: bool = True, + **kwargs, + ) -> 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""" + + if auth and 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} + ) + + resp = await self.sess.request(method, url, headers=headers_, json=data, **kwargs) + + if autorefresh and resp.status == 401: + await self.refresh_token() + resp = await self.sess.request(method, url, headers=headers_, json=data, **kwargs) + + match resp.status: + case 200: + self._log.debug(f'{action} done') + return resp + case 401: + 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') + raise NotFoundError(reason=resp) + case _: + 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 @@ -74,24 +141,16 @@ async def send_sms_code(self) -> bool: raise PhoneNumberError('Phone number missing') self._log.info(f'sending sms code') - async with await self.sess.post(url=send_sms_code, - json={ - 'phoneNumber': f'{self.phone_number}' - }) as phone: - match phone.status: - case 200: - self._log.debug(f'sms code sent') - return True - case 401: - self._log.error(f'could not send sms code, unauthorized') - raise UnauthorizedError(reason=phone) - case 404: - self._log.error(f'could not send sms code, not found') - raise NotFoundError(reason=phone) - case _: - self._log.error(f'could not send sms code, unhandled status') - raise UnidentifiedAPIError(reason=phone) + resp = await self.request(method='post', + action='send sms code', + url=send_sms_code, + auth=False, + headers=None, + data={'phoneNumber': f'{self.phone_number}'}, + autorefresh=False) + + return True if resp.status == 200 else False async def confirm_sms_code(self, sms_code: str | int) -> bool: """Confirms sms code sent to `Inpost.phone_number` and fetches tokens @@ -116,31 +175,28 @@ 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(f'confirming sms code') - async with await self.sess.post(url=confirm_sms_code, - headers=appjson, - json={ - "phoneNumber": self.phone_number, - "smsCode": sms_code, - "phoneOS": "Android" - }) as confirmation: - match confirmation.status: - case 200: - resp = await confirmation.json() - self.sms_code = sms_code - self.refr_token = resp['refreshToken'] - self.auth_token = resp['authToken'] - self._log.debug(f'sms code confirmed') - return True - case 401: - self._log.error(f'could not confirm sms code, unauthorized') - raise UnauthorizedError(reason=confirmation) - case 404: - self._log.error(f'could not confirm sms code, not found') - raise NotFoundError(reason=confirmation) - case _: - self._log.error(f'could not confirm sms code, unhandled status') - raise UnidentifiedAPIError(reason=confirmation) + resp = await self.request(method='post', + action='confirm sms code', + url=confirm_sms_code, + auth=False, + headers=appjson, + data={ + "phoneNumber": self.phone_number, + "smsCode": sms_code, + "phoneOS": "Android" + }, + autorefresh=False) + + if resp.status == 200: + auth_token_data = await resp.json() + self.sms_code = sms_code + self.refr_token = auth_token_data['refreshToken'] + self.auth_token = auth_token_data['authToken'] + self._log.debug(f'sms code confirmed') + return True + else: + return False async def refresh_token(self) -> bool: """Refreshes authorization token using refresh token @@ -158,32 +214,28 @@ async def refresh_token(self) -> bool: self._log.error(f'refresh token missing') raise RefreshTokenError(reason='Refresh token missing') - async with await self.sess.post(url=refresh_token, - headers=appjson, - json={ - "refreshToken": self.refr_token, - "phoneOS": "Android" - }) as confirmation: - match confirmation.status: - case 200: - resp = await confirmation.json() - if resp['reauthenticationRequired']: - self._log.error(f'could not refresh token, log in again') - raise ReAuthenticationError(reason='You need to log in again!') - - self.auth_token = resp['authToken'] - self._log.debug(f'token refreshed') - return True - case 401: - self._log.error(f'could not refresh token, unauthorized') - raise UnauthorizedError(reason=confirmation) - case 404: - self._log.error(f'could not refresh token, not found') - raise NotFoundError(reason=confirmation) - case _: - self._log.error(f'could not refresh token, unhandled status') - - raise UnidentifiedAPIError(reason=confirmation) + resp = await self.request(method='post', + action='refresh token', + url=refresh_token, + auth=False, + headers=appjson, + data={ + "refreshToken": self.refr_token, + "phoneOS": "Android" + }, + autorefresh=False) + + if resp.status == 200: + confirmation = await resp.json() + if confirmation['reauthenticationRequired']: + self._log.error(f'could not refresh token, log in again') + raise ReAuthenticationError(reason='You need to log in again!') + + self.auth_token = confirmation['authToken'] + self._log.debug(f'token refreshed') + return True + else: + return False async def logout(self) -> bool: """Logouts user from inpost api service @@ -200,26 +252,23 @@ async def logout(self) -> bool: self._log.error(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.post(url=logout, - headers={'Authorization': self.auth_token}) as resp: - match resp.status: - case 200: - self.phone_number = None - self.refr_token = None - self.auth_token = None - self.sms_code = None - self._log.debug('logged out') - return True - case 401: - self._log.error('could not log out, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not log out, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not log out, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='post', + action='logout', + url=logout, + auth=True, + headers=None, + data=None, + autorefresh=True) + + if resp.status == 200: + self.phone_number = None + self.refr_token = None + self.auth_token = None + self.sms_code = None + self._log.debug('logged out') + return True + else: + return False async def disconnect(self) -> bool: """Simplified method to logout and close user's session @@ -258,23 +307,17 @@ async def get_parcel(self, shipment_number: int | str, parse=False) -> dict | Pa self._log.error(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.get(url=f"{parcel}{shipment_number}", - 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) + 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) + + 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, @@ -328,83 +371,70 @@ async def get_parcels(self, self._log.error(f'wrong parcel type {parcel_type}') raise ParcelTypeError(reason=f'Unknown parcel type: {parcel_type}') - async with await self.sess.get(url=url, - headers={'Authorization': self.auth_token}, - ) as resp: - match resp.status: - case 200: - self._log.debug(f'received {parcel_type} parcels') - _parcels = (await resp.json())['parcels'] + resp = await self.request(method='get', + action='get parcels', + url=url, + auth=True, + headers=None, + data=None, + autorefresh=True) - if status is not None: - if isinstance(status, ParcelStatus): - status = [status] + if resp.status == 200: + self._log.debug(f'received {parcel_type} parcels') + _parcels = (await resp.json())['parcels'] - _parcels = (_parcel for _parcel in _parcels if ParcelStatus[_parcel['status']] in status) + if status is not None: + if isinstance(status, ParcelStatus): + status = [status] - if pickup_point is not None: - if isinstance(pickup_point, str): - pickup_point = [pickup_point] + _parcels = (_parcel for _parcel in _parcels if ParcelStatus[_parcel['status']] in status) - _parcels = (_parcel for _parcel in _parcels if - _parcel['pickUpPoint']['name'] in pickup_point) + if pickup_point is not None: + if isinstance(pickup_point, str): + pickup_point = [pickup_point] - if shipment_type is not None: - if isinstance(shipment_type, ParcelShipmentType): - shipment_type = [shipment_type] + _parcels = (_parcel for _parcel in _parcels if + _parcel['pickUpPoint']['name'] in pickup_point) - _parcels = (_parcel for _parcel in _parcels if - ParcelShipmentType[_parcel['shipmentType']] in shipment_type) + if shipment_type is not None: + if isinstance(shipment_type, ParcelShipmentType): + shipment_type = [shipment_type] - if parcel_size is not None: - if isinstance(parcel_size, ParcelCarrierSize): - parcel_size = [parcel_size] + _parcels = (_parcel for _parcel in _parcels if + ParcelShipmentType[_parcel['shipmentType']] in shipment_type) - _parcels = (_parcel for _parcel in _parcels if - ParcelCarrierSize[_parcel['parcelSize']] in parcel_size) + if parcel_size is not None: + if isinstance(parcel_size, ParcelCarrierSize): + parcel_size = [parcel_size] - if isinstance(parcel_size, ParcelLockerSize): - parcel_size = [parcel_size] + _parcels = (_parcel for _parcel in _parcels if + ParcelCarrierSize[_parcel['parcelSize']] in parcel_size) - _parcels = (_parcel for _parcel in _parcels if - ParcelLockerSize[_parcel['parcelSize']] in parcel_size) + if isinstance(parcel_size, ParcelLockerSize): + parcel_size = [parcel_size] - return _parcels if not parse else [Parcel(parcel_data=data, logger=self._log) for data in - _parcels] - case 401: - self._log.error(f'could not get parcels, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error(f'could not get parcels, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error(f'could not get parcels, unhandled status') + _parcels = (_parcel for _parcel in _parcels if + ParcelLockerSize[_parcel['parcelSize']] in parcel_size) - raise UnidentifiedAPIError(reason=resp) + 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') - async with await self.sess.get(url=f"{multi}{multi_uuid}", - headers={'Authorization': self.auth_token}, - ) as resp: - match resp.status: - case 200: - self._log.debug(f'parcel with multicompartment uuid {multi_uuid} received') - return await resp.json() if not parse else [Parcel(data, logger=self._log) for data in - (await resp.json())['parcels']] - case 401: - self._log.error(f'could not get parcel with multicompartment uuid {multi_uuid}, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error(f'could not get parcel with multicompartment uuid {multi_uuid}, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error(f'could not get parcel with multicompartment uuid {multi_uuid}, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + 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) + + 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']] async def collect_compartment_properties(self, shipment_number: str | int | None = None, parcel_obj: Parcel | None = None, location: dict | None = None) -> bool: @@ -440,31 +470,22 @@ async def collect_compartment_properties(self, shipment_number: str | int | None self._log.info(f'collecting compartment properties for {parcel_obj.shipment_number}') - async with await self.sess.post(url=collect, - headers={'Authorization': self.auth_token}, - json={ - 'parcel': parcel_obj.compartment_open_data, - 'geoPoint': location if location is not None else parcel_obj.mocked_location - }) as collect_resp: - match collect_resp.status: - case 200: - self._log.debug(f'collected compartment properties for {parcel_obj.shipment_number}') - parcel_obj.compartment_properties = await collect_resp.json() - self.parcel = parcel_obj - return True - case 401: - self._log.error(f'could not collect compartment properties for {parcel_obj.shipment_number}, ' - f'unauthorized') - raise UnauthorizedError(reason=collect_resp) - case 404: - self._log.error(f'could not collect compartment properties for {parcel_obj.shipment_number}, not ' - f'found') - raise NotFoundError(reason=collect_resp) - case _: - self._log.error(f'could not collect compartment properties for {parcel_obj.shipment_number}, ' - f'unhandled status') - - raise UnidentifiedAPIError(reason=collect_resp) + resp = await self.request(method='post', + action='collect compartment properties', + url=collect, + auth=True, + headers=None, + data={ + 'parcel': parcel_obj.compartment_open_data, + 'geoPoint': location if location is not None else parcel_obj.mocked_location + }, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'collected compartment properties for {parcel_obj.shipment_number}') + parcel_obj.compartment_properties = await resp.json() + self.parcel = parcel_obj + return True async def open_compartment(self) -> bool: """Opens compartment for `Inpost.parcel` object @@ -481,26 +502,17 @@ async def open_compartment(self) -> bool: self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.post(url=compartment_open, - headers={'Authorization': self.auth_token}, - json={ - 'sessionUuid': self.parcel.compartment_properties.session_uuid - }) as compartment_open_resp: - match compartment_open_resp.status: - case 200: - self._log.debug(f'opened comaprtment for {self.parcel.shipment_number}') - self.parcel.compartment_properties.location = await compartment_open_resp.json() - return True - case 401: - self._log.error(f'could not open compartment for {self.parcel.shipment_number}, unauthorized') - raise UnauthorizedError(reason=compartment_open_resp) - case 404: - self._log.error(f'could not open compartment for {self.parcel.shipment_number}, not found') - raise NotFoundError(reason=compartment_open_resp) - case _: - self._log.error(f'could not open compartment for {self.parcel.shipment_number}, unhandled status') + resp = await self.request(method='get', + 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) - raise UnidentifiedAPIError(reason=compartment_open_resp) + if resp.status == 200: + self._log.debug(f'opened compartment for {self.parcel.shipment_number}') + return True async def check_compartment_status(self, expected_status: CompartmentExpectedStatus = CompartmentExpectedStatus.OPENED) -> bool: @@ -524,30 +536,21 @@ async def check_compartment_status(self, self._log.debug(f'parcel missing') raise NoParcelError(reason='Parcel is not set') - async with await self.sess.post(url=compartment_status, - headers={'Authorization': self.auth_token}, - json={ - 'sessionUuid': self.parcel.compartment_properties.session_uuid, - 'expectedStatus': expected_status.name - }) as compartment_status_resp: - match compartment_status_resp.status: - case 200: - self._log.debug(f'checked compartment status for {self.parcel.shipment_number}') - self.parcel.compartment_status = (await compartment_status_resp.json())['status'] - return CompartmentExpectedStatus[ - (await compartment_status_resp.json())['status']] == expected_status - case 401: - self._log.error( - f'could not check compartment status for {self.parcel.shipment_number}, unauthorized') - raise UnauthorizedError(reason=compartment_status_resp) - case 404: - self._log.error(f'could not check compartment status for {self.parcel.shipment_number}, not found') - raise NotFoundError(reason=compartment_status_resp) - case _: - self._log.error( - f'could not check compartment status for {self.parcel.shipment_number}, unhandled status') - - raise UnidentifiedAPIError(reason=compartment_status_resp) + 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) + + 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 async def terminate_collect_session(self) -> bool: """Terminates user session in inpost api service @@ -564,27 +567,18 @@ async def terminate_collect_session(self) -> bool: self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.post(url=terminate_collect_session, - headers={'Authorization': self.auth_token}, - json={ - 'sessionUuid': self.parcel.compartment_properties.session_uuid - }) as terminate_resp: - match terminate_resp.status: - case 200: - self._log.debug(f'terminated collect session for {self.parcel.shipment_number}') - return True - case 401: - self._log.error( - f'could not terminate collect session for {self.parcel.shipment_number}, unauthorized') - raise UnauthorizedError(reason=terminate_resp) - case 404: - self._log.error(f'could not terminate collect session for {self.parcel.shipment_number}, not found') - raise NotFoundError(reason=terminate_resp) - case _: - self._log.error( - f'could not terminate collect session for {self.parcel.shipment_number}, unhandled status') - - raise UnidentifiedAPIError(reason=terminate_resp) + 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) + if resp.status == 200: + self._log.debug(f'terminated collect session for {self.parcel.shipment_number}') + return True async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | None = None, location: dict | None = None) -> bool: @@ -617,7 +611,7 @@ async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | 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'collecing parcel with shipment number {parcel_obj.shipment_number}') + self._log.info(f'collecting parcel with shipment number {parcel_obj.shipment_number}') if await self.collect_compartment_properties(parcel_obj=parcel_obj, location=location): if await self.open_compartment(): @@ -627,7 +621,8 @@ async def collect(self, shipment_number: str | None = None, parcel_obj: Parcel | return False async def close_compartment(self) -> bool: - """Checks whether actual compartment status and expected one matches then notifies inpost api that compartment is closed + """Checks whether actual compartment status and expected one matches then notifies inpost api that + compartment is closed :return: True if compartment status is closed and successfully terminates user's session else False :rtype: bool""" @@ -654,22 +649,16 @@ async def get_prices(self) -> dict: self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.get(url=parcel_prices, - headers={'Authorization': self.auth_token}) as resp: - match resp.status: - case 200: - self._log.debug(f'got parcel prices') - return await resp.json() - case 401: - self._log.error('could not get parcel prices, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not get parcel prices, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not get parcel prices, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='get', + action='get prices', + url=parcel_prices, + auth=True, + headers=None, + data=None, + autorefresh=True) + if resp.status == 200: + self._log.debug(f'got parcel prices') + return await resp.json() async def get_friends(self, parse=False) -> dict | List[Friend]: """Fetches user friends for inpost services @@ -688,23 +677,18 @@ async def get_friends(self, parse=False) -> dict | List[Friend]: self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.get(url=friendship, - headers={'Authorization': self.auth_token}) as resp: - match resp.status: - case 200: - self._log.debug(f'got user friends') - r = await resp.json() - return r if not parse else [Friend(friend_data=friend, logger=self._log) for friend in r['friends']] - case 401: - self._log.error('could not get user friends, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not get user friends, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not get user friends, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='get', + action='get friends', + url=friendship, + auth=True, + headers=None, + data=None, + autorefresh=True) + if resp.status == 200: + self._log.debug(f'got user friends') + _friends = await resp.json() + return _friends if not parse else [Friend(friend_data=friend, logger=self._log) for friend in + _friends['friends']] async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: self._log.info(f'getting parcel friends') @@ -713,30 +697,24 @@ async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> d self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.get(url=f"{friendship}{shipment_number}", - headers={'Authorization': self.auth_token}) as resp: - match resp.status: - case 200: - self._log.debug(f'got user 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']] - } - - case 401: - self._log.error('could not get user friends, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not get user friends, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not get user friends, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='get', + action='get parcel friends', + url=f"{friendship}{shipment_number}", + auth=True, + headers=None, + data=None, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'got parcel friends') + 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']] + } async def add_friend(self, name: str, phone_number: str | int, code: str | int, parse=False) -> dict | Friend: """Adds user friends for inpost services @@ -770,72 +748,45 @@ async def add_friend(self, name: str, phone_number: str | int, code: str | int, if isinstance(code, int): code = str(code) - async with await self.sess.post(url=validate_friendship, - headers={'Authorization': self.auth_token}, - json={'invitationCode': code}) as resp: - match resp.status: - case 200: - self._log.debug(f'validated friendship code') - async with await self.sess.post(url=accept_friendship, - headers={'Authorization': self.auth_token}, - json={'invitationCode': code, - 'friendName': name}): - match resp.status: - case 200: - self._log.debug(f'added user friend') - return await resp.json() if not parse else Friend(await resp.json(), - logger=self._log) - - case 401: - self._log.error('could not add user friends, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not add user friends, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not add user friends, unhandled status') - case 401: - self._log.error('could not validate friendship code, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not validate friendship code, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not validate friendship code, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='post', + action='add friend', + url=validate_friendship, + auth=True, + headers=None, + data={'invitationCode': code}, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'added user friend') + 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) - async with await self.sess.post(url=friendship, - headers={'Authorization': self.auth_token}, - json={'phoneNumber': phone_number, - 'name': name}) as resp: - match resp.status: - case 200: - self._log.debug(f'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": - return r if not parse else Friend.from_invitation(invitation_data=r, logger=self._log) - - else: - ... - case 401: - self._log.error('could not add user friends, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not add user friends, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not add user friends, unhandled status') - - raise UnidentifiedAPIError(reason=resp) + resp = await self.request(method='post', + action='add friend', + url=friendship, + auth=True, + headers=None, + data={ + 'phoneNumber': phone_number, + 'name': name + }, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'added user friend') + 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": + return r if not parse else Friend.from_invitation(invitation_data=r, logger=self._log) + + else: + self._log.debug(r) 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` @@ -873,22 +824,19 @@ async def remove_friend(self, uuid: str | None, name: str | None, phone_number: else: uuid = next((friend['uuid'] for friend in f['friends'] if friend['name'] == name)) - async with await self.sess.delete(url=f'{friendship}{uuid}', - headers={'Authorization': self.auth_token}) as resp: - match resp.status: - case 200: - self._log.debug(f'removed user friend') - return True - case 401: - self._log.error('could not remove user friend, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not remove user friend, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not remove user friend, unhandled status') + resp = await self.request(method='delete', + action='remove user friend', + url=f'{friendship}{uuid}', + auth=True, + headers=None, + data=None, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'removed user friend') + return True - raise UnidentifiedAPIError(reason=resp) + return False async def update_friend(self, uuid: str | None, phone_number: str | int | None, name: str): """Updates user friend for inpost services with specified `name` @@ -921,25 +869,22 @@ async def update_friend(self, uuid: str | None, phone_number: str | int | None, if uuid is None: uuid = next( - (friend['uuid'] for friend in (await self.get_friends())['friends'] if friend['phoneNumber'] == phone_number)) - - async with await self.sess.patch(url=f'{friends}{uuid}', - headers={'Authorization': self.auth_token}, - json={'name': name}) as resp: - match resp.status: - case 200: - self._log.debug(f'updated user friend') - return True - case 401: - self._log.error('could not update user friend, unauthorized') - raise UnauthorizedError(reason=resp) - case 404: - self._log.error('could not update user friend, not found') - raise NotFoundError(reason=resp) - case _: - self._log.error('could not update user friend, unhandled status') + (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) + + if resp.status == 200: + self._log.debug(f'updated user friend') + return True - raise UnidentifiedAPIError(reason=resp) + return False async def share_parcel(self, uuid: str, shipment_number: int | str): """Shares parcel to a pre-initialized friend @@ -961,29 +906,57 @@ async def share_parcel(self, uuid: str, shipment_number: int | str): self._log.debug(f'authorization token missing') raise NotAuthenticatedError(reason='Not logged in') - async with await self.sess.post(url=shared, - headers={'Authorization': self.auth_token}, - json={ - 'parcels': [ - { - 'shipmentNumber': shipment_number, - 'friendUuids': [ - uuid - ] - } - ], - }) as share: - match share.status: - case 200: - self._log.debug(f'shared parcel: {shipment_number}') - return True - case 401: - self._log.error(f'could not share parcel: {shipment_number}, unauthorized') - raise UnauthorizedError(reason=share) - case 404: - self._log.error(f'could not share parcel: {shipment_number}, not found') - raise NotFoundError(reason=share) - case _: - self._log.error(f'could not share parcel: {shipment_number}, unhandled status') - - raise UnidentifiedAPIError(reason=share) + resp = await self.request(method='post', + action=f'share parcel: {shipment_number}', + url=shared, + auth=True, + headers=None, + data={'parcels': [ + { + 'shipmentNumber': shipment_number, + 'friendUuids': [ + uuid + ] + } + ], + }, + autorefresh=True) + + if resp.status == 200: + self._log.debug(f'updated user friend') + 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 5dfa7fd..fb64578 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -4,10 +4,12 @@ from .statuses import ParcelCarrierSize, ParcelLockerSize, ParcelDeliveryType, ParcelShipmentType, \ ParcelAdditionalInsurance, ParcelType, ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, \ ParcelServiceName, ParcelStatus, ReturnsStatus -from .exceptions import NoParcelError, UnidentifiedParcelError, ParcelTypeError, NotAuthenticatedError, ReAuthenticationError, \ +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 + tickets, logout, multi, validate_friendship, accept_friendship, parcel_notifications from .friends import Friend +from .notifications import Notification diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index 2f2af3f..c2e946b 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -21,3 +21,4 @@ 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 diff --git a/inpost/static/notifications.py b/inpost/static/notifications.py new file mode 100644 index 0000000..03fe128 --- /dev/null +++ b/inpost/static/notifications.py @@ -0,0 +1,15 @@ +from arrow import get, Arrow + + +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'] diff --git a/pyproject.toml b/pyproject.toml index d63b585..605d51f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.0.9" +version = "0.0.10" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "] @@ -17,7 +17,7 @@ classifiers = [ "Intended Audience :: Developers", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", - "Development Status :: 2 - Pre-Alpha" + "Development Status :: 3 - Alpha" ] [tool.poetry.dependencies]