Compare commits
No commits in common. "66e3d5e8a70fb4e585641f38d4865433626f9537" and "9ed6925e1eec72f82b80cae53cc54c41fbb201f6" have entirely different histories.
66e3d5e8a7
...
9ed6925e1e
|
@ -1,7 +1,3 @@
|
||||||
# WARNING!
|
|
||||||
# THIS IS AN AUTOGENERATED FILE!
|
|
||||||
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
|
|
||||||
|
|
||||||
name: Build Python Container
|
name: Build Python Container
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
# WARNING!
|
|
||||||
# THIS IS AN AUTOGENERATED FILE!
|
|
||||||
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
|
|
||||||
|
|
||||||
name: Package Python
|
name: Package Python
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
# WARNING!
|
|
||||||
# THIS IS AN AUTOGENERATED FILE!
|
|
||||||
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
|
|
||||||
|
|
||||||
name: Run Python tests (through Pytest)
|
name: Run Python tests (through Pytest)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
# WARNING!
|
|
||||||
# THIS IS AN AUTOGENERATED FILE!
|
|
||||||
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
|
|
||||||
|
|
||||||
name: Verify Python project can be installed, loaded and have version checked
|
name: Verify Python project can be installed, loaded and have version checked
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
<!-- WARNING! -->
|
|
||||||
<!-- THIS IS AN AUTOGENERATED FILE! -->
|
|
||||||
<!-- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN! -->
|
|
||||||
|
|
||||||
# Conventions
|
# Conventions
|
||||||
|
|
||||||
When contributing code to this project, you MUST follow the requirements
|
When contributing code to this project, you MUST follow the requirements
|
||||||
|
|
11
README.md
11
README.md
|
@ -1,6 +1,8 @@
|
||||||
<!-- WARNING! -->
|
<!--- WARNING --->
|
||||||
<!-- THIS IS AN AUTOGENERATED FILE! -->
|
<!--- THIS IS AN AUTO-GENERATED FILE --->
|
||||||
<!-- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN! -->
|
<!--- MANUAL CHANGES CAN AND WILL BE OVERWRITTEN --->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Package-tracking
|
# Package-tracking
|
||||||
|
|
||||||
|
@ -34,10 +36,12 @@ Full list of requirements:
|
||||||
- [requests](https://pypi.org/project/requests/)
|
- [requests](https://pypi.org/project/requests/)
|
||||||
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
|
- [secret_loader](https://gitfub.space/Jmaa/secret_loader)
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Feel free to submit pull requests. Please follow the [Code Conventions](CONVENTIONS.md) when doing so.
|
Feel free to submit pull requests. Please follow the [Code Conventions](CONVENTIONS.md) when doing so.
|
||||||
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
Testing requires the [pytest](https://docs.pytest.org/en/stable/) library.
|
Testing requires the [pytest](https://docs.pytest.org/en/stable/) library.
|
||||||
|
@ -54,6 +58,7 @@ Test coverage can be run using the [`pytest-cov`](https://pypi.org/project/pytes
|
||||||
pytest --cov=package_tracking test
|
pytest --cov=package_tracking test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
from . import http, parcelsapp, secrets
|
||||||
from . import http, secrets
|
|
||||||
from clients.parcelsapp import ParcelsAppClient
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -11,9 +9,8 @@ def main():
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
logger.setLevel('INFO')
|
logger.setLevel('INFO')
|
||||||
|
|
||||||
parcelsapp_client: ParcelsAppClient = ParcelsAppClient(
|
parcelsapp_client: parcelsapp.ParcelsAppClient = parcelsapp.ParcelsAppClient(
|
||||||
requests.Session(),
|
secrets.PARCELS_API_KEY,
|
||||||
parcels_api_key=secrets.PARCELS_API_KEY,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
http.initialize_server(parcelsapp_client)
|
http.initialize_server(parcelsapp_client)
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import bottle
|
import bottle
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from . import database
|
from . import database, parcelsapp
|
||||||
from clients.parcelsapp import ParcelsAppClient
|
|
||||||
from clients.common import ApiError
|
|
||||||
|
|
||||||
PARCELSAPP_CLIENT: ParcelsAppClient | None = None
|
PARCELSAPP_CLIENT: parcelsapp.ParcelsAppClient | None = None
|
||||||
|
|
||||||
TEMPLATE = """
|
TEMPLATE = """
|
||||||
<doctype HTML>
|
<doctype HTML>
|
||||||
|
@ -151,7 +149,7 @@ def get_packages_from_parcels():
|
||||||
try:
|
try:
|
||||||
tracking_results = list(PARCELSAPP_CLIENT.get_tracking_status(tracking_numbers))
|
tracking_results = list(PARCELSAPP_CLIENT.get_tracking_status(tracking_numbers))
|
||||||
error_message = None
|
error_message = None
|
||||||
except ApiError as ex:
|
except parcelsapp.ParcelsApiError as ex:
|
||||||
tracking_results = []
|
tracking_results = []
|
||||||
error_message = str(ex)
|
error_message = str(ex)
|
||||||
|
|
||||||
|
@ -162,7 +160,7 @@ def get_packages_from_parcels():
|
||||||
return [(e, tracking_results_by_id.get(e.number)) for e in tracking_entries]
|
return [(e, tracking_results_by_id.get(e.number)) for e in tracking_entries]
|
||||||
|
|
||||||
|
|
||||||
def render_tracking(error_message: str | None = None, with_form: bool = False):
|
def render_tracking(tracking_results_with_name, error_message: str | None, with_form: bool):
|
||||||
tracking_results_with_name = get_packages_from_parcels()
|
tracking_results_with_name = get_packages_from_parcels()
|
||||||
tracking_results_with_name.sort(key=lambda x: x[1].latest_state().date if x[1] else TODAY, reverse=True)
|
tracking_results_with_name.sort(key=lambda x: x[1].latest_state().date if x[1] else TODAY, reverse=True)
|
||||||
return bottle.template(TEMPLATE, error_message=error_message, tracking_results=tracking_results_with_name, with_form=with_form)
|
return bottle.template(TEMPLATE, error_message=error_message, tracking_results=tracking_results_with_name, with_form=with_form)
|
||||||
|
@ -187,7 +185,7 @@ def add_tracking_number():
|
||||||
return bottle.redirect('/')
|
return bottle.redirect('/')
|
||||||
|
|
||||||
|
|
||||||
def initialize_server(parcelsapp_client: ParcelsAppClient):
|
def initialize_server(parcelsapp_client: parcelsapp.ParcelsAppClient):
|
||||||
global PARCELSAPP_CLIENT
|
global PARCELSAPP_CLIENT
|
||||||
PARCELSAPP_CLIENT = parcelsapp_client
|
PARCELSAPP_CLIENT = parcelsapp_client
|
||||||
bottle.run(host='0.0.0.0', port=8080, debug=False)
|
bottle.run(host='0.0.0.0', port=8080, debug=False)
|
||||||
|
|
101
package_tracking/parcelsapp.py
Normal file
101
package_tracking/parcelsapp.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import dataclasses
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
URL_TRACKING = 'https://parcelsapp.com/api/v3/shipments/tracking'
|
||||||
|
|
||||||
|
target_country = 'Denmark'
|
||||||
|
|
||||||
|
TRACKING_STATUS_CHECKING_INTERVAL = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class ParcelState:
|
||||||
|
date: datetime.datetime
|
||||||
|
status: str
|
||||||
|
carrier: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class ParcelInfo:
|
||||||
|
tracking_number: str
|
||||||
|
tracking_url: str
|
||||||
|
status: str
|
||||||
|
destination: str | None
|
||||||
|
origin: str | None
|
||||||
|
states: list[ParcelState]
|
||||||
|
# TODO: More fields
|
||||||
|
|
||||||
|
def latest_state(self) -> ParcelState:
|
||||||
|
return max(self.states, key=lambda state: state.date)
|
||||||
|
|
||||||
|
class ParcelsApiError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ParcelsAppClient:
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
assert api_key is not None, 'Missing API Key'
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
def _request_json(self, method: str, url: str, **kwargs) -> dict:
|
||||||
|
request_json_data = {'apiKey': self.api_key, 'language': 'en', **kwargs}
|
||||||
|
response = requests.request(
|
||||||
|
method=method, url=url, json=request_json_data,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
json_data = response.json()
|
||||||
|
if 'error' in json_data:
|
||||||
|
msg = 'Error from endpoint: {}'.format(json_data['error'])
|
||||||
|
raise ParcelsApiError(msg)
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
def check_tracking_status(self, uuid: str) -> dict:
|
||||||
|
"""Function to check tracking status with UUID."""
|
||||||
|
json_data = self._request_json('GET', URL_TRACKING, uuid=uuid)
|
||||||
|
if json_data['done']:
|
||||||
|
logger.info('Tracking complete')
|
||||||
|
return json_data
|
||||||
|
else:
|
||||||
|
logger.info('Tracking in progress...')
|
||||||
|
time.sleep(TRACKING_STATUS_CHECKING_INTERVAL)
|
||||||
|
return self.check_tracking_status(uuid)
|
||||||
|
|
||||||
|
def _get_tracking_status_to_json(self, tracking_ids: list[str]) -> dict:
|
||||||
|
shipments = [
|
||||||
|
{'trackingId': tracking_id, 'country': target_country}
|
||||||
|
for tracking_id in tracking_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
# Initiate tracking request
|
||||||
|
json_data = self._request_json('POST', URL_TRACKING, shipments=shipments)
|
||||||
|
if json_data.get('done'):
|
||||||
|
return json_data
|
||||||
|
|
||||||
|
return self.check_tracking_status(json_data['uuid'])
|
||||||
|
|
||||||
|
def get_tracking_status(self, tracking_ids: list[str]) -> Iterator[ParcelInfo]:
|
||||||
|
if len(tracking_ids) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for parcel_json in self._get_tracking_status_to_json(tracking_ids)['shipments']:
|
||||||
|
yield ParcelInfo(
|
||||||
|
tracking_number=parcel_json['trackingId'],
|
||||||
|
tracking_url=parcel_json['externalTracking'][0]['url'],
|
||||||
|
status=parcel_json['status'],
|
||||||
|
destination=parcel_json.get('destination'),
|
||||||
|
origin=parcel_json.get('origin'),
|
||||||
|
states=[
|
||||||
|
ParcelState(
|
||||||
|
status=s['status'],
|
||||||
|
date=datetime.datetime.fromisoformat(s['date']),
|
||||||
|
carrier=s['carrier'],
|
||||||
|
)
|
||||||
|
for s in parcel_json['states']
|
||||||
|
],
|
||||||
|
)
|
12
setup.py
12
setup.py
|
@ -1,6 +1,8 @@
|
||||||
# WARNING!
|
# WARNING
|
||||||
# THIS IS AN AUTOGENERATED FILE!
|
#
|
||||||
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN!
|
# THIS IS AN AUTOGENERATED FILE.
|
||||||
|
#
|
||||||
|
# MANUAL CHANGES CAN AND WILL BE OVERWRITTEN.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ Once run, it exposes the following endpoints:
|
||||||
PACKAGE_DESCRIPTION_SHORT = """
|
PACKAGE_DESCRIPTION_SHORT = """
|
||||||
Small alternative frontend for tracking packages and parcels.""".strip()
|
Small alternative frontend for tracking packages and parcels.""".strip()
|
||||||
|
|
||||||
|
|
||||||
def parse_version_file(text: str) -> str:
|
def parse_version_file(text: str) -> str:
|
||||||
match = re.match(r'^__version__\s*=\s*(["\'])([\d\.]+)\1$', text)
|
match = re.match(r'^__version__\s*=\s*(["\'])([\d\.]+)\1$', text)
|
||||||
if match is None:
|
if match is None:
|
||||||
|
@ -35,9 +38,11 @@ def parse_version_file(text: str) -> str:
|
||||||
raise Exception(msg)
|
raise Exception(msg)
|
||||||
return match.group(2)
|
return match.group(2)
|
||||||
|
|
||||||
|
|
||||||
with open(PACKAGE_NAME + '/_version.py') as f:
|
with open(PACKAGE_NAME + '/_version.py') as f:
|
||||||
version = parse_version_file(f.read())
|
version = parse_version_file(f.read())
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS_MAIN = [
|
REQUIREMENTS_MAIN = [
|
||||||
'bottle',
|
'bottle',
|
||||||
'requests',
|
'requests',
|
||||||
|
@ -45,6 +50,7 @@ REQUIREMENTS_MAIN = [
|
||||||
]
|
]
|
||||||
REQUIREMENTS_TEST = []
|
REQUIREMENTS_TEST = []
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=PACKAGE_NAME,
|
name=PACKAGE_NAME,
|
||||||
version=version,
|
version=version,
|
||||||
|
|
|
@ -3,3 +3,4 @@ def test_import_modules():
|
||||||
import package_tracking # noqa
|
import package_tracking # noqa
|
||||||
import package_tracking.database # noqa
|
import package_tracking.database # noqa
|
||||||
import package_tracking.http # noqa
|
import package_tracking.http # noqa
|
||||||
|
import package_tracking.parcelsapp # noqa
|
||||||
|
|
Loading…
Reference in New Issue
Block a user