taglogger-sdk 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. taglogger_sdk-1.0.0/.gitignore +14 -0
  2. taglogger_sdk-1.0.0/LICENSE +21 -0
  3. taglogger_sdk-1.0.0/PKG-INFO +298 -0
  4. taglogger_sdk-1.0.0/README.md +270 -0
  5. taglogger_sdk-1.0.0/pyproject.toml +64 -0
  6. taglogger_sdk-1.0.0/src/taglogger_sdk/__init__.py +55 -0
  7. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/__init__.py +110 -0
  8. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/battery.py +102 -0
  9. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/conditioning.py +80 -0
  10. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/geometry.py +260 -0
  11. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/segmentation.py +262 -0
  12. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/shaping.py +89 -0
  13. taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/types.py +137 -0
  14. taglogger_sdk-1.0.0/src/taglogger_sdk/client.py +72 -0
  15. taglogger_sdk-1.0.0/src/taglogger_sdk/errors.py +52 -0
  16. taglogger_sdk-1.0.0/src/taglogger_sdk/http.py +490 -0
  17. taglogger_sdk-1.0.0/src/taglogger_sdk/pagination.py +88 -0
  18. taglogger_sdk-1.0.0/src/taglogger_sdk/py.typed +0 -0
  19. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/__init__.py +17 -0
  20. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/account.py +22 -0
  21. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/fleet.py +45 -0
  22. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/geofences.py +56 -0
  23. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/share_links.py +61 -0
  24. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/shared.py +41 -0
  25. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/tags.py +82 -0
  26. taglogger_sdk-1.0.0/src/taglogger_sdk/resources/webhooks.py +102 -0
  27. taglogger_sdk-1.0.0/src/taglogger_sdk/types.py +366 -0
  28. taglogger_sdk-1.0.0/src/taglogger_sdk/webhooks.py +96 -0
  29. taglogger_sdk-1.0.0/tests/test_battery.py +77 -0
  30. taglogger_sdk-1.0.0/tests/test_conditioning.py +113 -0
  31. taglogger_sdk-1.0.0/tests/test_geometry.py +221 -0
  32. taglogger_sdk-1.0.0/tests/test_http.py +464 -0
  33. taglogger_sdk-1.0.0/tests/test_pagination.py +120 -0
  34. taglogger_sdk-1.0.0/tests/test_resources.py +222 -0
  35. taglogger_sdk-1.0.0/tests/test_segmentation.py +167 -0
  36. taglogger_sdk-1.0.0/tests/test_shaping.py +110 -0
  37. taglogger_sdk-1.0.0/tests/test_types.py +96 -0
  38. taglogger_sdk-1.0.0/tests/test_webhooks.py +172 -0
