Skip to content

Commit 7588569

Browse files
committed
Use picture and get consistently in methods
1 parent 079ac7c commit 7588569

6 files changed

Lines changed: 72 additions & 33 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ Tips:
120120
- `Device` helper (per‑device convenience)
121121
- `await device.refresh()` → hydrate cached state
122122
- `await device.get_location()` → parsed last location
123-
- `await device.fetch_pictures(n)` + `await device.download_photo(item)`
124-
- Commands: `await device.play_sound()`, `await device.take_front_photo()`,
125-
`await device.take_rear_photo()`, `await device.lock(message=None)`,
123+
- `await device.get_pictures(n)` + `await device.get_picture(item)`
124+
- Commands: `await device.play_sound()`, `await device.take_front_picture()`,
125+
`await device.take_rear_picture()`, `await device.lock(message=None)`,
126126
`await device.wipe(confirm=True)`
127127

128128
### Example: Lock device with a message

docs/MIGRATE_FROM_V1.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
7373

7474
| V1 | V2 (FmdClient) | V2 (Device) | Notes |
7575
|----|----------------|-------------|-------|
76-
| `await api.take_picture('back')` | `await client.take_picture('back')` | `await device.take_rear_photo()` | Device method preferred |
77-
| `await api.take_picture('front')` | `await client.take_picture('front')` | `await device.take_front_photo()` | Device method preferred |
76+
| `await api.take_picture('back')` | `await client.take_picture('back')` | `await device.take_rear_picture()` | Device method preferred (old: take_rear_photo deprecated) |
77+
| `await api.take_picture('front')` | `await client.take_picture('front')` | `await device.take_front_picture()` | Device method preferred (old: take_front_photo deprecated) |
7878
> Note: `Device.lock(message=None)` now supports passing an optional message string. The server may ignore the
7979
> message if UI or server versions don't yet consume it, but the base lock command will still be executed.
8080
@@ -92,8 +92,8 @@ client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
9292

9393
| V1 | V2 (FmdClient) | V2 (Device) | Notes |
9494
|----|----------------|-------------|-------|
95-
| `await api.get_pictures(10)` | `await client.get_pictures(10)` | `await device.fetch_pictures(10)` | Both available |
96-
| N/A | N/A | `await device.download_photo(blob)` | New helper method |
95+
| `await api.get_pictures(10)` | `await client.get_pictures(10)` | `await device.get_pictures(10)` | Both available (old: fetch_pictures deprecated) |
96+
| N/A | N/A | `await device.get_picture(blob)` | Helper method (old: download_photo deprecated) |
9797

9898
### Export Data
9999

@@ -153,8 +153,8 @@ await device.lock(message="Lost device") # Lock with message
153153
await device.wipe(confirm=True) # Factory reset (DESTRUCTIVE)
154154

