diff --git a/.gitignore b/.gitignore index 7fededd..6851364 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .venv +venv config.ini __pycache__ *.pyc diff --git a/LICENSE b/LICENSE index 6df5da1..abadcf3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023 Kumi Mitterer +Copyright (c) 2024 Kumi Mitterer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 169f382..8979942 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,23 @@ -# myDPD Python Client +# 4PX Python Client -This is a Python client for the myDPD Austria (https://mydpd.at) tracker. It allows you to track your shipments. - -It currently *only* supports DPD Austria. If you want to add support for other countries, feel free to open a pull request. Tracking for DPD shipments in other countries *may* work, but it is not guaranteed. +This is a Python client for the 4PX parcel tracker. It allows you to track your shipments. ## Installation ```bash -pip install dpdtrack +pip install track4px ``` ## Usage ```python -from dpdtrack import DPDAT, DPDRO +from track4px import Track4PX -api = DPDAT() # For tracking of DPD Austria packages -# api = DPDRO() for tracking of DPD Romania packages +api = Track4PX() # Realtime tracking tracking = api.tracking("YOUR_SHIPMENT_NUMBER") - -# Optionally pass the recipient's postal code to get more accurate results - -tracking = api.tracking("YOUR_SHIPMENT_NUMBER", "RECIPIENT_POSTAL_CODE") ``` ## License diff --git a/pyproject.toml b/pyproject.toml index 3f46e67..ce45e47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "dpdtrack" -version = "0.10.0" +name = "track4px" +version = "0.1.0" authors = [ { name="Kumi Mitterer", email="dpdtrack@kumi.email" }, ] -description = "Simple Python wrapper to fetch data from DPD Austria (mydpd.at) and Romania (dpd.ro)" +description = "Simple Python wrapper to fetch parcel tracking data from 4PX" readme = "README.md" license = { file="LICENSE" } requires-python = ">=3.10" @@ -18,9 +18,8 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "beautifulsoup4" ] [project.urls] -"Homepage" = "https://kumig.it/kumitterer/dpdtrack" -"Bug Tracker" = "https://kumig.it/kumitterer/dpdtrack/issues" \ No newline at end of file +"Homepage" = "https://git.private.coffee/kumi/track4px" +"Bug Tracker" = "https://git.private.coffee/kumi/track4px/issues" \ No newline at end of file diff --git a/src/dpdtrack/classes/__init__.py b/src/dpdtrack/classes/__init__.py deleted file mode 100644 index f7ff27f..0000000 --- a/src/dpdtrack/classes/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .http import HTTPRequest -from .api import DPDAT as DPD -from .api import DPDAT, DPDRO -from .shipment import Shipment, Event \ No newline at end of file diff --git a/src/dpdtrack/classes/api.py b/src/dpdtrack/classes/api.py deleted file mode 100644 index 1727d11..0000000 --- a/src/dpdtrack/classes/api.py +++ /dev/null @@ -1,118 +0,0 @@ -from hashlib import md5 -from configparser import ConfigParser -from urllib.parse import urlencode -from datetime import datetime - -import json - -import bs4 - -from .http import HTTPRequest -from .shipment import Shipment, Event - -class DPDAT: - """ DPD Austria API - - This API is used to track packages in Austria. It also seems to work for - packages in Germany, but this is not extensively tested. - """ - - SEARCH = "https://www.mydpd.at/jws.php/parcel/search" - VERIFY = "https://www.mydpd.at/jws.php/parcel/verify" - - def tracking(self, tracking_number: str, **kwargs): - """ Search for a tracking number """ - - postal_code = kwargs.get("postal_code", None) - wrap = kwargs.get("wrap", False) - - if postal_code is None: - endpoint = self.SEARCH - payload = tracking_number - else: - endpoint = self.VERIFY - payload = [tracking_number, postal_code] - - request = HTTPRequest(endpoint) - request.add_json_payload(payload) - - response = request.execute() - - if not wrap: - return response - - shipment = Shipment() - shipment.tracking_number = response["data"][0]["pno"] - shipment.courier = self.__class__.__name__ - - shipment.events = [] - - for event in response["data"][0]["lifecycle"]["entries"]: - event_obj = Event() - - if "depotData" in event and event["depotData"] is not None: - event_obj.location = ", ".join(event['depotData']) - else: - event_obj.location = None - - event_obj.timestamp = datetime.strptime(event["datetime"], "%Y%m%d%H%M%S") - event_obj.description = event['state']['text'] - event_obj.raw = json.dumps(event) - - shipment.events.append(event_obj) - - shipment.raw = json.dumps(response) - - return shipment - -class DPDRO: - """ DPD Romania API """ - - URL = "https://tracking.dpd.ro/?shipmentNumber=%s&language=%s" - - def tracking(self, tracking_number: str, **kwargs): - """ Search for a tracking number """ - - language = kwargs.get("language", "en") - wrap = kwargs.get("wrap", False) - - request = HTTPRequest(self.URL % (tracking_number, language)) - response = request.execute(False).decode() - - if not wrap: - return response - - response = bs4.BeautifulSoup(response, features="html.parser") - - shipment = Shipment() - - header_table = response.find("span", {"class", "spanTableHeader"}) - shipment.tracking_number = header_table.text.split()[0] - shipment.courier = self.__class__.__name__ - - if remote_data := header_table.find("a"): - remote_courier = remote_data.get("href") - if "dpd.de" in remote_courier: - remote_courier = "DPDDE" - elif "mydpd.at" in remote_courier: - remote_courier = "DPDAT" - - shipment.remote = [(remote_data.text, remote_courier)] - - shipment.events = [] - - data_table = response.find("table", {"class": "standard-table"}) - - for row in data_table.find_all("tr")[1:]: - event_obj = Event() - - date, time, event_obj.description, event_obj.location = row.find_all("td") - - event_obj.timestamp = datetime.strptime(f"{date.text} {time.text}", "%d.%m.%Y %H:%M:%S") - event_obj.raw = row.prettify() - - shipment.events.append(event_obj) - - shipment.raw = response.prettify() - - return shipment \ No newline at end of file diff --git a/src/dpdtrack/__init__.py b/src/track4px/__init__.py similarity index 100% rename from src/dpdtrack/__init__.py rename to src/track4px/__init__.py diff --git a/src/track4px/classes/__init__.py b/src/track4px/classes/__init__.py new file mode 100644 index 0000000..fe6eb1b --- /dev/null +++ b/src/track4px/classes/__init__.py @@ -0,0 +1,3 @@ +from .http import HTTPRequest +from .api import Track4PX +from .shipment import Shipment, Event \ No newline at end of file diff --git a/src/track4px/classes/api.py b/src/track4px/classes/api.py new file mode 100644 index 0000000..29acc10 --- /dev/null +++ b/src/track4px/classes/api.py @@ -0,0 +1,62 @@ +from datetime import datetime + +import json + +import bs4 + +from .http import HTTPRequest +from .shipment import Shipment, Event + +class Track4PX: + """ 4PX API + + This API is used to track packages from 4PX. + """ + + SEARCH = "https://track.4px.com/track/v2/front/listTrackV3" + + def tracking(self, tracking_number: str, **kwargs): + """ Search for a tracking number """ + + wrap = kwargs.get("wrap", False) + + request = HTTPRequest(self.SEARCH) + + payload = { + "queryCodes": [tracking_number], + "language": "en-us" + } + + request.add_json_payload(payload) + + response = request.execute() + + if not wrap: + return response + + """ + {"result":1,"message":"操作成功","data":[{"queryCode":"4PX3001291278502CN","serverCode":"06215215333330","shipperCode":"4PX3001291278502CN","channelTrackCode":null,"ctStartCode":"CN","ctEndCode":"AT","ctEndName":"AT","ctStartName":"China","status":1,"duration":3.0,"tracks":[{"tkCode":"FPX_I_RCUK","tkDesc":"Released from customs: customs cleared.","tkLocation":"","tkTimezone":null,"tkDate":"2024-09-05T18:18:00.000+0000","tkDateStr":"2024-09-06 02:18:00","tkCategoryCode":"I","tkCategoryName":"Import Clearance","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_M_ATA","tkDesc":"Arrival to the destination airport","tkLocation":"","tkTimezone":"UTC+02:00","tkDate":"2024-09-05T16:18:00.000+0000","tkDateStr":"2024-09-06 00:18:00","tkCategoryCode":"M","tkCategoryName":"Transiting by Air or Ship","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_M_DFOA","tkDesc":"Departure from the original airport","tkLocation":"","tkTimezone":"UTC+08:00","tkDate":"2024-09-05T05:09:00.000+0000","tkDateStr":"2024-09-05 13:09:00","tkCategoryCode":"M","tkCategoryName":"Transiting by Air or Ship","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_M_HA","tkDesc":"Hand over to airline.","tkLocation":"","tkTimezone":"UTC+08:00","tkDate":"2024-09-04T07:07:11.000+0000","tkDateStr":"2024-09-04 15:07:11","tkCategoryCode":"M","tkCategoryName":"Transiting by Air or Ship","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_C_ADFF","tkDesc":"Depart from facility to service provider.","tkLocation":"ShaTian,DongGuan","tkTimezone":"UTC+08:00","tkDate":"2024-09-03T00:19:17.000+0000","tkDateStr":"2024-09-03 08:19:17","tkCategoryCode":"C","tkCategoryName":"Operations in Warehouse","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_C_AAF","tkDesc":"Shipment arrived at facility and measured.","tkLocation":"ShaTian,DongGuan","tkTimezone":"UTC+08:00","tkDate":"2024-09-02T17:19:48.000+0000","tkDateStr":"2024-09-03 01:19:48","tkCategoryCode":"C","tkCategoryName":"Operations in Warehouse","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_C_SPLS","tkDesc":"4px picked up shipment.","tkLocation":"ShaTian,DongGuan","tkTimezone":"UTC+08:00","tkDate":"2024-09-02T17:19:47.000+0000","tkDateStr":"2024-09-03 01:19:47","tkCategoryCode":"C","tkCategoryName":"Operations in Warehouse","spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_O_IR","tkDesc":"Order data transmitted","tkLocation":"AUSTRIA","tkTimezone":"UTC+01:00","tkDate":"2024-09-02T11:30:00.000+0000","tkDateStr":"2024-09-02 19:30:00","tkCategoryCode":null,"tkCategoryName":null,"spTkSummary":null,"spTkZipCode":null},{"tkCode":"FPX_L_RPIF","tkDesc":"Parcel information received","tkLocation":"","tkTimezone":"UTC+08:00","tkDate":"2024-09-02T06:30:19.000+0000","tkDateStr":"2024-09-02 14:30:19","tkCategoryCode":"L","tkCategoryName":"Picking in Origin Country","spTkSummary":null,"spTkZipCode":null}],"hawbCodeSet":["4PX3001291278502CN","06215215333330"],"mutiPackage":false,"masterOrderNum":null,"returnStatusFlag":null,"channelContact":null}],"tag":"21"} + """ + + shipment = Shipment() + shipment.tracking_number = response["data"][0]["queryCode"] + shipment.courier = self.__class__.__name__ + + shipment.events = [] + + for event in response["data"][0]["tracks"]: + event_obj = Event() + + if "tkLocation" in event and event["tkLocation"]: + event_obj.location = event["tkLocation"] + + event_obj.category = event["tkCategoryName"] + event_obj.timestamp = datetime.strptime(event["tkDate"], "%Y-%m-%dT%H:%M:%S.%f%z") + event_obj.description = event['tkDesc'] + event_obj.raw = json.dumps(event) + + shipment.events.append(event_obj) + + shipment.raw = json.dumps(response) + + return shipment diff --git a/src/dpdtrack/classes/http.py b/src/track4px/classes/http.py similarity index 77% rename from src/dpdtrack/classes/http.py rename to src/track4px/classes/http.py index d97fb0e..283cca4 100644 --- a/src/dpdtrack/classes/http.py +++ b/src/track4px/classes/http.py @@ -4,7 +4,7 @@ import json class HTTPRequest(Request): - USER_AGENT = "Mozilla/5.0 (compatible; DPDTrack/dev; +https://kumig.it/kumitterer/dpdtrack)" + USER_AGENT = "Mozilla/5.0 (compatible; Track4PX/dev; +https://git.private.coffee/kumi/track4px)" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/dpdtrack/classes/shipment.py b/src/track4px/classes/shipment.py similarity index 76% rename from src/dpdtrack/classes/shipment.py rename to src/track4px/classes/shipment.py index 7c7bc1c..1a5820e 100644 --- a/src/dpdtrack/classes/shipment.py +++ b/src/track4px/classes/shipment.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple +from typing import List, Optional from datetime import datetime class Event: @@ -6,6 +6,7 @@ class Event: timestamp: datetime description: str + category: str location: str raw: str @@ -15,5 +16,4 @@ class Shipment: tracking_number: str courier: str events: Optional[List[Event]] = None - foreign: Optional[Tuple[str, str]] = None raw: str \ No newline at end of file