@@ -0,0 +1,14 @@
1
+ # Python build / cache artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ .venv/
14
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TagLogger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: taglogger-sdk
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the TagLogger API, with a bundled client-side location-analytics toolkit.
5
+ Project-URL: Homepage, https://taglogger.com
6
+ Project-URL: Documentation, https://taglogger.com/docs/api
7
+ Author: TagLogger
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: asset-tracking,geofence,gps,location-analytics,taglogger
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.0; extra == 'dev'
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Requires-Dist: ruff>=0.1; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # TagLogger Python SDK
30
+
31
+ Official Python SDK for the [TagLogger API](https://taglogger.com/docs/api) — read your tags and their locations, sync your whole fleet incrementally, manage geofences, webhooks and share links, and verify incoming webhook deliveries.
32
+
33
+ It also bundles **`taglogger_sdk.analytics`**, a pure-math, client-side location-analytics toolkit (conditioning, stops/trips, dwell, clustering, GeoJSON/polyline shaping, battery projection) that runs entirely in-process over the location data the API returns — no extra services, no network calls.
34
+
35
+ - **Zero runtime dependencies.** Standard library only.
36
+ - **Typed.** Ships `py.typed`; full type hints on the public surface.
37
+ - **Synchronous** and thread-safe to reuse: a single client instance carries no mutable per-call state.
38
+ - **Python 3.9+.**
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install taglogger-sdk
44
+ ```
45
+
46
+ The distribution installs as `taglogger-sdk`; you import it as `taglogger_sdk`.
47
+
48
+ ## Quick start
49
+
50
+ ```python
51
+ from taglogger_sdk import TagLoggerClient
52
+
53
+ client = TagLoggerClient(api_key="tl_live_...")
54
+
55
+ # One page of tags
56
+ page = client.tags.list(limit=50)
57
+ for tag in page["data"]:
58
+ print(tag["id"], tag.get("name"))
59
+
60
+ # Or stream every tag, paging transparently
61
+ for tag in client.tags.iterate():
62
+ print(tag["id"])
63
+
64
+ # Latest location for one tag
65
+ location = client.tags.location("tag-1")
66
+ print(location["data"], "recording paused:", location["recordingPaused"])
67
+ ```
68
+
69
+ Get an API key from your TagLogger dashboard under **Settings → API Keys**. Keys are scoped; each method below notes the scope it needs.
70
+
71
+ ## Configuration
72
+
73
+ ```python
74
+ client = TagLoggerClient(
75
+ api_key="tl_live_...",
76
+ base_url="https://api.taglogger.com", # override to target a staging deployment
77
+ auth_header="bearer", # "bearer" (default) or "x-api-key"
78
+ timeout_ms=30000,
79
+ max_retries=2, # GET/DELETE retried on 5xx/network; any method retried on 429
80
+ max_response_bytes=25 * 1024 * 1024, # cap on buffered response bytes (default 25 MiB)
81
+ default_headers={"X-Trace": "abc"},
82
+ user_agent="my-app/1.0",
83
+ )
84
+ ```
85
+
86
+ **Retries.** Transient failures are retried with randomized backoff: network errors and `5xx` on idempotent methods (`GET`/`DELETE`), and `429` on any method (honoring `Retry-After`). Non-idempotent writes are never silently retried after a transport failure.
87
+
88
+ ## Resources
89
+
90
+ Every list endpoint returns a `{ "data": [...], "nextCursor": str | None }` envelope and has a matching `iterate*` helper that auto-paginates.
91
+
92
+ ### Tags — `client.tags`
93
+
94
+ ```python
95
+ client.tags.list(limit=None, cursor=None) # read:tags
96
+ client.tags.iterate(limit=None) # read:tags -> Paginator[Tag]
97
+ client.tags.get("tag-1") # read:tags
98
+ client.tags.battery("tag-1") # read:tags
99
+ client.tags.location("tag-1") # read:locations -> { data, recordingPaused }
100
+ client.tags.history("tag-1", start=None, end=None, order="asc", limit=None, cursor=None) # read:locations
101
+ client.tags.iterate_history("tag-1", start=None, end=None, order="asc", limit=None) # read:locations
102
+ ```
103
+
104
+ `start` / `end` accept an ISO 8601 string or epoch milliseconds.
105
+
106
+ ### Fleet — `client.fleet`
107
+
108
+ Incremental, whole-fleet location sync. Pass the high-water mark from your last sync as `since` (exclusive); omit it for a full sync.
109
+
110
+ ```python
111
+ client.fleet.delta(since=None, limit=None, cursor=None) # read:locations
112
+ client.fleet.iterate_delta(since=None, limit=None) # read:locations -> Paginator[FleetDeltaItem]
113
+ ```
114
+
115
+ ### Geofences — `client.geofences`
116
+
117
+ ```python
118
+ client.geofences.list(limit=None, cursor=None) # read:geofences
119
+ client.geofences.iterate(limit=None) # read:geofences
120
+ client.geofences.get("gf-1") # read:geofences
121
+ client.geofences.create({ # manage:geofences
122
+ "name": "Yard",
123
+ "center": {"lat": 37.77, "lng": -122.42},
124
+ "radiusMeters": 150,
125
+ "targetTagIds": ["tag-1", "tag-2"], # or ["all"] for all-tags keys
126
+ })
127
+ client.geofences.update("gf-1", {"radiusMeters": 200}) # manage:geofences
128
+ client.geofences.delete("gf-1") # manage:geofences -> { id, deleted }
129
+ ```
130
+
131
+ ### Webhooks — `client.webhooks`
132
+
133
+ ```python
134
+ client.webhooks.list(limit=None, cursor=None) # manage:webhooks
135
+ client.webhooks.get("wh-1") # manage:webhooks
136
+ created = client.webhooks.create({ # manage:webhooks
137
+ "url": "https://example.com/hooks/taglogger",
138
+ "events": ["geofence.entry", "geofence.exit", "tag.offline"],
139
+ })
140
+ secret = created["signingSecret"] # shown ONCE — store it now
141
+ client.webhooks.update("wh-1", {"active": False}) # manage:webhooks
142
+ client.webhooks.rotate_secret("wh-1") # manage:webhooks -> new signingSecret (once)
143
+ client.webhooks.send_test("wh-1") # manage:webhooks
144
+ client.webhooks.deliveries("wh-1", limit=None, cursor=None) # manage:webhooks
145
+ client.webhooks.iterate_deliveries("wh-1") # manage:webhooks
146
+ client.webhooks.delete("wh-1") # manage:webhooks -> { id, deleted }
147
+ ```
148
+
149
+ The six event types are `geofence.entry`, `geofence.exit`, `geofence.dwell`, `tag.moved`, `tag.battery_low`, `tag.offline`.
150
+
151
+ ### Share links — `client.share_links`
152
+
153
+ ```python
154
+ client.share_links.list(tag_id=None, limit=None, cursor=None) # read:share-links
155
+ client.share_links.iterate(tag_id=None, limit=None) # read:share-links
156
+ client.share_links.get("sl-1") # read:share-links
157
+ client.share_links.create({"tagId": "tag-1", "expiresInSeconds": 86400}) # manage:share-links
158
+ client.share_links.revoke("sl-1") # manage:share-links -> { id, revoked, status }
159
+ ```
160
+
161
+ ### Account — `client.account`
162
+
163
+ ```python
164
+ client.account.get() # any valid key -> Account
165
+ client.account.usage(days=30) # any valid key -> per-key request counts (max 40 days)
166
+ ```
167
+
168
+ ## Pagination
169
+
170
+ `iterate*` methods return a `Paginator`. Iterate it for individual items, call `.pages()` for a page at a time, or `.all()` to collect everything. Pages are fetched lazily, so a large collection never has to fit in memory.
171
+
172
+ ```python
173
+ pager = client.tags.iterate(limit=100)
174
+
175
+ for tag in pager: # individual items, across all pages
176
+ ...
177
+
178
+ for page in pager.pages(): # one page (list) at a time
179
+ ...
180
+
181
+ every = pager.all() # eager list of every item
182
+ ```
183
+
184
+ ## Verifying incoming webhooks
185
+
186
+ Every delivery carries an `X-TagLogger-Signature` header (`sha256=<hex>`, an HMAC-SHA256 of the **raw** request body keyed by the webhook's signing secret). Verify against the raw bytes before trusting the payload — re-serializing the JSON changes the bytes and fails the check.
187
+
188
+ ```python
189
+ from taglogger_sdk import (
190
+ construct_webhook_event,
191
+ verify_webhook_signature,
192
+ SIGNATURE_HEADER,
193
+ TagLoggerError,
194
+ )
195
+
196
+ # Flask example
197
+ @app.post("/hooks/taglogger")
198
+ def handle():
199
+ raw = request.get_data() # raw bytes, not request.json
200
+ signature = request.headers.get(SIGNATURE_HEADER)
201
+ try:
202
+ event = construct_webhook_event(raw, signature, WEBHOOK_SECRET)
203
+ except TagLoggerError:
204
+ return "", 400 # invalid_signature or invalid_body
205
+ # event is the parsed payload, e.g. {"event": "geofence.entry", ...}
206
+ return "", 200
207
+ ```
208
+
209
+ `verify_webhook_signature(raw_body, signature, secret) -> bool` is the lower-level check; it returns `False` (never raises) on a missing header or mismatch.
210
+
211
+ ## Error handling
212
+
213
+ Any non-2xx response, or a transport failure, raises `TagLoggerError`:
214
+
215
+ ```python
216
+ from taglogger_sdk import TagLoggerError
217
+
218
+ try:
219
+ client.tags.get("does-not-exist")
220
+ except TagLoggerError as err:
221
+ err.code # stable machine-readable code, e.g. "not_found"
222
+ err.status # HTTP status, or 0 for a transport/network failure
223
+ err.message
224
+ err.request_id # from X-Request-Id, when present
225
+ err.is_rate_limit # True on HTTP 429
226
+ err.is_transport # True when the request never reached the server
227
+ err.retry_after_seconds # parsed from Retry-After, when present
228
+ ```
229
+
230
+ ## Analytics — `taglogger_sdk.analytics`
231
+
232
+ A bundled, dependency-free toolkit that turns the location data the API returns into stops, trips, dwell intervals, clusters and map-ready shapes. Everything runs locally; nothing is sent anywhere.
233
+
234
+ ```python
235
+ from taglogger_sdk.analytics import (
236
+ fixes_from_location_points,
237
+ condition_fixes,
238
+ detect_stops,
239
+ detect_trips,
240
+ to_feature_collection,
241
+ )
242
+
243
+ # Pull a tag's history, then analyze it in-process
244
+ page = client.tags.history("tag-1", order="asc", limit=500)
245
+ fixes = fixes_from_location_points(page["data"])
246
+
247
+ clean = condition_fixes(fixes, max_accuracy_meters=100)
248
+ stops = detect_stops(clean, radius_meters=50, min_duration_ms=300_000) # 5 min
249
+ trips = detect_trips(clean, stops)
250
+
251
+ geojson = to_feature_collection(trips=trips, stops=stops) # ready for any map
252
+ ```
253
+
254
+ Battery projection from a series of fractional-level readings (`0.0`–`1.0`) that
255
+ you collect over time. Note that `client.tags.battery("tag-1")` reports a coarse
256
+ integer `status` code (`0` unknown … `4` critical; see `BatteryStatus`) and its
257
+ numeric `level` is currently always `None`, so
258
+ the estimator is designed for series you assemble yourself — for example by
259
+ recording the last known good fractional level alongside its timestamp:
260
+
261
+ ```python
262
+ from taglogger_sdk.analytics import BatterySample, estimate_battery_days_remaining
263
+
264
+ # Two fractional-level samples taken roughly a week apart.
265
+ est = estimate_battery_days_remaining([
266
+ BatterySample(level=0.92, timestamp_ms=1_700_000_000_000),
267
+ BatterySample(level=0.58, timestamp_ms=1_700_600_000_000),
268
+ ])
269
+ print(est.days_remaining, est.drain_per_day, est.reason)
270
+ ```
271
+
272
+ **Available helpers**
273
+
274
+ - **Types & adapters:** `Fix`, `Stop`, `Trip`, `Cluster`, `DwellInterval`, `BoundingBox`, `LatLng`, `BatterySample`, `BatteryEstimate`, `fix_from_location_point`, `fixes_from_location_points`
275
+ - **Conditioning:** `sort_by_time`, `dedupe_by_timestamp`, `condition_fixes`
276
+ - **Segmentation:** `detect_stops`, `detect_trips`, `compute_dwell`, `cluster_fixes` (DBSCAN)
277
+ - **Geometry:** `haversine_meters`, `bearing_degrees`, `centroid`, `bounding_box`, `path_length_meters`, `point_in_polygon`, `is_within_radius`, `douglas_peucker`, `encode_polyline`, `decode_polyline`
278
+ - **Shaping:** `to_point`, `to_line_string`, `to_polyline`, `to_feature_collection`, `trip_to_feature`, `stop_to_feature`, `fixes_to_feature`
279
+
280
+ **Performance:** `douglas_peucker` runs in `O(n log n)` on typical tracks but can reach `O(n**2)` on a path that retains nearly every vertex (a dense zig-zag). For very large inputs, down-sample first or raise `epsilon_meters`. `encode_polyline` raises `ValueError` on non-finite (`nan`/`inf`) coordinates.
281
+
282
+ ## Custom transport
283
+
284
+ Networking is pluggable. Inject any callable matching the transport signature to run hermetic tests or route requests through your own stack:
285
+
286
+ ```python
287
+ from taglogger_sdk import TagLoggerClient
288
+ from taglogger_sdk.http import HttpResponse
289
+
290
+ def fake_transport(method, url, headers, body, timeout):
291
+ return HttpResponse(status=200, headers={"content-type": "application/json"}, text='{"data": []}')
292
+
293
+ client = TagLoggerClient(api_key="tl_test_x", transport=fake_transport)
294
+ ```
295
+
296
+ ## License
297
+
298
+ MIT
@@ -0,0 +1,270 @@
1
+ # TagLogger Python SDK
2
+
3
+ Official Python SDK for the [TagLogger API](https://taglogger.com/docs/api) — read your tags and their locations, sync your whole fleet incrementally, manage geofences, webhooks and share links, and verify incoming webhook deliveries.
4
+
5
+ It also bundles **`taglogger_sdk.analytics`**, a pure-math, client-side location-analytics toolkit (conditioning, stops/trips, dwell, clustering, GeoJSON/polyline shaping, battery projection) that runs entirely in-process over the location data the API returns — no extra services, no network calls.
6
+
7
+ - **Zero runtime dependencies.** Standard library only.
8
+ - **Typed.** Ships `py.typed`; full type hints on the public surface.
9
+ - **Synchronous** and thread-safe to reuse: a single client instance carries no mutable per-call state.
10
+ - **Python 3.9+.**
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install taglogger-sdk
16
+ ```
17
+
18
+ The distribution installs as `taglogger-sdk`; you import it as `taglogger_sdk`.
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ from taglogger_sdk import TagLoggerClient
24
+
25
+ client = TagLoggerClient(api_key="tl_live_...")
26
+
27
+ # One page of tags
28
+ page = client.tags.list(limit=50)
29
+ for tag in page["data"]:
30
+ print(tag["id"], tag.get("name"))
31
+
32
+ # Or stream every tag, paging transparently
33
+ for tag in client.tags.iterate():
34
+ print(tag["id"])
35
+
36
+ # Latest location for one tag
37
+ location = client.tags.location("tag-1")
38
+ print(location["data"], "recording paused:", location["recordingPaused"])
39
+ ```
40
+
41
+ Get an API key from your TagLogger dashboard under **Settings → API Keys**. Keys are scoped; each method below notes the scope it needs.
42
+
43
+ ## Configuration
44
+
45
+ ```python
46
+ client = TagLoggerClient(
47
+ api_key="tl_live_...",
48
+ base_url="https://api.taglogger.com", # override to target a staging deployment
49
+ auth_header="bearer", # "bearer" (default) or "x-api-key"
50
+ timeout_ms=30000,
51
+ max_retries=2, # GET/DELETE retried on 5xx/network; any method retried on 429
52
+ max_response_bytes=25 * 1024 * 1024, # cap on buffered response bytes (default 25 MiB)
53
+ default_headers={"X-Trace": "abc"},
54
+ user_agent="my-app/1.0",
55
+ )
56
+ ```
57
+
58
+ **Retries.** Transient failures are retried with randomized backoff: network errors and `5xx` on idempotent methods (`GET`/`DELETE`), and `429` on any method (honoring `Retry-After`). Non-idempotent writes are never silently retried after a transport failure.
59
+
60
+ ## Resources
61
+
62
+ Every list endpoint returns a `{ "data": [...], "nextCursor": str | None }` envelope and has a matching `iterate*` helper that auto-paginates.
63
+
64
+ ### Tags — `client.tags`
65
+
66
+ ```python
67
+ client.tags.list(limit=None, cursor=None) # read:tags
68
+ client.tags.iterate(limit=None) # read:tags -> Paginator[Tag]
69
+ client.tags.get("tag-1") # read:tags
70
+ client.tags.battery("tag-1") # read:tags
71
+ client.tags.location("tag-1") # read:locations -> { data, recordingPaused }
72
+ client.tags.history("tag-1", start=None, end=None, order="asc", limit=None, cursor=None) # read:locations
73
+ client.tags.iterate_history("tag-1", start=None, end=None, order="asc", limit=None) # read:locations
74
+ ```
75
+
76
+ `start` / `end` accept an ISO 8601 string or epoch milliseconds.
77
+
78
+ ### Fleet — `client.fleet`
79
+
80
+ Incremental, whole-fleet location sync. Pass the high-water mark from your last sync as `since` (exclusive); omit it for a full sync.
81
+
82
+ ```python
83
+ client.fleet.delta(since=None, limit=None, cursor=None) # read:locations
84
+ client.fleet.iterate_delta(since=None, limit=None) # read:locations -> Paginator[FleetDeltaItem]
85
+ ```
86
+
87
+ ### Geofences — `client.geofences`
88
+
89
+ ```python
90
+ client.geofences.list(limit=None, cursor=None) # read:geofences
91
+ client.geofences.iterate(limit=None) # read:geofences
92
+ client.geofences.get("gf-1") # read:geofences
93
+ client.geofences.create({ # manage:geofences
94
+ "name": "Yard",
95
+ "center": {"lat": 37.77, "lng": -122.42},
96
+ "radiusMeters": 150,
97
+ "targetTagIds": ["tag-1", "tag-2"], # or ["all"] for all-tags keys
98
+ })
99
+ client.geofences.update("gf-1", {"radiusMeters": 200}) # manage:geofences
100
+ client.geofences.delete("gf-1") # manage:geofences -> { id, deleted }
101
+ ```
102
+
103
+ ### Webhooks — `client.webhooks`
104
+
105
+ ```python
106
+ client.webhooks.list(limit=None, cursor=None) # manage:webhooks
107
+ client.webhooks.get("wh-1") # manage:webhooks
108
+ created = client.webhooks.create({ # manage:webhooks
109
+ "url": "https://example.com/hooks/taglogger",
110
+ "events": ["geofence.entry", "geofence.exit", "tag.offline"],
111
+ })
112
+ secret = created["signingSecret"] # shown ONCE — store it now
113
+ client.webhooks.update("wh-1", {"active": False}) # manage:webhooks
114
+ client.webhooks.rotate_secret("wh-1") # manage:webhooks -> new signingSecret (once)
115
+ client.webhooks.send_test("wh-1") # manage:webhooks
116
+ client.webhooks.deliveries("wh-1", limit=None, cursor=None) # manage:webhooks
117
+ client.webhooks.iterate_deliveries("wh-1") # manage:webhooks
118
+ client.webhooks.delete("wh-1") # manage:webhooks -> { id, deleted }
119
+ ```
120
+
121
+ The six event types are `geofence.entry`, `geofence.exit`, `geofence.dwell`, `tag.moved`, `tag.battery_low`, `tag.offline`.
122
+
123
+ ### Share links — `client.share_links`
124
+
125
+ ```python
126
+ client.share_links.list(tag_id=None, limit=None, cursor=None) # read:share-links
127
+ client.share_links.iterate(tag_id=None, limit=None) # read:share-links
128
+ client.share_links.get("sl-1") # read:share-links
129
+ client.share_links.create({"tagId": "tag-1", "expiresInSeconds": 86400}) # manage:share-links
130
+ client.share_links.revoke("sl-1") # manage:share-links -> { id, revoked, status }
131
+ ```
132
+
133
+ ### Account — `client.account`
134
+
135
+ ```python
136
+ client.account.get() # any valid key -> Account
137
+ client.account.usage(days=30) # any valid key -> per-key request counts (max 40 days)
138
+ ```
139
+
140
+ ## Pagination
141
+
142
+ `iterate*` methods return a `Paginator`. Iterate it for individual items, call `.pages()` for a page at a time, or `.all()` to collect everything. Pages are fetched lazily, so a large collection never has to fit in memory.
143
+
144
+ ```python
145
+ pager = client.tags.iterate(limit=100)
146
+
147
+ for tag in pager: # individual items, across all pages
148
+ ...
149
+
150
+ for page in pager.pages(): # one page (list) at a time
151
+ ...
152
+
153
+ every = pager.all() # eager list of every item
154
+ ```
155
+
156
+ ## Verifying incoming webhooks
157
+
158
+ Every delivery carries an `X-TagLogger-Signature` header (`sha256=<hex>`, an HMAC-SHA256 of the **raw** request body keyed by the webhook's signing secret). Verify against the raw bytes before trusting the payload — re-serializing the JSON changes the bytes and fails the check.
159
+
160
+ ```python
161
+ from taglogger_sdk import (
162
+ construct_webhook_event,
163
+ verify_webhook_signature,
164
+ SIGNATURE_HEADER,
165
+ TagLoggerError,
166
+ )
167
+
168
+ # Flask example
169
+ @app.post("/hooks/taglogger")
170
+ def handle():
171
+ raw = request.get_data() # raw bytes, not request.json
172
+ signature = request.headers.get(SIGNATURE_HEADER)
173
+ try:
174
+ event = construct_webhook_event(raw, signature, WEBHOOK_SECRET)
175
+ except TagLoggerError:
176
+ return "", 400 # invalid_signature or invalid_body
177
+ # event is the parsed payload, e.g. {"event": "geofence.entry", ...}
178
+ return "", 200
179
+ ```
180
+
181
+ `verify_webhook_signature(raw_body, signature, secret) -> bool` is the lower-level check; it returns `False` (never raises) on a missing header or mismatch.
182
+
183
+ ## Error handling
184
+
185
+ Any non-2xx response, or a transport failure, raises `TagLoggerError`:
186
+
187
+ ```python
188
+ from taglogger_sdk import TagLoggerError
189
+
190
+ try:
191
+ client.tags.get("does-not-exist")
192
+ except TagLoggerError as err:
193
+ err.code # stable machine-readable code, e.g. "not_found"
194
+ err.status # HTTP status, or 0 for a transport/network failure
195
+ err.message
196
+ err.request_id # from X-Request-Id, when present
197
+ err.is_rate_limit # True on HTTP 429
198
+ err.is_transport # True when the request never reached the server
199
+ err.retry_after_seconds # parsed from Retry-After, when present
200
+ ```
201
+
202
+ ## Analytics — `taglogger_sdk.analytics`
203
+
204
+ A bundled, dependency-free toolkit that turns the location data the API returns into stops, trips, dwell intervals, clusters and map-ready shapes. Everything runs locally; nothing is sent anywhere.
205
+
206
+ ```python
207
+ from taglogger_sdk.analytics import (
208
+ fixes_from_location_points,
209
+ condition_fixes,
210
+ detect_stops,
211
+ detect_trips,
212
+ to_feature_collection,
213
+ )
214
+
215
+ # Pull a tag's history, then analyze it in-process
216
+ page = client.tags.history("tag-1", order="asc", limit=500)
217
+ fixes = fixes_from_location_points(page["data"])
218
+
219
+ clean = condition_fixes(fixes, max_accuracy_meters=100)
220
+ stops = detect_stops(clean, radius_meters=50, min_duration_ms=300_000) # 5 min
221
+ trips = detect_trips(clean, stops)
222
+
223
+ geojson = to_feature_collection(trips=trips, stops=stops) # ready for any map
224
+ ```
225
+
226
+ Battery projection from a series of fractional-level readings (`0.0`–`1.0`) that
227
+ you collect over time. Note that `client.tags.battery("tag-1")` reports a coarse
228
+ integer `status` code (`0` unknown … `4` critical; see `BatteryStatus`) and its
229
+ numeric `level` is currently always `None`, so
230
+ the estimator is designed for series you assemble yourself — for example by
231
+ recording the last known good fractional level alongside its timestamp:
232
+
233
+ ```python
234
+ from taglogger_sdk.analytics import BatterySample, estimate_battery_days_remaining
235
+
236
+ # Two fractional-level samples taken roughly a week apart.
237
+ est = estimate_battery_days_remaining([
238
+ BatterySample(level=0.92, timestamp_ms=1_700_000_000_000),
239
+ BatterySample(level=0.58, timestamp_ms=1_700_600_000_000),
240
+ ])
241
+ print(est.days_remaining, est.drain_per_day, est.reason)
242
+ ```
243
+
244
+ **Available helpers**
245
+
246
+ - **Types & adapters:** `Fix`, `Stop`, `Trip`, `Cluster`, `DwellInterval`, `BoundingBox`, `LatLng`, `BatterySample`, `BatteryEstimate`, `fix_from_location_point`, `fixes_from_location_points`
247
+ - **Conditioning:** `sort_by_time`, `dedupe_by_timestamp`, `condition_fixes`
248
+ - **Segmentation:** `detect_stops`, `detect_trips`, `compute_dwell`, `cluster_fixes` (DBSCAN)
249
+ - **Geometry:** `haversine_meters`, `bearing_degrees`, `centroid`, `bounding_box`, `path_length_meters`, `point_in_polygon`, `is_within_radius`, `douglas_peucker`, `encode_polyline`, `decode_polyline`
250
+ - **Shaping:** `to_point`, `to_line_string`, `to_polyline`, `to_feature_collection`, `trip_to_feature`, `stop_to_feature`, `fixes_to_feature`
251
+
252
+ **Performance:** `douglas_peucker` runs in `O(n log n)` on typical tracks but can reach `O(n**2)` on a path that retains nearly every vertex (a dense zig-zag). For very large inputs, down-sample first or raise `epsilon_meters`. `encode_polyline` raises `ValueError` on non-finite (`nan`/`inf`) coordinates.
253
+
254
+ ## Custom transport
255
+
256
+ Networking is pluggable. Inject any callable matching the transport signature to run hermetic tests or route requests through your own stack:
257
+
258
+ ```python
259
+ from taglogger_sdk import TagLoggerClient
260
+ from taglogger_sdk.http import HttpResponse
261
+
262
+ def fake_transport(method, url, headers, body, timeout):
263
+ return HttpResponse(status=200, headers={"content-type": "application/json"}, text='{"data": []}')
264
+
265
+ client = TagLoggerClient(api_key="tl_test_x", transport=fake_transport)
266
+ ```
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ # Distribution (PyPI) name. The import package is ``taglogger_sdk`` (see
7
+ # [tool.hatch.build.targets.wheel] below) — it matches the distribution name and
8
+ # avoids colliding with the unrelated ``taglogger`` package already on PyPI.
9
+ name = "taglogger-sdk"
10
+ version = "1.0.0"
11
+ description = "Official Python SDK for the TagLogger API, with a bundled client-side location-analytics toolkit."
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ license = { text = "MIT" }
15
+ authors = [{ name = "TagLogger" }]
16
+ keywords = ["taglogger", "asset-tracking", "geofence", "location-analytics", "gps"]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=7", "mypy>=1.0", "ruff>=0.1"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://taglogger.com"
37
+ Documentation = "https://taglogger.com/docs/api"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/taglogger_sdk"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ pythonpath = ["src"]
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py39"
49
+
50
+ [tool.ruff.lint]
51
+ select = ["E", "F", "I", "UP", "B", "W"]
52
+
53
+ [tool.mypy]
54
+ python_version = "3.9"
55
+ strict = true
56
+ files = ["src/taglogger_sdk"]
57
+
58
+ [[tool.mypy.overrides]]
59
+ # The resources layer is the single boundary where typed method signatures meet
60
+ # the untyped JSON decoded off the wire. Returning that decoded ``Any`` as the
61
+ # declared response type is intentional here; every other module stays fully
62
+ # strict (this only relaxes the return-an-Any warning, nothing else).
63
+ module = "taglogger_sdk.resources.*"
64
+ warn_return_any = false