class PickupDetails(APIView): @swagger_auto_schema(tags=['Pickups'], operation_id=f"{ENDPOINT_ID}retrieve", operation_summary="Retrieve a pickup", responses={ 200: Pickup(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url /v1/pickups/<PICKUP_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request, pk: str): """Retrieve a scheduled pickup.""" pickup = models.Pickup.access_by(request).get(pk=pk) return Response(Pickup(pickup).data) @swagger_auto_schema(tags=['Pickups'], operation_id=f"{ENDPOINT_ID}update", operation_summary="Update a pickup", responses={ 200: OperationConfirmation(), 400: ErrorResponse() }, request_body=PickupUpdateData(), code_examples=[{ 'lang': 'bash', 'source': ''' curl --request PATCH \\ --url /v1/pickups/<PICKUP_ID> \\ --header 'Authorization: Token <API_KEY>' \\ --data '{ "address": { "phone_number": "514-000-0000", "residential": false, "email": "*****@*****.**" }, "ready_time": "13:00", "closing_time": "20:00", }' ''' }]) def patch(self, request: Request, pk: str): """ Modify a pickup for one or many shipments with labels already purchased. """ pickup = models.Pickup.access_by(request).get(pk=pk) instance = SerializerDecorator[PickupUpdateData]( pickup, data=request.data, context=request).save().instance return Response(Pickup(instance).data, status=status.HTTP_200_OK)
class ShipmentDetail(APIView): @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}retrieve", operation_summary="Retrieve a shipment", responses={ 200: Shipment(), 400: ErrorResponse() }, ) def get(self, request: Request, pk: str): """ Retrieve a shipment. """ shipment = models.Shipment.access_by(request).get(pk=pk) return Response(Shipment(shipment).data) @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}cancel", operation_summary="Cancel a shipment", responses={ 200: OperationResponse(), 400: ErrorResponse() }, ) def delete(self, request: Request, pk: str): """ Void a shipment with the associated label. """ shipment = models.Shipment.access_by(request).get(pk=pk) if shipment.status not in [ ShipmentStatus.purchased.value, ShipmentStatus.created.value, ]: raise PurplShipApiException( f"The shipment is '{shipment.status}' and can therefore not be cancelled anymore...", code="state_error", status_code=status.HTTP_409_CONFLICT, ) if shipment.pickup_shipments.exists(): raise PurplShipApiException( (f"This shipment is scheduled for pickup '{shipment.pickup_shipments.first().pk}' " "Please cancel this shipment from the pickup before."), code="state_error", status_code=status.HTTP_409_CONFLICT, ) confirmation = SerializerDecorator[ShipmentCancelSerializer]( shipment, data={}, context=request).save() return Response(OperationResponse(confirmation.instance).data)
class WebhookDetail(APIView): @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}retrieve", operation_summary="Retrieve a webhook", responses={ 200: Webhook(), 400: ErrorResponse() }) def get(self, request: Request, pk: str): """ Retrieve a webhook. """ webhook = models.Webhook.access_by(request).get(pk=pk) return Response(Webhook(webhook).data) @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}update", operation_summary="Update a webhook", request_body=WebhookData(), responses={ 200: Webhook(), 400: ErrorResponse() }) def patch(self, request: Request, pk: str): """ update a webhook. """ webhook = models.Webhook.access_by(request).get(pk=pk) SerializerDecorator[WebhookSerializer](webhook, data=request.data).save() return Response(Webhook(webhook).data) @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}remove", operation_summary="Remove a webhook", responses={ 200: Operation(), 400: ErrorResponse() }) def delete(self, request: Request, pk: str): """ Remove a webhook. """ webhook = models.Webhook.access_by(request).get(pk=pk) webhook.delete(keep_parents=True) serializer = Operation(dict(operation="Remove webhook", success=True)) return Response(serializer.data)
class ShippingCancel(APIView): @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}void_label", operation_summary="Void a shipment label", query_serializer=TestFilters(), request_body=ShipmentCancelRequest(), responses={ 200: OperationResponse(), 400: ErrorResponse() }, manual_parameters=[ openapi.Parameter('carrier_name', in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES), ], ) def post(self, request: Request, carrier_name: str): """ Cancel a shipment and the label previously created """ test_filter = SerializerDecorator[TestFilters]( data=request.query_params).data payload = SerializerDecorator[ShipmentCancelRequest]( data=request.data).data response = Shipments.cancel(payload, context=request, carrier_name=carrier_name, **test_filter) return Response(OperationResponse(response).data, status=status.HTTP_202_ACCEPTED)
class ShippingDetails(APIView): @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}buy_label", operation_summary="Buy a shipment label", request_body=ShippingRequest(), responses={ 200: Shipment(), 400: ErrorResponse() }, ) def post(self, request: Request): """ Once the shipping rates are retrieved, provide the required info to submit the shipment by specifying your preferred rate. """ payload = SerializerDecorator[ShippingRequestValidation]( data=request.data).data response = Shipments.create( payload, resolve_tracking_url=( lambda tracking_number, carrier_name: reverse( "purplship.server.proxy:shipment-tracking", kwargs=dict(tracking_number=tracking_number, carrier_name=carrier_name)))) return Response(Shipment(response).data, status=status.HTTP_201_CREATED)
class PickupList(GenericAPIView): pagination_class = type('CustomPagination', (LimitOffsetPagination, ), dict(default_limit=20)) filter_backends = (filters.DjangoFilterBackend, ) filterset_class = PickupFilters model = models.Pickup @swagger_auto_schema(tags=['Pickups'], operation_id=f"{ENDPOINT_ID}list", operation_summary="List shipment pickups", responses={ 200: Pickups(), 400: ErrorResponse() }, manual_parameters=PickupFilters.parameters, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url '/v1/pickups' \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request): """ Retrieve all scheduled pickups. """ pickups = self.filter_queryset(self.get_queryset()) response = self.paginate_queryset(Pickup(pickups, many=True).data) return self.get_paginated_response(response)
class PickupCancel(APIView): @swagger_auto_schema(tags=['Pickups'], operation_id=f"{ENDPOINT_ID}cancel", operation_summary="Cancel a pickup", responses={ 200: OperationConfirmation(), 400: ErrorResponse() }, request_body=PickupCancelData(), code_examples=[{ 'lang': 'bash', 'source': ''' curl --request POST \\ --url /v1/pickups/<PICKUP_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def post(self, request: Request, pk: str): """ Cancel a pickup of one or more shipments. """ pickup = models.Pickup.access_by(request).get(pk=pk) confirmation = SerializerDecorator[PickupCancelData]( pickup, data=request.data, context=request).save().instance return Response(OperationConfirmation(confirmation).data, status=status.HTTP_200_OK)
class ShipmentParcels(APIView): @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}add_parcel", operation_summary="Add a shipment parcel", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=ParcelData(), ) def post(self, request: Request, pk: str): """ Add a parcel to an existing shipment for a multi-parcel shipment. """ shipment = (models.Shipment.access_by(request).exclude( status=ShipmentStatus.cancelled.value).get(pk=pk)) if shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "Shipment already 'purchased'", code="state_error", status_code=status.HTTP_409_CONFLICT, ) parcel = (SerializerDecorator[ParcelSerializer]( data=request.data, context=request).save().instance) shipment.shipment_parcels.add(parcel) reset_related_shipment_rates(shipment) return Response(Shipment(shipment).data)
class DiscardCommodities(APIView): @swagger_auto_schema(tags=['Customs'], operation_id=f"{ENDPOINT_ID}discard_commodity", operation_summary="Discard a commodity", responses={ 200: Operation(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request DELETE \\ --url /v1/customs_info/<CUSTOMS_INFO_ID>/commodities/<COMMODITY_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def delete(self, request: Request, pk: str, ck: str): """ Discard a customs commodity. """ customs = models.Customs.access_by(request).get(pk=pk) shipment = customs.shipment_set.first() if shipment is not None and shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "The shipment related to this customs info has been 'purchased' and cannot be modified", status_code=status.HTTP_409_CONFLICT, code='state_error') commodity = customs.commodities.get(pk=ck) commodity.delete(keep_parents=True) serializer = Operation( dict(operation="Discard customs commodity", success=True)) return Response(serializer.data)
class PickupCancel(APIView): @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}cancel_pickup", operation_summary="Cancel a pickup", query_serializer=TestFilters(), request_body=PickupCancelRequest(), responses={ 200: OperationResponse(), 400: ErrorResponse() }, manual_parameters=[ openapi.Parameter('carrier_name', in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES), ], ) def post(self, request: Request, carrier_name: str): """ Cancel a pickup previously scheduled """ test_filter = SerializerDecorator[TestFilters]( data=request.query_params).data payload = SerializerDecorator[PickupCancelRequest]( data=request.data).data response = Pickups.cancel(payload, context=request, carrier_name=carrier_name, **test_filter) return Response(OperationResponse(response).data, status=status.HTTP_200_OK)
class TrackersDetails(APIView): permission_classes = [IsAuthenticatedOrReadOnly] @swagger_auto_schema( tags=["Trackers"], operation_id=f"{ENDPOINT_ID}retrieves", operation_summary="Retrieves a shipment tracker", responses={ 200: TrackingStatus(), 404: ErrorResponse() }, ) def get(self, request: Request, id_or_tracking_number: str): """ Retrieve a shipment tracker """ __filter = Q(pk=id_or_tracking_number) | Q( tracking_number=id_or_tracking_number) trackers = models.Tracking.objects.filter(__filter) if len(trackers) == 0: models.Tracking.objects.get(__filter) return Response(TrackingStatus(trackers.first()).data) @swagger_auto_schema( tags=["Trackers"], operation_id=f"{ENDPOINT_ID}remove", operation_summary="Discard a shipment tracker", responses={ 200: Operation(), 400: ErrorResponse() }, ) def delete(self, request: Request, id_or_tracking_number: str): """ Discard a shipment tracker. """ tracker = models.Tracking.access_by(request).get( Q(pk=id_or_tracking_number) | Q(tracking_number=id_or_tracking_number)) tracker.delete(keep_parents=True) serializer = Operation( dict(operation="Discard a tracker", success=True)) return Response(serializer.data)
class PickupRequest(APIView): @swagger_auto_schema(tags=['Pickups'], operation_id=f"{ENDPOINT_ID}schedule", operation_summary="Schedule a pickup", responses={ 200: Pickup(), 400: ErrorResponse() }, query_serializer=TestFilters(), request_body=PickupData(), code_examples=[{ 'lang': 'bash', 'source': ''' curl --request POST \\ --url /v1/pickups/<PICKUP_ID> \\ --header 'Authorization: Token <API_KEY>' \\ --data '{ "pickup_date": "2020-10-25", "address": { "address_line1": "125 Church St", "person_name": "John Doe", "city": "Moncton", "country_code": "CA", "postal_code": "E1C4Z8", "state_code": "NB", }, "ready_time": "13:00", "closing_time": "17:00", "instruction": "Should not be folded", "package_location": "At the main entrance hall", "tracking_numbers": [ "8545763607864201002" ] }' ''' }]) def post(self, request: Request, carrier_name: str): """ Schedule a pickup for one or many shipments with labels already purchased. """ carrier_filter = { **SerializerDecorator[TestFilters](data=request.query_params).data, "carrier_name": carrier_name, } pickup = SerializerDecorator[PickupData]( data=request.data, context=request).save(carrier_filter=carrier_filter).instance return Response(Pickup(pickup).data, status=status.HTTP_201_CREATED)
class ShipmentRates(APIView): logging_methods = ["GET"] @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}rates", operation_summary="Fetch new shipment rates", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=ShipmentRateData(), ) def post(self, request: Request, pk: str): """ Refresh the list of the shipment rates """ shipment = (models.Shipment.access_by(request).exclude( status=ShipmentStatus.cancelled.value).get(pk=pk)) rate_payload = SerializerDecorator[ShipmentRateData]( data=request.data).data carrier_ids = (rate_payload["carrier_ids"] if "carrier_ids" in rate_payload else shipment.carrier_ids) carriers = Carriers.list( active=True, capability="shipping", context=request, test=shipment.test_mode, carrier_ids=carrier_ids, ) rate_response: RateResponse = (SerializerDecorator[RateSerializer]( context=request, data={ **ShipmentData(shipment).data, **rate_payload }).save(carriers=carriers).instance) SerializerDecorator[ShipmentSerializer]( shipment, context=request, data={ "rates": Rate(rate_response.rates, many=True).data, "messages": DP.to_dict(rate_response.messages), **rate_payload, }, ).save(carriers=carriers) return Response(Shipment(shipment).data)
class WebhookList(GenericAPIView): pagination_class = LimitOffsetPagination default_limit = 20 @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all webhooks", responses={ 200: Webhooks(), 400: ErrorResponse() }, query_serializer=WebhookFilters) def get(self, request: Request): """ Retrieve all webhooks. """ query = (SerializerDecorator[WebhookFilters]( data=request.query_params).data if any(request.query_params) else {}) webhooks = models.Webhook.access_by(request).filter(**query) response = self.paginate_queryset(Webhook(webhooks, many=True).data) return self.get_paginated_response(response) @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}create", operation_summary="Create a webhook", request_body=WebhookData(), responses={ 200: Webhook(), 400: ErrorResponse() }) def post(self, request: Request): """Create a new webhook.""" webhook = SerializerDecorator[WebhookSerializer]( data=request.data, context=request).save().instance return Response(Webhook(webhook).data, status=status.HTTP_201_CREATED)
class ShipmentOptions(APIView): @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}set_options", operation_summary="Add shipment options", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=openapi.Schema( title="options", type=openapi.TYPE_OBJECT, additional_properties=True, ), ) def post(self, request: Request, pk: str): """ Add one or many options to your shipment.<br/> **eg:**<br/> - add shipment **insurance** - specify the preferred transaction **currency** - setup a **cash collected on delivery** option ```json { "insurance": 120, "currency": "USD" } ``` And many more, check additional options available in the [reference](#operation/all_references). """ shipment = (models.Shipment.access_by(request).exclude( status=ShipmentStatus.cancelled.value).get(pk=pk)) if shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "Shipment already 'purchased'", code="state_error", status_code=status.HTTP_409_CONFLICT, ) payload: dict = dict(options=DP.to_dict(request.data), shipment_rates=[], messages=[]) SerializerDecorator[ShipmentSerializer](shipment, data=payload).save() return Response(Shipment(shipment).data)
class CustomsCommodities(APIView): @swagger_auto_schema(tags=['Customs'], operation_id=f"{ENDPOINT_ID}add_commodity", operation_summary="Add a commodity", responses={ 200: Customs(), 400: ErrorResponse() }, request_body=CommodityData(), code_examples=[{ 'lang': 'bash', 'source': ''' curl --request POST \\ --url /v1/customs_info/<CUSTOMS_INFO_ID>/commodities \\ --header 'Authorization: Token <API_KEY>' \\ --header 'Content-Type: application/json' \\ --data '{ "weight": 1, "weight_unit": "KG", "quantity": 1, "sku": "XXXXX0000123", "value_amount": 25, "value_currency": "USD", "origin_country": "CA" }' ''' }]) def post(self, request: Request, pk: str): """ Add a customs commodity. """ customs = models.Customs.access_by(request).get(pk=pk) shipment = customs.shipment_set.first() if shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "The associated shipment is already 'purchased'", status_code=status.HTTP_409_CONFLICT, code='state_error') commodity = SerializerDecorator[CommoditySerializer]( data=request.data, context=request).save().instance customs.commodities.add(commodity) return Response(Customs(commodity.customs_set.first()).data)
class WebhookTest(APIView): @swagger_auto_schema(tags=['Webhooks'], operation_id=f"{ENDPOINT_ID}test", operation_summary="Test a webhook", request_body=WebhookTestRequest(), responses={ 200: Operation(), 400: ErrorResponse() }) def post(self, request: Request, pk: str): """ test a webhook. """ webhook = models.Webhook.access_by(request).get(pk=pk) notification, *_ = notify_subscribers([webhook], request.data) _, response = notification serializer = Operation( dict(operation="Test Webhook", success=response.ok)) return Response(serializer.data)
class ShipmentPurchase(APIView): @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}purchase", operation_summary="Buy a shipment label", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=ShipmentPurchaseData(), ) def post(self, request: Request, pk: str): """ Select your preferred rates to buy a shipment label. """ shipment = (models.Shipment.access_by(request).exclude( status=ShipmentStatus.cancelled.value).get(pk=pk)) if shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( f"The shipment is '{shipment.status}' and therefore already {ShipmentStatus.purchased.value}", code="state_error", status_code=status.HTTP_409_CONFLICT, ) # Submit shipment to carriers response: Shipment = (SerializerDecorator[ShipmentPurchaseSerializer]( context=request, data={ **Shipment(shipment).data, **SerializerDecorator[ShipmentPurchaseData](data=request.data).data, }, ).save().instance) # Update shipment state SerializerDecorator[ShipmentSerializer](shipment, data=DP.to_dict(response), context=request).save() create_shipment_tracker(shipment, context=request) return Response(Shipment(shipment).data)
class RateViewAPI(APIView): @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}fetch_rates", operation_summary="Fetch shipment rates", operation_description=DESCRIPTIONS, responses={200: RateResponse(), 400: ErrorResponse()}, request_body=RateRequest(), query_serializer=TestFilters(), ) def post(self, request: Request): payload = SerializerDecorator[RateRequest](data=request.data).data test_filter = SerializerDecorator[TestFilters](data=request.query_params).data response = Rates.fetch(payload, context=request, **test_filter) return Response( RateResponse(response).data, status=status.HTTP_207_MULTI_STATUS if len(response.messages) > 0 else status.HTTP_201_CREATED )
class ShipmentCustoms(APIView): @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}add_customs", operation_summary="Add a customs declaration", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=CustomsData(), ) def post(self, request: Request, pk: str): """ Add the customs declaration for the shipment if non existent. """ shipment = (models.Shipment.access_by(request).exclude( status=ShipmentStatus.cancelled.value).get(pk=pk)) if shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "Shipment already 'purchased'", code="state_error", status_code=status.HTTP_409_CONFLICT, ) if shipment.customs is not None: raise PurplShipApiException( "Shipment customs declaration already defined", code="state_error", status_code=status.HTTP_409_CONFLICT, ) payload: dict = dict(customs=DP.to_dict(request.data), shipment_rates=[], messages=[]) SerializerDecorator[ShipmentSerializer](shipment, data=payload, context=request).save() return Response(Shipment(shipment).data)
class TrackersCreate(APIView): logging_methods = ["GET"] @swagger_auto_schema( tags=["Trackers"], operation_id=f"{ENDPOINT_ID}create", operation_summary="Create a shipment tracker", query_serializer=TestFilters(), responses={ 200: TrackingStatus(), 404: ErrorResponse() }, manual_parameters=[ openapi.Parameter( "carrier_name", in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES, ), ], ) def get(self, request: Request, carrier_name: str, tracking_number: str): """ This API creates or retrieves (if existent) a tracking status object containing the details and events of a shipping in progress. """ carrier_filter = { **SerializerDecorator[TestFilters](data=request.query_params).data, "carrier_name": carrier_name, } tracking = (models.Tracking.access_by(request).filter( tracking_number=tracking_number).first()) instance = (SerializerDecorator[TrackingSerializer]( tracking, data=dict(tracking_number=tracking_number), context=request).save(carrier_filter=carrier_filter).instance) return Response(TrackingStatus(instance).data)
class CarrierList(GenericAPIView): pagination_class = LimitOffsetPagination default_limit = 100 @swagger_auto_schema(tags=['Carriers'], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all carriers", responses={ 200: CarriersSettingsList(), 400: ErrorResponse() }, query_serializer=CarrierFilters, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url '/v1/carriers' \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request): """ Returns the list of configured carriers """ query = SerializerDecorator[CarrierFilters]( data=request.query_params).data carriers = [ carrier.data for carrier in Carriers.list(**{ **query, 'context': request }) ] response = self.paginate_queryset( CarrierSettings(carriers, many=True).data) return self.get_paginated_response(response)
class TrackerList(GenericAPIView): pagination_class = type("CustomPagination", (LimitOffsetPagination, ), dict(default_limit=20)) filter_backends = (filters.DjangoFilterBackend, ) filterset_class = TrackerFilters model = models.Tracking def get_queryset(self): queryset = super().get_queryset() _filters = tuple() query_params = getattr(self.request, "query_params", {}) carrier_name = query_params.get("carrier_name") if carrier_name is not None: _filters += (Q( **{f"tracking_carrier__{carrier_name}settings__isnull": False }), ) return queryset.filter(*_filters) @swagger_auto_schema( tags=["Trackers"], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all shipment trackers", responses={ 200: Trackers(), 400: ErrorResponse() }, manual_parameters=TrackerFilters.parameters, ) def get(self, request: Request): """ Retrieve all shipment trackers. """ trackers = self.filter_queryset(self.get_queryset()) response = self.paginate_queryset( TrackingStatus(trackers, many=True).data) return self.get_paginated_response(response)
class TrackingAPIView(APIView): logging_methods = ['GET'] @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}track_shipment", operation_summary="Track a shipment", query_serializer=TestFilters(), responses={ 200: TrackingResponse(), 400: ErrorResponse() }, manual_parameters=[ openapi.Parameter('carrier_name', in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES), ], ) def get(self, request: Request, carrier_name: str, tracking_number: str): """ You can track a shipment by specifying the carrier and the shipment tracking number. """ test_filter = SerializerDecorator[TestFilters]( data=request.query_params).data payload = SerializerDecorator[TrackingRequest](data=dict( tracking_numbers=[tracking_number])).data response = Shipments.track(payload, context=request, carrier_name=carrier_name, **test_filter) return Response(TrackingResponse(response).data, status=status.HTTP_200_OK if response.tracking is not None else status.HTTP_404_NOT_FOUND)
class ParcelList(GenericAPIView): queryset = models.Parcel.objects pagination_class = type('CustomPagination', (LimitOffsetPagination, ), dict(default_limit=20)) @swagger_auto_schema(tags=['Parcels'], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all parcels", responses={ 200: Parcels(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url '/v1/parcels' \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request): """ Retrieve all stored parcels. """ parcels = models.Parcel.access_by(request).filter( shipment_parcels=None) serializer = Parcel(parcels, many=True) response = self.paginate_queryset(serializer.data) return self.get_paginated_response(response) @swagger_auto_schema(tags=['Parcels'], operation_id=f"{ENDPOINT_ID}create", operation_summary="Create a parcel", request_body=ParcelData(), responses={ 200: Parcel(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request POST \\ --url /v1/parcels \\ --header 'Authorization: Token <API_KEY>' \\ --header 'Content-Type: application/json' \\ --data '{ "weight": 1, "weight_unit": "KG", "package_preset": "canadapost_corrugated_small_box" }' ''' }]) def post(self, request: Request): """ Create a new parcel. """ parcel = SerializerDecorator[ParcelSerializer]( data=request.data, context=request).save().instance return Response(Parcel(parcel).data, status=status.HTTP_201_CREATED)
class ParcelDetail(APIView): @swagger_auto_schema(tags=['Parcels'], operation_id=f"{ENDPOINT_ID}retrieve", operation_summary="Retrieve a parcel", responses={ 200: Parcel(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url /v1/parcels/<PARCEL_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request, pk: str): """ Retrieve a parcel. """ address = models.Parcel.access_by(request).get(pk=pk) return Response(Parcel(address).data) @swagger_auto_schema(tags=['Parcels'], operation_id=f"{ENDPOINT_ID}update", operation_summary="Update a parcel", request_body=ParcelData(), responses={ 200: Parcel(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request PATCH \\ --url /v1/parcels/<PARCEL_ID> \\ --header 'Authorization: Token <API_KEY>' \\ --header 'Content-Type: application/json' \\ --data '{ "weight": 1.2, }' ''' }]) def patch(self, request: Request, pk: str): """ modify an existing parcel's details. """ parcel = models.Parcel.access_by(request).get(pk=pk) shipment = parcel.shipment_parcels.first() if shipment is not None and shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "The shipment related to this parcel has been 'purchased' and can no longer be modified", status_code=status.HTTP_409_CONFLICT, code='state_error') SerializerDecorator[ParcelSerializer](parcel, data=request.data).save() reset_related_shipment_rates(shipment) return Response(Parcel(parcel).data) @swagger_auto_schema(tags=['Parcels'], operation_id=f"{ENDPOINT_ID}discard", operation_summary="Remove a parcel", responses={ 200: Operation(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request DELETE \\ --url /v1/parcels/<PARCEL_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def delete(self, request: Request, pk: str): """ Remove a parcel. """ parcel = models.Parcel.access_by(request).get(pk=pk) shipment = parcel.shipment_parcels.first() if shipment is not None and ( shipment.status == ShipmentStatus.purchased.value or len(shipment.shipment_parcels.all()) == 1): raise PurplShipApiException( "A shipment attached to this parcel is purchased or has only one parcel. The parcel cannot be removed!", status_code=status.HTTP_409_CONFLICT, code='state_error') parcel.delete(keep_parents=True) shipment.shipment_parcels.set( shipment.shipment_parcels.exclude(id=parcel.id)) serializer = Operation(dict(operation="Remove parcel", success=True)) reset_related_shipment_rates(shipment) return Response(serializer.data)
class PickupDetails(APIView): @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}schedule_pickup", operation_summary="Schedule a pickup", query_serializer=TestFilters(), request_body=PickupRequest(), responses={ 200: PickupResponse(), 400: ErrorResponse() }, manual_parameters=[ openapi.Parameter('carrier_name', in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES), ], ) def post(self, request: Request, carrier_name: str): """ Schedule one or many parcels pickup """ test_filter = SerializerDecorator[TestFilters]( data=request.query_params).data payload = SerializerDecorator[PickupRequest](data=request.data).data response = Pickups.schedule(payload, context=request, carrier_name=carrier_name, **test_filter) return Response(PickupResponse(response).data, status=status.HTTP_201_CREATED) @swagger_auto_schema( tags=['Proxy'], operation_id=f"{ENDPOINT_ID}update_pickup", operation_summary="Update a pickup", query_serializer=TestFilters(), request_body=PickupUpdateRequest(), responses={ 200: PickupResponse(), 400: ErrorResponse() }, manual_parameters=[ openapi.Parameter('carrier_name', in_=openapi.IN_PATH, type=openapi.TYPE_STRING, enum=CARRIER_NAMES), ], ) def put(self, request: Request, carrier_name: str): """ Modify a scheduled pickup """ test_filter = SerializerDecorator[TestFilters]( data=request.query_params).data payload = SerializerDecorator[PickupUpdateRequest]( data=request.data).data response = Pickups.update(payload, context=request, carrier_name=carrier_name, **test_filter) return Response(PickupResponse(response).data, status=status.HTTP_200_OK)
class ShipmentList(GenericAPIView): pagination_class = type("CustomPagination", (LimitOffsetPagination, ), dict(default_limit=20)) filter_backends = (filters.DjangoFilterBackend, ) filterset_class = ShipmentFilters model = models.Shipment def get_queryset(self): queryset = super().get_queryset() _filters = tuple() query_params = getattr(self.request, "query_params", {}) carrier_name = query_params.get("carrier_name") if carrier_name is not None: _filters += ( Q(meta__rate_provider=carrier_name) | Q( **{ f"selected_rate_carrier__{carrier_name}settings__isnull": False }), ) return queryset.filter(*_filters) @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all shipments", responses={ 200: Shipments(), 400: ErrorResponse() }, manual_parameters=ShipmentFilters.parameters, ) def get(self, request: Request): """ Retrieve all shipments. """ shipments = self.filter_queryset(self.get_queryset()) response = self.paginate_queryset(Shipment(shipments, many=True).data) return self.get_paginated_response(response) @swagger_auto_schema( tags=["Shipments"], operation_id=f"{ENDPOINT_ID}create", operation_summary="Create a shipment", responses={ 200: Shipment(), 400: ErrorResponse() }, request_body=ShipmentData(), query_serializer=ShipmentMode(), ) def post(self, request: Request): """ Create a new shipment instance. """ carrier_filter = { **SerializerDecorator[ShipmentMode](data=request.query_params).data } shipment = (SerializerDecorator[ShipmentSerializer]( data=request.data, context=request).save(carrier_filter=carrier_filter).instance) return Response(Shipment(shipment).data, status=status.HTTP_201_CREATED)
class AddressDetail(APIView): @swagger_auto_schema(tags=['Addresses'], operation_id=f"{ENDPOINT_ID}retrieve", operation_summary="Retrieve an address", responses={ 200: Address(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url /v1/addresses/<ADDRESS_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request, pk: str): """ Retrieve an address. """ address = models.Address.access_by(request).get(pk=pk) return Response(Address(address).data) @swagger_auto_schema(tags=['Addresses'], operation_id=f"{ENDPOINT_ID}update", operation_summary="Update an address", request_body=AddressData(), responses={ 200: Address(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request PATCH \\ --url /v1/addresses/<ADDRESS_ID> \\ --header 'Authorization: Token <API_KEY>' \\ --header 'Content-Type: application/json' \\ --data '{ "city": "Pierrefonds" }' ''' }]) def patch(self, request: Request, pk: str): """ update an address. """ address = models.Address.access_by(request).get(pk=pk) shipment = address.shipper.first() or address.recipient.first() if shipment is not None and shipment.status == ShipmentStatus.purchased.value: raise PurplShipApiException( "The shipment related to this address has been 'purchased' and can no longer be modified", status_code=status.HTTP_409_CONFLICT, code='state_error') SerializerDecorator[AddressSerializer](address, data=request.data).save() reset_related_shipment_rates(shipment) return Response(Address(address).data) @swagger_auto_schema(tags=['Addresses'], operation_id=f"{ENDPOINT_ID}discard", operation_summary="Discard an address", responses={ 200: Operation(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request DELETE \\ --url /v1/addresses/<ADDRESS_ID> \\ --header 'Authorization: Token <API_KEY>' ''' }]) def delete(self, request: Request, pk: str): """ Discard an address. """ address = models.Address.access_by(request).get(pk=pk) shipment = address.shipper.first() or address.recipient.first() if shipment is not None: raise PurplShipApiException( "This address is linked to a shipment and cannot be removed", status_code=status.HTTP_409_CONFLICT, code='state_error') address.delete(keep_parents=True) serializer = Operation(dict(operation="Discard address", success=True)) return Response(serializer.data)
class AddressList(GenericAPIView): queryset = models.Address.objects pagination_class = type('CustomPagination', (LimitOffsetPagination, ), dict(default_limit=20)) @swagger_auto_schema(tags=['Addresses'], operation_id=f"{ENDPOINT_ID}list", operation_summary="List all addresses", responses={ 200: Addresses(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request GET \\ --url '/v1/addresses' \\ --header 'Authorization: Token <API_KEY>' ''' }]) def get(self, request: Request): """ Retrieve all addresses. """ addresses = models.Address.access_by(request).filter(shipper=None, recipient=None) response = self.paginate_queryset(Address(addresses, many=True).data) return self.get_paginated_response(response) @swagger_auto_schema(tags=['Addresses'], operation_id=f"{ENDPOINT_ID}create", operation_summary="Create an address", request_body=AddressData(), responses={ 200: Address(), 400: ErrorResponse() }, code_examples=[{ 'lang': 'bash', 'source': ''' curl --request POST \\ --url /v1/addresses \\ --header 'Authorization: Token <API_KEY>' \\ --header 'Content-Type: application/json' \\ --data '{ "address_line1": "125 Church St", "person_name": "John Doe", "company_name": "A corp.", "phone_number": "+1 514 000 0000", "city": "Moncton", "country_code": "CA", "postal_code": "E1C4Z8", "residential": false, "state_code": "NB" }' ''' }]) def post(self, request: Request): """ Create a new address. """ address = SerializerDecorator[AddressSerializer]( data=request.data, context=request).save().instance return Response(Address(address).data, status=status.HTTP_201_CREATED)