inmotion-sdk 0.2.3__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.
- inmotion_sdk-0.2.3/.gitignore +18 -0
- inmotion_sdk-0.2.3/LICENSE +21 -0
- inmotion_sdk-0.2.3/PKG-INFO +488 -0
- inmotion_sdk-0.2.3/README.md +460 -0
- inmotion_sdk-0.2.3/inmotion/__init__.py +37 -0
- inmotion_sdk-0.2.3/inmotion/accounts.py +229 -0
- inmotion_sdk-0.2.3/inmotion/activities.py +381 -0
- inmotion_sdk-0.2.3/inmotion/activity_config.py +90 -0
- inmotion_sdk-0.2.3/inmotion/api.py +2499 -0
- inmotion_sdk-0.2.3/inmotion/apikey.py +52 -0
- inmotion_sdk-0.2.3/inmotion/apikey_client.py +170 -0
- inmotion_sdk-0.2.3/inmotion/audit.py +18 -0
- inmotion_sdk-0.2.3/inmotion/credentials_client.py +215 -0
- inmotion_sdk-0.2.3/inmotion/datastream.py +205 -0
- inmotion_sdk-0.2.3/inmotion/devkey.py +52 -0
- inmotion_sdk-0.2.3/inmotion/event.py +69 -0
- inmotion_sdk-0.2.3/inmotion/exceptions.py +30 -0
- inmotion_sdk-0.2.3/inmotion/folio.py +149 -0
- inmotion_sdk-0.2.3/inmotion/model.py +99 -0
- inmotion_sdk-0.2.3/inmotion/models.py +2413 -0
- inmotion_sdk-0.2.3/inmotion/py.typed +0 -0
- inmotion_sdk-0.2.3/inmotion/raster_overlay.py +67 -0
- inmotion_sdk-0.2.3/inmotion/shape.py +86 -0
- inmotion_sdk-0.2.3/inmotion/shapegenerator.py +74 -0
- inmotion_sdk-0.2.3/inmotion/upload.py +114 -0
- inmotion_sdk-0.2.3/inmotion/user.py +125 -0
- inmotion_sdk-0.2.3/inmotion/utils.py +110 -0
- inmotion_sdk-0.2.3/pyproject.toml +40 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__pycache__
|
|
2
|
+
.bsp
|
|
3
|
+
.venv
|
|
4
|
+
.env*
|
|
5
|
+
!.env.example
|
|
6
|
+
.idea
|
|
7
|
+
.python-version
|
|
8
|
+
out
|
|
9
|
+
outputs
|
|
10
|
+
dist
|
|
11
|
+
docs
|
|
12
|
+
uv.lock
|
|
13
|
+
/inmotion-python-sdk.iml
|
|
14
|
+
examples/*.csv
|
|
15
|
+
|
|
16
|
+
# Coding-agent context (kept locally, not committed)
|
|
17
|
+
CLAUDE.md
|
|
18
|
+
.claude/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Waring
|
|
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,488 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: inmotion-sdk
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: An SDK for integrating with inMotion API
|
|
5
|
+
Project-URL: Homepage, https://github.com/jwaring/inmotion-python-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/jwaring/inmotion-python-sdk
|
|
7
|
+
Project-URL: Issues, https://github.com/jwaring/inmotion-python-sdk/issues
|
|
8
|
+
Project-URL: Documentation, https://inmotion.io/assets/sdk-docs/inmotion.html
|
|
9
|
+
Author-email: Jason Waring <jason@softwaringsolutions.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
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 :: Only
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: marshmallow
|
|
19
|
+
Requires-Dist: marshmallow-dataclass
|
|
20
|
+
Requires-Dist: pycryptodome
|
|
21
|
+
Requires-Dist: python-dotenv
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pdoc; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: tox; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# inmotion-python-sdk
|
|
30
|
+
|
|
31
|
+
A Python Software Development Kit (SDK) for integration with inMotion APIs.
|
|
32
|
+
|
|
33
|
+
This is still a fledgling project as only a handful of endpoints have been implemented.
|
|
34
|
+
|
|
35
|
+
Full API reference (generated from docstrings): https://inmotion.io/assets/sdk-docs/inmotion.html
|
|
36
|
+
|
|
37
|
+
# Prerequisites
|
|
38
|
+
|
|
39
|
+
* Python 3.10 or higher
|
|
40
|
+
* An inMotion account with either a Dev Key/Secret + API Key pair, or a username/password —
|
|
41
|
+
see "Authenticating" below for where to create these
|
|
42
|
+
|
|
43
|
+
# Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install inmotion-sdk
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The distribution is `inmotion-sdk`, but the importable package is still `inmotion`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import inmotion
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
# Quickstart
|
|
56
|
+
|
|
57
|
+
Every operation in this SDK hangs off a `session`, obtained from a client. There are two ways to
|
|
58
|
+
authenticate, depending on what credentials you have.
|
|
59
|
+
|
|
60
|
+
## Authenticating with a Dev Key/Secret + API Key
|
|
61
|
+
|
|
62
|
+
Create a Dev Key/Secret under your inMotion account's `Settings` menu, `Dev Keys` tab (only shown
|
|
63
|
+
if the account allows key creation), and an API Key under the `API Keys` tab (`Consumer` authority
|
|
64
|
+
or higher is recommended). You'll also need your account key.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from inmotion.apikey_client import InMotionAPIKeyClient
|
|
68
|
+
|
|
69
|
+
client = InMotionAPIKeyClient(
|
|
70
|
+
base_url="https://api.inmotion.io",
|
|
71
|
+
dev_key="...",
|
|
72
|
+
dev_secret="...",
|
|
73
|
+
api_key="...",
|
|
74
|
+
)
|
|
75
|
+
session = client.get_session(account="...")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Authenticating with a username and password
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from inmotion.credentials_client import InMotionCredentialsClient
|
|
82
|
+
|
|
83
|
+
client = InMotionCredentialsClient(base_url="https://api.inmotion.io", dev_key="...", dev_secret="...")
|
|
84
|
+
session = client.get_session(account="...", username="...", password="...")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Once you have a `session`, it exposes the feature areas documented below, e.g.
|
|
88
|
+
`session.activities()`, `session.accounts()`, `session.folio()`.
|
|
89
|
+
|
|
90
|
+
# Examples
|
|
91
|
+
|
|
92
|
+
`examples/` has two small, self-contained scripts demonstrating the Site and Track activity APIs:
|
|
93
|
+
|
|
94
|
+
- `site_timeseries_example.py` — generates a synthetic weather-sensor CSV (temperature + humidity),
|
|
95
|
+
then reads it back and uploads it as a Site activity timeseries.
|
|
96
|
+
- `track_timeseries_example.py` — generates a synthetic vehicle-track CSV (lat/lon/altitude +
|
|
97
|
+
speed), then reads it back and uploads it as a Track activity timeseries.
|
|
98
|
+
|
|
99
|
+
Each script both generates its own input data and uploads it, so there's nothing to fetch — they're
|
|
100
|
+
meant to be read top-to-bottom as a concrete illustration of find-or-create-activity plus
|
|
101
|
+
publish-records. To run one:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cp examples/.env.example examples/.env # fill in real credentials
|
|
105
|
+
python3 examples/site_timeseries_example.py
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
# Feature Areas
|
|
109
|
+
|
|
110
|
+
## Upgrading from an older version
|
|
111
|
+
|
|
112
|
+
If you were already calling `find_track_activity`, `find_site_activity`, `get_track_records`,
|
|
113
|
+
`get_site_records`, `find_activities_within_time_range`, or `find_latest_activity_stats`, note that
|
|
114
|
+
earlier versions of this SDK issued every HTTP request as a `POST`, even for these read-only,
|
|
115
|
+
`GET`-only endpoints. Against a real inMotion server this either silently invoked the wrong
|
|
116
|
+
operation or returned a 404. These methods now issue the correct HTTP verb — no code changes are
|
|
117
|
+
required to call them, but double-check any code that was working around the previous failures.
|
|
118
|
+
|
|
119
|
+
`InMotionCredentialsClient.connect(...)` has been renamed to `get_session(...)` to match
|
|
120
|
+
`InMotionAPIKeyClient`, and now authenticates against the current `/api/latest/authenticate`
|
|
121
|
+
endpoint rather than the deprecated `/api/authenticate`.
|
|
122
|
+
|
|
123
|
+
## Track and Site Activities
|
|
124
|
+
|
|
125
|
+
In addition to create/update/find/records, the activities interface now supports the full
|
|
126
|
+
lifecycle of a track or site activity:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
activities = session.activities()
|
|
130
|
+
|
|
131
|
+
activities.delete_track_activity(track_key)
|
|
132
|
+
activities.unlock_track_activity(track_key)
|
|
133
|
+
activities.find_all_track_records(track_key) # no time-range restriction
|
|
134
|
+
records_bytes = activities.download_track_records(track_key, "csv") # 'csv', 'json' or 'gpx'
|
|
135
|
+
|
|
136
|
+
activities.share_track_activity(track_key, "any") # or "private"
|
|
137
|
+
activities.unshare_track_activity(track_key, "any")
|
|
138
|
+
activities.find_shared_track_activity(track_key)
|
|
139
|
+
activities.find_all_shared_track_records(track_key)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The same methods exist for sites (`delete_site_activity`, `unlock_site_activity`,
|
|
143
|
+
`find_all_site_records`, `download_site_records`, `share_site_activity`, `unshare_site_activity`,
|
|
144
|
+
`find_shared_site_activity`, `find_shared_site_records`).
|
|
145
|
+
|
|
146
|
+
Cross-account analytics, a batch create/update endpoint, and activity master data are also
|
|
147
|
+
available:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
activities.find_activity_master_data() # profile types and activity types
|
|
151
|
+
|
|
152
|
+
analytics = activities.find_activity_analytics(ActivityAnalyticsRequestModel(groupBy=["activityType"]))
|
|
153
|
+
track_metrics = activities.find_activity_track_metrics(ActivityAnalyticsRequestModel(accounts=[account_key]))
|
|
154
|
+
variable_stats = activities.find_activity_variable_stats(ActivityAnalyticsRequestModel(accounts=[account_key]))
|
|
155
|
+
|
|
156
|
+
activities.find_latest_activity_stats_by_type(since, CoordinateConvention.TRACK)
|
|
157
|
+
|
|
158
|
+
activities.batch_record_update(ActivityBatchCommandsModel(
|
|
159
|
+
tracks=TrackActivityBatchCommandsModel(create=[TrackCreateActivityBatchModel(activity=..., recordInterval=1000)]),
|
|
160
|
+
))
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Accounts
|
|
164
|
+
|
|
165
|
+
`session.accounts()` exposes account management operations:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
accounts = session.accounts()
|
|
169
|
+
|
|
170
|
+
account = accounts.find_account(account_key)
|
|
171
|
+
tags = accounts.find_account_tags(account_key)
|
|
172
|
+
accounts.update_account(account_key, AccountModel(name="Acme", address=None, accountType="1", attrs={}, profiles=[]))
|
|
173
|
+
|
|
174
|
+
users = accounts.find_account_users(account_key)
|
|
175
|
+
accounts.register_account_user(account_key, user_key, privileges)
|
|
176
|
+
accounts.unregister_account_user(account_key, user_key)
|
|
177
|
+
accounts.batch_update_account_users(account_key, [AccountUpdateBatchCommandModel(action="register", userName="jdoe", privileges=None)])
|
|
178
|
+
|
|
179
|
+
new_account = accounts.create_account_only(AccountModel(name="New Co", address=None, accountType="I", attrs={}, profiles=[]))
|
|
180
|
+
accounts.mark_account_for_deletion(account_key, and_user=False)
|
|
181
|
+
|
|
182
|
+
accounts.find_my_accounts() # accounts the authenticated user belongs to
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
It also manages an account's Standard Data Type/Variant Type overrides (submitted as raw YAML
|
|
186
|
+
text, requires the "custom-sdt" account feature) and its Device Config lifecycle (requires the
|
|
187
|
+
"custom-device-config" account feature):
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
accounts.list_standard_data_types(account_key)
|
|
191
|
+
accounts.create_standard_data_type(account_key, yaml_document)
|
|
192
|
+
accounts.update_standard_data_type(account_key, key, yaml_document)
|
|
193
|
+
accounts.delete_standard_data_type(account_key, key)
|
|
194
|
+
# ...and the equivalent list/create/update/delete_standard_data_variant_type methods
|
|
195
|
+
|
|
196
|
+
accounts.fetch_device_configs(account_key) # merged, consumer-facing fetch
|
|
197
|
+
accounts.list_device_configs(account_key) # full version history, for an editing UI
|
|
198
|
+
created = accounts.create_device_config(account_key, yaml_document)
|
|
199
|
+
accounts.start_device_config_development(account_key, created["name"])
|
|
200
|
+
accounts.save_device_config(account_key, created["name"], yaml_document)
|
|
201
|
+
accounts.publish_device_config(account_key, created["name"], "1.0.0")
|
|
202
|
+
accounts.withdraw_device_config(account_key, created["name"], 1)
|
|
203
|
+
accounts.republish_device_config(account_key, created["name"], 1)
|
|
204
|
+
accounts.discard_device_config_development(account_key, created["name"])
|
|
205
|
+
accounts.delete_device_config(account_key, created["name"])
|
|
206
|
+
|
|
207
|
+
# System-wide (non-account-scoped) global tier, and a combined sync delta across global + accounts
|
|
208
|
+
accounts.fetch_global_device_configs()
|
|
209
|
+
accounts.sync_device_configs(DeviceConfigSyncRequestModel(accountKeys=[account_key]))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Activity Configuration
|
|
213
|
+
|
|
214
|
+
`session.activity_config()` manages the Quality Control, Processing, and Custom Data sections of
|
|
215
|
+
an activity's configuration, and supports bad-period detection for track activities:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
config = session.activity_config()
|
|
219
|
+
|
|
220
|
+
full_config = config.find_activity_config(activity_key)
|
|
221
|
+
|
|
222
|
+
config.update_qc_config(activity_key, ActivityConfigQCUpdateModel(qualityControl=QCConfigModel(regions=[...])))
|
|
223
|
+
config.update_processing_config(activity_key, ActivityConfigProcessingUpdateModel(processing={...}))
|
|
224
|
+
config.update_custom_data_config(activity_key, ActivityConfigCustomDataUpdateModel(entries=[...]))
|
|
225
|
+
|
|
226
|
+
# Workflow: detect candidate bad periods on a track activity, then merge them into the QC config
|
|
227
|
+
detected = config.detect_bad_periods(activity_key, ActivityConfigBadPeriodDetectRequestModel())
|
|
228
|
+
config.merge_bad_periods(activity_key, ActivityConfigBadPeriodMergeRequestModel(
|
|
229
|
+
periods=detected.periods, detectorVersion="v1", dryRun=False))
|
|
230
|
+
|
|
231
|
+
regions = config.generate_qc_regions(activity_key, ActivityConfigQCRegionGenerateRequestModel())
|
|
232
|
+
|
|
233
|
+
config.delete_activity_config(activity_key)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Developer Keys and API Keys
|
|
237
|
+
|
|
238
|
+
`session.dev_keys()` and `session.api_keys()` manage the developer key / secret pairs and API keys
|
|
239
|
+
issued against an account:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
dev_keys = session.dev_keys()
|
|
243
|
+
keys = dev_keys.find_dev_keys(account_key)
|
|
244
|
+
new_key = dev_keys.create_dev_key(account_key, AccountDevKeyCreatorModel(name="ci", hmacEnabled=True, expiryOn=None))
|
|
245
|
+
dev_keys.update_dev_key(account_key, new_key.devKey, AccountDevKeyUpdatorModel(name="ci-renamed", hmacEnabled=None))
|
|
246
|
+
dev_keys.delete_dev_key(account_key, new_key.devKey)
|
|
247
|
+
|
|
248
|
+
api_keys = session.api_keys()
|
|
249
|
+
api_key = api_keys.create_api_key(account_key, AccountAPIKeyCreatorModel(name="integration", privs=privileges, expiryOn=None))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## User Management
|
|
253
|
+
|
|
254
|
+
`session.user()` manages the currently authenticated user, and account-scoped user registration:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
user = session.user()
|
|
258
|
+
|
|
259
|
+
user.find_user_attributes()
|
|
260
|
+
user.update_user_attributes(UserAttributesModel(userName="jdoe", displayName="J Doe", email="j@x.com",
|
|
261
|
+
publicUserName=False, firstName=None, lastName=None,
|
|
262
|
+
avatarUrl=None, attrs={}))
|
|
263
|
+
|
|
264
|
+
# Register a new user and grant them a privilege level ('view', 'contribute', or 'admin') on an account
|
|
265
|
+
user.create_user_against_account(account_key, "view", registration)
|
|
266
|
+
|
|
267
|
+
user.request_password_reset(UserPasswordRequestModel(userNameOrEmail="jdoe"))
|
|
268
|
+
user.unregister_from_account(account_key)
|
|
269
|
+
|
|
270
|
+
# A one-time code key pair, for device pairing/bootstrap flows
|
|
271
|
+
otc = user.create_otc()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Uploads
|
|
275
|
+
|
|
276
|
+
`session.upload()` manages file uploads (e.g. track/route/coverage data files) and their
|
|
277
|
+
processing lifecycle:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
upload = session.upload()
|
|
281
|
+
|
|
282
|
+
created = upload.upload_file(account_key, "/path/to/track.gpx", content_type="application/gpx+xml")
|
|
283
|
+
uuid = next(iter(created))
|
|
284
|
+
|
|
285
|
+
upload.find_upload_metadata(uuid)
|
|
286
|
+
upload.update_upload_metadata(uuid, UploadMetadataChangeCommandModel(
|
|
287
|
+
mimeType="application/gpx+xml", nature="track", attributes={}))
|
|
288
|
+
|
|
289
|
+
preview = upload.find_upload_preview(uuid, "track")
|
|
290
|
+
upload.process_upload(uuid) # commit the upload into inMotion once its nature/metadata are set
|
|
291
|
+
|
|
292
|
+
upload.cancel_upload(uuid) # or, once no longer needed:
|
|
293
|
+
upload.delete_upload(uuid)
|
|
294
|
+
|
|
295
|
+
upload.find_uploads(account_key) # all tracked uploads for the account
|
|
296
|
+
|
|
297
|
+
upload.upload_diagnostics(account_key, "/path/to/crash.log") # stored server-side, outside the tracked-upload pipeline
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Note:** `upload_file`'s multipart request signing has been verified against the server's
|
|
301
|
+
signing/verification code (`APIActions.scala`) but not yet against a live inMotion instance —
|
|
302
|
+
test it against `.env.test` before relying on it in production.
|
|
303
|
+
|
|
304
|
+
## Folio
|
|
305
|
+
|
|
306
|
+
`session.folio()` manages folios: a tree-structured document attached to an account - a versioned
|
|
307
|
+
root plus an arbitrary tree of named sections, each holding items that are either inline
|
|
308
|
+
structured text or references to an Activity, DataStream, or another Folio.
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
folio_api = session.folio()
|
|
312
|
+
|
|
313
|
+
root = FolioRootModel(attrs={}, items=[], sections=[])
|
|
314
|
+
f = folio_api.create_folio(FolioModel(name="Site A", description="...", accountKey=account_key, owner=user_key, created=0, root=root))
|
|
315
|
+
folio_api.update_folio(f.key, FolioModel(name="Site A (renamed)", description="...", accountKey=account_key, owner=user_key, created=0, root=root))
|
|
316
|
+
folio_api.find_folio(f.key)
|
|
317
|
+
folio_api.find_folios(account_key, name="Site A")
|
|
318
|
+
folio_api.find_folios_by_reference(account_key, data_stream_key)
|
|
319
|
+
|
|
320
|
+
# Sections and items are addressed by a "/"-separated `path` from the root (omitted = the root itself)
|
|
321
|
+
folio_api.create_section(f.key, FolioSectionCreateModel(name="Sensors", description="..."))
|
|
322
|
+
folio_api.find_section(f.key, path="Sensors", deep=True)
|
|
323
|
+
folio_api.update_section(f.key, FolioSectionUpdateModel(description="Updated"), path="Sensors")
|
|
324
|
+
|
|
325
|
+
folio_api.add_items(f.key, [FolioItemModel(kind="dataStream", name="Reading 1", dataStreamKey=ds_key, owned=True)], path="Sensors")
|
|
326
|
+
folio_api.update_item(f.key, "Reading 1", FolioItemModel(kind="text", name="Reading 1", format="YAML", content="..."), path="Sensors")
|
|
327
|
+
folio_api.delete_item(f.key, "Reading 1", path="Sensors")
|
|
328
|
+
folio_api.delete_items(f.key, ["Reading 2"], path="Sensors", cascade=True)
|
|
329
|
+
|
|
330
|
+
folio_api.delete_section(f.key, path="Sensors", cascade=True)
|
|
331
|
+
folio_api.validate_folio(f.key) # check against the folio's optional template, if any
|
|
332
|
+
|
|
333
|
+
folio_api.delete_folio(f.key)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Shapes
|
|
337
|
+
|
|
338
|
+
`session.shape()` manages shapes: a named, classified collection of polygons (which may have
|
|
339
|
+
holes/islands), stored as a single GeoJSON FeatureCollection, and owned directly by an account
|
|
340
|
+
(not gated by Folio's role/contributor model).
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
shape_api = session.shape()
|
|
344
|
+
|
|
345
|
+
s = shape_api.create_shape(ShapeModel(name="Field 12", accountKey=account_key, geojson=geojson_str))
|
|
346
|
+
shape_api.find_shapes(account_key, classification="Boundary")
|
|
347
|
+
shape_api.find_shape(s.key)
|
|
348
|
+
|
|
349
|
+
shape_api.update_shape(s.key, ShapeUpdateModel(name="Field 12 (renamed)"))
|
|
350
|
+
shape_api.update_shape_geometry(s.key, ShapeGeometryModel(geojson=updated_geojson_str))
|
|
351
|
+
|
|
352
|
+
shape_api.delete_shape(s.key)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
A track activity's GPS records can also be converted into a standalone Route shape (gated by the
|
|
356
|
+
"track-to-shape" account feature) via `session.activities().convert_track_to_route(track_key)`,
|
|
357
|
+
which returns the new shape's key.
|
|
358
|
+
|
|
359
|
+
## Events
|
|
360
|
+
|
|
361
|
+
`session.events()` manages events: an Activity peer of Track/Site whose payload is arbitrary
|
|
362
|
+
(photo, sqlite file, diagnostics, ...) rather than structured data - a single point in space/time,
|
|
363
|
+
optionally carrying a thumbnail/icon.
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
events = session.events()
|
|
367
|
+
|
|
368
|
+
location = EventLocationModel(latitude=51.5, longitude=-0.1, timeUtc=int(time.time() * 1000))
|
|
369
|
+
event = events.create_event(EventCreatorModel(dataStream=data_stream_creator, location=location))
|
|
370
|
+
|
|
371
|
+
events.update_event(event.dataStream.key, EventUpdateModel(dataStream=data_stream_creator))
|
|
372
|
+
events.find_event(event.dataStream.key)
|
|
373
|
+
events.unlock_event(event.dataStream.key)
|
|
374
|
+
|
|
375
|
+
events.find_events(DataStreamFilterModel(accounts=[account_key]))
|
|
376
|
+
events.find_nearby_events(EventNearbyFilterModel(
|
|
377
|
+
accounts=[account_key], minTime=0, maxTime=int(time.time() * 1000),
|
|
378
|
+
minLatitude=51.0, maxLatitude=52.0, minLongitude=-1.0, maxLongitude=1.0))
|
|
379
|
+
|
|
380
|
+
events.delete_event(event.dataStream.key)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Data Streams
|
|
384
|
+
|
|
385
|
+
`session.data_stream()` manages data streams and their two kinds of data channel: hyperslab
|
|
386
|
+
(array/gridded numeric data) and blob (byte-oriented data, e.g. images or arbitrary binary blobs).
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
ds_api = session.data_stream()
|
|
390
|
+
|
|
391
|
+
ds = ds_api.create_data_stream(DataStreamCreatorModel(
|
|
392
|
+
name="Weather Station 1", description="...", account=account_key, owner=user_key, tags=[],
|
|
393
|
+
sourceIdentifier="ws1", sourceCategory="weather", sourceProfile="standard", sourceName="WS1",
|
|
394
|
+
acqConv="raw", coordConv="wgs84", timezone="UTC", attrs={}, created=0))
|
|
395
|
+
|
|
396
|
+
ds_api.find_data_stream(ds.key)
|
|
397
|
+
ds_api.find_data_streams(DataStreamFilterModel(accounts=[account_key]))
|
|
398
|
+
ds_api.find_data_streams_by_name(account_key, "Weather")
|
|
399
|
+
|
|
400
|
+
# Hyperslab (numeric) channels
|
|
401
|
+
ds_api.create_hyperslab_channel(ds.key, DataChannelCreatorModel(
|
|
402
|
+
channelType="temperature", profiles={}, unlimitedDim="time", fixedDims={}, vars={}, created=0))
|
|
403
|
+
ds_api.update_invariant_hyperslab_data(ds.key, "temperature", {"units": "celsius"})
|
|
404
|
+
ds_api.update_hyperslab_record_data(ds.key, "temperature", {"timeUtc": [...], "value": [...]})
|
|
405
|
+
ds_api.find_hyperslab_record_data(ds.key, "temperature", start, end)
|
|
406
|
+
|
|
407
|
+
# Blob (byte-oriented) channels
|
|
408
|
+
ds_api.create_blob_channel(ds.key, DataChannelCreatorModel(
|
|
409
|
+
channelType="image", profiles={}, unlimitedDim=None, fixedDims={}, vars={}, created=0))
|
|
410
|
+
ds_api.update_invariant_blob_data(ds.key, "image", "raw", image_bytes)
|
|
411
|
+
ds_api.find_latest_blob_record_data(ds.key, "image", "raw")
|
|
412
|
+
raw_bytes = ds_api.open_blob_stream(ds.key, blob_key)
|
|
413
|
+
|
|
414
|
+
ds_api.find_blobs(DataStreamFilterModel(accounts=[account_key]))
|
|
415
|
+
ds_api.unlock_data_stream(ds.key)
|
|
416
|
+
ds_api.delete_data_stream(ds.key)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Note:** the hyperslab data endpoints (`find`/`update_invariant_hyperslab_data`,
|
|
420
|
+
`find`/`update_hyperslab_record_data`) and the three channel management calls
|
|
421
|
+
(`create`/`update`/`delete_hyperslab_channel`, `create`/`update`/`delete_blob_channel`) return a raw
|
|
422
|
+
`dict` rather than a typed model — the server itself has no fixed schema for these (the shape is
|
|
423
|
+
derived per data-channel variable definition), so a fixed dataclass here would be guessing a schema
|
|
424
|
+
the server doesn't have. The blob byte-upload methods (`update_invariant_blob_data`,
|
|
425
|
+
`update_blob_record_data`) have been verified against the server's signing code but not yet against
|
|
426
|
+
a live inMotion instance — test them against `.env.test` before relying on them in production.
|
|
427
|
+
|
|
428
|
+
# Development
|
|
429
|
+
|
|
430
|
+
The following sections are for contributing to the SDK itself, not for consuming it.
|
|
431
|
+
|
|
432
|
+
## Build
|
|
433
|
+
|
|
434
|
+
To build the SDK, you can use the following command:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
uv build
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
## Generate Docs
|
|
441
|
+
|
|
442
|
+
Static HTML API docs, generated from the package's docstrings via [pdoc](https://pdoc.dev/):
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
bin/generate-docs.sh
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Output lands in `docs/` by default (override with `INMOTION_DOCS_DIR`); it's gitignored since
|
|
449
|
+
it's a generated artifact, not source.
|
|
450
|
+
|
|
451
|
+
## Unit Tests
|
|
452
|
+
|
|
453
|
+
The `tests/` directory contains a `pytest`-based unit test suite covering request signing, error
|
|
454
|
+
handling, and the API key / credentials client authentication flows. These tests mock all HTTP
|
|
455
|
+
calls, so no live inMotion environment is required.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
source .venv/bin/activate
|
|
459
|
+
uv pip install -e ".[dev]"
|
|
460
|
+
python -m pytest tests/
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Integration Tests
|
|
464
|
+
|
|
465
|
+
* Ensure that there is an inMotion integration test environment available.
|
|
466
|
+
* Configure the environment file (`.env.test` in the root directory) with the necessary credentials and URLs.
|
|
467
|
+
|
|
468
|
+
```dotenv
|
|
469
|
+
BASE_URL="http://localhost:9000"
|
|
470
|
+
DEV_KEY="xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
471
|
+
DEV_SECRET="xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
472
|
+
API_KEY="xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
473
|
+
ACCOUNT="xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
The `DEV_KEY`, `DEV_SECRET` can be created using the inMotion `Settings ...` menu under the right hand side. If the account allows the
|
|
477
|
+
creation of keys, a tab called `Dev Keys` will be shown. Create the key / secret and copy the values.
|
|
478
|
+
|
|
479
|
+
The `API_KEY` can be created using the `API Keys` tab. The `ACCOUNT` key and will need to be copied. Note that it is recommended that the
|
|
480
|
+
API Key be at least `Consumer` authority to support the text.
|
|
481
|
+
|
|
482
|
+
Please run the tests in a virtual environment to avoid dependency conflicts.
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
source .venv/bin/activate
|
|
486
|
+
uv pip install -e .
|
|
487
|
+
python3 scripts/test.py
|
|
488
|
+
```
|