155155
# Pictures
156-
pictures = await device.fetch_pictures(10)
157-
photo_result = await device.download_photo(pictures[0])
156+
pictures = await device.get_pictures(10)
157+
photo_result = await device.get_picture(pictures[0])
158158
```
159159

160160
---

docs/PROPOSAL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Date: 2025-11-01
1818

1919
Top-level components:
2020
- FmdClient: an async client that manages session, authentication tokens, request throttling, and device discovery.
21-
- Device: represents a single device and exposes async methods to interact with it (async refresh(), async play_sound(), async get_location(), async take_front_photo(), async take_rear_photo(), async lock_device(), async wipe_device(), etc).
21+
- Device: represents a single device and exposes async methods to interact with it (async refresh(), async play_sound(), async get_location(), async take_front_picture(), async take_rear_picture(), async lock_device(), async wipe_device(), etc).
2222
- Exceptions: typed exceptions for common error cases (AuthenticationError, DeviceNotFoundError, FmdApiError, RateLimitError).
2323
- Utilities: small helpers for caching, TTL-based per-device caches, retry/backoff, JSON parsing.
2424

@@ -52,8 +52,8 @@ async def example():
5252
await device.play_sound()
5353

5454
# Take front and rear photos
55-
front = await device.take_front_photo()
56-
rear = await device.take_rear_photo()
55+
front = await device.take_front_picture()
56+
rear = await device.take_rear_picture()
5757

5858
# Lock device with message
5959
await device.lock_device(message="Lost phone — call me")
@@ -95,9 +95,9 @@ Core classes and signatures (proposal):
9595
- async get_location(self, *, force: bool = False) -> Optional[Location]
9696
- Returns last known location (calls refresh if expired or force=True)
9797
- async play_sound(self, *, volume: Optional[int] = None) -> None
98-
- async take_front_photo(self) -> Optional[bytes]
98+
- async take_front_picture(self) -> Optional[bytes]
9999
- Requests a front-facing photo; returns raw bytes of image if available.
100-
- async take_rear_photo(self) -> Optional[bytes]
100+
- async take_rear_picture(self) -> Optional[bytes]
101101
- Requests a rear-facing photo; returns raw bytes of image if available.
102102
- async lock_device(self, *, passcode: Optional[str] = None, message: Optional[str] = None) -> None
103103
- async wipe_device(self, *, confirm: bool = False) -> None
@@ -130,7 +130,7 @@ Core classes and signatures (proposal):
130130
- All request payloads, parsing, and business rules will reuse the logic currently implemented in the repository (parsing of responses, mapping fields to device properties, handling of play sound semantics, etc.). No functional changes to endpoints or command behavior are intended.
131131
- Where current code uses synchronous HTTP (requests), the new client will use asyncio/aiohttp to make non-blocking calls. Helpers will be introduced to convert existing request/response handling functions to async easily.
132132
- Device.refresh() mirrors current "get devices" and "refresh device" flows: fetch the device status endpoint, parse location, battery, and update fields.
133-
- Photo functions: take_front_photo() and take_rear_photo() call the corresponding FMD endpoints (if supported). They should return either a PhotoResult object (preferred) or None if not supported by the device/account. Implementations should include sensible timeouts and handle partial results gracefully.
133+
- Photo functions: take_front_picture() and take_rear_picture() call the corresponding FMD endpoints (if supported). They should return either a PhotoResult object (preferred) or None if not supported by the device/account. Implementations should include sensible timeouts and handle partial results gracefully.
134134
- Caching: to avoid hitting rate limits and reduce backend load, a per-device TTL cache will be implemented (configurable; default 30 seconds). get_location() uses cached data unless force=True or stale.
135135
- Rate limiting: a shared RateLimiter object will enforce a maximum requests-per-second or requests-per-minute per client instance. Simple token-bucket or asyncio.Semaphore + sleep-backoff will be sufficient.
136136
- Retries: transient HTTP errors will be retried with an exponential backoff (configurable; default 3 retries).

fmd_api/device.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import json
1010
from datetime import datetime, timezone
11+
import warnings
1112
from typing import Optional, AsyncIterator, List, Dict, Any
1213

1314
from .models import Location, PhotoResult
@@ -75,13 +76,28 @@ async def play_sound(self) -> bool:
7576
return await self.client.send_command("ring")
7677

7778
async def take_front_photo(self) -> bool:
78-
return await self.client.take_picture("front")
79+
warnings.warn(
80+
"Device.take_front_photo() is deprecated; use take_front_picture()",
81+
DeprecationWarning,
82+
stacklevel=2,
83+
)
84+
return await self.take_front_picture()
7985

8086
async def take_rear_photo(self) -> bool:
81-
return await self.client.take_picture("back")
87+
warnings.warn(
88+
"Device.take_rear_photo() is deprecated; use take_rear_picture()",
89+
DeprecationWarning,
90+
stacklevel=2,
91+
)
92+
return await self.take_rear_picture()
8293

8394
async def fetch_pictures(self, num_to_get: int = -1) -> List[dict]:
84-
return await self.client.get_pictures(num_to_get=num_to_get)
95+
warnings.warn(
96+
"Device.fetch_pictures() is deprecated; use get_pictures()",
97+
DeprecationWarning,
98+
stacklevel=2,
99+
)
100+
return await self.get_pictures(num_to_get=num_to_get)
85101

86102
async def download_photo(self, picture_blob_b64: str) -> PhotoResult:
87103
"""
@@ -90,6 +106,29 @@ async def download_photo(self, picture_blob_b64: str) -> PhotoResult:
90106
The fmd README says picture data is double-encoded: encrypted blob -> base64 string -> image bytes.
91107
We decrypt the blob to get a base64-encoded image string; decode that to bytes and return.
92108
"""
109+
warnings.warn(
110+
"Device.download_photo() is deprecated; use get_picture()",
111+
DeprecationWarning,
112+
stacklevel=2,
113+
)
114+
return await self.get_picture(picture_blob_b64)
115+
116+
async def take_front_picture(self) -> bool:
117+
"""Request a picture from the front camera."""
118+
return await self.client.take_picture("front")
119+
120+
async def take_rear_picture(self) -> bool:
121+
"""Request a picture from the rear camera."""
122+
return await self.client.take_picture("back")
123+
124+
async def get_pictures(self, num_to_get: int = -1) -> List[dict]:
125+
"""Get picture blobs (metadata) from the server.
126+
127+
Returns the raw list from the server (typically base64-encoded encrypted blobs)."""
128+
return await self.client.get_pictures(num_to_get=num_to_get)
129+
130+
async def get_picture(self, picture_blob_b64: str) -> PhotoResult:
131+
"""Decrypt and decode a single picture blob into a PhotoResult."""
93132
decrypted = self.client.decrypt_data_blob(picture_blob_b64)
94133
# decrypted is bytes, often containing a base64-encoded image (as text)
95134
try:

tests/functional/test_device.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Test: Device class flows (refresh, get_location, fetch_pictures, download_photo)
2+
Test: Device class flows (refresh, get_location, get_pictures, get_picture)
33
Usage:
44
python tests/functional/test_device.py
55
"""
@@ -32,17 +32,17 @@ async def main():
3232
loc = await device.get_location()
3333
print("Cached location:", loc)
3434
# fetch pictures and attempt to download the first one
35-
pics = await device.fetch_pictures(5)
35+
pics = await device.get_pictures(5)
3636
print("Pictures listed:", len(pics))
3737
if pics:
3838
try:
39-
photo = await device.download_photo(pics[0])
39+
photo = await device.get_picture(pics[0])
4040
fn = "device_photo.jpg"
4141
with open(fn, "wb") as f:
4242
f.write(photo.data)
4343
print("Saved device photo to", fn)
4444
except Exception as e:
45-
print("Failed to download photo:", e)
45+
print("Failed to get picture:", e)
4646
finally:
4747
await client.close()
4848

tests/unit/test_device.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def decrypt(self, packet, padding_obj):
4646

4747

4848
@pytest.mark.asyncio
49-
async def test_device_fetch_and_download_picture(monkeypatch):
49+
async def test_device_get_and_decode_picture(monkeypatch):
5050
client = FmdClient("https://fmd.example.com")
5151
# Provide dummy private key that decrypts session packet into all-zero key
5252

@@ -75,10 +75,10 @@ def decrypt(self, packet, padding_obj):
7575
client.access_token = "token"
7676
device = Device(client, "alice")
7777
try:
78-
pics = await device.fetch_pictures()
78+
pics = await device.get_pictures()
7979
assert len(pics) == 1
8080
# download the picture and verify we got PNGDATA bytes
81-
photo = await device.download_photo(pics[0])
81+
photo = await device.get_picture(pics[0])
8282
assert photo.data == b"PNGDATA"
8383
assert photo.mime_type.startswith("image/")
8484
finally:
@@ -344,7 +344,7 @@ def decrypt(self, packet, padding_obj):
344344

345345
@pytest.mark.asyncio
346346
async def test_device_picture_commands():
347-
"""Test Device picture-related command shortcuts."""
347+
"""Test Device picture-related command shortcuts (new names)."""
348348
client = FmdClient("https://fmd.example.com")
349349
client.access_token = "token"
350350

@@ -358,16 +358,16 @@ def sign(self, message_bytes, pad, algo):
358358
device = Device(client, "test-device")
359359

360360
with aioresponses() as m:
361-
# take_front_photo
361+
# take_front_picture
362362
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
363-
# take_rear_photo
363+
# take_rear_picture
364364
m.post("https://fmd.example.com/api/v1/command", status=200, body="OK")
365365

366366
try:
367-
result1 = await device.take_front_photo()
367+
result1 = await device.take_front_picture()
368368
assert result1 is True
369369

370-
result2 = await device.take_rear_photo()
370+
result2 = await device.take_rear_picture()
371371
assert result2 is True
372372
finally:
373373
await client.close()
@@ -626,8 +626,8 @@ def decrypt(self, packet, padding_obj):
626626

627627

628628
@pytest.mark.asyncio
629-
async def test_device_fetch_pictures():
630-
"""Test Device.fetch_pictures method."""
629+
async def test_device_get_pictures():
630+
"""Test Device.get_pictures method."""
631631
client = FmdClient("https://fmd.example.com")
632632
client.access_token = "token"
633633

@@ -639,7 +639,7 @@ async def test_device_fetch_pictures():
639639
m.put("https://fmd.example.com/api/v1/pictures", payload={"Data": mock_pictures})
640640

641641
try:
642-
pictures = await device.fetch_pictures(num_to_get=1)
642+
pictures = await device.get_pictures(num_to_get=1)
643643
assert len(pictures) == 1
644644
assert pictures[0]["id"] == 0
645645
finally:

0 commit comments

Comments
 (0)