# fmd_api: Python client for interacting with FMD (fmd-foss.org)
This directory contains Python scripts for interacting with an FMD (Find My Device) server, including authentication, key retrieval, and location data decryption.
For more information on this open source alternative to Google's Find My Device service, read the Credits section at the bottom of this README.
In this repo you'll find fmd_api.py is the tool supporting fmd_client.py, used in most of the examples.
## Prerequisites
- Python 3.7+
- Install dependencies:
```
pip install requests argon2-cffi cryptography
```
## Scripts Overview
### Main Client
#### `fmd_client.py`
**The primary tool for bulk data export.** Downloads locations and/or pictures, saving them to a directory or ZIP archive.
**Usage:**
```bash
python fmd_client.py --url <server_url> --id <fmd_id> --password <password> --output <path> [--locations [N]] [--pictures [N]]
```
**Options:**
- `--locations [N]`: Export all locations, or specify N for the most recent N locations
- `--pictures [N]`: Export all pictures, or specify N for the most recent N pictures
- `--output`: Output directory or `.zip` file path
- `--session`: Session duration in seconds (default: 3600)
**Examples:**
```bash
# Export all locations to CSV
python fmd_client.py --url https://fmd.example.com --id alice --password secret --output data --locations
# Export last 10 locations and 5 pictures to ZIP
python fmd_client.py --url https://fmd.example.com --id alice --password secret --output export.zip --locations 10 --pictures 5
```
### Debugging Scripts
Located in `debugging/`, these scripts help test individual workflows and troubleshoot issues.
#### `fmd_get_location.py`
**End-to-end test:** Authenticates, retrieves, and decrypts the latest location in one step.
**Usage:**
```bash
cd debugging
python fmd_get_location.py --url <server_url> --id <fmd_id> --password <password>
```
#### `fmd_export_data.py`
**Test native export:** Downloads the server's pre-packaged export ZIP (if available).
**Usage:**
```bash
cd debugging
python fmd_export_data.py --url <server_url> --id <fmd_id> --password <password> --output export.zip
```
#### `request_location_example.py`
**Request new location:** Triggers a device to capture and upload a new location update.
**Usage:**
```bash
cd debugging
python request_location_example.py --url <server_url> --id <fmd_id> --password <password> [--provider all|gps|cell|last] [--wait SECONDS]
```
**Options:**
- `--provider`: Location provider to use (default: all)
- `all`: Use all available providers (GPS, network, fused)
- `gps`: GPS only (most accurate, slower)
- `cell`: Cellular network (faster, less accurate)
- `last`: Don't request new location, just get last known
- `--wait`: Seconds to wait for location update (default: 30)
**Example:**
```bash
# Request GPS location and wait 45 seconds
python request_location_example.py --url https://fmd.example.com --id alice --password secret --provider gps --wait 45
# Quick cellular network location
python request_location_example.py --url https://fmd.example.com --id alice --password secret --provider cell --wait 20
```
#### `diagnose_blob.py`
**Diagnostic tool:** Analyzes encrypted blob structure to troubleshoot decryption issues.
**Usage:**
```bash
cd debugging
python diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>
```
Shows:
- Private key size and type
- Actual blob size vs. expected structure
- Analysis of RSA session key packet layout
- First/last bytes in hex for inspection
## Core Library
### `fmd_api.py`
The foundational API library providing the `FmdApi` class. Handles:
- Authentication (salt retrieval, Argon2id password hashing, token management)
- Encrypted private key retrieval and decryption
- Data blob decryption (RSA-OAEP + AES-GCM)
- Location and picture retrieval
- Command sending (request location updates, ring, lock, camera)
- Commands are cryptographically signed using RSA-PSS to prove authenticity
**For application developers:** See [LOCATION_FIELDS.md](LOCATION_FIELDS.md) for detailed documentation on extracting and using accuracy, altitude, speed, and heading fields.
**Quick example:**
```python
import asyncio
import json
from fmd_api import FmdApi
async def main():
# Authenticate (automatically retrieves and decrypts private key)
api = await FmdApi.create("https://fmd.example.com", "alice", "secret")
# Request a new location update
await api.request_location('gps') # or 'all', 'cell', 'last'
await asyncio.sleep(30) # Wait for device to respond
# Get locations
locations = await api.get_all_locations(num_to_get=10) # Last 10, or -1 for all
# Decrypt a location blob
decrypted_data = api.decrypt_data_blob(locations[0])
location = json.loads(decrypted_data)
# Access fields (use .get() for optional fields)
lat = location['lat']
lon = location['lon']
speed = location.get('speed') # Optional, only when moving
heading = location.get('heading') # Optional, only when moving
# Send commands (see Available Commands section below)
await api.send_command('ring') # Make device ring
await api.send_command('bluetooth on') # Enable Bluetooth
await api.send_command('camera front') # Take picture with front camera
asyncio.run(main())
```
### Available Commands
The FMD Android app supports a comprehensive set of commands. You can send them using `api.send_command(command)` or use the convenience methods and constants:
#### Location Requests
```python
# Using convenience method
await api.request_location('gps') # GPS only
await api.request_location('all') # All providers (default)
await api.request_location('cell') # Cellular network only
# Using send_command directly
await api.send_command('locate gps')
await api.send_command('locate')
await api.send_command('locate cell')
await api.send_command('locate last') # Last known, no new request
# Using constants
from fmd_api import FmdCommands
await api.send_command(FmdCommands.LOCATE_GPS)
```
#### Device Control
```python
# Ring device
await api.send_command('ring')
await api.send_command(FmdCommands.RING)
# Lock device screen
await api.send_command('lock')
await api.send_command(FmdCommands.LOCK)
# ⚠️ Delete/wipe device (DESTRUCTIVE - factory reset!)
await api.send_command('delete')
await api.send_command(FmdCommands.DELETE)
```
#### Camera
```python
# Using convenience method
await api.take_picture('back') # Rear camera (default)
await api.take_picture('front') # Front camera (selfie)
# Using send_command
await api.send_command('camera back')
await api.send_command('camera front')
# Using constants
await api.send_command(FmdCommands.CAMERA_BACK)
await api.send_command(FmdCommands.CAMERA_FRONT)
```
#### Bluetooth
```python
# Using convenience method
await api.toggle_bluetooth(True) # Enable
await api.toggle_bluetooth(False) # Disable
# Using send_command
await api.send_command('bluetooth on')
await api.send_command('bluetooth off')
# Using constants
await api.send_command(FmdCommands.BLUETOOTH_ON)
await api.send_command(FmdCommands.BLUETOOTH_OFF)
```
**Note:** Android 12+ requires BLUETOOTH_CONNECT permission.
#### Do Not Disturb Mode
```python
# Using convenience method
await api.toggle_do_not_disturb(True) # Enable DND
await api.toggle_do_not_disturb(False) # Disable DND
# Using send_command
await api.send_command('nodisturb on')
await api.send_command('nodisturb off')
# Using constants
await api.send_command(FmdCommands.NODISTURB_ON)
await api.send_command(FmdCommands.NODISTURB_OFF)
```
**Note:** Requires Do Not Disturb Access permission.
#### Ringer Mode
```python
# Using convenience method
await api.set_ringer_mode('normal') # Sound + vibrate
await api.set_ringer_mode('vibrate') # Vibrate only
await api.set_ringer_mode('silent') # Silent (also enables DND)
# Using send_command
await api.send_command('ringermode normal')
await api.send_command('ringermode vibrate')
await api.send_command('ringermode silent')
# Using constants
await api.send_command(FmdCommands.RINGERMODE_NORMAL)
await api.send_command(FmdCommands.RINGERMODE_VIBRATE)
await api.send_command(FmdCommands.RINGERMODE_SILENT)
```
**Note:** Setting to "silent" also enables Do Not Disturb (Android behavior). Requires Do Not Disturb Access permission.
#### Device Information
```python
# Get network statistics (IP addresses, WiFi SSID/BSSID)
await api.get_device_stats()
await api.send_command('stats')
await api.send_command(FmdCommands.STATS)
# Get battery and GPS status
await api.send_command('gps')
await api.send_command(FmdCommands.GPS)
```
**Note:** `stats` command requires Location permission to access WiFi information.
#### Command Testing Script
Test any command easily:
```bash
cd debugging
python test_command.py <command> --url <server_url> --id <fmd_id> --password <password>
# Examples
python test_command.py "ring" --url https://fmd.example.com --id alice --password secret
python test_command.py "bluetooth on" --url https://fmd.example.com --id alice --password secret
python test_command.py "ringermode vibrate" --url https://fmd.example.com --id alice --password secret
```
## Troubleshooting
### Empty or Invalid Blobs
If you see warnings like `"Blob too small for decryption"`, the server returned empty/corrupted data. This can happen when:
- No location data was uploaded for that time period
- Data was deleted or corrupted server-side
- The server returns placeholder values for missing data
The client will skip these automatically and report the count at the end.
### Debugging Decryption Issues
Use `debugging/diagnose_blob.py` to analyze blob structure:
```bash
cd debugging
python diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>
```
This shows the actual blob size, expected structure, and helps identify if the RSA key size or encryption format has changed.
## Notes
- All scripts use Argon2id password hashing and AES-GCM/RSA-OAEP encryption, matching the FMD web client
- Blobs must be at least 396 bytes (384 RSA session key + 12 IV + ciphertext) to be valid
- Base64 data from the server may be missing padding - use `_pad_base64()` helper when needed
- **Location data fields**:
- Always present: `time`, `provider`, `bat` (battery %), `lat`, `lon`, `date` (Unix ms)
- Optional (depending on provider): `accuracy` (meters), `altitude` (meters), `speed` (m/s), `heading` (degrees)
- Picture data is double-encoded: encrypted blob → base64 string → actual image bytes
## Credits
This project is a client for the open-source FMD (Find My Device) server. The FMD project provides a decentralized, self-hostable alternative to commercial device tracking services.
- **[fmd-foss.org](https://fmd-foss.org/)**: The official project website, offering general information, documentation, and news.
- **[fmd-foss on GitLab](https://gitlab.com/fmd-foss)**: The official GitLab group hosting the source code for the server, Android client, web UI, and other related projects.
- **[fmd.nulide.de](https://fmd.nulide.de/)**: A generously hosted public instance of the FMD server available for community use.
Raw data
{
"_id": null,
"home_page": "https://github.com/devinslick/fmd-client",
"name": "fmd-api",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": null,
"keywords": "fmd, find-my-device, location, tracking, device-tracking, api-client",
"author": "Devin Slick",
"author_email": "Devin Slick <fmd_client_github@devinslick.com>",
"download_url": "https://files.pythonhosted.org/packages/bb/59/a349037012c4cc411db155bb039c71caf919d68c175fa9f70907f728a9c8/fmd_api-0.1.0.tar.gz",
"platform": null,
"description": "# fmd_api: Python client for interacting with FMD (fmd-foss.org)\n\nThis directory contains Python scripts for interacting with an FMD (Find My Device) server, including authentication, key retrieval, and location data decryption.\nFor more information on this open source alternative to Google's Find My Device service, read the Credits section at the bottom of this README.\nIn this repo you'll find fmd_api.py is the tool supporting fmd_client.py, used in most of the examples. \n\n## Prerequisites\n- Python 3.7+\n- Install dependencies:\n ```\n pip install requests argon2-cffi cryptography\n ```\n\n## Scripts Overview\n\n### Main Client\n\n#### `fmd_client.py`\n**The primary tool for bulk data export.** Downloads locations and/or pictures, saving them to a directory or ZIP archive.\n\n**Usage:**\n```bash\npython fmd_client.py --url <server_url> --id <fmd_id> --password <password> --output <path> [--locations [N]] [--pictures [N]]\n```\n\n**Options:**\n- `--locations [N]`: Export all locations, or specify N for the most recent N locations\n- `--pictures [N]`: Export all pictures, or specify N for the most recent N pictures\n- `--output`: Output directory or `.zip` file path\n- `--session`: Session duration in seconds (default: 3600)\n\n**Examples:**\n```bash\n# Export all locations to CSV\npython fmd_client.py --url https://fmd.example.com --id alice --password secret --output data --locations\n\n# Export last 10 locations and 5 pictures to ZIP\npython fmd_client.py --url https://fmd.example.com --id alice --password secret --output export.zip --locations 10 --pictures 5\n```\n\n### Debugging Scripts\n\nLocated in `debugging/`, these scripts help test individual workflows and troubleshoot issues.\n\n#### `fmd_get_location.py`\n**End-to-end test:** Authenticates, retrieves, and decrypts the latest location in one step.\n\n**Usage:**\n```bash\ncd debugging\npython fmd_get_location.py --url <server_url> --id <fmd_id> --password <password>\n```\n\n#### `fmd_export_data.py`\n**Test native export:** Downloads the server's pre-packaged export ZIP (if available).\n\n**Usage:**\n```bash\ncd debugging\npython fmd_export_data.py --url <server_url> --id <fmd_id> --password <password> --output export.zip\n```\n\n#### `request_location_example.py`\n**Request new location:** Triggers a device to capture and upload a new location update.\n\n**Usage:**\n```bash\ncd debugging\npython request_location_example.py --url <server_url> --id <fmd_id> --password <password> [--provider all|gps|cell|last] [--wait SECONDS]\n```\n\n**Options:**\n- `--provider`: Location provider to use (default: all)\n - `all`: Use all available providers (GPS, network, fused)\n - `gps`: GPS only (most accurate, slower)\n - `cell`: Cellular network (faster, less accurate)\n - `last`: Don't request new location, just get last known\n- `--wait`: Seconds to wait for location update (default: 30)\n\n**Example:**\n```bash\n# Request GPS location and wait 45 seconds\npython request_location_example.py --url https://fmd.example.com --id alice --password secret --provider gps --wait 45\n\n# Quick cellular network location\npython request_location_example.py --url https://fmd.example.com --id alice --password secret --provider cell --wait 20\n```\n\n#### `diagnose_blob.py`\n**Diagnostic tool:** Analyzes encrypted blob structure to troubleshoot decryption issues.\n\n**Usage:**\n```bash\ncd debugging\npython diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>\n```\n\nShows:\n- Private key size and type\n- Actual blob size vs. expected structure\n- Analysis of RSA session key packet layout\n- First/last bytes in hex for inspection\n\n## Core Library\n\n### `fmd_api.py`\nThe foundational API library providing the `FmdApi` class. Handles:\n- Authentication (salt retrieval, Argon2id password hashing, token management)\n- Encrypted private key retrieval and decryption\n- Data blob decryption (RSA-OAEP + AES-GCM)\n- Location and picture retrieval\n- Command sending (request location updates, ring, lock, camera)\n - Commands are cryptographically signed using RSA-PSS to prove authenticity\n\n**For application developers:** See [LOCATION_FIELDS.md](LOCATION_FIELDS.md) for detailed documentation on extracting and using accuracy, altitude, speed, and heading fields.\n\n**Quick example:**\n```python\nimport asyncio\nimport json\nfrom fmd_api import FmdApi\n\nasync def main():\n # Authenticate (automatically retrieves and decrypts private key)\n api = await FmdApi.create(\"https://fmd.example.com\", \"alice\", \"secret\")\n\n # Request a new location update\n await api.request_location('gps') # or 'all', 'cell', 'last'\n await asyncio.sleep(30) # Wait for device to respond\n\n # Get locations\n locations = await api.get_all_locations(num_to_get=10) # Last 10, or -1 for all\n\n # Decrypt a location blob\n decrypted_data = api.decrypt_data_blob(locations[0])\n location = json.loads(decrypted_data)\n \n # Access fields (use .get() for optional fields)\n lat = location['lat']\n lon = location['lon']\n speed = location.get('speed') # Optional, only when moving\n heading = location.get('heading') # Optional, only when moving\n \n # Send commands (see Available Commands section below)\n await api.send_command('ring') # Make device ring\n await api.send_command('bluetooth on') # Enable Bluetooth\n await api.send_command('camera front') # Take picture with front camera\n\nasyncio.run(main())\n```\n\n### Available Commands\n\nThe FMD Android app supports a comprehensive set of commands. You can send them using `api.send_command(command)` or use the convenience methods and constants:\n\n#### Location Requests\n```python\n# Using convenience method\nawait api.request_location('gps') # GPS only\nawait api.request_location('all') # All providers (default)\nawait api.request_location('cell') # Cellular network only\n\n# Using send_command directly\nawait api.send_command('locate gps')\nawait api.send_command('locate')\nawait api.send_command('locate cell')\nawait api.send_command('locate last') # Last known, no new request\n\n# Using constants\nfrom fmd_api import FmdCommands\nawait api.send_command(FmdCommands.LOCATE_GPS)\n```\n\n#### Device Control\n```python\n# Ring device\nawait api.send_command('ring')\nawait api.send_command(FmdCommands.RING)\n\n# Lock device screen\nawait api.send_command('lock')\nawait api.send_command(FmdCommands.LOCK)\n\n# \u26a0\ufe0f Delete/wipe device (DESTRUCTIVE - factory reset!)\nawait api.send_command('delete')\nawait api.send_command(FmdCommands.DELETE)\n```\n\n#### Camera\n```python\n# Using convenience method\nawait api.take_picture('back') # Rear camera (default)\nawait api.take_picture('front') # Front camera (selfie)\n\n# Using send_command\nawait api.send_command('camera back')\nawait api.send_command('camera front')\n\n# Using constants\nawait api.send_command(FmdCommands.CAMERA_BACK)\nawait api.send_command(FmdCommands.CAMERA_FRONT)\n```\n\n#### Bluetooth\n```python\n# Using convenience method\nawait api.toggle_bluetooth(True) # Enable\nawait api.toggle_bluetooth(False) # Disable\n\n# Using send_command\nawait api.send_command('bluetooth on')\nawait api.send_command('bluetooth off')\n\n# Using constants\nawait api.send_command(FmdCommands.BLUETOOTH_ON)\nawait api.send_command(FmdCommands.BLUETOOTH_OFF)\n```\n\n**Note:** Android 12+ requires BLUETOOTH_CONNECT permission.\n\n#### Do Not Disturb Mode\n```python\n# Using convenience method\nawait api.toggle_do_not_disturb(True) # Enable DND\nawait api.toggle_do_not_disturb(False) # Disable DND\n\n# Using send_command\nawait api.send_command('nodisturb on')\nawait api.send_command('nodisturb off')\n\n# Using constants\nawait api.send_command(FmdCommands.NODISTURB_ON)\nawait api.send_command(FmdCommands.NODISTURB_OFF)\n```\n\n**Note:** Requires Do Not Disturb Access permission.\n\n#### Ringer Mode\n```python\n# Using convenience method\nawait api.set_ringer_mode('normal') # Sound + vibrate\nawait api.set_ringer_mode('vibrate') # Vibrate only\nawait api.set_ringer_mode('silent') # Silent (also enables DND)\n\n# Using send_command\nawait api.send_command('ringermode normal')\nawait api.send_command('ringermode vibrate')\nawait api.send_command('ringermode silent')\n\n# Using constants\nawait api.send_command(FmdCommands.RINGERMODE_NORMAL)\nawait api.send_command(FmdCommands.RINGERMODE_VIBRATE)\nawait api.send_command(FmdCommands.RINGERMODE_SILENT)\n```\n\n**Note:** Setting to \"silent\" also enables Do Not Disturb (Android behavior). Requires Do Not Disturb Access permission.\n\n#### Device Information\n```python\n# Get network statistics (IP addresses, WiFi SSID/BSSID)\nawait api.get_device_stats()\nawait api.send_command('stats')\nawait api.send_command(FmdCommands.STATS)\n\n# Get battery and GPS status\nawait api.send_command('gps')\nawait api.send_command(FmdCommands.GPS)\n```\n\n**Note:** `stats` command requires Location permission to access WiFi information.\n\n#### Command Testing Script\nTest any command easily:\n```bash\ncd debugging\npython test_command.py <command> --url <server_url> --id <fmd_id> --password <password>\n\n# Examples\npython test_command.py \"ring\" --url https://fmd.example.com --id alice --password secret\npython test_command.py \"bluetooth on\" --url https://fmd.example.com --id alice --password secret\npython test_command.py \"ringermode vibrate\" --url https://fmd.example.com --id alice --password secret\n```\n\n## Troubleshooting\n\n### Empty or Invalid Blobs\nIf you see warnings like `\"Blob too small for decryption\"`, the server returned empty/corrupted data. This can happen when:\n- No location data was uploaded for that time period\n- Data was deleted or corrupted server-side\n- The server returns placeholder values for missing data\n\nThe client will skip these automatically and report the count at the end.\n\n### Debugging Decryption Issues\nUse `debugging/diagnose_blob.py` to analyze blob structure:\n```bash\ncd debugging\npython diagnose_blob.py --url <server_url> --id <fmd_id> --password <password>\n```\n\nThis shows the actual blob size, expected structure, and helps identify if the RSA key size or encryption format has changed.\n\n## Notes\n- All scripts use Argon2id password hashing and AES-GCM/RSA-OAEP encryption, matching the FMD web client\n- Blobs must be at least 396 bytes (384 RSA session key + 12 IV + ciphertext) to be valid\n- Base64 data from the server may be missing padding - use `_pad_base64()` helper when needed\n- **Location data fields**:\n - Always present: `time`, `provider`, `bat` (battery %), `lat`, `lon`, `date` (Unix ms)\n - Optional (depending on provider): `accuracy` (meters), `altitude` (meters), `speed` (m/s), `heading` (degrees)\n- Picture data is double-encoded: encrypted blob \u2192 base64 string \u2192 actual image bytes\n\n## Credits\n\nThis project is a client for the open-source FMD (Find My Device) server. The FMD project provides a decentralized, self-hostable alternative to commercial device tracking services.\n\n- **[fmd-foss.org](https://fmd-foss.org/)**: The official project website, offering general information, documentation, and news.\n- **[fmd-foss on GitLab](https://gitlab.com/fmd-foss)**: The official GitLab group hosting the source code for the server, Android client, web UI, and other related projects.\n- **[fmd.nulide.de](https://fmd.nulide.de/)**: A generously hosted public instance of the FMD server available for community use.\n",
"bugtrack_url": null,
"license": null,
"summary": "A Python client for the FMD server API",
"version": "0.1.0",
"project_urls": {
"Documentation": "https://github.com/devinslick/fmd_api#readme",
"Homepage": "https://github.com/devinslick/fmd_api",
"Issues": "https://github.com/devinslick/fmd_api/issues",
"Repository": "https://github.com/devinslick/fmd_api"
},
"split_keywords": [
"fmd",
" find-my-device",
" location",
" tracking",
" device-tracking",
" api-client"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "8992b7a94e5e27423c3a5e843e7edda31154fcc7cc0c9dcbaffc62da55280a3e",
"md5": "c28896039186bfe741589437ab06fd71",
"sha256": "6cfad67a300a8f77b6dec152a20a1a4784820e78a56fafaa679802c279d0bb2e"
},
"downloads": -1,
"filename": "fmd_api-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c28896039186bfe741589437ab06fd71",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 13834,
"upload_time": "2025-10-23T02:04:37",
"upload_time_iso_8601": "2025-10-23T02:04:37.211158Z",
"url": "https://files.pythonhosted.org/packages/89/92/b7a94e5e27423c3a5e843e7edda31154fcc7cc0c9dcbaffc62da55280a3e/fmd_api-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "bb59a349037012c4cc411db155bb039c71caf919d68c175fa9f70907f728a9c8",
"md5": "8687d1d6fef3ccb55197d9397980b5f7",
"sha256": "d1f3042278b012d2537cc0a9a7903b875d9a8517642b35a912a3b5552d3b0b19"
},
"downloads": -1,
"filename": "fmd_api-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "8687d1d6fef3ccb55197d9397980b5f7",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 14297,
"upload_time": "2025-10-23T02:04:38",
"upload_time_iso_8601": "2025-10-23T02:04:38.307121Z",
"url": "https://files.pythonhosted.org/packages/bb/59/a349037012c4cc411db155bb039c71caf919d68c175fa9f70907f728a9c8/fmd_api-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-23 02:04:38",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "devinslick",
"github_project": "fmd-client",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "fmd-api"
}