Skip to content

Commit 76b4ee0

Browse files
committed
ci: add pre-commit hooks for local linting and refactor publish workflow to PR-based TestPyPI
1 parent a2ba091 commit 76b4ee0

File tree

11 files changed

+197
-277
lines changed

11 files changed

+197
-277
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,4 @@ jobs:
9494
path: dist/
9595

9696
- name: Publish package distributions to PyPI
97-
uses: pypa/gh-action-pypi-publish@release/v1
97+
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/client.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -317,13 +317,9 @@ async def _make_api_request(
317317
continue
318318

319319
# Transient server errors -> retry (except for unsafe command POSTs)
320-
if resp.status in (500, 502, 503, 504) and not (
321-
is_command and method.upper() == "POST"
322-
):
320+
if resp.status in (500, 502, 503, 504) and not (is_command and method.upper() == "POST"):
323321
if attempts_left > 0:
324-
delay = _compute_backoff(
325-
self.backoff_base, backoff_attempt, self.backoff_max, self.jitter
326-
)
322+
delay = _compute_backoff(self.backoff_base, backoff_attempt, self.backoff_max, self.jitter)
327323
log.warning(
328324
f"Server error {resp.status}. "
329325
f"Retrying in {delay:.2f}s ({attempts_left} retries left)..."
@@ -350,11 +346,7 @@ async def _make_api_request(
350346
# Sanitize: don't log full JSON which may contain tokens/sensitive data
351347
if log.isEnabledFor(logging.DEBUG):
352348
# Log safe metadata only
353-
keys = (
354-
list(json_data.keys())
355-
if isinstance(json_data, dict)
356-
else "non-dict"
357-
)
349+
keys = list(json_data.keys()) if isinstance(json_data, dict) else "non-dict"
358350
log.debug(f"{endpoint} JSON response received with keys: {keys}")
359351
return json_data["Data"]
360352
except (KeyError, ValueError, json.JSONDecodeError) as e:

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dev = [
4545
"black",
4646
"flake8",
4747
"mypy",
48+
"pre-commit",
4849
]
4950

5051
# --- IMPORTANT CHANGE ---
@@ -77,4 +78,4 @@ exclude_lines = [
7778
"raise NotImplementedError",
7879
"if __name__ == .__main__.:",
7980
"if TYPE_CHECKING:",
80-
]
81+
]

0 commit comments

Comments
 (0)