From 5a0506226786639f9b6d6cea041852a3bd18b8e0 Mon Sep 17 00:00:00 2001 From: Kumi Date: Fri, 6 Sep 2024 08:24:27 +0200 Subject: [PATCH] feat: fork from DPD to 4PX tracking Renamed project from DPDTrack to Track4PX and updated all relevant files, including README, LICENSE, and pyproject.toml. Removed DPD specific code and implemented 4PX tracking API. Rationale: - Shifts the focus of the package from DPD to 4PX data. - Streamlines tracking functionality for 4PX users. Consequences: - Users need to update their imports and installation process. - Changes in package semantics and functionality. --- .gitignore | 1 + LICENSE | 2 +- README.md | 17 +-- pyproject.toml | 11 +- src/dpdtrack/classes/__init__.py | 4 - src/dpdtrack/classes/api.py | 118 ------------------ src/{dpdtrack => track4px}/__init__.py | 0 src/track4px/classes/__init__.py | 3 + src/track4px/classes/api.py | 62 +++++++++ src/{dpdtrack => track4px}/classes/http.py | 2 +- .../classes/shipment.py | 4 +- 11 files changed, 80 insertions(+), 144 deletions(-) delete mode 100644 src/dpdtrack/classes/__init__.py delete mode 100644 src/dpdtrack/classes/api.py rename src/{dpdtrack => track4px}/__init__.py (100%) create mode 100644 src/track4px/classes/__init__.py create mode 100644 src/track4px/classes/api.py rename src/{dpdtrack => track4px}/classes/http.py (77%) rename src/{dpdtrack => track4px}/classes/shipment.py (76%) 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