Skip to content

Commit 8747d14

Browse files
committed
Minor fixes and improvements recommended during review
1 parent 719c65b commit 8747d14

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

docs/MIGRATE_FROM_V1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Migrating from fmd_api v1 to v2```markdown
1+
# Migrating from fmd_api v1 to v2
22

33
# Migrating from fmd_api v1 (module style) to v2 (FmdClient + Device)
44

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Release v2.0.0: Async client, TLS enforcement, retries/backoff, and CI upgrades
2+
3+
## Summary
4+
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.
5+
6+
## Highlights
7+
- **Async client**
8+
- New `FmdClient` with an async factory (`await FmdClient.create(...)`) and async context manager (`async with ...`).
9+
- Clean session lifecycle management and connection pooling controls.
10+
- **TLS/SSL**
11+
- HTTPS-only `base_url` enforcement (rejects plain HTTP).
12+
- Configurable validation: `ssl=False` (dev only) or pass a custom `ssl.SSLContext`.
13+
- Certificate pinning example included in docs.
14+
- **Reliability**
15+
- Timeouts applied across all HTTP requests.
16+
- Retries with exponential backoff and optional jitter for 5xx and connection errors.
17+
- 429 rate-limit handling with Retry-After support.
18+
- Safe behavior for command POSTs (no unsafe retries).
19+
- **Features**
20+
- Client-side export to ZIP (locations + pictures).
21+
- Device helper with convenience actions (e.g., request location, play sound, take picture).
22+
- **Safety and ergonomics**
23+
- Sanitized logging (no sensitive payloads); token masking helper.
24+
- Typed package (`py.typed`) and mypy-clean.
25+
- **CI/CD**
26+
- GitHub Actions: lint (flake8), type-check (mypy), unit tests matrix (Ubuntu/Windows; Py 3.8–3.12).
27+
- Coverage with branch analysis and Codecov upload + badges.
28+
- Publish workflows for TestPyPI (non-main) and PyPI (main or Release).
29+
30+
## Breaking changes
31+
- API surface moved to async:
32+
- Before (v1, sync): `client = FmdApi(...); client.authenticate(...); client.get_all_locations()`
33+
- Now (v2, async):
34+
- `client = await FmdClient.create(base_url, fmd_id, password, ...)`
35+
- `await client.get_locations(...)`, `await client.get_pictures(...)`, `await client.send_command(...)`
36+
- Prefer: `async with FmdClient.create(...) as client: ...`
37+
- Transport requirements:
38+
- `base_url` must be HTTPS; plain HTTP raises `ValueError`.
39+
- Python versions:
40+
- Targets Python 3.8+ (3.7 removed from classifiers).
41+
42+
## Migration guide
43+
- Replace `FmdApi` usage with the async `FmdClient`:
44+
- Use `await FmdClient.create(...)` and `async with` for safe resource management.
45+
- Update all calls to await the async methods.
46+
- TLS and self-signed setups:
47+
- For dev-only scenarios: pass `ssl=False`.
48+
- For proper self-signed use: pass a custom `ssl.SSLContext`.
49+
- For high-security setups: consider the certificate pinning example in README.
50+
- Connection tuning:
51+
- Optional: `conn_limit`, `conn_limit_per_host`, `keepalive_timeout` on the TCPConnector via client init.
52+
53+
## Error handling and semantics
54+
- 401 triggers one automatic re-authenticate then retries the request.
55+
- 429 honors Retry-After, otherwise uses exponential backoff with jitter.
56+
- Transient 5xx/connection errors are retried (except unsafe command POST replays).
57+
- Exceptions are normalized to `FmdApiException` where appropriate; messages mask sensitive data.
58+
59+
## Documentation and examples
60+
- README: TLS/self-signed guidance, warnings, and certificate pinning example.
61+
- Debugging: `pin_cert_example.py` demonstrates secure pinning and avoids CLI secrets.
62+
63+
## CI/CD and release automation
64+
- Tests: unit suite expanded; functional tests run when credentials are available.
65+
- Coverage: ~83% overall; XML + branch coverage uploaded to Codecov (badges included).
66+
- Workflows:
67+
- `test.yml`: runs on push/PR for all branches (lint, mypy, unit tests matrix, coverage, optional functional tests).
68+
- `publish.yml`: builds on push/releases; publishes to TestPyPI for non-main pushes, PyPI on main or release.
69+
70+
## Checklist
71+
- [x] All unit tests pass
72+
- [x] Flake8 clean
73+
- [x] Mypy clean
74+
- [x] Coverage collected and uploaded
75+
- [x] README/docs updated (TLS, pinning, badges)
76+
- [x] Packaging: sdist and wheel built; publish workflows configured
77+
78+
## Notes
79+
- Use `ssl=False` sparingly and never in production.
80+
- Consider adding Dependabot and security scanning in a follow-up.
81+
- A `CHANGELOG.md` entry for 2.0.0 is recommended if not already included.
82+
83+
> BREAKING CHANGE: v2 switches to an async client, enforces HTTPS, and drops Python 3.7; synchronous v1 usage must be migrated as noted above.

tests/functional/test_locations.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ async def main():
2525
if len(sys.argv) > 1:
2626
try:
2727
num = int(sys.argv[1])
28-
except BaseException:
29-
pass
28+
except (ValueError, TypeError):
29+
# Ignore invalid CLI value; keep default of -1 (fetch all)
30+
num = -1
3031
from fmd_api import FmdClient
3132

3233
client = await FmdClient.create(creds["BASE_URL"], creds["FMD_ID"], creds["PASSWORD"])

tests/utils/read_credentials.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ def read_credentials(path: Optional[Union[str, Path]] = None) -> Dict[str, str]:
2424
if "=" in ln:
2525
k, v = ln.split("=", 1)
2626
creds[k.strip()] = v.strip()
27-
# fallback to environment for keys not provided
27+
# Fallback to environment for keys not provided.
28+
# Only accept non-empty values to avoid silently using empty strings.
2829
for k in ("BASE_URL", "FMD_ID", "PASSWORD", "DEVICE_ID"):
29-
if k not in creds and os.getenv(k):
30-
creds[k] = os.getenv(k)
30+
if k not in creds:
31+
val = os.getenv(k)
32+
if val: # skip None and empty strings
33+
creds[k] = val
3134
return creds

0 commit comments

Comments
 (0)