Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/publish-testpypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Publish to TestPyPI (Manual)

# Manual workflow to publish to TestPyPI for testing before production release
on:
workflow_dispatch:
inputs:
confirm:
description: 'Type "publish" to confirm TestPyPI upload'
required: true
type: string

permissions:
contents: read
id-token: write

jobs:
build_and_publish:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
# Only run if user typed "publish" to confirm
if: github.event.inputs.confirm == 'publish'
environment: testpypi

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install build tooling
run: python -m pip install --upgrade pip build

- name: Build distributions
run: python -m build --sdist --wheel

- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true

- name: Output installation command
run: |
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
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"
36 changes: 4 additions & 32 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Publish Python Package

# Trigger on:
# - pushes to any branch (we'll publish to TestPyPI for non-main branches and to PyPI for main)
# - published GitHub Releases (keep existing behavior for canonical releases)
# - pushes to main (publish to PyPI after merge)
# - published GitHub Releases (publish to PyPI for versioned releases)
on:
push:
branches: ["**"]
branches: [main]
release:
types: [published]

Expand Down Expand Up @@ -41,34 +41,6 @@ jobs:
name: python-package-distributions
path: dist/

# Publish from pushes to non-main branches -> TestPyPI
publish_testpypi:
name: Publish to TestPyPI (branches except main)
runs-on: ubuntu-latest
needs: build_sdist_and_wheel
# Only run for pushes (not releases) and only for branches that are NOT main
if: |
github.event_name == 'push' &&
startsWith(github.ref, 'refs/heads/') &&
github.ref != 'refs/heads/main'
environment: testpypi
permissions:
id-token: write
contents: read

steps:
- name: Download dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
# repository-url directs upload to TestPyPI
repository-url: https://test.pypi.org/legacy/

# Publish from pushes to main OR when a Release is published -> Production PyPI
publish_pypi:
name: Publish to PyPI (main branch or GitHub Release)
Expand All @@ -93,4 +65,4 @@ jobs:
path: dist/

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@release/v1
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ fmd-server/
fmd-android/

#credentials file
examples/tests/credentials.txt
examples/tests/credentials.txt
48 changes: 48 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Pre-commit hooks configuration
# See https://pre-commit.com for more information
repos:
# General file cleanup
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: '^tests/functional/'
- id: end-of-file-fixer
exclude: '^tests/functional/'
- id: check-yaml
- id: check-toml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: debug-statements
- id: mixed-line-ending
args: ['--fix=lf']

# Python code formatting with black
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3

# Python linting with flake8
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
args: ['--count', '--show-source', '--statistics']
exclude: '^tests/functional/'

# Python type checking with mypy
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies:
- types-aiofiles
- aiohttp
- argon2-cffi
- cryptography
args: ['--install-types', '--non-interactive']
exclude: '^tests/'
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Tips:
- `set_ringer_mode("normal|vibrate|silent")`
- `get_device_stats()`


- Low‑level: `decrypt_data_blob(b64_blob)`

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

MIT © 2025 Devin Slick
MIT © 2025 Devin Slick
12 changes: 6 additions & 6 deletions docs/HOME_ASSISTANT_REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def _make_api_request(self, ..., timeout: int = 30):
- `pyproject.toml`: `2.0.0.dev8` (PEP 440 compliant)
- `_version.py`: `2.0.0-dev8` (uses hyphen instead of dot)

**Location:**
**Location:**
- `pyproject.toml` line 3
- `fmd_api/_version.py` line 1

