diff --git a/inpost/api.py b/inpost/api.py index e445c75..6f321f2 100644 --- a/inpost/api.py +++ b/inpost/api.py @@ -21,6 +21,12 @@ def __init__(self): def __repr__(self): 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() + @classmethod async def from_phone_number(cls, phone_number: str | int): """`Classmethod` to initialize :class:`Inpost` object with phone number @@ -340,7 +346,8 @@ async def get_parcels(self, 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): @@ -362,7 +369,8 @@ async def get_parcels(self, _parcels = (_parcel for _parcel in _parcels if ParcelLockerSize[_parcel['parcelSize']] in parcel_size) - return _parcels if not parse else [Parcel(parcel_data=data, logger=self._log) for data in _parcels] + 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) @@ -374,6 +382,30 @@ async def get_parcels(self, raise UnidentifiedAPIError(reason=resp) + 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) + async def collect_compartment_properties(self, shipment_number: str | int | None = None, parcel_obj: Parcel | None = None, location: dict | None = None) -> bool: """Validates sent data and fetches required compartment properties for opening @@ -394,8 +426,6 @@ async def collect_compartment_properties(self, shipment_number: str | int | None .. warning:: you must fill in only one parameter - shipment_number or parcel_obj!""" - self._log.info(f'collecting compartment properties for {shipment_number}') - 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!') @@ -408,6 +438,8 @@ async def collect_compartment_properties(self, shipment_number: str | int | 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}') + async with await self.sess.post(url=collect, headers={'Authorization': self.auth_token}, json={ @@ -416,18 +448,21 @@ async def collect_compartment_properties(self, shipment_number: str | int | None }) as collect_resp: match collect_resp.status: case 200: - self._log.debug(f'collected compartment properties for {shipment_number}') + 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 {shipment_number}, unauthorized') + 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 {shipment_number}, not found') + 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 {shipment_number}, unhandled status') + self._log.error(f'could not collect compartment properties for {parcel_obj.shipment_number}, ' + f'unhandled status') raise UnidentifiedAPIError(reason=collect_resp) @@ -635,3 +670,320 @@ async def get_prices(self) -> dict: self._log.error('could not get parcel prices, unhandled status') raise UnidentifiedAPIError(reason=resp) + + async def get_friends(self, parse=False) -> dict | List[Friend]: + """Fetches user friends for inpost services + + :param parse: switch for parsing response + :type parse: bool + :return: :class:`dict` of user friends for inpost services + :rtype: 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 friends') + + if not self.auth_token: + 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) + + async def get_parcel_friends(self, shipment_number: int | str, parse=False) -> dict: + self._log.info(f'getting parcel friends') + + if not self.auth_token: + 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) + + async def add_friend(self, name: str, phone_number: str | int, code: str | int, parse=False) -> dict | Friend: + """Adds user friends for inpost services + + :param name: name of further inpost friend + :type name: str + :param phone_number: further friend phone number + :type phone_number: str | int + :param code: used when you have a friendship code from your friend + :type code: str | int + :param parse: switch for parsing response + :type parse: bool + :return: :class:`dict` with friends details + :rtype: dict + :raises NotAuthenticatedError: User not authenticated in inpost service + :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""" + + self._log.info(f'adding user friend') + + if not self.auth_token: + self._log.debug(f'authorization token missing') + raise NotAuthenticatedError(reason='Not logged in') + + if len(name) > 20: + raise ValueError(f'Name too long: {name} (over 20 characters') + + if code: + 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) + + 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) + + 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` + + :param uuid: uuid of inpost friend to remove + :type uuid: str | None + :param name: name of inpost friend to remove + :type name: str | None + :param phone_number: phone number of inpost friend to remove + :type phone_number: str | int | None + :return: True if friend is removed + :rtype: bool + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnauthorizedError: Unauthorized access to inpost services, + :raises NotFoundError: Friend not found + :raises UnidentifiedAPIError: Unexpected thing happened + :raises ValueError: Name length exceeds 20 characters""" + + self._log.info(f'adding user friend') + + if not self.auth_token: + self._log.debug(f'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)') + + if isinstance(phone_number, int): + phone_number = str(phone_number) + + if uuid is None: + f = await self.get_friends() + if 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)) + + 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') + + raise UnidentifiedAPIError(reason=resp) + + async def update_friend(self, uuid: str | None, phone_number: str | int | None, name: str): + """Updates user friend for inpost services with specified `name` + + :param uuid: uuid of inpost friend to update + :type uuid: str | None + :param name: new name of inpost friend + :type name: str + :param phone_number: phone number of inpost friend to update + :type phone_number: str | int | None + :return: True if friend is updated + :rtype: bool + :raises NotAuthenticatedError: User not authenticated in inpost service + :raises UnauthorizedError: Unauthorized access to inpost services, + :raises NotFoundError: Friend not found + :raises UnidentifiedAPIError: Unexpected thing happened + :raises ValueError: Name length exceeds 20 characters""" + + self._log.info(f'updating user friend') + + if not self.auth_token: + self._log.debug(f'authorization token missing') + raise NotAuthenticatedError(reason='Not logged in') + + if len(name) > 20: + 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)) + + 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') + + raise UnidentifiedAPIError(reason=resp) + + async def share_parcel(self, uuid: str, shipment_number: int | str): + """Shares parcel to a pre-initialized friend + + :param uuid: uuid of inpost friend to update + :type uuid: str + :param shipment_number: Parcel's shipment number + :type 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""" + + 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') + + 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) diff --git a/inpost/static/__init__.py b/inpost/static/__init__.py index 6032d26..5dfa7fd 100644 --- a/inpost/static/__init__.py +++ b/inpost/static/__init__.py @@ -3,10 +3,11 @@ from .headers import appjson from .statuses import ParcelCarrierSize, ParcelLockerSize, ParcelDeliveryType, ParcelShipmentType, \ ParcelAdditionalInsurance, ParcelType, ParcelOwnership, CompartmentExpectedStatus, CompartmentActualStatus, \ - ParcelServiceName, ParcelStatus + ParcelServiceName, ParcelStatus, ReturnsStatus from .exceptions import NoParcelError, UnidentifiedParcelError, ParcelTypeError, NotAuthenticatedError, ReAuthenticationError, \ PhoneNumberError, SmsCodeError, RefreshTokenError, UnidentifiedAPIError, UserLocationError, \ - UnidentifiedError, NotFoundError, UnauthorizedError, SingleParamError + 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, friends, shared, sent, returns, parcel_prices, \ - tickets, logout + compartment_open, compartment_status, terminate_collect_session, friendship, shared, sent, returns, parcel_prices, \ + tickets, logout, multi, validate_friendship, accept_friendship +from .friends import Friend diff --git a/inpost/static/endpoints.py b/inpost/static/endpoints.py index da566fe..2f2af3f 100644 --- a/inpost/static/endpoints.py +++ b/inpost/static/endpoints.py @@ -7,11 +7,14 @@ 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 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 -friends: str = 'https://api-inmobile-pl.easypack24.net/v1/friends/' # get +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 diff --git a/inpost/static/exceptions.py b/inpost/static/exceptions.py index 3eb41d7..7319c79 100644 --- a/inpost/static/exceptions.py +++ b/inpost/static/exceptions.py @@ -89,6 +89,11 @@ class SingleParamError(BaseInpostError): 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 new file mode 100644 index 0000000..026921a --- /dev/null +++ b/inpost/static/friends.py @@ -0,0 +1,34 @@ +import logging + +from arrow import get, Arrow + + +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 + + if self.invitaion_code: + self._log.debug(f'created friendship with {self.name} using from_invitation') + else: + 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 __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("\'", "") diff --git a/inpost/static/parcels.py b/inpost/static/parcels.py index 419e3d4..687ca79 100644 --- a/inpost/static/parcels.py +++ b/inpost/static/parcels.py @@ -9,7 +9,20 @@ from inpost.static.statuses import * -class Parcel: +class BaseParcel: + def __init__(self, parcel_data: dict, logger: logging.Logger): + self.shipment_number: str = parcel_data['shipmentNumber'] + self._log: logging.Logger = logger.getChild(f'{__class__.__name__}.{self.shipment_number}') + self.status: ParcelStatus = ParcelStatus[parcel_data['status']] + # self.parcel_size: ParcelLockerSize | ParcelCarrierSize = ParcelLockerSize[parcel_data['parcelSize']] \ + # if self.shipment_type == ParcelShipmentType.parcel else ParcelCarrierSize[parcel_data['parcelSize']] + 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 :param parcel_data: :class:`dict` containing all parcel data @@ -19,7 +32,8 @@ class Parcel: def __init__(self, parcel_data: dict, logger: logging.Logger): """Constructor method""" - self.shipment_number: str = parcel_data['shipmentNumber'] + super().__init__(parcel_data, logger) + # self.shipment_number: str = parcel_data['shipmentNumber'] 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['openCode'] if 'openCode' in parcel_data else None @@ -27,7 +41,7 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): 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.expiry_date: arrow | None = get(parcel_data['expiryDate']) if 'expiryDate' in parcel_data else None + # self.expiry_date: arrow | None = get(parcel_data['expiryDate']) if 'expiryDate' 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) @@ -37,10 +51,10 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): 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 = parcel_data['endOfWeekCollection'] - self.operations: Operations = Operations(operations_data=parcel_data['operations'], logger=self._log) + # self.operations: Operations = Operations(operations_data=parcel_data['operations'], logger=self._log) self.status: ParcelStatus = ParcelStatus[parcel_data['status']] - self.event_log: List[EventLog] = [EventLog(eventlog_data=event, logger=self._log) - for event in parcel_data['eventLog']] + # self.event_log: List[EventLog] = [EventLog(eventlog_data=event, logger=self._log) + # for event in parcel_data['eventLog']] self.avizo_transaction_status: str = parcel_data['avizoTransactionStatus'] self.shared_to: List[SharedTo] = [SharedTo(sharedto_data=person, logger=self._log) for person in parcel_data['sharedTo']] @@ -63,7 +77,7 @@ def __init__(self, parcel_data: dict, logger: logging.Logger): self._log.debug(f'unexpected ownership status: {parcel_data["ownershipStatus"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + 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): @@ -83,7 +97,7 @@ def open_code(self) -> str | None: self._log.debug('got open code') return self._open_code - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @property @@ -97,7 +111,7 @@ def generate_qr_image(self) -> BytesIO | None: self._log.debug('got qr image') return self._qr_code.qr_image - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @property @@ -111,7 +125,7 @@ def compartment_properties(self): self._log.debug('got compartment properties') return self._compartment_properties - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @compartment_properties.setter @@ -126,7 +140,7 @@ def compartment_properties(self, compartmentproperties_data: dict): self._compartment_properties = CompartmentProperties(compartmentproperties_data=compartmentproperties_data, logger=self._log) - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') @property def compartment_location(self): @@ -139,20 +153,20 @@ def compartment_location(self): self._log.debug('got compartment location') return self._compartment_properties.location if self._compartment_properties else None - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @compartment_location.setter - def compartment_location(self, location_data): + 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('setting compartment location') + self._log.debug(f'setting compartment location with {location_data}') if self.shipment_type == ParcelShipmentType.parcel: self._log.debug('compartment location set') self._compartment_properties.location = location_data - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') @property def compartment_status(self) -> CompartmentActualStatus | None: @@ -166,17 +180,17 @@ def compartment_status(self) -> CompartmentActualStatus | None: self._log.debug('got compartment status') return self._compartment_properties.status if self._compartment_properties else None - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @compartment_status.setter def compartment_status(self, status): - self._log.debug('setting compartment status') + self._log.debug(f'setting compartment status with {status}') if self.shipment_type == ParcelShipmentType.parcel: self._log.debug('compartment status set') self._compartment_properties.status = status - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') @property def compartment_open_data(self): @@ -193,7 +207,7 @@ def compartment_open_data(self): 'receiverPhoneNumber': self.receiver.phone_number } - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') return None @property @@ -211,9 +225,45 @@ def mocked_location(self): 'accuracy': round(random.uniform(1, 4), 1) } - self._log.debug('wrong ParcelShipmentType') + self._log.debug(f'wrong ParcelShipmentType: {repr(self.shipment_type)}') + return None + + @property + def is_multicompartment(self): + """Specifies if parcel is in multi compartment + :return: True if parcel is in multicompartment + :rtype: bool""" + return self.multi_compartment is not None + + @property + def is_main_multicompartment(self): + """Specifies if parcel is main parcel in multi compartment + :return: True if parcel is in multicompartment + :rtype: bool""" + if self.is_multicompartment: + return self.multi_compartment.shipment_numbers is not None + return None + # @property + # def get_from_multicompartment(self): + # return + + +class ReturnParcel(BaseParcel): + def __init__(self, parcel_data: dict, logger: logging.Logger): + super().__init__(parcel_data, logger) + self.uuid: str = parcel_data['uuid'] + self.rma: str = parcel_data['rma'] + self.organization_name: str = parcel_data['organizationName'] + self.created_date: arrow = parcel_data['createdDate'] + self.accepted_date: arrow = parcel_data['acceptedDate'] + self.expiry_date: arrow = parcel_data['expiryDate'] + self.sent_date: arrow = parcel_data['sentDate'] + self.delivered_date: arrow = parcel_data['deliveredDate'] + self.order_number: str = parcel_data['orderNumber'] + self.form_type: str = parcel_data['formType'] + class Receiver: """Object representation of :class:`Parcel` receiver @@ -233,7 +283,7 @@ def __init__(self, receiver_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -253,7 +303,7 @@ def __init__(self, sender_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + 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: @@ -296,7 +346,7 @@ def __init__(self, pickuppoint_data: dict, logger: logging.Logger): self._log.debug(f'unknown delivery type: {pickuppoint_data["type"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + 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: @@ -332,7 +382,7 @@ def __init__(self, multicompartment_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -359,12 +409,13 @@ def __init__(self, operations_data: dict, logger: logging.Logger): 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') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -379,17 +430,18 @@ class EventLog: def __init__(self, eventlog_data: dict, logger: logging.Logger): """Constructor method""" self.type: str = eventlog_data['type'] - self.name: ParcelStatus = ParcelStatus[eventlog_data['name']] + 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') - if self.name == ParcelStatus.UNKNOWN: - self._log.debug(f'unknown parcel status: {eventlog_data["name"]}') + if self.name == ParcelStatus.UNKNOWN or self.name == ReturnsStatus.UNKNOWN: + self._log.debug(f'unknown {self.type}: {eventlog_data["name"]}') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -411,7 +463,7 @@ def __init__(self, sharedto_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -431,7 +483,7 @@ def __init__(self, qrcode_data: str, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + 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 @@ -482,7 +534,7 @@ def __init__(self, compartmentlocation_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items() if k != '_log') return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") @@ -505,7 +557,7 @@ def __init__(self, compartmentproperties_data: dict, logger: logging.Logger): self._log.debug('created') def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + 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 diff --git a/inpost/static/statuses.py b/inpost/static/statuses.py index d3b3158..5ce91ce 100644 --- a/inpost/static/statuses.py +++ b/inpost/static/statuses.py @@ -15,10 +15,6 @@ def __getattribute__(cls, item): except KeyError as error: return cls.UNKNOWN - def __repr__(self): - fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) - return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") - def get_all(cls): return [getattr(cls, name) for name in cls.__members__] @@ -62,6 +58,10 @@ def __eq__(self, other): return False + def __repr__(self): + fields = tuple(f"{k}={v}" for k, v in self.__dict__.items()) + return self.__class__.__name__ + str(tuple(sorted(fields))).replace("\'", "") + class ParcelCarrierSize(ParcelBase): """:class:`Enum` that holds parcel size for carrier shipment type""" @@ -143,12 +143,12 @@ class ParcelStatus(ParcelBase): DELIVERED = 'Doręczona' CANCELED = 'Anulowana' # TODO: translate from app CLAIMED = 'Zareklamowana' - STACK_IN_CUSTOMER_SERVICE_POINT = 'Umieszczona w punkcie obsługi klienta' # TODO: translate from app + 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 = 'Paczka w paczkomacie' # 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' @@ -160,6 +160,13 @@ class ParcelStatus(ParcelBase): 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' + + class ParcelOwnership(ParcelBase): """:class:`Enum` that holds parcel ownership types""" UNKNOWN = 'UNKNOWN DATA' diff --git a/pyproject.toml b/pyproject.toml index 8dd2a1e..d63b585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "inpost" -version = "0.0.5" +version = "0.0.9" description = "Asynchronous InPost package allowing you to manage existing incoming parcels without mobile app" authors = ["loboda4450 ", "MrKazik99 "] maintainers = ["loboda4450 "]