diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b492ac1..8d386d9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,7 @@ before_script: test: stage: test script: - - echo "[KeyDelivery]" > config.ini + - echo "[FedEx]" > config.ini - echo "key = ${API_KEY}" >> config.ini - echo "secret = ${API_SECRET}" >> config.ini - python -m unittest test.py diff --git a/LICENSE b/LICENSE index cad43dc..6eba08b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023 Kumi Mitterer +Copyright (c) 2023 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 e5c44cd..063ab65 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,25 @@ -# KeyDelivery API Python Client +# FedEx Tracking API Python Client -This is a Python client for the KeyDelivery API. It is a wrapper around the [KeyDelivery](https://kd100.com/) API, which allows you to track your shipments. +This is a Python client for the [FedEx Tracking API](https://developer.fedex.com/api/en-at/catalog/track/v1/docs.html). -It is not fully featured yet, but it is a good starting point. +It is not fully featured yet, but it is a good starting point. It requires you to have a FedEx developer account and an API key. ## Installation ```bash -pip install git+https://kumig.it/kumitterer/pykeydelivery +pip install git+https://kumig.it/kumitterer/fedextrack.git ``` ## Usage ```python -from keydelivery import KeyDelivery +from fedextrack import FedEx -api = KeyDelivery("YOUR_API_KEY", "YOUR_API_SECRET") - -# Find carrier by shipment number - -carrier_options = api.detect_carrier("YOUR_SHIPMENT_NUMBER") +api = FedEx("YOUR_API_KEY", "YOUR_API_SECRET") # Realtime tracking -tracking = api.realtime("CARRIER_CODE", "YOUR_SHIPMENT_NUMBER") +tracking = api.tracking("YOUR_SHIPMENT_NUMBER") ``` ## License diff --git a/config.dist.ini b/config.dist.ini index bac223f..4e4d377 100644 --- a/config.dist.ini +++ b/config.dist.ini @@ -1,3 +1,3 @@ -[KeyDelivery] +[FedEx] key = api_key secret = api_secret diff --git a/pyproject.toml b/pyproject.toml index 54ad196..aa95eb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,12 +3,12 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "pykeydelivery" +name = "fedextrack" version = "0.9.0" authors = [ - { name="Kumi Mitterer", email="pykeydelivery@kumi.email" }, + { name="Kumi Mitterer", email="fedextrack@kumi.email" }, ] -description = "Simple Python wrapper to fetch data from KeyDelivery (kd100.com)" +description = "Simple Python wrapper to fetch data from FedEx Tracking API" readme = "README.md" license = { file="LICENSE" } requires-python = ">=3.10" @@ -19,5 +19,5 @@ classifiers = [ ] [project.urls] -"Homepage" = "https://kumig.it/kumitterer/pykeydelivery" -"Bug Tracker" = "https://kumig.it/kumitterer/pykeydelivery/issues" \ No newline at end of file +"Homepage" = "https://kumig.it/kumitterer/fedextrack" +"Bug Tracker" = "https://kumig.it/kumitterer/fedextrack/issues" \ No newline at end of file diff --git a/src/pykeydelivery/__init__.py b/src/fedextrack/__init__.py similarity index 100% rename from src/pykeydelivery/__init__.py rename to src/fedextrack/__init__.py diff --git a/src/fedextrack/classes/__init__.py b/src/fedextrack/classes/__init__.py new file mode 100644 index 0000000..f2c815b --- /dev/null +++ b/src/fedextrack/classes/__init__.py @@ -0,0 +1,2 @@ +from .http import HTTPRequest +from .fedex import FedEx \ No newline at end of file diff --git a/src/fedextrack/classes/fedex.py b/src/fedextrack/classes/fedex.py new file mode 100644 index 0000000..d6b6560 --- /dev/null +++ b/src/fedextrack/classes/fedex.py @@ -0,0 +1,74 @@ +from hashlib import md5 +from configparser import ConfigParser +from urllib.parse import urlencode + +import json + +from .http import HTTPRequest + + +class FedEx: + BASE_URL = "https://apis.fedex.com/" + TRACK_BY_NUMBER = "track/v1/trackingnumbers" + OAUTH_TOKEN = "oauth/token" + + def __init__(self, key: str, secret: str, base_url: str = BASE_URL): + self.key = key + self.secret = secret + self.base_url = base_url + + def get_token(self): + message = { + "grant_type": "client_credentials", + "client_id": self.key, + "client_secret": self.secret, + } + + request = self.get_request(self.OAUTH_TOKEN, {}, False) + + request.add_header("Content-Type", "application/x-www-form-urlencoded") + request.data = urlencode(message).encode("utf-8") + + response = request.execute() + return response["access_token"] + + @classmethod + def from_config(cls, config: ConfigParser | str, section: str = "FedEx") -> "FedEx": + if isinstance(config, str): + temp_config = ConfigParser() + temp_config.read(config) + config = temp_config + + key = config.get(section, "key") + secret = config.get(section, "secret") + base_url = config.get(section, "base_url", fallback=cls.BASE_URL) + + return cls(key, secret, base_url) + + def get_request(self, endpoint: str, message: dict = {}, add_token: bool = True) -> HTTPRequest: + url = self.base_url + endpoint + + request = HTTPRequest(url) + + if message: + request.add_json_payload(message) + + if add_token: + request.add_header("Authorization", "Bearer " + self.get_token()) + + return request + + def track_by_tracking_number(self, tracking_number: str, include_detailed_scans: bool = True) -> bytes: + message = { + "include_detailed_scans": include_detailed_scans, + "trackingInfo": [ + { + "trackingNumberInfo": { + "trackingNumber": tracking_number, + } + } + ] + } + + request = self.get_request(self.TRACK_BY_NUMBER, message) + return request.execute() diff --git a/src/fedextrack/classes/http.py b/src/fedextrack/classes/http.py new file mode 100644 index 0000000..451fddc --- /dev/null +++ b/src/fedextrack/classes/http.py @@ -0,0 +1,37 @@ +from urllib.request import Request, urlopen +from urllib.error import HTTPError +from io import BytesIO + +import gzip +import json + + +class HTTPRequest(Request): + USER_AGENT = "Mozilla/5.0 (compatible; FedExTrack/dev; +https://kumig.it/kumitterer/fedextrack)" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_header("User-Agent", self.USER_AGENT) + + @staticmethod + def read(response): + if response.info().get('Content-Encoding') == 'gzip': + buf = BytesIO(response.read()) + f = gzip.GzipFile(fileobj=buf) + return f.read() + + return response.read() + + def execute(self, load_json: bool = True, *args, **kwargs): + try: + response = self.read(urlopen(self, *args, **kwargs)) + except HTTPError as e: + print(self.read(e)) + raise + if load_json: + response = json.loads(response) + return response + + def add_json_payload(self, payload: dict): + self.add_header("Content-Type", "application/json") + self.data = json.dumps(payload).encode("utf-8") diff --git a/src/pykeydelivery/classes/__init__.py b/src/pykeydelivery/classes/__init__.py deleted file mode 100644 index ad5576c..0000000 --- a/src/pykeydelivery/classes/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .http import HTTPRequest -from .keydelivery import KeyDelivery \ No newline at end of file diff --git a/src/pykeydelivery/classes/http.py b/src/pykeydelivery/classes/http.py deleted file mode 100644 index d1613df..0000000 --- a/src/pykeydelivery/classes/http.py +++ /dev/null @@ -1,21 +0,0 @@ -from urllib.request import Request, urlopen - -import json - - -class HTTPRequest(Request): - USER_AGENT = "Mozilla/5.0 (compatible; PyKeyDelivery/dev; +https://kumig.it/kumitterer/pykeydelivery)" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.add_header("User-Agent", self.USER_AGENT) - - def execute(self, load_json: bool = True, *args, **kwargs): - response = urlopen(self, *args, **kwargs).read() - if load_json: - response = json.loads(response) - return response - - def add_json_payload(self, payload: dict): - self.add_header("Content-Type", "application/json") - self.data = json.dumps(payload).encode("utf-8") diff --git a/src/pykeydelivery/classes/keydelivery.py b/src/pykeydelivery/classes/keydelivery.py deleted file mode 100644 index 675ac52..0000000 --- a/src/pykeydelivery/classes/keydelivery.py +++ /dev/null @@ -1,61 +0,0 @@ -from hashlib import md5 -from configparser import ConfigParser - -import json - -from .http import HTTPRequest - - -class KeyDelivery: - BASE_URL = "https://www.kd100.com/api/v1/" - - def __init__(self, key: str, secret: str, base_url: str = BASE_URL): - self.key = key - self.secret = secret - self.base_url = base_url - - @classmethod - def from_config(cls, config: ConfigParser | str, section: str = "KeyDelivery") -> "KeyDelivery": - if isinstance(config, str): - temp_config = ConfigParser() - temp_config.read(config) - config = temp_config - - key = config.get(section, "key") - secret = config.get(section, "secret") - base_url = config.get(section, "base_url", fallback=cls.BASE_URL) - - return cls(key, secret, base_url) - - def get_signature(self, message: dict) -> str: - content = json.dumps(message) - data = (content + self.key + self.secret).encode("utf-8") - return md5(data).hexdigest().upper() - - def get_request(self, endpoint: str, message: dict) -> HTTPRequest: - url = self.base_url + endpoint - signature = self.get_signature(message) - - request = HTTPRequest(url) - request.add_json_payload(message) - request.add_header("API-Key", self.key) - request.add_header("signature", signature) - - return request - - def realtime(self, carrier: str, tracking_number: str) -> bytes: - message = { - "carrier_id": carrier, - "tracking_number": tracking_number, - } - - request = self.get_request("tracking/realtime", message) - return request.execute() - - def detect_carrier(self, tracking_number: str) -> bytes: - message = { - "tracking_number": tracking_number, - } - - request = self.get_request("carriers/detect", message) - return request.execute() diff --git a/test.py b/test.py index 280602e..745b8db 100644 --- a/test.py +++ b/test.py @@ -3,7 +3,7 @@ from configparser import ConfigParser import json -from pykeydelivery import * +from fedextrack import * class TestHTTPRequest(TestCase): def test_http_request(self): @@ -19,20 +19,17 @@ class TestHTTPRequest(TestCase): self.assertEqual(response["headers"]["Content-Type"], "application/json") self.assertEqual(response["json"]["foo"], "bar") -class TestKeyDelivery(TestCase): +class TestFedEx(TestCase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = ConfigParser() self.config.read("config.ini") - self.keydelivery = KeyDelivery.from_config(self.config) + self.fedex = FedEx.from_config(self.config) - def test_detect_carrier(self): - response = self.keydelivery.detect_carrier("483432314669") - self.assertEqual(response["code"], 200) - - def test_realtime(self): - response = self.keydelivery.realtime("gls", "483432314669") - self.assertEqual(response["code"], 200) + def test_tracking(self): + tracking_number = "702395541585" + response = self.fedex.tracking(tracking_number) + self.assertEqual(response["output"]["completeTrackResults"][0]["trackingNumber"], tracking_number) if __name__ == "__main__": main() \ No newline at end of file