{"id":44751989,"url":"https://github.com/srichs/python-spot-client","last_synced_at":"2026-02-15T23:14:54.527Z","repository":{"id":337743553,"uuid":"1114727239","full_name":"srichs/python-spot-client","owner":"srichs","description":"A python client for the Spot API (https://www.findmespot.com).","archived":false,"fork":false,"pushed_at":"2026-02-11T04:20:38.000Z","size":123,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-11T08:40:14.882Z","etag":null,"topics":["client","findmespot","gps","python","spot","tracker"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/python-spot-client/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/srichs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-11T19:51:03.000Z","updated_at":"2026-02-11T04:23:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/srichs/python-spot-client","commit_stats":null,"previous_names":["srichs/python-spot-client"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/srichs/python-spot-client","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Fpython-spot-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Fpython-spot-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Fpython-spot-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Fpython-spot-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/srichs","download_url":"https://codeload.github.com/srichs/python-spot-client/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/srichs%2Fpython-spot-client/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29411138,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-13T06:24:03.484Z","status":"ssl_error","status_checked_at":"2026-02-13T06:23:12.830Z","response_time":78,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["client","findmespot","gps","python","spot","tracker"],"created_at":"2026-02-15T23:14:53.850Z","updated_at":"2026-02-15T23:14:54.520Z","avatar_url":"https://github.com/srichs.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# python-spot-client\n\n[![CI](https://github.com/srichs/python-spot-client/actions/workflows/ci.yml/badge.svg)](https://github.com/srichs/python-spot-client/actions/workflows/ci.yml)\n[![Docs](https://img.shields.io/github/actions/workflow/status/srichs/python-spot-client/ci.yml?branch=main\u0026label=docs)](https://github.com/srichs/python-spot-client/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/python-spot-client.svg)](https://pypi.org/project/python-spot-client/)\n\nAn asynchronous Python client for retrieving message feeds from the [SPOT device API](https://www.findmespot.com/en-us/support/spot-gen4/get-help/general/public-api-and-xml-feed).\n\n## Features\n\n- Simple asynchronous wrapper around the SPOT device feed endpoints, with optional synchronous entry points for quick scripts.\n- Data classes that model devices, feeds, and messages returned by the API.\n- Helpers for retrieving the latest messages, paginated history, or data within a time window.\n\n## Installation\n\nInstall the latest published release from PyPI:\n\n```bash\npip install python-spot-client\n```\n\nFor local development, clone this repository and install the runtime dependencies together with the\ndocumentation, test, and linting extras:\n\n```bash\npip install -e .[docs,test,lint]\n```\n\nOr use the convenience dev requirements file:\n\n```bash\npip install -r requirements-dev.txt\n```\n\n## Quick start\n\nTo authenticate requests, pass one or more `(feed_id, password)` tuples when\ncreating the client.\n\nThe client wraps the SPOT REST API and exposes models that make it easier to\ninspect devices, feeds, and messages. The example below fetches the latest\nmessages from a feed and prints each device's most recent location.\n\n```python\nimport asyncio\nfrom spot_api import SpotApi\n\n\nasync def main():\n    feed_list = [(\"\u003cyour-feed-id\u003e\", \"\u003cfeed-password\u003e\")]\n\n    async with SpotApi(feed_list) as client:\n        await client.request_latest()\n\n        for message in client.feed.get_latest_messages():\n            print(\n                f\"Device {message.messenger_name} \"\n                f\"({message.messenger_id}) at {message.get_location()} \"\n                f\"on {message.date_time}\"\n            )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Usage\n\nFor scripts that do not already run an event loop, call the synchronous\nwrappers instead:\n\n```python\nwith SpotApi([(\"\u003cyour-feed-id\u003e\", \"\u003cfeed-password\u003e\")]) as client:\n    result = client.request_latest_sync()\n```\n\nOr manage lifecycle manually:\n\n```python\nclient = SpotApi([(\"\u003cyour-feed-id\u003e\", \"\u003cfeed-password\u003e\")])\nresult = client.request_latest_sync()\nclient.close()\n```\n\nThe synchronous helpers still rely on the underlying asynchronous client. The\ncontext manager handles teardown automatically, and the explicit `close`\nmethod runs the same asynchronous shutdown under the hood. If you provide your\nown `httpx.AsyncClient`, `close` and the context manager will leave it alone so\nyou can manage its lifecycle yourself.\n\n### Handling SPOT throttling\n\nSPOT requests that clients wait at least 2.5 minutes (150 seconds) between\nrequests for the same feed and a couple of seconds when switching between\ndifferent feeds. The client tracks these windows automatically and will return a\n`SpotRequestResult` with status `SpotRequestStatus.THROTTLED` when you should\npause. Inspect the accompanying `wait_seconds` value to determine how long to\nsleep before retrying:\n\n```python\nimport asyncio\n\nfrom spot_api import SpotApi, SpotRequestStatus\n\n\nasync def main() -\u003e None:\n    async with SpotApi([(\"\u003cfeed-id\u003e\", \"\u003cpassword\u003e\")]) as client:\n        result = await client.request()\n        if result.status is SpotRequestStatus.THROTTLED:\n            await asyncio.sleep(result.wait_seconds)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nWhen the SPOT API responds with HTTP 429, the same status is returned together\nwith an estimated wait. The exponential backoff used by\n`SpotApi.request_all_pages` will never exceed the documented 150 second window.\n\n### Requesting paginated history\n\nTo fetch paginated historical results instead of just the latest message, call\n`SpotApi.request` and pass the `page` number you need:\n\n```python\nawait client.request(page=2)\n```\n\nYou can also iterate every available page with retry-aware pagination. The\niterator yields a `SpotRequestResult` after each request, allowing you to react\nto throttling events before the next page is requested:\n\n```python\nasync for result in client.request_all_pages(merge_existing=True):\n    if result.status is SpotRequestStatus.THROTTLED:\n        await asyncio.sleep(result.wait_seconds)\n        continue\n\n    if result.succeeded:\n        # Feed data is merged into client.feed automatically.\n        print(f\"Fetched page with {len(client.feed.get_latest_messages())} messages\")\n```\n\n### Requesting a date range\n\nIf you know the exact time window to inspect, provide start and end datetimes\nusing `request_with_dates`:\n\n```python\nimport datetime\n\nstart = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)\nend = datetime.datetime(2023, 1, 31, 23, 59, 59, tzinfo=datetime.timezone.utc)\nawait client.request_with_dates(start, end)\n```\n\nThe `SpotFeed` object associated with the `SpotApi` instance is populated after\nany request. Inspect its `devices` dictionary or use helper methods such as\n`get_latest_messages`, `iter_messages`, or `iter_messages_by_type` to work with\nthe returned data.\n\n### Interpreting request results\n\nEvery request helper returns a `SpotRequestResult` object containing:\n\n- `status`: the high-level result state (for example success or throttled).\n- `succeeded`: convenience boolean equivalent to a successful status.\n- `wait_seconds`: recommended wait time when requests are throttled.\n\nThis makes it straightforward to branch on status without parsing exceptions or\nraw HTTP responses.\n\n### Working with feeds and messages\n\n`SpotFeed` and `SpotMessage` provide several convenience helpers for common\ninspection patterns:\n\n- `SpotFeed.get_latest_messages()` returns a list of the newest message per\n  device.\n- `SpotFeed.iter_messages()` yields every message across the feed in\n  reverse-chronological order, while `iter_messages_by_type()` filters by the\n  message type string.\n- `SpotMessage.get_location()` formats the message's coordinates into a\n  human-readable string (for example, `\"34.05, -118.25, 100\"`).\n\nThese utilities make it easier to work with the deserialized data classes\nwithout needing to remember the raw JSON structure returned by SPOT.\n\n## Documentation\n\nProject documentation is generated with [Sphinx](https://www.sphinx-doc.org/)\nusing the inline module docstrings. Install the documentation extras and build\nthe HTML output locally:\n\n```bash\npip install -e .[docs]\ncd docs\nmake html\n```\n\nor\n\n```bash\npython -m sphinx -b html docs/ docs/build\n```\n\nThe generated site will be available under `docs/build/index.html` and\nincludes a short quick-start tour alongside the full API reference.\n\n## API references\n\n- Library API reference: generated documentation in the `docs/` directory (see above).\n- Official SPOT public API reference: [Public API and XML feed documentation](https://www.findmespot.com/en-us/support/spot-gen4/get-help/general/public-api-and-xml-feed).\n\n## Development\n\nThis project requires Python 3.11 or newer. After installing the test and lint\nextras (`pip install -e .[test,lint]`), run the unit tests with:\n\n```bash\npytest\n```\n\nThe project metadata and build configuration live in `pyproject.toml`. Static\nanalysis helpers (mypy and ruff) are also included in the `lint` optional\ndependency group.\n\n## Security notes\n\n- Treat feed passwords like credentials; avoid committing them to source\n  control.\n- Prefer environment variables or a local secrets manager for production usage\n  instead of hard-coding feed credentials directly in scripts.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrichs%2Fpython-spot-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsrichs%2Fpython-spot-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsrichs%2Fpython-spot-client/lists"}