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.
This commit is contained in:
Kumi 2024-09-06 08:24:27 +02:00
parent 2405ea2824
commit 5a05062267
Signed by: kumi
GPG key ID: ECBCC9082395383F
11 changed files with 80 additions and 144 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.venv .venv
venv
config.ini config.ini
__pycache__ __pycache__
*.pyc *.pyc

View file

@ -1,4 +1,4 @@
Copyright (c) 2023 Kumi Mitterer <dpdtrack@kumi.email> Copyright (c) 2024 Kumi Mitterer <track4px@kumi.email>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -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. This is a Python client for the 4PX parcel 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.
## Installation ## Installation
```bash ```bash
pip install dpdtrack pip install track4px
``` ```
## Usage ## Usage
```python ```python
from dpdtrack import DPDAT, DPDRO from track4px import Track4PX
api = DPDAT() # For tracking of DPD Austria packages api = Track4PX()
# api = DPDRO() for tracking of DPD Romania packages
# Realtime tracking # Realtime tracking
tracking = api.tracking("YOUR_SHIPMENT_NUMBER") 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 ## License

View file

@ -3,12 +3,12 @@ requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "dpdtrack" name = "track4px"
version = "0.10.0" version = "0.1.0"
authors = [ authors = [
{ name="Kumi Mitterer", email="dpdtrack@kumi.email" }, { 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" readme = "README.md"
license = { file="LICENSE" } license = { file="LICENSE" }
requires-python = ">=3.10" requires-python = ">=3.10"
@ -18,9 +18,8 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
"beautifulsoup4"
] ]
[project.urls] [project.urls]
"Homepage" = "https://kumig.it/kumitterer/dpdtrack" "Homepage" = "https://git.private.coffee/kumi/track4px"
"Bug Tracker" = "https://kumig.it/kumitterer/dpdtrack/issues" "Bug Tracker" = "https://git.private.coffee/kumi/track4px/issues"

View file

@ -1,4 +0,0 @@
from .http import HTTPRequest
from .api import DPDAT as DPD
from .api import DPDAT, DPDRO
from .shipment import Shipment, Event

View file

@ -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

View file

@ -0,0 +1,3 @@
from .http import HTTPRequest
from .api import Track4PX
from .shipment import Shipment, Event

View file

@ -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

View file

@ -4,7 +4,7 @@ import json
class HTTPRequest(Request): 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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View file

@ -1,4 +1,4 @@
from typing import List, Optional, Tuple from typing import List, Optional
from datetime import datetime from datetime import datetime
class Event: class Event:
@ -6,6 +6,7 @@ class Event:
timestamp: datetime timestamp: datetime
description: str description: str
category: str
location: str location: str
raw: str raw: str
@ -15,5 +16,4 @@ class Shipment:
tracking_number: str tracking_number: str
courier: str courier: str
events: Optional[List[Event]] = None events: Optional[List[Event]] = None
foreign: Optional[Tuple[str, str]] = None
raw: str raw: str