pax_global_header00006660000000000000000000000064150254540050014512gustar00rootroot0000000000000052 comment=75cf6d6fc1099b9ac57162061a9ce759b05f439c shaiu-pyseventeentrack-75cf6d6/000077500000000000000000000000001502545400500166415ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/.bandit.yaml000066400000000000000000000002551502545400500210460ustar00rootroot00000000000000--- tests: - B103 - B108 - B306 - B307 - B313 - B314 - B315 - B316 - B317 - B318 - B319 - B320 - B325 - B601 - B602 - B604 - B608 - B609 shaiu-pyseventeentrack-75cf6d6/.codeclimate.yml000066400000000000000000000003401502545400500217100ustar00rootroot00000000000000--- engines: duplication: enabled: true config: languages: - python fixme: enabled: true radon: enabled: true ratings: paths: - "**.py" exclude_paths: - dist/ - docs/ - tests/ shaiu-pyseventeentrack-75cf6d6/.coveragerc000066400000000000000000000001061502545400500207570ustar00rootroot00000000000000[run] source = pyseventeentrack omit = pyseventeentrack/track.py shaiu-pyseventeentrack-75cf6d6/.github/000077500000000000000000000000001502545400500202015ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/.github/ISSUE_TEMPLATE/000077500000000000000000000000001502545400500223645ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007641502545400500250650ustar00rootroot00000000000000--- 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. shaiu-pyseventeentrack-75cf6d6/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006471502545400500261200ustar00rootroot00000000000000--- 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. shaiu-pyseventeentrack-75cf6d6/.github/labeler.yml000066400000000000000000000003711502545400500223330ustar00rootroot00000000000000--- # 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 shaiu-pyseventeentrack-75cf6d6/.github/pull_request_template.md000066400000000000000000000006131502545400500251420ustar00rootroot00000000000000**Describe what the PR does:** **Does this fix a specific issue?** Fixes https://github.com/shaiu/pyseventeentrack/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`. shaiu-pyseventeentrack-75cf6d6/.github/release-drafter.yml000066400000000000000000000007141502545400500237730ustar00rootroot00000000000000--- 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 shaiu-pyseventeentrack-75cf6d6/.github/stale.yml000066400000000000000000000035571502545400500220460ustar00rootroot00000000000000--- # 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. shaiu-pyseventeentrack-75cf6d6/.github/workflows/000077500000000000000000000000001502545400500222365ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/.github/workflows/ci-cd.yml000066400000000000000000000012211502545400500237340ustar00rootroot00000000000000# .github/workflows/ci-cd.yml on: release: types: [created] jobs: pypi-publish: name: Upload release to PyPI runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel poetry - name: Build package run: | poetry build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 shaiu-pyseventeentrack-75cf6d6/.github/workflows/ci.yaml000066400000000000000000000122451502545400500235210ustar00rootroot00000000000000--- name: CI on: pull_request: branches: - dev - master push: branches: - dev - master env: CACHE_VERSION: 1 DEFAULT_PYTHON: "3.12" PRE_COMMIT_CACHE: ~/.cache/pre-commit UV_CACHE_DIR: /tmp/uv-cache jobs: info: name: Collect information & changes data outputs: pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} python_versions: ${{ steps.info.outputs.python_versions }} runs-on: ubuntu-22.04 steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.7 - name: Generate partial Python venv restore key id: generate_python_cache_key run: | echo key=venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }} >> $GITHUB_OUTPUT - name: Generate partial pre-commit restore key id: generate_pre-commit_cache_key run: >- echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT pre-commit: name: Prepare pre-commit base runs-on: ubuntu-22.04 needs: - info steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.1.1 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v4.2.3 with: path: venv key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version pip install "$(grep '^uv' < requirements_test.txt)" uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | . venv/bin/activate pre-commit install-hooks lint-ruff-format: name: Check ruff-format runs-on: ubuntu-22.04 needs: - info - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v4.1.7 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5.1.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv uses: actions/cache/restore@v4.2.3 with: path: venv fail-on-cache-miss: true key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache/restore@v4.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true key: >- ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Run ruff-format run: | . venv/bin/activate pre-commit run --hook-stage manual ruff-format --all-files --show-diff-on-failure env: RUFF_OUTPUT_FORMAT: github test: name: Tests runs-on: ubuntu-latest needs: - lint-ruff-format strategy: matrix: python-version: - "3.11" - "3.12" steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 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 needs: - test steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ env.DEFAULT_PYTHON }} 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=pyseventeentrack tests - uses: codecov/codecov-action@v4.0.1 with: token: ${{ secrets.CODECOV_TOKEN }} shaiu-pyseventeentrack-75cf6d6/.gitignore000066400000000000000000000001561502545400500206330ustar00rootroot00000000000000.coverage .mypy_cache .tox/ .venv __pycache__/ pyseventeentrack.egg-info coverage.xml poetry.lock tags .vscodeshaiu-pyseventeentrack-75cf6d6/.pre-commit-config.yaml000066400000000000000000000026511502545400500231260ustar00rootroot00000000000000--- repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.5.1 hooks: # Run the linter. - id: ruff args: [ --fix ] # Run the formatter. - id: ruff-format - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - id: bandit args: - --quiet - --format=custom - --configfile=.bandit.yaml files: ^pyseventeentrack/.+\.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://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: local hooks: # Run mypy through our wrapper script in order to get the possible # pyenv and/or virtualenv activated; it may not have been e.g. if # committing from a GUI tool that was not launched from an activated # shell. - id: mypy name: mypy entry: script/run-in-env.sh mypy language: script types_or: [ python, pyi ] require_serial: true - id: pylint name: pylint entry: script/run-in-env.sh pylint -j 0 language: script types_or: [ python, pyi ] shaiu-pyseventeentrack-75cf6d6/.rtx.toml000066400000000000000000000000701502545400500204260ustar00rootroot00000000000000[tools] python = { version="3.11", virtualenv=".venv" } shaiu-pyseventeentrack-75cf6d6/.whitesource000066400000000000000000000003251502545400500212030ustar00rootroot00000000000000{ "scanSettings": { "baseBranches": [] }, "checkRunSettings": { "vulnerableCheckRunConclusionLevel": "failure", "displayMode": "diff" }, "issueSettings": { "minSeverityLevel": "LOW" } }shaiu-pyseventeentrack-75cf6d6/AUTHORS.md000066400000000000000000000001321502545400500203040ustar00rootroot00000000000000# Contributions to `pyseventeentrack` ## Owners - Shai Ungar (https://github.com/shaiu) shaiu-pyseventeentrack-75cf6d6/LICENSE000066400000000000000000000020531502545400500176460ustar00rootroot00000000000000MIT License Copyright (c) 2018 Shai Ungar 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. shaiu-pyseventeentrack-75cf6d6/README.md000066400000000000000000000101041502545400500201140ustar00rootroot00000000000000# πŸ“¦ pyseventeentrack: A Simple Python API for 17track.net [![CI](https://github.com/shaiu/pyseventeentrack/workflows/CI/badge.svg)](https://github.com/shaiu/pyseventeentrack/actions) [![PyPi](https://img.shields.io/pypi/v/pyseventeentrack.svg)](https://pypi.python.org/pypi/pyseventeentrack) [![Version](https://img.shields.io/pypi/pyversions/pyseventeentrack.svg)](https://pypi.python.org/pypi/pyseventeentrack) [![License](https://img.shields.io/pypi/l/pyseventeentrack.svg)](https://github.com/shaiu/pyseventeentrack/blob/master/LICENSE) [![Code Coverage](https://codecov.io/gh/shaiu/pyseventeentrack/graph/badge.svg?token=3PSG5EV6F7)](https://codecov.io/gh/shaiu/pyseventeentrack) [![Maintainability](https://api.codeclimate.com/v1/badges/af60d65b69d416136fc9/maintainability)](https://codeclimate.com/github/shaiu/pyseventeentrack/maintainability) [![Say Thanks](https://img.shields.io/badge/SayThanks-!-1EAEDB.svg)](https://saythanks.io/to/shaiu) `pyseventeentrack` 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 `pyseventeentrack` is currently supported on: * Python 3.11 * Python 3.12 # Installation ```python pip install pyseventeentrack ``` # Usage ```python import asyncio from aiohttp import ClientSession from pyseventeentrack 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() # >>> [pyseventeentrack.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 pyseventeentrack 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/shaiu/pyseventeentrack/issues) or [initiate a discussion on one](https://github.com/shaiu/pyseventeentrack/issues/new). 2. [Fork the repository](https://github.com/shaiu/pyseventeentrack/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! shaiu-pyseventeentrack-75cf6d6/examples/000077500000000000000000000000001502545400500204575ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/examples/__init__.py000066400000000000000000000000271502545400500225670ustar00rootroot00000000000000"""Define examples.""" shaiu-pyseventeentrack-75cf6d6/examples/test_api.py000066400000000000000000000020011502545400500226320ustar00rootroot00000000000000"""Run an example script to quickly test a 17track.net account.""" import asyncio import logging from aiohttp import ClientSession from pyseventeentrack import Client from pyseventeentrack.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()) shaiu-pyseventeentrack-75cf6d6/pyproject.toml000066400000000000000000000031701502545400500215560ustar00rootroot00000000000000[build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyseventeentrack" version = "1.1.1" description = "A Simple Python API for 17track.net" readme = "README.md" authors = ["Shai Ungar "] license = "MIT" repository = "https://github.com/shaiu/pyseventeentrack" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] [tool.poetry.dependencies] aiohttp = ">=3.9.5" attrs = ">=19.3" python = "^3.9.0" pytz = ">=2021.1" cryptography = ">=45.0.3" [tool.poetry.dev-dependencies] aresponses = "^3.0.0" pre-commit = "^2.0.1" pytest = "^8.0.0" pytest-aiohttp = "^1.0.0" pytest-cov = "^3.0.0" [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. select = ["E4", "E7", "E9", "F"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto" shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/000077500000000000000000000000001502545400500222335ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/__init__.py000066400000000000000000000001071502545400500243420ustar00rootroot00000000000000"""Define module-level imports.""" from .client import Client # noqa shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/client.py000066400000000000000000000033441502545400500240670ustar00rootroot00000000000000"""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( # pylint: disable=too-many-arguments 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}") from err finally: if not use_running_session: await session.close() shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/encrypt.py000066400000000000000000000025011502545400500242670ustar00rootroot00000000000000"""This module handles the RSA encryption for the 17track API.""" import base64 from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend PUBLIC_KEY_PEM = """ -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Y5iQN3VNofXPtZXYZe9 75ojD+Gb+yPBrlrKj2t4XvYHE+pYmFzGPTvDmB3t1OfHujdgBVc3VJBSFsHezm4kz 4iqIChHLKeFvuux+i/Uq+zo1QdC72qteUMHF925qPLe3xU/QJj6BFR9mA4VrUwXt8 eWI58ozizBH31PclxiPNT+yYYXRUV3QJvbZ+FJGL3gYUu1k44WILQzDZfBJMRf+My LHZew6+XtYB8E2+PXc/R7TtLPcMDsPvARrAJMhu5b+yfwJM1zOFChAz3U1w0Zkj1y VJQaa/aktmfd0KyFkU2M0xNpPIIcQkywUGCMZEmJkEIyyV/I+H/NvQ+qqU5llwIDAQAB -----END PUBLIC KEY----- """ def rsa_encrypt(password: str) -> str: """ Encrypts a string using the public key with PKCS1v15 padding. Args: password: The string to be encrypted. Returns: A base64 encoded string of the encrypted data. """ public_key = serialization.load_pem_public_key( PUBLIC_KEY_PEM.encode(), backend=default_backend() ) if not isinstance(public_key, rsa.RSAPublicKey): raise TypeError("The public key is not a valid RSA key.") encrypted = public_key.encrypt(password.encode(), padding.PKCS1v15()) return base64.b64encode(encrypted).decode() shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/errors.py000066400000000000000000000004701502545400500241220ustar00rootroot00000000000000"""Define module exceptions.""" class SeventeenTrackError(Exception): """Define a base error.""" class InvalidTrackingNumberError(SeventeenTrackError): """Define an error for an invalid tracking number.""" class RequestError(SeventeenTrackError): """Define an error for HTTP request errors.""" shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/package.py000066400000000000000000000171121502545400500242020ustar00rootroot00000000000000"""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: "Alert", } PACKAGE_TYPE_MAP: Dict[int, str] = { 0: "Unknown", 1: "Small Registered Package", 2: "Registered Parcel", 3: "EMS Package", } @attr.s(frozen=True) class Package: # pylint: disable=too-few-public-methods,too-many-instance-attributes """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) shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/profile.py000066400000000000000000000157221502545400500242540ustar00rootroot00000000000000"""Define interaction with a user profile.""" import json import logging from typing import Callable, Coroutine, List, Optional, Union from .encrypt import rsa_encrypt 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/user-api/v1/sign-in-by-password" 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={ "source": 0, "account": email, "password": rsa_encrypt(password), }, ) _LOGGER.debug("Login response: %s", login_resp) account_data = login_resp.get("data") if not account_data or not account_data.get("gid"): _LOGGER.error( "Login response successful (code 0) but 'gid' is missing or empty in 'data': %s", login_resp, ) return False self.account_id = account_data["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 as err: raise InvalidTrackingNumberError( f"Recently added package not found by tracking number: {tracking_number}" ) from err _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}") async def archive_package(self, tracking_number: str): """Archive a package by tracking number.""" packages = await self.packages() 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) archive_resp: dict = await self._request( "post", API_URL_BUYER, json={ "version": "1.0", "method": "SetTrackArchived", "param": {"TrackInfoIds": [internal_id]}, }, ) _LOGGER.debug("Archive package response: %s", archive_resp) code = archive_resp.get("Code") if code != 0: raise RequestError(f"Non-zero status code in response: {code}") shaiu-pyseventeentrack-75cf6d6/pyseventeentrack/track.py000066400000000000000000000032501502545400500237110ustar00rootroot00000000000000"""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 shaiu-pyseventeentrack-75cf6d6/renovate.json000066400000000000000000000002611502545400500213560ustar00rootroot00000000000000{ "extends": [ "config:base", "group:all", "schedule:monthly", ":disableDependencyDashboard" ], "labels": ["dependencies"], "separateMinorPatch": true } shaiu-pyseventeentrack-75cf6d6/requirements_test.txt000066400000000000000000000023051502545400500231640ustar00rootroot00000000000000aiohttp==3.9.5 aioresponses==0.7.6 aiosignal==1.3.1 aresponses==3.0.0 astroid==3.2.3 attrs==23.2.0 build==1.2.1 CacheControl==0.14.0 certifi==2024.7.4 cffi==1.16.0 cfgv==3.4.0 charset-normalizer==3.3.2 cleo==2.1.0 coverage==7.5.4 crashtest==0.4.1 cryptography==45.0.3 dill==0.3.8 distlib==0.3.8 dulwich==0.21.7 fastjsonschema==2.20.0 filelock==3.15.4 frozenlist==1.4.1 identify==2.6.0 idna==3.7 iniconfig==2.0.0 installer==0.7.0 isort==5.13.2 jaraco.classes==3.4.0 keyring==24.3.1 mccabe==0.7.0 more-itertools==10.3.0 msgpack==1.0.8 multidict==6.0.5 mypy==1.10.1 mypy-extensions==1.0.0 nodeenv==1.9.1 packaging==24.1 pexpect==4.9.0 pkginfo==1.11.1 platformdirs==4.2.2 pluggy==1.5.0 poetry==1.8.3 poetry-core==1.9.0 poetry-plugin-export==1.8.0 pre-commit==3.7.1 ptyprocess==0.7.0 py==1.11.0 pycparser==2.22 pylint==3.2.5 pyproject_hooks==1.1.0 pytest==8.2.2 pytest-aiohttp==0.3.0 pytest-asyncio==0.23.7 pytest-cov==2.8.1 pytz==2024.1 PyYAML==6.0.1 rapidfuzz==3.9.4 requests==2.32.3 requests-toolbelt==1.0.0 ruff==0.5.1 shellingham==1.5.4 toml==0.10.2 tomlkit==0.13.0 trove-classifiers==2024.7.2 types-pytz==2024.1.0.20240417 typing_extensions==4.12.2 urllib3==2.2.2 uv==0.2.24 virtualenv==20.26.3 xattr==1.1.0 yarl==1.9.4 shaiu-pyseventeentrack-75cf6d6/script/000077500000000000000000000000001502545400500201455ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/script/release000077500000000000000000000024711502545400500215170ustar00rootroot00000000000000#!/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 shaiu-pyseventeentrack-75cf6d6/script/run-in-env.sh000077500000000000000000000012451502545400500225040ustar00rootroot00000000000000#!/usr/bin/env sh set -eu # Used in venv activate script. # Would be an error if undefined. OSTYPE="${OSTYPE-}" # Activate pyenv and virtualenv if present, then run the specified command # pyenv, pyenv-virtualenv if [ -s .python-version ]; then PYENV_VERSION=$(head -n 1 .python-version) export PYENV_VERSION fi if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then . "${VIRTUAL_ENV}/bin/activate" else # other common virtualenvs my_path=$(git rev-parse --show-toplevel) for venv in venv .venv .; do if [ -f "${my_path}/${venv}/bin/activate" ]; then . "${my_path}/${venv}/bin/activate" break fi done fi exec "$@" shaiu-pyseventeentrack-75cf6d6/script/setup000077500000000000000000000002151502545400500212310ustar00rootroot00000000000000#!/bin/sh set -e # Install all dependencies: pip3 install poetry poetry lock poetry install # Install pre-commit hooks: pre-commit install shaiu-pyseventeentrack-75cf6d6/script/test000077500000000000000000000002131502545400500210460ustar00rootroot00000000000000#!/bin/sh set -e # Run pytest with coverage: py.test -s --verbose --cov-report term-missing --cov-report xml --cov=pyseventeentrack tests shaiu-pyseventeentrack-75cf6d6/tests/000077500000000000000000000000001502545400500200035ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/tests/__init__.py000066400000000000000000000000341502545400500221110ustar00rootroot00000000000000"""Define package tests.""" shaiu-pyseventeentrack-75cf6d6/tests/common.py000066400000000000000000000010451502545400500216450ustar00rootroot00000000000000"""Define common test utilities.""" import os import pytest from aresponses import ResponsesMockServer # type: ignore TEST_EMAIL = "user@email.com" TEST_PASSWORD = "password" @pytest.fixture(autouse=True) async def aresponses(loop): """replace aresponses""" async with ResponsesMockServer(loop=loop) as server: yield server 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() shaiu-pyseventeentrack-75cf6d6/tests/fixtures/000077500000000000000000000000001502545400500216545ustar00rootroot00000000000000shaiu-pyseventeentrack-75cf6d6/tests/fixtures/__init__.py000066400000000000000000000000341502545400500237620ustar00rootroot00000000000000"""Define test fixtures.""" shaiu-pyseventeentrack-75cf6d6/tests/fixtures/add_package_existing_response.json000066400000000000000000000004621502545400500306040ustar00rootroot00000000000000{ "Json": { "Items": [ { "TrackInfoId": "0", "TrackNo": "1234567890987654321", "ResultCode": -11010101 } ], "CanTrackNum": 40, "UseTrackNum": 4, "SuccessNum": 0, "ErrorNum": 1 }, "Code": -11010101, "Message": "Number(s) already exists." }shaiu-pyseventeentrack-75cf6d6/tests/fixtures/add_package_response.json000066400000000000000000000003701502545400500266700ustar00rootroot00000000000000{ "Json": { "Items": [ { "TrackInfoId": "0", "TrackNo": "1234567890987654321", "ResultCode": 0 } ], "CanTrackNum": 40, "UseTrackNum": 3, "SuccessNum": 1, "ErrorNum": 0 }, "Code": 0 }shaiu-pyseventeentrack-75cf6d6/tests/fixtures/archive_package_response.json000066400000000000000000000000171502545400500275570ustar00rootroot00000000000000{ "Code": 0 }shaiu-pyseventeentrack-75cf6d6/tests/fixtures/archive_package_response_failure_response.json000066400000000000000000000000271502545400500332050ustar00rootroot00000000000000{"Json":{},"Code":-100}shaiu-pyseventeentrack-75cf6d6/tests/fixtures/authentication_failure_response.json000066400000000000000000000001101502545400500312030ustar00rootroot00000000000000{ "Code": -6, "Message": "You haven't logged in for a long time." } shaiu-pyseventeentrack-75cf6d6/tests/fixtures/authentication_success_response.json000066400000000000000000000003321502545400500312320ustar00rootroot00000000000000{ "data": { "FUserRole": 4, "FNickname": "John Doe", "FEmail": "john.doe@company.com", "FLanguage": "en", "FCountry": 100, "FPhoto": 10001, "gid": "1234567890987654321" }, "code": 0 } shaiu-pyseventeentrack-75cf6d6/tests/fixtures/packages_response.json000066400000000000000000000044101502545400500262420ustar00rootroot00000000000000{ "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" } ] }shaiu-pyseventeentrack-75cf6d6/tests/fixtures/packages_response_with_unknown_state.json000066400000000000000000000026321502545400500322600ustar00rootroot00000000000000{ "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 } ] }shaiu-pyseventeentrack-75cf6d6/tests/fixtures/set_friendly_name_failure_response.json000066400000000000000000000000271502545400500316620ustar00rootroot00000000000000{"Json":{},"Code":-100}shaiu-pyseventeentrack-75cf6d6/tests/fixtures/set_friendly_name_response.json000066400000000000000000000000241502545400500301500ustar00rootroot00000000000000{"Json":{},"Code":0}shaiu-pyseventeentrack-75cf6d6/tests/fixtures/summary_response.json000066400000000000000000000012101502545400500261540ustar00rootroot00000000000000{ "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 } shaiu-pyseventeentrack-75cf6d6/tests/test_client.py000066400000000000000000000012061502545400500226710ustar00rootroot00000000000000"""Define tests for the client object.""" import aiohttp import pytest from pyseventeentrack import Client from pyseventeentrack.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") # pylint: disable=protected-access shaiu-pyseventeentrack-75cf6d6/tests/test_profile.py000066400000000000000000000361451502545400500230650ustar00rootroot00000000000000"""Define tests for the client object.""" import aiohttp import pytest from pyseventeentrack import Client from pyseventeentrack.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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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["Alert"] == 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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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", "/user-api/v1/sign-in-by-password", "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") @pytest.mark.asyncio async def test_archive_package(aresponses): """Test archiving a package.""" aresponses.add( "user.17track.net", "/user-api/v1/sign-in-by-password", "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), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("archive_package_response.json"), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.profile.login(TEST_EMAIL, TEST_PASSWORD) res = await client.profile.archive_package("1234567890987654321") assert res is None @pytest.mark.asyncio async def test_archive_package_non_existing(aresponses): """Test archiving a non existing package.""" aresponses.add( "user.17track.net", "/user-api/v1/sign-in-by-password", "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), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("archive_package_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.archive_package("1234567890987654321111") @pytest.mark.asyncio async def test_archive_package_error_response(aresponses): """Test archiving a package with failed response.""" aresponses.add( "user.17track.net", "/user-api/v1/sign-in-by-password", "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), ) aresponses.add( "buyer.17track.net", "/orderapi/call", "post", aresponses.Response( text=load_fixture("archive_package_response_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.archive_package("1234567890987654321")