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.
- taglogger_sdk-1.0.0/.gitignore +14 -0
- taglogger_sdk-1.0.0/LICENSE +21 -0
- taglogger_sdk-1.0.0/PKG-INFO +298 -0
- taglogger_sdk-1.0.0/README.md +270 -0
- taglogger_sdk-1.0.0/pyproject.toml +64 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/__init__.py +55 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/__init__.py +110 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/battery.py +102 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/conditioning.py +80 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/geometry.py +260 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/segmentation.py +262 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/shaping.py +89 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/analytics/types.py +137 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/client.py +72 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/errors.py +52 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/http.py +490 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/pagination.py +88 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/py.typed +0 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/__init__.py +17 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/account.py +22 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/fleet.py +45 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/geofences.py +56 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/share_links.py +61 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/shared.py +41 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/tags.py +82 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/resources/webhooks.py +102 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/types.py +366 -0
- taglogger_sdk-1.0.0/src/taglogger_sdk/webhooks.py +96 -0
- taglogger_sdk-1.0.0/tests/test_battery.py +77 -0
- taglogger_sdk-1.0.0/tests/test_conditioning.py +113 -0
- taglogger_sdk-1.0.0/tests/test_geometry.py +221 -0
- taglogger_sdk-1.0.0/tests/test_http.py +464 -0
- taglogger_sdk-1.0.0/tests/test_pagination.py +120 -0
- taglogger_sdk-1.0.0/tests/test_resources.py +222 -0
- taglogger_sdk-1.0.0/tests/test_segmentation.py +167 -0
- taglogger_sdk-1.0.0/tests/test_shaping.py +110 -0
- taglogger_sdk-1.0.0/tests/test_types.py +96 -0
- taglogger_sdk-1.0.0/tests/test_webhooks.py +172 -0
|
@@ -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
|