Skip to content

Commit 9e231b8

Browse files
authored
Release v2: Async client, TLS enforcement, retries/backoff, and CI upgrades
## Summary This major release delivers a production-ready async client with robust TLS handling, resilient request logic (timeouts, retries, rate limits), and a fully wired CI pipeline (lint, type-check, tests, coverage). It replaces the legacy synchronous v1 client and prepares the project for Home Assistant and broader integration. ## Highlights - **Async client** - New `FmdClient` with an async factory (`await FmdClient.create(...)`) and async context manager (`async with ...`). - Clean session lifecycle management and connection pooling controls. - **TLS/SSL** - HTTPS-only `base_url` enforcement (rejects plain HTTP). - Configurable validation: `ssl=False` (dev only) or pass a custom `ssl.SSLContext`. - Certificate pinning example included in docs. - **Reliability** - Timeouts applied across all HTTP requests. - Retries with exponential backoff and optional jitter for 5xx and connection errors. - 429 rate-limit handling with Retry-After support. - Safe behavior for command POSTs (no unsafe retries). - **Features** - Client-side export to ZIP (locations + pictures). - Device helper with convenience actions (e.g., request location, play sound, take picture). - **Safety and ergonomics** - Sanitized logging (no sensitive payloads); token masking helper. - Typed package (`py.typed`) and mypy-clean. - **CI/CD** - GitHub Actions: lint (flake8), type-check (mypy), unit tests matrix (Ubuntu/Windows; Py 3.8–3.12). - Coverage with branch analysis and Codecov upload + badges. - Publish workflows for TestPyPI (non-main) and PyPI (main or Release). ## Breaking changes - API surface moved to async: - Before (v1, sync): `client = FmdApi(...); client.authenticate(...); client.get_all_locations()` - Now (v2, async): - `client = await FmdClient.create(base_url, fmd_id, password, ...)` - `await client.get_locations(...)`, `await client.get_pictures(...)`, `await client.send_command(...)` - Prefer: `async with FmdClient.create(...) as client: ...` - Transport requirements: - `base_url` must be HTTPS; plain HTTP raises `ValueError`. - Python versions: - Targets Python 3.8+ (3.7 removed from classifiers). ## Migration guide - Replace `FmdApi` usage with the async `FmdClient`: - Use `await FmdClient.create(...)` and `async with` for safe resource management. - Update all calls to await the async methods. - TLS and self-signed setups: - For dev-only scenarios: pass `ssl=False`. - For proper self-signed use: pass a custom `ssl.SSLContext`. - For high-security setups: consider the certificate pinning example in README. - Connection tuning: - Optional: `conn_limit`, `conn_limit_per_host`, `keepalive_timeout` on the TCPConnector via client init. ## Error handling and semantics - 401 triggers one automatic re-authenticate then retries the request. - 429 honors Retry-After, otherwise uses exponential backoff with jitter. - Transient 5xx/connection errors are retried (except unsafe command POST replays). - Exceptions are normalized to `FmdApiException` where appropriate; messages mask sensitive data. ## Documentation and examples - README: TLS/self-signed guidance, warnings, and certificate pinning example. - Debugging: `pin_cert_example.py` demonstrates secure pinning and avoids CLI secrets. ## CI/CD and release automation - Tests: unit suite expanded; functional tests run when credentials are available. - Coverage: ~83% overall; XML + branch coverage uploaded to Codecov (badges included). - Workflows: - `test.yml`: runs on push/PR for all branches (lint, mypy, unit tests matrix, coverage, optional functional tests). - `publish.yml`: builds on push/releases; publishes to TestPyPI for non-main pushes, PyPI on main or release. ## Checklist - [x] All unit tests pass - [x] Flake8 clean - [x] Mypy clean - [x] Coverage collected and uploaded - [x] README/docs updated (TLS, pinning, badges) - [x] Packaging: sdist and wheel built; publish workflows configured ## Notes - Use `ssl=False` sparingly and never in production. - Consider adding Dependabot and security scanning in a follow-up. - A `CHANGELOG.md` entry for 2.0.0 is recommended if not already included. > BREAKING CHANGE: v2 switches to an async client, enforces HTTPS, and drops Python 3.7; synchronous v1 usage must be migrated as noted above.
2 parents af1801a + 8747d14 commit 9e231b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5550
-2766
lines changed

