Skip to content

Commit e7a7540

Browse files
authored
Merge pull request #7 from devinslick/increase-code-test-coverage
CI job improvements
2 parents 2ba88a2 + 6262bb6 commit e7a7540

File tree

13 files changed

+393
-460
lines changed

13 files changed

+393
-460
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Publish to TestPyPI (Manual)
2+
3+
# Manual workflow to publish to TestPyPI for testing before production release
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
confirm:
8+
description: 'Type "publish" to confirm TestPyPI upload'
9+
required: true
10+
type: string
11+
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
jobs:
17+
build_and_publish:
18+
name: Build and publish to TestPyPI
19+
runs-on: ubuntu-latest
20+
# Only run if user typed "publish" to confirm
21+
if: github.event.inputs.confirm == 'publish'
22+
environment: testpypi
23+
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.x"
32+
33+
- name: Install build tooling
34+
run: python -m pip install --upgrade pip build
35+
36+
- name: Build distributions
37+
run: python -m build --sdist --wheel
38+
39+
- name: Publish package distributions to TestPyPI
40+
uses: pypa/gh-action-pypi-publish@release/v1
41+
with:
42+
repository-url: https://test.pypi.org/legacy/
43+
skip-existing: true
44+
45+
- name: Output installation command
46+
run: |
47+
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
48+
echo "::notice::Package published to TestPyPI. Test with: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple fmd_api==$VERSION"

.github/workflows/publish.yml

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: Publish Python Package
22

33
# 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)
4+
# - pushes to main (publish to PyPI after merge)
5+
# - published GitHub Releases (publish to PyPI for versioned releases)
66
on:
77
push:
8-
branches: ["**"]
8+
branches: [main]
99
release:
1010
types: [published]
1111

@@ -41,34 +41,6 @@ jobs:
4141
name: python-package-distributions
4242
path: dist/
4343

44-
# Publish from pushes to non-main branches -> TestPyPI
45-
publish_testpypi:
46-
name: Publish to TestPyPI (branches except main)
47-
runs-on: ubuntu-latest
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
55-
permissions:
56-
id-token: write
57-
contents: read
58-
59-
steps:
60-
- name: Download dists
61-
uses: actions/download-artifact@v4
62-
with:
63-
name: python-package-distributions
64-
path: dist/
65-
66-
- name: Publish package distributions to TestPyPI
67-
uses: pypa/gh-action-pypi-publish@release/v1
68-
with:
69-
# repository-url directs upload to TestPyPI
70-
repository-url: https://test.pypi.org/legacy/
71-
7244
# Publish from pushes to main OR when a Release is published -> Production PyPI
7345
publish_pypi:
7446
name: Publish to PyPI (main branch or GitHub Release)
@@ -93,4 +65,4 @@ jobs:
9365
path: dist/
9466

9567
- name: Publish package distributions to PyPI
96-
uses: pypa/gh-action-pypi-publish@release/v1
68+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@ fmd-server/
4848
fmd-android/
4949

5050
#credentials file
51-
examples/tests/credentials.txt
51+
examples/tests/credentials.txt

.pre-commit-config.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Pre-commit hooks configuration
2+
# See https://pre-commit.com for more information
3+
repos:
4+
# General file cleanup
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
6+
rev: v5.0.0
7+
hooks:
8+
- id: trailing-whitespace
9+
exclude: '^tests/functional/'
10+
- id: end-of-file-fixer
11+
exclude: '^tests/functional/'
12+
- id: check-yaml
13+
- id: check-toml
14+
- id: check-json
15+
- id: check-added-large-files
16+
args: ['--maxkb=1000']
17+
- id: check-merge-conflict
18+
- id: debug-statements
19+
- id: mixed-line-ending
20+
args: ['--fix=lf']
21+
22+
# Python code formatting with black
23+
- repo: https://github.com/psf/black
24+
rev: 24.10.0
25+
hooks:
26+
- id: black
27+
language_version: python3
28+
29+
# Python linting with flake8
30+
- repo: https://github.com/pycqa/flake8
31+
rev: 7.1.1
32+
hooks:
33+
- id: flake8
34+
args: ['--count', '--show-source', '--statistics']
35+
exclude: '^tests/functional/'
36+
37+
# Python type checking with mypy
38+
- repo: https://github.com/pre-commit/mirrors-mypy
39+
rev: v1.13.0
40+
hooks:
41+
- id: mypy
42+
additional_dependencies:
43+
- types-aiofiles
44+
- aiohttp
45+
- argon2-cffi
46+
- cryptography
47+
args: ['--install-types', '--non-interactive']
48+
exclude: '^tests/'

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Tips:
114114
- `set_ringer_mode("normal|vibrate|silent")`
115115
- `get_device_stats()`
116116

