pax_global_header00006660000000000000000000000064141536726730014530gustar00rootroot0000000000000052 comment=b26a49675d2dd63aa3c71f5bf094b46f5ff448fc bachya-py17track-b26a496/000077500000000000000000000000001415367267300151415ustar00rootroot00000000000000bachya-py17track-b26a496/.bandit.yaml000066400000000000000000000002551415367267300173460ustar00rootroot00000000000000--- tests: - B103 - B108 - B306 - B307 - B313 - B314 - B315 - B316 - B317 - B318 - B319 - B320 - B325 - B601 - B602 - B604 - B608 - B609 bachya-py17track-b26a496/.codeclimate.yml000066400000000000000000000003401415367267300202100ustar00rootroot00000000000000--- engines: duplication: enabled: true config: languages: - python fixme: enabled: true radon: enabled: true ratings: paths: - "**.py" exclude_paths: - dist/ - docs/ - tests/ bachya-py17track-b26a496/.coveragerc000066400000000000000000000000701415367267300172570ustar00rootroot00000000000000[run] source = py17track omit = py17track/track.py bachya-py17track-b26a496/.flake8000066400000000000000000000001611415367267300163120ustar00rootroot00000000000000[flake8] ignore = E203, E266, E501, F811, W503 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 bachya-py17track-b26a496/.github/000077500000000000000000000000001415367267300165015ustar00rootroot00000000000000bachya-py17track-b26a496/.github/ISSUE_TEMPLATE/000077500000000000000000000000001415367267300206645ustar00rootroot00000000000000bachya-py17track-b26a496/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007641415367267300233650ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. bachya-py17track-b26a496/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006471415367267300244200ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. bachya-py17track-b26a496/.github/labeler.yml000066400000000000000000000003711415367267300206330ustar00rootroot00000000000000--- # Number of labels to fetch (optional). Defaults to 20 numLabels: 40 # These labels will not be used even if the issue contains them (optional). # Pass a blank array if no labels are to be excluded. # excludeLabels: [] excludeLabels: - pinned bachya-py17track-b26a496/.github/pull_request_template.md000066400000000000000000000006051415367267300234430ustar00rootroot00000000000000**Describe what the PR does:** **Does this fix a specific issue?** Fixes https://github.com/bachya/py17track/issues/ **Checklist:** - [ ] Confirm that one or more new tests are written for the new functionality. - [ ] Run tests and ensure everything passes (with 100% test coverage). - [ ] Update `README.md` with any new documentation. - [ ] Add yourself to `AUTHORS.md`. bachya-py17track-b26a496/.github/release-drafter.yml000066400000000000000000000007141415367267300222730ustar00rootroot00000000000000--- categories: - title: "🚨 Breaking Changes" labels: - "breaking change" - title: "🚀 Features" labels: - "enhancement" - title: "🐛 Bug Fixes" labels: - "bug" - title: "🧰 Maintenance" labels: - "ci/cd" - "dependencies" - "maintenance" - "tooling" change-template: "- $TITLE (#$NUMBER)" name-template: "$NEXT_PATCH_VERSION" tag-template: "$NEXT_PATCH_VERSION" template: | $CHANGES bachya-py17track-b26a496/.github/stale.yml000066400000000000000000000035571415367267300203460ustar00rootroot00000000000000--- # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale daysUntilStale: 90 # Number of days of inactivity before an Issue or Pull Request with the stale # label is closed. Set to false to disable. If disabled, issues still need to # be closed manually, but will remain marked as stale. daysUntilClose: 7 # Only issues or pull requests with all of these labels are check if stale. # Defaults to `[]` (disabled) onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. # Set to `[]` to disable exemptLabels: - help wanted # Set to true to ignore issues in a project (defaults to false) exemptProjects: true # Set to true to ignore issues in a milestone (defaults to false) exemptMilestones: true # Set to true to ignore issues with an assignee (defaults to false) exemptAssignees: false # Label to use when marking as stale staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when removing the stale label. # unmarkComment: > # Your comment here. # Comment to post when closing a stale Issue or Pull Request. # closeComment: > # Your comment here. # Limit the number of actions per hour, from 1-30. Default is 30 limitPerRun: 30 # Limit to only `issues` or `pulls` # only: issues # Handle pull requests a little bit faster and with an adjusted comment. pulls: daysUntilStale: 30 exemptProjects: false markComment: > This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. bachya-py17track-b26a496/.github/workflows/000077500000000000000000000000001415367267300205365ustar00rootroot00000000000000bachya-py17track-b26a496/.github/workflows/ci.yaml000066400000000000000000000030451415367267300220170ustar00rootroot00000000000000--- name: CI on: pull_request: branches: - dev - master push: branches: - dev - master jobs: test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.6" - "3.7" - "3.8" - "3.9" - "3.10" steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} architecture: x64 - run: | python -m venv venv venv/bin/pip install -r requirements_test.txt venv/bin/py.test tests/ coverage: name: Test Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: "3.x" architecture: x64 - run: | python -m venv venv venv/bin/pip install -r requirements_test.txt venv/bin/py.test \ -s \ --verbose \ --cov-report term-missing \ --cov-report xml \ --cov=py17track tests - uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} lint: name: "Linting & Static Analysis" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: "3.x" architecture: x64 - uses: pre-commit/action@v2.0.3 env: SKIP: no-commit-to-branch bachya-py17track-b26a496/.github/workflows/publish.yaml000066400000000000000000000007131415367267300230710ustar00rootroot00000000000000--- name: "Publish to PyPI" on: push: tags: - "*" jobs: publish_to_pypi: runs-on: ubuntu-latest steps: - name: Checkout Code uses: actions/checkout@v2 - name: Set up Python 3.7 uses: actions/setup-python@v2 with: python-version: 3.7 - name: Publish to PyPI run: | pip install poetry poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_KEY }} bachya-py17track-b26a496/.github/workflows/release.yaml000066400000000000000000000003741415367267300230460ustar00rootroot00000000000000--- name: "Draft Release" on: push: branches: - "dev" jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} bachya-py17track-b26a496/.gitignore000066400000000000000000000001471415367267300171330ustar00rootroot00000000000000.coverage .mypy_cache .tox/ .venv __pycache__/ py17track.egg-info coverage.xml poetry.lock tags .vscodebachya-py17track-b26a496/.pre-commit-config.yaml000066400000000000000000000032351415367267300214250ustar00rootroot00000000000000--- repos: - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - id: bandit args: - --quiet - --format=custom - --configfile=.bandit.yaml files: ^py17track/.+\.py$ - repo: https://github.com/python/black rev: 21.7b0 hooks: - id: black args: - --safe - --quiet language_version: python3 files: ^((py17track|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v1.16.0 hooks: - id: codespell args: - --skip="./.*,*.json" - --quiet-level=4 exclude_types: [json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.0.1 files: ^py17track/.+\.py$ - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: - id: isort additional_dependencies: - toml files: ^(py17track|tests)/.+\.py$ - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.790 hooks: - id: mypy files: ^py17track/.+\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: check-json - id: no-commit-to-branch args: - --branch=dev - --branch=master - repo: https://github.com/PyCQA/pydocstyle rev: 5.0.2 hooks: - id: pydocstyle files: ^((py17track|tests)/.+)?[^/]+\.py$ - repo: https://github.com/gruntwork-io/pre-commit rev: v0.1.12 hooks: - id: shellcheck files: ^script/.+ bachya-py17track-b26a496/.whitesource000066400000000000000000000003251415367267300175030ustar00rootroot00000000000000{ "scanSettings": { "baseBranches": [] }, "checkRunSettings": { "vulnerableCheckRunConclusionLevel": "failure", "displayMode": "diff" }, "issueSettings": { "minSeverityLevel": "LOW" } }bachya-py17track-b26a496/AUTHORS.md000066400000000000000000000002021415367267300166020ustar00rootroot00000000000000# Contributions to `py17track` ## Owners - Aaron Bach (https://github.com/bachya) - Olivér Falvai (https://github.com/ofalvai) bachya-py17track-b26a496/LICENSE000066400000000000000000000020531415367267300161460ustar00rootroot00000000000000MIT License Copyright (c) 2018 Aaron Bach Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bachya-py17track-b26a496/README.md000066400000000000000000000077361415367267300164350ustar00rootroot00000000000000# 📦 py17track: A Simple Python API for 17track.net [![CI](https://github.com/bachya/py17track/workflows/CI/badge.svg)](https://github.com/bachya/py17track/actions) [![PyPi](https://img.shields.io/pypi/v/py17track.svg)](https://pypi.python.org/pypi/py17track) [![Version](https://img.shields.io/pypi/pyversions/py17track.svg)](https://pypi.python.org/pypi/py17track) [![License](https://img.shields.io/pypi/l/py17track.svg)](https://github.com/bachya/py17track/blob/master/LICENSE) [![Code Coverage](https://codecov.io/gh/bachya/py17track/branch/master/graph/badge.svg)](https://codecov.io/gh/bachya/py17track) [![Maintainability](https://api.codeclimate.com/v1/badges/af60d65b69d416136fc9/maintainability)](https://codeclimate.com/github/bachya/py17track/maintainability) [![Say Thanks](https://img.shields.io/badge/SayThanks-!-1EAEDB.svg)](https://saythanks.io/to/bachya) `py17track` is a simple Python library to track packages in [17track.net](http://www.17track.net/) accounts. Since this is uses an unofficial API, there's no guarantee that 17track.net will provide every field for every package, all the time. Additionally, this API may stop working at any moment. # Python Versions `py17track` is currently supported on: * Python 3.6 * Python 3.7 * Python 3.8 * Python 3.9 * Python 3.10 # Installation ```python pip install py17track ``` # Usage ```python import asyncio from aiohttp import ClientSession from py17track import Client async def main() -> None: """Run!""" client = Client() # Login to 17track.net: await client.profile.login('', '') # Get the account ID: client.profile.account_id # >>> 1234567890987654321 # Get a summary of the user's packages: summary = await client.profile.summary() # >>> {'In Transit': 3, 'Expired': 3, ... } # Get all packages associated with a user's account: packages = await client.profile.packages() # >>> [py17track.package.Package(..), ...] # Add new packages by tracking number await client.profile.add_package('', '') asyncio.run(main()) ``` By default, the library creates a new connection to 17track with each coroutine. If you are calling a large number of coroutines (or merely want to squeeze out every second of runtime savings possible), an [`aiohttp`](https://github.com/aio-libs/aiohttp) `ClientSession` can be used for connection pooling: ```python import asyncio from aiohttp import ClientSession from py17track import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) # ... asyncio.run(main()) ``` Each `Package` object has the following info: * `destination_country`: the country the package was shipped to * `friendly_name`: the human-friendly name of the package * `info`: a text description of the latest status * `location`: the current location (if known) * `timestamp`: the timestamp of the latest event * `origin_country`: the country the package was shipped from * `package_type`: the type of package (if known) * `status`: the overall package status ("In Transit", "Delivered", etc.) * `tracking_info_language`: the language of the tracking info * `tracking_number`: the all-important tracking number # Contributing 1. [Check for open features/bugs](https://github.com/bachya/py17track/issues) or [initiate a discussion on one](https://github.com/bachya/py17track/issues/new). 2. [Fork the repository](https://github.com/bachya/py17track/fork). 3. (_optional, but highly recommended_) Create a virtual environment: `python3 -m venv .venv` 4. (_optional, but highly recommended_) Enter the virtual environment: `source ./.venv/bin/activate` 5. Install the dev environment: `script/setup` 6. Code your new feature or bug fix. 7. Write tests that cover your new functionality. 8. Run tests and ensure 100% code coverage: `script/test` 9. Update `README.md` with any new documentation. 10. Add yourself to `AUTHORS.md`. 11. Submit a pull request! bachya-py17track-b26a496/examples/000077500000000000000000000000001415367267300167575ustar00rootroot00000000000000bachya-py17track-b26a496/examples/__init__.py000066400000000000000000000000271415367267300210670ustar00rootroot00000000000000"""Define examples.""" bachya-py17track-b26a496/examples/test_api.py000066400000000000000000000017621415367267300211470ustar00rootroot00000000000000"""Run an example script to quickly test a 17track.net account.""" import asyncio import logging from aiohttp import ClientSession from py17track import Client from py17track.errors import SeventeenTrackError _LOGGER = logging.getLogger() async def main() -> None: """Create the aiohttp session and run the example.""" logging.basicConfig(level=logging.INFO) async with ClientSession() as session: try: client = Client(session=session) await client.profile.login("", "") _LOGGER.info("Account ID: %s", client.profile.account_id) # await client.profile.add_package("", "") summary = await client.profile.summary() _LOGGER.info("Account Summary: %s", summary) packages = await client.profile.packages() _LOGGER.info("Package Summary: %s", packages) except SeventeenTrackError as err: print(err) asyncio.run(main()) bachya-py17track-b26a496/py17track/000077500000000000000000000000001415367267300167665ustar00rootroot00000000000000bachya-py17track-b26a496/py17track/__init__.py000066400000000000000000000001061415367267300210740ustar00rootroot00000000000000"""Define module-level imports.""" from .client import Client # noqa bachya-py17track-b26a496/py17track/client.py000066400000000000000000000032641415367267300206230ustar00rootroot00000000000000"""Define a 17track.net client.""" from typing import Optional from aiohttp import ClientSession, ClientTimeout from aiohttp.client_exceptions import ClientError from .errors import RequestError from .profile import Profile # from .track import Track DEFAULT_TIMEOUT: int = 10 class Client: # pylint: disable=too-few-public-methods """Define the client.""" def __init__(self, *, session: Optional[ClientSession] = None) -> None: """Initialize.""" self._session: Optional[ClientSession] = session self.profile: Profile = Profile(self._request) # This is disabled until a workaround can be found: # self.track = Track(self._request) async def _request( self, method: str, url: str, *, headers: Optional[dict] = None, params: Optional[dict] = None, json: Optional[dict] = None, ) -> dict: """Make a request against the RainMachine device.""" use_running_session = self._session and not self._session.closed if use_running_session: session = self._session else: session = ClientSession(timeout=ClientTimeout(total=DEFAULT_TIMEOUT)) assert session try: async with session.request( method, url, headers=headers, params=params, json=json ) as resp: resp.raise_for_status() data: dict = await resp.json(content_type=None) return data except ClientError as err: raise RequestError(f"Error requesting data from {url}: {err}") finally: if not use_running_session: await session.close() bachya-py17track-b26a496/py17track/errors.py000066400000000000000000000005261415367267300206570ustar00rootroot00000000000000"""Define module exceptions.""" class SeventeenTrackError(Exception): """Define a base error.""" pass class InvalidTrackingNumberError(SeventeenTrackError): """Define an error for an invalid tracking number.""" pass class RequestError(SeventeenTrackError): """Define an error for HTTP request errors.""" pass bachya-py17track-b26a496/py17track/package.py000066400000000000000000000171221415367267300207360ustar00rootroot00000000000000"""Define a simple structure for a package.""" from datetime import datetime from typing import Dict, Optional import attr from pytz import UTC, timezone COUNTRY_MAP: Dict[int, str] = { 0: "Unknown", 102: "Afghanistan", 103: "Albania", 104: "Algeria", 105: "Andorra", 106: "Angola", 108: "Antarctica", 110: "Antigua and Barbuda", 112: "Argentina", 113: "Armenia", 115: "Australia", 116: "Austria", 117: "Azerbaijan", 201: "Bahamas", 202: "Bahrain", 203: "Bangladesh", 204: "Barbados", 205: "Belarus", 206: "Belgium", 207: "Belize", 208: "Benin", 210: "Bhutan", 211: "Bolivia", 212: "Bosnia and Herzegovina", 213: "Botswana", 215: "Brazil", 216: "Brunei", 217: "Bulgaria", 218: "Burkina Faso", 219: "Burundi", 301: "China", 302: "Cambodia", 303: "Cameroon", 304: "Canada", 306: "Cape Verde", 308: "Central African Republic", 310: "Chile", 312: "Ivory Coast", 313: "Colombia", 314: "Comoros", 315: "Congo-Brazzaville", 316: "Congo-Kinshasa", 317: "Cook Islands", 318: "Costa Rica", 319: "Croatia", 320: "Cuba", 321: "Cyprus", 322: "Czech Republic", 323: "Chad", 401: "Denmark", 402: "Djibouti", 403: "Dominica", 404: "Dominican Republic", 501: "Ecuador", 502: "Egypt", 503: "United Arab Emirates", 504: "Estonia", 505: "Ethiopia", 506: "Eritrea", 507: "Equatorial Guinea", 508: "East Timor", 603: "Fiji", 604: "Finland", 605: "France", 701: "Gabon", 702: "Gambia", 703: "Georgia", 704: "Germany", 705: "Ghana", 707: "Greece", 709: "Grenada", 712: "Guatemala", 713: "Guinea", 714: "Guyana", 716: "Guinea-Bissau", 801: "Hong Kong [CN]", 802: "Haiti", 804: "Honduras", 805: "Hungary", 901: "Iceland", 902: "India", 903: "Indonesia", 904: "Iran", 905: "Ireland", 906: "Israel", 907: "Italy", 908: "Iraq", 1001: "Jamaica", 1002: "Japan", 1003: "Jordan", 1101: "Kazakhstan", 1102: "Kenya", 1103: "United Kingdom", 1104: "Kiribati", 1105: "Korea, South", 1106: "Korea, North", 1107: "Kosovo", 1108: "Kuwait", 1109: "Kyrgyzstan", 1201: "Laos", 1202: "Latvia", 1203: "Lebanon", 1204: "Lesotho", 1205: "Liberia", 1206: "Libya", 1207: "Liechtenstein", 1208: "Lithuania", 1209: "Saint Lucia", 1210: "Luxembourg", 1301: "Macao [CN]", 1302: "Macedonia", 1303: "Madagascar", 1304: "Malawi", 1305: "Malaysia", 1306: "Maldives", 1307: "Mali", 1308: "Malta", 1310: "Marshall Islands", 1312: "Mauritania", 1313: "Mauritius", 1314: "Mexico", 1315: "Federated States of Micronesia", 1316: "Moldova", 1317: "Monaco", 1318: "Mongolia", 1319: "Montenegro", 1321: "Morocco", 1322: "Mozambique", 1323: "Myanmar", 1401: "Namibia", 1402: "Nauru", 1403: "Nepal", 1404: "Netherlands", 1406: "New Zealand", 1407: "Nicaragua", 1408: "Norway", 1409: "Niger", 1410: "Nigeria", 1501: "Oman", 1601: "Pakistan", 1602: "Palestine", 1603: "Panama", 1604: "Papua New Guinea", 1605: "Paraguay", 1606: "Peru", 1607: "Philippines", 1608: "Poland", 1610: "Portugal", 1614: "Palau", 1701: "Qatar", 1802: "Romania", 1803: "Russian Federation", 1804: "Rwanda", 1902: "Saint Vincent and the Grenadines", 1903: "El Salvador", 1905: "San Marino", 1906: "Sao Tome and Principe", 1907: "Saudi Arabia", 1908: "Senegal", 1909: "Serbia", 1911: "Seychelles", 1912: "Sierra Leone", 1913: "Singapore", 1914: "Slovakia", 1915: "Slovenia", 1916: "Solomon Islands", 1917: "South Africa", 1918: "Spain", 1919: "Sri Lanka", 1920: "Sudan", 1921: "Suriname", 1923: "Swaziland", 1924: "Sweden", 1925: "Switzerland", 1926: "Syrian Arab Republic", 1927: "Saint Kitts and Nevis", 1928: "Samoa", 1929: "Somalia", 1930: "Scotland", 1932: "South Ossetia", 2001: "Taiwan [CN]", 2002: "Tajikistan", 2003: "Tanzania", 2004: "Thailand", 2005: "Togo", 2006: "Tonga", 2007: "Trinidad and Tobago", 2009: "Tuvalu", 2010: "Tunisia", 2011: "Turkey", 2012: "Turkmenistan", 2101: "Uganda", 2102: "Ukraine", 2103: "Uzbekistan", 2104: "Uruguay", 2105: "United States", 2202: "Vanuatu", 2203: "Venezuela", 2204: "Vietnam", 2205: "Vatican City", 2302: "Western Sahara", 2501: "Yemen", 2601: "Zambia", 2602: "Zimbabwe", 8901: "Overseas Territory [ES]", 9001: "Overseas Territory [GB]", 9002: "Anguilla [GB]", 9003: "Ascension [GB]", 9004: "Bermuda [GB]", 9005: "Cayman Islands [GB]", 9006: "Gibraltar [GB]", 9007: "Guernsey [GB]", 9008: "Saint Helena [GB]", 9101: "Overseas Territory [FI]", 9102: "Ã…aland Islands [FI]", 9201: "Overseas Territory [NL]", 9202: "Antilles [NL]", 9203: "Aruba [NL]", 9301: "Overseas Territory [PT]", 9401: "Overseas Territory [NO]", 9501: "Overseas Territory [AU]", 9502: "Norfolk Island [AU]", 9601: "Overseas Territory [DK]", 9602: "Faroe Islands [DK]", 9603: "Greenland [DK]", 9701: "Overseas Territory [FR]", 9702: "New Caledonia [FR]", 9801: "Overseas Territory [US]", 9901: "Overseas Territory [NZ]", } PACKAGE_STATUS_MAP: Dict[int, str] = { 0: "Not Found", 10: "In Transit", 20: "Expired", 30: "Ready to be Picked Up", 35: "Undelivered", 40: "Delivered", 50: "Returned", } PACKAGE_TYPE_MAP: Dict[int, str] = { 0: "Unknown", 1: "Small Registered Package", 2: "Registered Parcel", 3: "EMS Package", } @attr.s( frozen=True ) # pylint: disable=too-few-public-methods,too-many-instance-attributes class Package: """Define a package object.""" tracking_number: str = attr.ib() destination_country: int = attr.ib(default=0) id: Optional[str] = attr.ib(default=None) friendly_name: Optional[str] = attr.ib(default=None) info_text: Optional[str] = attr.ib(default=None) location: str = attr.ib(default="") timestamp: str = attr.ib(default="") origin_country: int = attr.ib(default=0) package_type: int = attr.ib(default=0) status: int = attr.ib(default=0) tracking_info_language: str = attr.ib(default="Unknown") tz: str = attr.ib(default="UTC") def __attrs_post_init__(self): """Do some post-init processing.""" object.__setattr__( self, "destination_country", COUNTRY_MAP[self.destination_country] ) object.__setattr__(self, "origin_country", COUNTRY_MAP[self.origin_country]) object.__setattr__(self, "package_type", PACKAGE_TYPE_MAP[self.package_type]) object.__setattr__( self, "status", PACKAGE_STATUS_MAP.get(self.status, "Unknown") ) if self.timestamp is not None: tz = timezone(self.tz) try: timestamp = tz.localize( datetime.strptime(self.timestamp, "%Y-%m-%d %H:%M") ) except ValueError: try: timestamp = tz.localize( datetime.strptime(self.timestamp, "%Y-%m-%d %H:%M:%S") ) except ValueError: timestamp = datetime(1970, 1, 1, tzinfo=UTC) if self.tz != "UTC": timestamp = timestamp.astimezone(UTC) object.__setattr__(self, "timestamp", timestamp) bachya-py17track-b26a496/py17track/profile.py000066400000000000000000000133571415367267300210110ustar00rootroot00000000000000"""Define interaction with a user profile.""" import json import logging from typing import Callable, Coroutine, List, Optional, Union from .errors import InvalidTrackingNumberError, RequestError from .package import PACKAGE_STATUS_MAP, Package _LOGGER: logging.Logger = logging.getLogger(__name__) API_URL_BUYER: str = "https://buyer.17track.net/orderapi/call" API_URL_USER: str = "https://user.17track.net/userapi/call" class Profile: """Define a 17track.net profile manager.""" def __init__(self, request: Callable[..., Coroutine]) -> None: """Initialize.""" self._request: Callable[..., Coroutine] = request self.account_id: Optional[str] = None async def login(self, email: str, password: str) -> bool: """Login to the profile.""" login_resp: dict = await self._request( "post", API_URL_USER, json={ "version": "1.0", "method": "Signin", "param": {"Email": email, "Password": password, "CaptchaCode": ""}, "sourcetype": 0, }, ) _LOGGER.debug("Login response: %s", login_resp) if login_resp.get("Code") != 0: return False self.account_id = login_resp["Json"]["gid"] return True async def packages( self, package_state: Union[int, str] = "", show_archived: bool = False, tz: str = "UTC", ) -> list: """Get the list of packages associated with the account.""" packages_resp: dict = await self._request( "post", API_URL_BUYER, json={ "version": "1.0", "method": "GetTrackInfoList", "param": { "IsArchived": show_archived, "Item": "", "Page": 1, "PerPage": 40, "PackageState": package_state, "Sequence": "0", }, "sourcetype": 0, }, ) _LOGGER.debug("Packages response: %s", packages_resp) packages: List[Package] = [] for package in packages_resp.get("Json", []): event: dict = {} last_event_raw: str = package.get("FLastEvent") if last_event_raw: event = json.loads(last_event_raw) kwargs: dict = { "id": package.get("FTrackInfoId"), "destination_country": package.get("FSecondCountry", 0), "friendly_name": package.get("FRemark"), "info_text": event.get("z"), "location": " ".join([event.get("c", ""), event.get("d", "")]).strip(), "timestamp": event.get("a"), "tz": tz, "origin_country": package.get("FFirstCountry", 0), "package_type": package.get("FTrackStateType", 0), "status": package.get("FPackageState", 0), } packages.append(Package(package["FTrackNo"], **kwargs)) return packages async def summary(self, show_archived: bool = False) -> dict: """Get a quick summary of how many packages are in an account.""" summary_resp: dict = await self._request( "post", API_URL_BUYER, json={ "version": "1.0", "method": "GetIndexData", "param": {"IsArchived": show_archived}, "sourcetype": 0, }, ) _LOGGER.debug("Summary response: %s", summary_resp) results: dict = {} for kind in summary_resp.get("Json", {}).get("eitem", []): key = PACKAGE_STATUS_MAP.get(kind["e"], "Unknown") value = kind["ec"] results[key] = value if key not in results else results[key] + value return results 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]}, }, ) _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 packages = await self.packages() try: new_package = next( p for p in packages if p.tracking_number == tracking_number ) except StopIteration: raise InvalidTrackingNumberError( f"Recently added package not found by tracking number: {tracking_number}" ) _LOGGER.debug("Found internal ID of recently added package: %s", new_package.id) await self.set_friendly_name(new_package.id, friendly_name) async def set_friendly_name(self, internal_id: str, friendly_name: str): """Set a friendly name to an already added tracking number. 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}, }, ) _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}") bachya-py17track-b26a496/py17track/track.py000066400000000000000000000032471415367267300204520ustar00rootroot00000000000000"""Define interaction with an individual package.""" from typing import Callable, Coroutine, List from .errors import InvalidTrackingNumberError from .package import Package API_URL_TRACK: str = "https://t.17track.net/restapi/track" class Track: # pylint: disable=too-few-public-methods """Define a 17track.net package manager.""" def __init__(self, request: Callable[..., Coroutine]) -> None: """Initialize.""" self._request: Callable[..., Coroutine] = request async def find(self, *tracking_numbers: str) -> list: """Get tracking info for one or more tracking numbers.""" data: dict = {"data": [{"num": num} for num in tracking_numbers]} tracking_resp: dict = await self._request("post", API_URL_TRACK, json=data) if not tracking_resp.get("dat"): raise InvalidTrackingNumberError("Invalid data") packages: List[Package] = [] for info in tracking_resp["dat"]: package_info: dict = info.get("track", {}) if not package_info: continue kwargs: dict = { "destination_country": package_info.get("c"), "info_text": package_info.get("z0", {}).get("z"), "location": package_info.get("z0", {}).get("c"), "timestamp": package_info.get("z0", {}).get("a"), "origin_country": package_info.get("b"), "package_type": package_info.get("d", 0), "status": package_info.get("e", 0), "tracking_info_language": package_info.get("ln1", "Unknown"), } packages.append(Package(info["no"], **kwargs)) return packages bachya-py17track-b26a496/pylintrc000066400000000000000000000003631415367267300167320ustar00rootroot00000000000000[MESSAGES CONTROL] # Reasons disabled: # bad-continuation - Invalid attack on black # unnecessary-pass - This can hurt readability disable= bad-continuation, unnecessary-pass [REPORTS] reports=no [FORMAT] expected-line-ending-format=LF bachya-py17track-b26a496/pyproject.toml000066400000000000000000000027601415367267300200620ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 35.0.2", "wheel >= 0.29.0", "poetry>=0.12"] build-backend = "poetry.core.masonry.api" [tool.isort] combine_as_imports = true default_section = "THIRDPARTY" force_grid_wrap = 0 force_sort_within_sections = true forced_separate = "tests" include_trailing_comma = true indent = " " known_first_party = "py17track,examples,tests" line_length = 88 multi_line_output = 3 not_skip = "__init__.py" sections = "FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" use_parentheses = true [tool.poetry] name = "py17track" version = "2021.12.2" description = "A Simple Python API for 17track.net" readme = "README.md" authors = ["Aaron Bach "] license = "MIT" repository = "https://github.com/bachya/py17track" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] [tool.poetry.dependencies] aiohttp = ">=3.8.0" attrs = ">=19.3" python = "^3.6.1" pytz = ">=2021.1" [tool.poetry.dev-dependencies] aresponses = "^2.0.0" pre-commit = "^2.0.1" pytest = "^6.2.5" pytest-aiohttp = "^0.3.0" pytest-cov = "^3.0.0" bachya-py17track-b26a496/renovate.json000066400000000000000000000002611415367267300176560ustar00rootroot00000000000000{ "extends": [ "config:base", "group:all", "schedule:monthly", ":disableDependencyDashboard" ], "labels": ["dependencies"], "separateMinorPatch": true } bachya-py17track-b26a496/requirements_test.txt000066400000000000000000000001441415367267300214630ustar00rootroot00000000000000aiohttp>=3.8.0 aresponses==1.1.2 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest==6.2.5 pytz>=2021.1 bachya-py17track-b26a496/script/000077500000000000000000000000001415367267300164455ustar00rootroot00000000000000bachya-py17track-b26a496/script/release000077500000000000000000000024711415367267300200170ustar00rootroot00000000000000#!/usr/bin/env bash set -e REPO_PATH="$( dirname "$( cd "$(dirname "$0")" ; pwd -P )" )" if [ "$(git rev-parse --abbrev-ref HEAD)" != "dev" ]; then echo "Refusing to publish a release from a branch other than dev" exit 1 fi if [ -z "$(command -v poetry)" ]; then echo "Poetry needs to be installed to run this script: pip3 install poetry" exit 1 fi function generate_version { latest_tag="$(git tag --sort=committerdate | tail -1)" month="$(date +'%Y.%m')" if [[ "$latest_tag" =~ "$month".* ]]; then patch="$(echo "$latest_tag" | cut -d . -f 3)" ((patch=patch+1)) echo "$month.$patch" else echo "$month.0" fi } # Temporarily uninstall pre-commit hooks so that we can push to dev and master: pre-commit uninstall # Pull the latest dev: git pull origin dev # Generate the next version (in the format YEAR.MONTH.RELEASE_NUMER): new_version=$(generate_version) # Update the PyPI package version: sed -i "" "s/^version = \".*\"/version = \"$new_version\"/g" "$REPO_PATH/pyproject.toml" git add pyproject.toml # Commit, tag, and push: git commit -m "Bump version to $new_version" git tag "$new_version" git push && git push --tags # Merge dev into master: git checkout master git merge dev git push git checkout dev # Re-initialize pre-commit: pre-commit install bachya-py17track-b26a496/script/setup000077500000000000000000000002151415367267300175310ustar00rootroot00000000000000#!/bin/sh set -e # Install all dependencies: pip3 install poetry poetry lock poetry install # Install pre-commit hooks: pre-commit install bachya-py17track-b26a496/script/test000077500000000000000000000002041415367267300173460ustar00rootroot00000000000000#!/bin/sh set -e # Run pytest with coverage: py.test -s --verbose --cov-report term-missing --cov-report xml --cov=py17track tests bachya-py17track-b26a496/tests/000077500000000000000000000000001415367267300163035ustar00rootroot00000000000000bachya-py17track-b26a496/tests/__init__.py000066400000000000000000000000341415367267300204110ustar00rootroot00000000000000"""Define package tests.""" bachya-py17track-b26a496/tests/common.py000066400000000000000000000004631415367267300201500ustar00rootroot00000000000000"""Define common test utilities.""" import os TEST_EMAIL = "user@email.com" TEST_PASSWORD = "password" def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path, encoding="utf-8") as fptr: return fptr.read() bachya-py17track-b26a496/tests/fixtures/000077500000000000000000000000001415367267300201545ustar00rootroot00000000000000bachya-py17track-b26a496/tests/fixtures/__init__.py000066400000000000000000000000341415367267300222620ustar00rootroot00000000000000"""Define test fixtures.""" bachya-py17track-b26a496/tests/fixtures/add_package_existing_response.json000066400000000000000000000004621415367267300271040ustar00rootroot00000000000000{ "Json": { "Items": [ { "TrackInfoId": "0", "TrackNo": "1234567890987654321", "ResultCode": -11010101 } ], "CanTrackNum": 40, "UseTrackNum": 4, "SuccessNum": 0, "ErrorNum": 1 }, "Code": -11010101, "Message": "Number(s) already exists." }bachya-py17track-b26a496/tests/fixtures/add_package_response.json000066400000000000000000000003701415367267300251700ustar00rootroot00000000000000{ "Json": { "Items": [ { "TrackInfoId": "0", "TrackNo": "1234567890987654321", "ResultCode": 0 } ], "CanTrackNum": 40, "UseTrackNum": 3, "SuccessNum": 1, "ErrorNum": 0 }, "Code": 0 }bachya-py17track-b26a496/tests/fixtures/authentication_failure_response.json000066400000000000000000000001101415367267300275030ustar00rootroot00000000000000{ "Code": -6, "Message": "You haven't logged in for a long time." } bachya-py17track-b26a496/tests/fixtures/authentication_success_response.json000066400000000000000000000003321415367267300275320ustar00rootroot00000000000000{ "Json": { "FUserRole": 4, "FNickname": "John Doe", "FEmail": "john.doe@company.com", "FLanguage": "en", "FCountry": 100, "FPhoto": 10001, "gid": "1234567890987654321" }, "Code": 0 } bachya-py17track-b26a496/tests/fixtures/packages_response.json000066400000000000000000000044101415367267300245420ustar00rootroot00000000000000{ "pageInfo": { "Page": 1, "PerPage": 40, "TotalCount": 1 }, "Json": [ { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2018-04-23 12:02\",\"b\":null,\"c\":\"Paris\",\"d\":\"\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2018-05-04 20:22:13" }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2019-02-26 01:05:34\",\"b\":null,\"c\":\"\",\"d\":\"Spain\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2019-03-02 10:02:03" }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2021-03-05\",\"b\":null,\"c\":\"Milano\",\"d\":\"Italy\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2021-03-05 13:02:03" }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2018-11-22 13:33\",\"b\":null,\"c\":\"\",\"d\":\"\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2018-11-25 14:05:13" }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2018-05-04 20:22:13" } ] }bachya-py17track-b26a496/tests/fixtures/packages_response_with_unknown_state.json000066400000000000000000000026321415367267300305600ustar00rootroot00000000000000{ "pageInfo": { "Page": 1, "PerPage": 40, "TotalCount": 1 }, "Json": [ { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2021-03-05\",\"b\":null,\"c\":\"Milano\",\"d\":\"Italy\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2021-03-05 13:02:03" }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "{\"a\":\"2018-11-22 13:33\",\"b\":null,\"c\":\"\",\"d\":\"\",\"z\":\"Arrival at Destination Post\"}", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2018-11-25 14:05:13", "FPackageState": 10 }, { "FTrackInfoId": "1234567890987654321", "FTrackNo": "1234567890987654321", "FFirstCarrier": 0, "FFirstCarrierSource": 2, "FSecondCarrier": 0, "FSecondCarrierSource": 2, "FLastEvent": "", "FIsArchived": false, "FRemark": "", "FTrackStateType": 0, "FCreateTime": "2018-05-04 20:22:13", "FPackageState":5000 } ] }bachya-py17track-b26a496/tests/fixtures/set_friendly_name_failure_response.json000066400000000000000000000000271415367267300301620ustar00rootroot00000000000000{"Json":{},"Code":-100}bachya-py17track-b26a496/tests/fixtures/set_friendly_name_response.json000066400000000000000000000000241415367267300264500ustar00rootroot00000000000000{"Json":{},"Code":0}bachya-py17track-b26a496/tests/fixtures/summary_response.json000066400000000000000000000012101415367267300244540ustar00rootroot00000000000000{ "Json": { "utn": { "cnum": "40", "unum": "9", "inum": "1", "anum": 22 }, "eitem": [ { "e": 10, "ec": 6 }, { "e": 0, "ec": 2 }, { "e": 5, "ec": 2 }, { "e": 20, "ec": 0 }, { "e": 30, "ec": 0 }, { "e": 32, "ec": 1 }, { "e": 35, "ec": 0 }, { "e": 40, "ec": 0 }, { "e": 50, "ec": 0 }, { "e": 5000, "ec": 0 } ] }, "Code": 0 } bachya-py17track-b26a496/tests/test_client.py000066400000000000000000000011231415367267300211670ustar00rootroot00000000000000"""Define tests for the client object.""" import aiohttp import pytest from py17track import Client from py17track.errors import RequestError @pytest.mark.asyncio async def test_bad_request(aresponses): """Test that a failed login returns the correct response.""" aresponses.add( "random.domain", "/no/good", "get", aresponses.Response(text="", status=404) ) with pytest.raises(RequestError): async with aiohttp.ClientSession() as session: client = Client(session=session) await client._request("get", "https://random.domain/no/good") bachya-py17track-b26a496/tests/test_profile.py000066400000000000000000000275601415367267300213660ustar00rootroot00000000000000"""Define tests for the client object.""" from datetime import datetime import aiohttp import pytest from pytz import UTC, timezone from py17track import Client from py17track.errors import InvalidTrackingNumberError, RequestError from .common import TEST_EMAIL, TEST_PASSWORD, load_fixture @pytest.mark.asyncio async def test_login_failure(aresponses): """Test that a failed login returns the correct response.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_failure_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) login_result = await client.profile.login(TEST_EMAIL, TEST_PASSWORD) assert login_result is False @pytest.mark.asyncio async def test_login_success(aresponses): """Test that a successful login returns the correct response.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) login_result = await client.profile.login(TEST_EMAIL, TEST_PASSWORD) assert login_result is True @pytest.mark.asyncio async def test_no_explicit_session(aresponses): """Test not providing an explicit aiohttp ClientSession.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) client = Client() login_result = await client.profile.login(TEST_EMAIL, TEST_PASSWORD) assert login_result is True @pytest.mark.asyncio async def test_packages(aresponses): """Test getting packages.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) packages = await client.profile.packages() assert len(packages) == 5 assert packages[0].location == "Paris" assert packages[1].location == "Spain" assert packages[2].location == "Milano Italy" assert packages[3].location == "" @pytest.mark.asyncio async def test_packages_with_unknown_state(aresponses): """Test getting packages.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("packages_response_with_unknown_state.json"), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) packages = await client.profile.packages() assert len(packages) == 3 assert packages[0].status == "Not Found" assert packages[1].status == "In Transit" assert packages[2].status == "Unknown" @pytest.mark.asyncio async def test_packages_default_timezone(aresponses): """Test getting packages with default timezone.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) packages = await client.profile.packages() assert len(packages) == 5 assert packages[0].timestamp.isoformat() == "2018-04-23T12:02:00+00:00" assert packages[1].timestamp.isoformat() == "2019-02-26T01:05:34+00:00" assert packages[2].timestamp.isoformat() == "1970-01-01T00:00:00+00:00" @pytest.mark.asyncio async def test_packages_user_defined_timezone(aresponses): """Test getting packages with user-defined timezone.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) packages = await client.profile.packages(tz="Asia/Jakarta") assert len(packages) == 5 assert packages[0].timestamp.isoformat() == "2018-04-23T05:02:00+00:00" assert packages[1].timestamp.isoformat() == "2019-02-25T18:05:34+00:00" assert packages[2].timestamp.isoformat() == "1970-01-01T00:00:00+00:00" @pytest.mark.asyncio async def test_summary(aresponses): """Test getting package summary.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("summary_response.json"), status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) summary = await client.profile.summary() assert summary["Delivered"] == 0 assert summary["Expired"] == 0 assert summary["In Transit"] == 6 assert summary["Not Found"] == 2 assert summary["Ready to be Picked Up"] == 0 assert summary["Returned"] == 0 assert summary["Undelivered"] == 0 assert summary["Unknown"] == 3 @pytest.mark.asyncio async def test_add_new_package(aresponses): """Test adding a new package.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("add_package_response.json"), status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) await client.profile.add_package("LP00432912409987") @pytest.mark.asyncio async def test_add_new_package_with_friendly_name(aresponses): """Test adding a new package with friendly name.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("add_package_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("set_friendly_name_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) await client.profile.add_package("1234567890987654321", "Friendly name") @pytest.mark.asyncio async def test_add_new_package_with_friendly_name_not_found(aresponses): """Test adding a new package with friendly name but package not found after adding it.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("add_package_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("set_friendly_name_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: with pytest.raises(InvalidTrackingNumberError): client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) await client.profile.add_package("1234567890987654321567", "Friendly name") @pytest.mark.asyncio async def test_add_new_package_with_friendly_name_error_response(aresponses): """Test adding a new package with friendly name but setting the name fails.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("add_package_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response(text=load_fixture("packages_response.json"), status=200), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("set_friendly_name_failure_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: with pytest.raises(RequestError): client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) await client.profile.add_package("1234567890987654321", "Friendly name") @pytest.mark.asyncio async def test_add_existing_package(aresponses): """Test adding an existing new package.""" aresponses.add( "user.17track.net", "/userapi/call", "post", aresponses.Response( text=load_fixture("authentication_success_response.json"), status=200 ), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("add_package_existing_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: with pytest.raises(RequestError): client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) await client.profile.add_package("1234567890987654321")