.coverage

52 KB
Binary file not shown.

.flake8

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[flake8]
2+
max-line-length = 120
3+
extend-ignore = E203, W503
4+
exclude =
5+
.git,
6+
__pycache__,
7+
.venv,
8+
venv,
9+
build,
10+
dist,
11+
*.egg-info,
12+
.pytest_cache,
13+
.mypy_cache
14+
per-file-ignores =
15+
# Functional tests import after path manipulation
16+
tests/functional/*.py: E402

.github/workflows/publish.yml

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
name: Publish Python Package
22

3-
# 1. Trigger the workflow when a new GitHub Release is published
3+
# Trigger on:
4+
# - pushes to any branch (we'll publish to TestPyPI for non-main branches and to PyPI for main)
5+
# - published GitHub Releases (keep existing behavior for canonical releases)
46
on:
7+
push:
8+
branches: ["**"]
59
release:
610
types: [published]
711

12+
permissions:
13+
contents: read
14+
id-token: write
15+
816
jobs:
917
build_sdist_and_wheel:
1018
name: Build distribution 📦
1119
runs-on: ubuntu-latest
20+
outputs:
21+
dist-path: ${{ steps.upload.outputs.artifact-path }}
1222
steps:
1323
- name: Checkout repository
1424
uses: actions/checkout@v4
@@ -18,70 +28,69 @@ jobs:
1828
with:
1929
python-version: "3.x"
2030

21-
# Install the 'build' tool, which creates the distribution files
22-
- name: Install build dependencies
23-
run: python -m pip install build
31+
- name: Install build tooling
32+
run: python -m pip install --upgrade pip build
2433

25-
# Create source and wheel distribution archives in the 'dist/' directory
2634
- name: Build distributions
27-
run: python -m build
35+
run: python -m build --sdist --wheel
2836

29-
# Store the built artifacts for the next jobs to download
3037
- name: Upload distribution artifacts
38+
id: upload
3139
uses: actions/upload-artifact@v4
3240
with:
3341
name: python-package-distributions
3442
path: dist/
3543

36-
# --- PyPI Release Job (Secure, using OIDC) ---
37-
pypi_publish:
38-
name: Publish to PyPI
39-
needs: [build_sdist_and_wheel] # Ensure the build job succeeds first
44+
# Publish from pushes to non-main branches -> TestPyPI
45+
publish_testpypi:
46+
name: Publish to TestPyPI (branches except main)
4047
runs-on: ubuntu-latest
41-
# Only publish to PyPI for full releases (not pre-releases)
42-
if: github.event.release.prerelease == false
43-
44-
# 2. Use the environment name you configured in PyPI (Optional, but recommended)
45-
environment: pypi
46-
47-
# 3. CRITICAL: This is the mandatory permission for Trusted Publishers (OIDC)
48+
needs: build_sdist_and_wheel
49+
# Only run for pushes (not releases) and only for branches that are NOT main
50+
if: |
51+
github.event_name == 'push' &&
52+
startsWith(github.ref, 'refs/heads/') &&
53+
github.ref != 'refs/heads/main'
54+
environment: testpypi
4855
permissions:
49-
id-token: write
50-
contents: read # Required to read the repository contents
56+
id-token: write
57+
contents: read
5158

5259
steps:
53-
- name: Download all the dists
60+
- name: Download dists
5461
uses: actions/download-artifact@v4
5562
with:
5663
name: python-package-distributions
5764
path: dist/
5865

59-
# 4. The PyPA action handles the OIDC token exchange and Twine upload securely
60-
- name: Publish package distributions to PyPI
66+
- name: Publish package distributions to TestPyPI
6167
uses: pypa/gh-action-pypi-publish@release/v1
62-
63-
# --- TestPyPI Release Job (Optional, for pre-release testing) ---
64-
testpypi_publish:
65-
name: Publish to TestPyPI
66-
needs: [build_sdist_and_wheel]
67-
runs-on: ubuntu-latest
68-
69-
# Use the environment name you configured in TestPyPI
70-
environment: testpypi
68+
with:
69+
# repository-url directs upload to TestPyPI
70+
repository-url: https://test.pypi.org/legacy/
7171

72+
# Publish from pushes to main OR when a Release is published -> Production PyPI
73+
publish_pypi:
74+
name: Publish to PyPI (main branch or GitHub Release)
75+
runs-on: ubuntu-latest
76+
needs: build_sdist_and_wheel
77+
# Run when:
78+
# - push to main branch, or
79+
# - release published event (keeps existing release-based behavior)
80+
if: |
81+
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
82+
(github.event_name == 'release' && github.event.action == 'published')
83+
environment: pypi
7284
permissions:
7385
id-token: write
7486
contents: read
7587

7688
steps:
77-
- name: Download all the dists
89+
- name: Download dists
7890
uses: actions/download-artifact@v4
7991
with:
8092
name: python-package-distributions
8193
path: dist/
82-
83-
# The 'repository-url' is what makes this target TestPyPI
84-
- name: Publish package distributions to TestPyPI
85-
uses: pypa/gh-action-pypi-publish@release/v1
86-
with:
87-
repository-url: https://test.pypi.org/legacy/
94+
95+
- name: Publish package distributions to PyPI
96+
uses: pypa/gh-action-pypi-publish@release/v1

.github/workflows/test.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
lint:
11+
name: Lint and Type Check
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 2
18+
19+
- name: Set up Python 3.11
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.11"
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e ".[dev]"
28+
29+
- name: Run flake8
30+
run: |
31+
python -m flake8 fmd_api/ tests/ --count --show-source --statistics
32+
33+
- name: Run mypy
34+
run: |
35+
python -m mypy fmd_api/
36+
37+
test:
38+
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
39+
runs-on: ${{ matrix.os }}
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
os: [ubuntu-latest, windows-latest]
44+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
45+
46+
steps:
47+
- name: Checkout code
48+
uses: actions/checkout@v4
49+
with:
50+
fetch-depth: 2
51+
52+
- name: Set up Python ${{ matrix.python-version }}
53+
uses: actions/setup-python@v5
54+
with:
55+
python-version: ${{ matrix.python-version }}
56+
57+
- name: Install dependencies
58+
run: |
59+
python -m pip install --upgrade pip
60+
pip install -e ".[dev]"
61+
62+
- name: Run unit tests with coverage
63+
run: |
64+
python -m pytest tests/unit --cov=fmd_api --cov-branch --cov-report=xml --cov-report=term -v
65+
66+
- name: Upload coverage to Codecov
67+
uses: codecov/codecov-action@v5
68+
with:
69+
files: ./coverage.xml
70+
flags: unittests
71+
name: codecov-${{ matrix.os }}-py${{ matrix.python-version }}
72+
fail_ci_if_error: false
73+
token: ${{ secrets.CODECOV_TOKEN }}
74+
75+
- name: Run functional tests (if credentials available)
76+
# Skip on PRs from forks where secrets aren't available
77+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
78+
continue-on-error: true
79+
run: |
80+
python -m pytest tests/functional -v
81+
env:
82+
# Optional: set up test credentials as repository secrets
83+
FMD_BASE_URL: ${{ secrets.FMD_BASE_URL }}
84+
FMD_ID: ${{ secrets.FMD_ID }}
85+
FMD_PASSWORD: ${{ secrets.FMD_PASSWORD }}

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ __pycache__/
33
*.pyc
44
*.pyo
55
*.pyd
6+
*.zip
7+
8+
# User configuration files
9+
*.env
10+
credentials.txt
11+
*.jpg
612

713
# C extensions
814
*.so
@@ -39,4 +45,7 @@ env/
3945

4046
# FMD Server and android app files
4147
fmd-server/
42-
fmd-android/
48+
fmd-android/
49+
50+
#credentials file
51+
examples/tests/credentials.txt

0 commit comments

Comments
 (0)