117-
117+
118118
- Low‑level: `decrypt_data_blob(b64_blob)`
119119

120120
- `Device` helper (per‑device convenience)
@@ -171,4 +171,4 @@ This client targets the FMD ecosystem:
171171
- https://gitlab.com/fmd-foss
172172
- Public community instance: https://fmd.nulide.de/
173173

174-
MIT © 2025 Devin Slick
174+
MIT © 2025 Devin Slick

docs/HOME_ASSISTANT_REVIEW.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ async def _make_api_request(self, ..., timeout: int = 30):
7979
- `pyproject.toml`: `2.0.0.dev8` (PEP 440 compliant)
8080
- `_version.py`: `2.0.0-dev8` (uses hyphen instead of dot)
8181

82-
**Location:**
82+
**Location:**
8383
- `pyproject.toml` line 3
8484
- `fmd_api/_version.py` line 1
8585

@@ -143,7 +143,7 @@ if resp.status == 429:
143143
class FmdClient:
144144
async def __aenter__(self):
145145
return self
146-
146+
147147
async def __aexit__(self, exc_type, exc, tb):
148148
await self.close()
149149
```
@@ -191,9 +191,9 @@ async with await FmdClient.create(...) as client:
191191
- Line ~88: Logs may include auth details
192192
- Line ~203: Logs full JSON responses which may contain tokens
193193

194-
**Fix:**
194+
**Fix:**
195195
- Sanitize all log output
196-
- Mask tokens: `log.debug(f"Token: {token[:8]}...")`
196+
- Mask tokens: `log.debug(f"Token: {token[:8]}...")`
197197
- Add guards: `if log.isEnabledFor(logging.DEBUG):`
198198

199199
**HA Rationale:** Security and privacy requirement for production systems.
@@ -364,7 +364,7 @@ async def decrypt_data_blob_async(self, data_b64: str) -> bytes:
364364
async def get_locations(...) -> List[str]:
365365
"""
366366
...
367-
367+
368368
Raises:
369369
AuthenticationError: If authentication fails
370370
FmdApiException: If server returns error
@@ -384,7 +384,7 @@ async def get_locations(...) -> List[str]:
384384

385385
**Location:** Test configuration
386386

387-
**Fix:**
387+
**Fix:**
388388
- Add `pytest-cov` to dev dependencies
389389
- Configure coverage in `pyproject.toml`
390390
- Add coverage reporting to CI workflow

docs/MIGRATE_FROM_V1.md

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -175,16 +175,16 @@ from fmd_api import FmdApi
175175

176176
async def main():
177177
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")
178-
178+
179179
# Request new location
180180
await api.request_location('gps')
181181
await asyncio.sleep(30)
182-
182+
183183
# Get locations
184184
blobs = await api.get_all_locations(1)
185185
location_json = api.decrypt_data_blob(blobs[0])
186186
location = json.loads(location_json)
187-
187+
188188
print(f"Lat: {location['lat']}, Lon: {location['lon']}")
189189
await api.close()
190190

@@ -199,16 +199,16 @@ from fmd_api import FmdClient
199199

200200
async def main():
201201
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
202-
202+
203203
# Request new location
204204
await client.request_location('gps')
205205
await asyncio.sleep(30)
206-
206+
207207
# Get locations
208208
blobs = await client.get_locations(1)
209209
location_json = client.decrypt_data_blob(blobs[0])
210210
location = json.loads(location_json)
211-
211+
212212
print(f"Lat: {location['lat']}, Lon: {location['lon']}")
213213
await client.close()
214214

@@ -223,14 +223,14 @@ from fmd_api import FmdClient, Device
223223
async def main():
224224
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
225225
device = Device(client, "alice")
226-
226+
227227
# Request and get location (simplified)
228228
await client.request_location('gps')
229229
await asyncio.sleep(30)
230-
230+
231231
location = await device.get_location(force=True)
232232
print(f"Lat: {location.lat}, Lon: {location.lon}")
233-
233+
234234
await client.close()
235235

236236
asyncio.run(main())
@@ -244,16 +244,16 @@ from fmd_api import FmdApi, FmdCommands
244244

245245
async def control_device():
246246
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")
247-
247+
248248
# Using constants
249249
await api.send_command(FmdCommands.RING)
250250
await api.send_command(FmdCommands.BLUETOOTH_ON)
251-
251+
252252
# Using convenience methods
253253
await api.toggle_bluetooth(True)
254254
await api.toggle_do_not_disturb(True)
255255
await api.set_ringer_mode('vibrate')
256-
256+
257257
await api.close()
258258
```
259259

