Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[run]
source = pyseventeentrack

omit =
tests/*
examples/*
pyseventeentrack/track.py
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ async def main() -> None:
# Add new packages by tracking number
await client.profile.add_package('<TRACKING NUMBER>', '<FRIENDLY NAME>')

# Archive / activate / delete packages
await client.profile.archive_package('<TRACKING NUMBER>')
await client.profile.activate_package('<TRACKING NUMBER>')
await client.profile.delete_package('<TRACKING NUMBER>')

# Set tag type or carriers (internal tracking ID required)
await client.profile.set_tag_type('<TRACK INFO ID>', '0')
await client.profile.set_carrier('<TRACK INFO ID>', '<FIRST CARRIER>', '<SECOND CARRIER>')

# Fetch and update order info (internal tracking ID required)
order_info = await client.profile.order_info_by_id('<TRACK INFO ID>')
# >>> {'opn': 'Acme', 'ptoid': '123', 'pt': '01', 'otime': '2024-12-01'}
await client.profile.save_order_info('<TRACK INFO ID>', opn='Acme', ptoid='123', pt='01', otime='2024-12-01')


asyncio.run(main())
```
Expand Down Expand Up @@ -116,4 +130,3 @@ Each `Package` object has the following info:
9. Update `README.md` with any new documentation.
10. Add yourself to `AUTHORS.md`.
11. Submit a pull request!

189 changes: 143 additions & 46 deletions pyseventeentrack/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ def __init__(self, request: Callable[..., Coroutine]) -> None:
self._request: Callable[..., Coroutine] = request
self.account_id: Optional[str] = None

async def _buyer_api_call(
self, method: str, param: dict, log_message: str
) -> dict:
"""Call the buyer API and validate the response."""
response: dict = await self._request(
"post",
API_URL_BUYER,
json={
"version": "1.0",
"method": method,
"param": param,
},
)

_LOGGER.debug(log_message, response)

code = response.get("Code")
if code != 0:
raise RequestError(f"Non-zero status code in response: {code}")

return response

async def _get_package_by_tracking_number(
self, tracking_number: str
) -> Package:
"""Get a package by tracking number."""
packages = await self.packages()

try:
return next(p for p in packages if p.tracking_number == tracking_number)
except StopIteration as err:
raise InvalidTrackingNumberError(
f"Package not found by tracking number: {tracking_number}"
) from err

async def login(self, email: str, password: str) -> bool:
"""Login to the profile."""
login_resp: dict = await self._request(
Expand Down Expand Up @@ -121,22 +156,12 @@ async def add_package(
self, tracking_number: str, friendly_name: Optional[str] = None
):
"""Add a package by tracking number to the tracking list."""
add_resp: dict = await self._request(
"post",
API_URL_BUYER,
json={
"version": "1.0",
"method": "AddTrackNo",
"param": {"TrackNos": [tracking_number]},
},
add_resp = await self._buyer_api_call(
"AddTrackNo",
{"TrackNos": [tracking_number]},
"Add package response: %s",
)

_LOGGER.debug("Add package response: %s", add_resp)

code = add_resp.get("Code")
if code != 0:
raise RequestError(f"Non-zero status code in response: {code}")

if not friendly_name:
return

Expand All @@ -159,49 +184,121 @@ async def set_friendly_name(self, internal_id: str, friendly_name: str):

internal_id is not the tracking number, it's the ID of an existing package.
"""
remark_resp: dict = await self._request(
"post",
API_URL_BUYER,
json={
"version": "1.0",
"method": "SetTrackRemark",
"param": {"TrackInfoId": internal_id, "Remark": friendly_name},
},
await self._buyer_api_call(
"SetTrackRemark",
{"TrackInfoId": internal_id, "Remark": friendly_name},
"Set friendly name response: %s",
)

_LOGGER.debug("Set friendly name response: %s", remark_resp)

code = remark_resp.get("Code")
if code != 0:
raise RequestError(f"Non-zero status code in response: {code}")

async def archive_package(self, tracking_number: str):
"""Archive a package by tracking number."""
packages = await self.packages()
package = await self._get_package_by_tracking_number(tracking_number)

try:
package = next(p for p in packages if p.tracking_number == tracking_number)
except StopIteration as err:
raise InvalidTrackingNumberError(
f"Package not found by tracking number: {tracking_number}"
) from err
internal_id = package.id

_LOGGER.debug("Found internal ID of package: %s", internal_id)

await self._buyer_api_call(
"SetTrackArchived",
{"TrackInfoIds": [internal_id]},
"Archive package response: %s",
)

async def activate_package(self, tracking_number: str):
"""Activate (unarchive) a package by tracking number."""
package = await self._get_package_by_tracking_number(tracking_number)

internal_id = package.id

_LOGGER.debug("Found internal ID of package: %s", internal_id)

archive_resp: dict = await self._request(
"post",
API_URL_BUYER,
json={
"version": "1.0",
"method": "SetTrackArchived",
"param": {"TrackInfoIds": [internal_id]},
await self._buyer_api_call(
"SetTrackActivate",
{"TrackInfoIds": [internal_id]},
"Activate package response: %s",
)

async def delete_package(self, tracking_number: str):
"""Delete a package by tracking number."""
package = await self._get_package_by_tracking_number(tracking_number)

internal_id = package.id

_LOGGER.debug("Found internal ID of package: %s", internal_id)

await self._buyer_api_call(
"DelTrackNo",
{"TrackInfoIds": [internal_id]},
"Delete package response: %s",
)

async def set_tag_type(self, internal_id: str, tag: str):
"""Set the tag type for an existing package."""
await self._buyer_api_call(
"SetTrackTagType",
{"tid": internal_id, "tag": tag},
"Set tag type response: %s",
)

async def set_carrier(
self, internal_id: str, first_carrier: str, second_carrier: str = "0"
):
"""Set the carrier(s) for an existing package."""
await self._buyer_api_call(
"SetTrackCarrier",
{
"TrackInfoId": internal_id,
"FirstCarrier": first_carrier,
"SecondCarrier": second_carrier,
},
"Set carrier response: %s",
)

_LOGGER.debug("Archive package response: %s", archive_resp)
async def track_info_by_id(self, *track_info_ids: str, isa: bool = False) -> list:
"""Get tracking info by internal tracking IDs."""
if not track_info_ids:
return []

code = archive_resp.get("Code")
if code != 0:
raise RequestError(f"Non-zero status code in response: {code}")
track_resp = await self._buyer_api_call(
"GetTrackInfoById",
{"isa": isa, "tids": list(track_info_ids)},
"Track info by ID response: %s",
)

return track_resp.get("Json", [])

async def order_info_by_id(self, internal_id: str) -> dict:
"""Get order info by internal tracking ID."""
order_resp = await self._buyer_api_call(
"GetOrderInfoById",
{"tid": internal_id},
"Order info response: %s",
)

return order_resp.get("Json", {}).get("order", {})

async def save_order_info(
self,
internal_id: str,
*,
opn: Optional[str] = None,
ptoid: Optional[str] = None,
pt: Optional[str] = None,
otime: Optional[str] = None,
):
"""Save order info for an existing package."""
param: dict = {"tid": internal_id}
optional_params = {"opn": opn, "ptoid": ptoid, "pt": pt, "otime": otime}
param.update(
{
key: value
for key, value in optional_params.items()
if value is not None
}
)

await self._buyer_api_call(
"SaveOrderInfo",
param,
"Save order info response: %s",
)
1 change: 1 addition & 0 deletions tests/fixtures/activate_package_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/delete_package_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/order_info_by_id_failure_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/order_info_by_id_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {"order": {"opn": "Acme", "ptoid": "123", "pt": "01", "otime": "2024-12-01"}}}
1 change: 1 addition & 0 deletions tests/fixtures/save_order_info_failure_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/save_order_info_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/set_carrier_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/set_carrier_response_failure_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/set_tag_type_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/set_tag_type_response_failure_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": {}}
1 change: 1 addition & 0 deletions tests/fixtures/track_info_by_id_failure_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 1, "Json": []}
1 change: 1 addition & 0 deletions tests/fixtures/track_info_by_id_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"Code": 0, "Json": [{"FTrackInfoId": "1234567890987654321", "FPackageState": "10", "FTrackStateType": 2}]}
22 changes: 22 additions & 0 deletions tests/test_encrypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Define tests for encryption utilities."""

import pytest

from pyseventeentrack import encrypt


def test_rsa_encrypt_invalid_key(monkeypatch):
"""Test rsa_encrypt raises when key is not RSA."""

class DummyKey:
"""Non-RSA key placeholder."""

def fake_load_pem_public_key(*_args, **_kwargs):
return DummyKey()

monkeypatch.setattr(
encrypt.serialization, "load_pem_public_key", fake_load_pem_public_key
)

with pytest.raises(TypeError):
encrypt.rsa_encrypt("password")
Loading