Expand Down Expand Up @@ -143,7 +143,7 @@ if resp.status == 429:
class FmdClient:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
await self.close()
```
Expand Down Expand Up @@ -191,9 +191,9 @@ async with await FmdClient.create(...) as client:
- Line ~88: Logs may include auth details
- Line ~203: Logs full JSON responses which may contain tokens

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

**HA Rationale:** Security and privacy requirement for production systems.
Expand Down Expand Up @@ -364,7 +364,7 @@ async def decrypt_data_blob_async(self, data_b64: str) -> bytes:
async def get_locations(...) -> List[str]:
"""
...

Raises:
AuthenticationError: If authentication fails
FmdApiException: If server returns error
Expand All @@ -384,7 +384,7 @@ async def get_locations(...) -> List[str]:

**Location:** Test configuration

**Fix:**
**Fix:**
- Add `pytest-cov` to dev dependencies
- Configure coverage in `pyproject.toml`
- Add coverage reporting to CI workflow
Expand Down
44 changes: 22 additions & 22 deletions docs/MIGRATE_FROM_V1.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,16 @@ from fmd_api import FmdApi

async def main():
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")

# Request new location
await api.request_location('gps')
await asyncio.sleep(30)

# Get locations
blobs = await api.get_all_locations(1)
location_json = api.decrypt_data_blob(blobs[0])
location = json.loads(location_json)

print(f"Lat: {location['lat']}, Lon: {location['lon']}")
await api.close()

Expand All @@ -199,16 +199,16 @@ from fmd_api import FmdClient

async def main():
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")

# Request new location
await client.request_location('gps')
await asyncio.sleep(30)

# Get locations
blobs = await client.get_locations(1)
location_json = client.decrypt_data_blob(blobs[0])
location = json.loads(location_json)

print(f"Lat: {location['lat']}, Lon: {location['lon']}")
await client.close()

Expand All @@ -223,14 +223,14 @@ from fmd_api import FmdClient, Device
async def main():
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
device = Device(client, "alice")

# Request and get location (simplified)
await client.request_location('gps')
await asyncio.sleep(30)

location = await device.get_location(force=True)
print(f"Lat: {location.lat}, Lon: {location.lon}")

await client.close()

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

async def control_device():
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")

# Using constants
await api.send_command(FmdCommands.RING)
await api.send_command(FmdCommands.BLUETOOTH_ON)

# Using convenience methods
await api.toggle_bluetooth(True)
await api.toggle_do_not_disturb(True)
await api.set_ringer_mode('vibrate')

await api.close()
```

Expand All @@ -263,16 +263,16 @@ from fmd_api import FmdClient

async def control_device():
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")

# Use strings directly (constants removed)
await client.send_command('ring')
await client.send_command('bluetooth on')

# Using convenience methods (renamed from toggle_* to set_*)
await client.set_bluetooth(True)
await client.set_do_not_disturb(True)
await client.set_ringer_mode('vibrate')

await client.close()
```

Expand All @@ -283,15 +283,15 @@ from fmd_api import FmdClient, Device
async def control_device():
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
device = Device(client, "alice")

# Use device methods for cleaner API
await device.play_sound()

# Settings still use client
await client.set_bluetooth(True)
await client.set_do_not_disturb(True)
await client.set_ringer_mode('vibrate')

await client.close()
```

Expand All @@ -304,13 +304,13 @@ from fmd_api import FmdApi

async def get_history():
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")

blobs = await api.get_all_locations(10)
for blob in blobs:
location_json = api.decrypt_data_blob(blob)
location = json.loads(location_json)
print(f"Date: {location['date']}, Lat: {location['lat']}, Lon: {location['lon']}")

await api.close()
```

Expand All @@ -321,11 +321,11 @@ from fmd_api import FmdClient, Device
async def get_history():
client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
device = Device(client, "alice")

# Async iterator with automatic decryption
async for location in device.get_history(limit=10):
print(f"Date: {location.date}, Lat: {location.lat}, Lon: {location.lon}")

await client.close()
```

Expand Down
6 changes: 3 additions & 3 deletions docs/PROPOSAL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Proposal: fmd_api v2 — Device-centric async interface

Status: Draft
Author: devinslick (proposal by Copilot Space)
Status: Draft
Author: devinslick (proposal by Copilot Space)
Date: 2025-11-01

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

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.
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.
2 changes: 1 addition & 1 deletion docs/PROPOSED_BRANCH_AND_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ Next steps after branch creation:
5. Iterate on rate-limiter/cache and add streaming helpers for export_data_zip.

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?
```
```
2 changes: 1 addition & 1 deletion fmd_api/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.2"
__version__ = "2.0.3"
Loading
Loading