@@ -263,16 +263,16 @@ from fmd_api import FmdClient
263263

264264
async def control_device():
265265
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
266-
266+
267267
# Use strings directly (constants removed)
268268
await client.send_command('ring')
269269
await client.send_command('bluetooth on')
270-
270+
271271
# Using convenience methods (renamed from toggle_* to set_*)
272272
await client.set_bluetooth(True)
273273
await client.set_do_not_disturb(True)
274274
await client.set_ringer_mode('vibrate')
275-
275+
276276
await client.close()
277277
```
278278

@@ -283,15 +283,15 @@ from fmd_api import FmdClient, Device
283283
async def control_device():
284284
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
285285
device = Device(client, "alice")
286-
286+
287287
# Use device methods for cleaner API
288288
await device.play_sound()
289-
289+
290290
# Settings still use client
291291
await client.set_bluetooth(True)
292292
await client.set_do_not_disturb(True)
293293
await client.set_ringer_mode('vibrate')
294-
294+
295295
await client.close()
296296
```
297297

@@ -304,13 +304,13 @@ from fmd_api import FmdApi
304304

305305
async def get_history():
306306
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")
307-
307+
308308
blobs = await api.get_all_locations(10)
309309
for blob in blobs:
310310
location_json = api.decrypt_data_blob(blob)
311311
location = json.loads(location_json)
312312
print(f"Date: {location['date']}, Lat: {location['lat']}, Lon: {location['lon']}")
313-
313+
314314
await api.close()
315315
```
316316

@@ -321,11 +321,11 @@ from fmd_api import FmdClient, Device
321321
async def get_history():
322322
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
323323
device = Device(client, "alice")
324-
324+
325325
# Async iterator with automatic decryption
326326
async for location in device.get_history(limit=10):
327327
print(f"Date: {location.date}, Lat: {location.lat}, Lon: {location.lon}")
328-
328+
329329
await client.close()
330330
```
331331

docs/PROPOSAL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Proposal: fmd_api v2 — Device-centric async interface
22

3-
Status: Draft
4-
Author: devinslick (proposal by Copilot Space)
3+
Status: Draft
4+
Author: devinslick (proposal by Copilot Space)
55
Date: 2025-11-01
66

77
## Goals
@@ -286,4 +286,4 @@ This proposal updates the earlier draft to:
286286
- Include explicit methods for taking front and rear photos.
287287
- Drop an in-code legacy compatibility layer and instead provide a small migration README.
288288

289-
If you approve, I will create a branch and a PR that implements the core FmdClient and Device class with the initial methods (authenticate, get_devices, get_device, Device.refresh, Device.get_location, Device.play_sound, Device.take_front_photo, Device.take_rear_photo), plus tests and example usage.
289+
If you approve, I will create a branch and a PR that implements the core FmdClient and Device class with the initial methods (authenticate, get_devices, get_device, Device.refresh, Device.get_location, Device.play_sound, Device.take_front_photo, Device.take_rear_photo), plus tests and example usage.

docs/PROPOSED_BRANCH_AND_STRUCTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ Next steps after branch creation:
4242
5. Iterate on rate-limiter/cache and add streaming helpers for export_data_zip.
4343

4444
If you'd like, I can now generate the initial skeleton files for this branch (client.py, device.py, types.py, exceptions.py, helpers.py, docs/MIGRATE_FROM_V1.md, examples/async_example.py, PROPOSAL.md). Which files would you like me to create first?
45-
```
45+
```

fmd_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.2"
1+
__version__ = "2.0.3"

0 commit comments

Comments
 (0)