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:
parent
2405ea2824
commit
5a05062267
11 changed files with 80 additions and 144 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
.venv
|
.venv
|
||||||
|
venv
|
||||||
config.ini
|
config.ini
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||||
|
|
17
README.md
17
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.
|
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
|
||||||
|
|
|
@ -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"
|
|
@ -1,4 +0,0 @@
|
||||||
from .http import HTTPRequest
|
|
||||||
from .api import DPDAT as DPD
|
|
||||||
from .api import DPDAT, DPDRO
|
|
||||||
from .shipment import Shipment, Event
|
|
|
@ -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
|
|
3
src/track4px/classes/__init__.py
Normal file
3
src/track4px/classes/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .http import HTTPRequest
|
||||||
|
from .api import Track4PX
|
||||||
|
from .shipment import Shipment, Event
|
62
src/track4px/classes/api.py
Normal file
62
src/track4px/classes/api.py
Normal 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
|
|
@ -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)
|
|
@ -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
|
Loading…
Reference in a new issue