wildtrax-client 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,30 @@
1
+ """wildtrax-client -- Python client for the WildTrax 2027 HTTP /api control surface."""
2
+
3
+ from .client import WildTrax, DEFAULT_PORT
4
+ from .errors import (
5
+ WildTraxError,
6
+ ConnectionFailed,
7
+ PendingApprovalError,
8
+ PermissionDenied,
9
+ NotFound,
10
+ RecordingInProgress,
11
+ RateLimited,
12
+ BadRequest,
13
+ ServerError,
14
+ )
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ "WildTrax",
20
+ "DEFAULT_PORT",
21
+ "WildTraxError",
22
+ "ConnectionFailed",
23
+ "PendingApprovalError",
24
+ "PermissionDenied",
25
+ "NotFound",
26
+ "RecordingInProgress",
27
+ "RateLimited",
28
+ "BadRequest",
29
+ "ServerError",
30
+ ]
@@ -0,0 +1,475 @@
1
+ """WildTrax 2027 HTTP /api client.
2
+
3
+ A thin, dependency-free (stdlib urllib) client for the WildTrax desktop server's
4
+ HTTP control surface. Mirrors the same actions the OSC protocol exposes -- two
5
+ transports, one capability set.
6
+
7
+ from wildtrax_client import WildTrax
8
+
9
+ wt = WildTrax("192.168.1.155") # default port 5659
10
+ wt.wait_for_approval() # block until host approves this device
11
+ wt.transport.record()
12
+ wt.tracks.mute(1, True) # mute Track 1 (indices are 1-based)
13
+ wt.metadata.scene("SC07")
14
+ print(wt.session_state())
15
+
16
+ Indices are 1-BASED. Every index argument (track index, pad index, row index,
17
+ file index, matrix row/col, ...) is 1-based, matching the WildTrax UI (Track 1,
18
+ Input 1) and the OSC surface -- so `wt.tracks.mute(1)` mutes the first track, the
19
+ one shown as "Track 1". The server converts 1-based -> 0-based internally at the
20
+ HTTP seam; pass what the UI shows. (This is the v27 convention across both the
21
+ HTTP and OSC public surfaces; the desktop's internal models remain 0-based.)
22
+
23
+ Approval model: the first request from a new device lands in the desktop Session
24
+ Manager as *pending* and returns 202. A human approves it once (and grants Write
25
+ for write actions). Until then, calls raise PendingApprovalError; use
26
+ wait_for_approval() to poll, or catch the exception.
27
+ """
28
+
29
+ import json
30
+ import time
31
+ import urllib.error
32
+ import urllib.parse
33
+ import urllib.request
34
+
35
+ from . import errors
36
+
37
+ DEFAULT_PORT = 5659
38
+ DEFAULT_TIMEOUT = 5.0
39
+
40
+
41
+ class WildTrax:
42
+ """Connection to one WildTrax server's HTTP /api surface."""
43
+
44
+ def __init__(self, host, port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT):
45
+ self.base_url = "http://{}:{}".format(host, port)
46
+ self.timeout = timeout
47
+ # Namespaced action groups (mirror the /api route groups).
48
+ self.transport = _Transport(self)
49
+ self.tracks = _Tracks(self)
50
+ self.metadata = _Metadata(self)
51
+ self.config = _Config(self)
52
+ self.soundboard = _Soundboard(self)
53
+ self.playlist = _Playlist(self)
54
+ self.export = _ExportFiles(self)
55
+ self.report = _SoundReport(self)
56
+
57
+ # ---- transport -------------------------------------------------------
58
+
59
+ def _request(self, method, route, body=None):
60
+ """Issue an HTTP request. Returns parsed JSON (dict) or {} for empty
61
+ bodies. Raises the mapped WildTraxError on a non-2xx status."""
62
+ url = self.base_url + route
63
+ data = None
64
+ headers = {}
65
+ if body is not None:
66
+ data = json.dumps(body).encode("utf-8")
67
+ headers["Content-Type"] = "application/json"
68
+
69
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
70
+ try:
71
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
72
+ return self._parse(resp.status, resp.read())
73
+ except urllib.error.HTTPError as e:
74
+ # urllib raises on 4xx/5xx; the body still carries our JSON error.
75
+ return self._parse(e.code, e.read())
76
+ except urllib.error.URLError as e:
77
+ raise errors.ConnectionFailed(
78
+ "could not reach {}: {}".format(url, e.reason)
79
+ )
80
+
81
+ def _parse(self, status, raw):
82
+ try:
83
+ payload = json.loads(raw) if raw else {}
84
+ except (ValueError, TypeError):
85
+ payload = {}
86
+
87
+ # 202 is "accepted but not approved yet" -- urllib treats it as success
88
+ # (no HTTPError), so it must be intercepted here before the 2xx return.
89
+ if status == 202:
90
+ raise errors.PendingApprovalError(
91
+ "device awaiting approval in WildTrax Session Manager"
92
+ )
93
+ if 200 <= status < 300:
94
+ return payload
95
+
96
+ msg = payload.get("error") or payload.get("message") or "HTTP {}".format(status)
97
+ if status == 400:
98
+ raise errors.BadRequest(msg)
99
+ if status == 403:
100
+ raise errors.PermissionDenied(msg)
101
+ if status == 404:
102
+ raise errors.NotFound(msg)
103
+ if status == 409:
104
+ raise errors.RecordingInProgress(msg)
105
+ if status == 429:
106
+ raise errors.RateLimited(msg)
107
+ raise errors.ServerError("{} ({})".format(msg, status))
108
+
109
+ def _get(self, route):
110
+ return self._request("GET", route)
111
+
112
+ def _post(self, route, **body):
113
+ # Drop None values so optional fields are omitted, not sent as null.
114
+ clean = {k: v for k, v in body.items() if v is not None}
115
+ return self._request("POST", route, clean or None)
116
+
117
+ # ---- discovery / approval -------------------------------------------
118
+
119
+ def hello(self):
120
+ """GET /api/hello -- {app, version, status}. Registers this device as a
121
+ pending client if it isn't known yet. Does not raise on pending."""
122
+ return self._get("/api/hello")
123
+
124
+ def is_approved(self):
125
+ """True once the host has approved this device."""
126
+ status = self.hello().get("status", "")
127
+ return status.startswith("approved")
128
+
129
+ def can_write(self):
130
+ """True once approved AND granted Write permission."""
131
+ return self.hello().get("status") == "approved"
132
+
133
+ def wait_for_approval(self, timeout=120.0, poll_interval=1.0):
134
+ """Block until the host approves this device in Session Manager, or
135
+ raise PendingApprovalError after `timeout` seconds. Returns the hello
136
+ payload once approved."""
137
+ deadline = time.monotonic() + timeout
138
+ while True:
139
+ info = self.hello()
140
+ if str(info.get("status", "")).startswith("approved"):
141
+ return info
142
+ if time.monotonic() >= deadline:
143
+ raise errors.PendingApprovalError(
144
+ "still pending after {:.0f}s -- approve this device in the "
145
+ "WildTrax Session Manager".format(timeout)
146
+ )
147
+ time.sleep(poll_interval)
148
+
149
+ # ---- readback --------------------------------------------------------
150
+
151
+ def session_state(self):
152
+ """GET /api/session/state -- the full session snapshot."""
153
+ return self._get("/api/session/state")
154
+
155
+ def recording(self):
156
+ """GET /api/record/state -- {recording: bool}."""
157
+ return self._get("/api/record/state").get("recording", False)
158
+
159
+ def get_tracks(self):
160
+ return self._get("/api/tracks").get("tracks")
161
+
162
+ def get_metadata(self):
163
+ return self._get("/api/metadata")
164
+
165
+ # ---- per-element readback (HTTP mirror of the OSC flat read-side) -----
166
+ # All indices are 1-BASED (== the WildTrax UI and the OSC surface).
167
+
168
+ def get_pad(self, pad_index):
169
+ """GET /api/soundboard/<i> -- one soundboard pad's JSON (1-based)."""
170
+ return self._get(f"/api/soundboard/{pad_index}")
171
+
172
+ def get_playlist_row(self, row_index):
173
+ """GET /api/playlist/<i> -- one playlist row's JSON (1-based)."""
174
+ return self._get(f"/api/playlist/{row_index}")
175
+
176
+ def get_export_file(self, file_index):
177
+ """GET /api/exportfiles/<i> -- one export file's JSON (1-based)."""
178
+ return self._get(f"/api/exportfiles/{file_index}")
179
+
180
+ def get_matrix(self, which):
181
+ """GET /api/matrix/<which> -- full input/output matrix snapshot."""
182
+ return self._get(f"/api/matrix/{which}")
183
+
184
+ def get_soundreport_entry(self, row_index):
185
+ """GET /api/soundreport/<i> -- one sound-report entry's JSON (1-based)."""
186
+ return self._get(f"/api/soundreport/{row_index}")
187
+
188
+ def get_session_client(self, client_index):
189
+ """GET /api/session/clients/<i> -- one client record's JSON (1-based)."""
190
+ return self._get(f"/api/session/clients/{client_index}")
191
+
192
+
193
+ # --------------------------------------------------------------------------
194
+ # Namespaced action groups. Each mirrors the matching /api/<group>/* routes and
195
+ # the JSON body the server expects (same field names as the OSC command dicts).
196
+ # --------------------------------------------------------------------------
197
+
198
+
199
+ class _Group:
200
+ def __init__(self, client):
201
+ self._c = client
202
+
203
+
204
+ class _Transport(_Group):
205
+ def record(self):
206
+ """Start recording / advance take (server decides based on state)."""
207
+ return self._c._post("/api/record/start")
208
+
209
+ def stop(self):
210
+ return self._c._post("/api/record/stop")
211
+
212
+ def false_start(self):
213
+ """Request a false start (the desktop confirms)."""
214
+ return self._c._post("/api/record/falsestart")
215
+
216
+ def false_start_confirmed(self):
217
+ """Pre-authorized false start (skip desktop confirmation)."""
218
+ return self._c._post("/api/record/falsestart/confirmed")
219
+
220
+
221
+ class _Tracks(_Group):
222
+ def arm(self, index, value=True):
223
+ return self._c._post("/api/track/arm", trackIndex=index, value=value)
224
+
225
+ def mute(self, index, value=True):
226
+ return self._c._post("/api/track/mute", trackIndex=index, value=value)
227
+
228
+ def monitor(self, index, value=True):
229
+ return self._c._post("/api/track/monitor", trackIndex=index, value=value)
230
+
231
+ def pan(self, index, pan):
232
+ """pan: 0=Left, 1=Right, 2=Center (desktop PanState values)."""
233
+ return self._c._post("/api/track/pan", trackIndex=index, pan=pan)
234
+
235
+ def name(self, index, name):
236
+ return self._c._post("/api/track/name", trackIndex=index, name=name)
237
+
238
+ def add(self):
239
+ return self._c._post("/api/track/add")
240
+
241
+ def remove(self, index):
242
+ return self._c._post("/api/track/remove", trackIndex=index)
243
+
244
+ def set_count(self, count):
245
+ return self._c._post("/api/track/count", count=count)
246
+
247
+
248
+ class _Metadata(_Group):
249
+ def project(self, value):
250
+ return self._c._post("/api/metadata/project", value=value)
251
+
252
+ def scene(self, value):
253
+ return self._c._post("/api/metadata/scene", value=value)
254
+
255
+ def slate(self, value):
256
+ return self._c._post("/api/metadata/slate", value=value)
257
+
258
+ def take(self, value):
259
+ return self._c._post("/api/metadata/take", value=value)
260
+
261
+ def roll(self, value):
262
+ return self._c._post("/api/metadata/roll", value=value)
263
+
264
+ def note(self, value):
265
+ return self._c._post("/api/metadata/note", value=value)
266
+
267
+ def add_marker(self):
268
+ return self._c._post("/api/metadata/marker/add")
269
+
270
+ def add_note_to_bank(self, note):
271
+ return self._c._post("/api/metadata/notebank/add", note=note)
272
+
273
+ def remove_note_from_bank(self, index):
274
+ return self._c._post("/api/metadata/notebank/remove", index=index)
275
+
276
+ def insert_quick_note(self, note_index):
277
+ return self._c._post("/api/metadata/notebank/insert", noteIndex=note_index)
278
+
279
+ # next-take template
280
+ def next_project(self, value):
281
+ return self._c._post("/api/metadata/next/project", value=value)
282
+
283
+ def next_scene(self, value):
284
+ return self._c._post("/api/metadata/next/scene", value=value)
285
+
286
+ def next_slate(self, value):
287
+ return self._c._post("/api/metadata/next/slate", value=value)
288
+
289
+ def next_take(self, value):
290
+ return self._c._post("/api/metadata/next/take", value=value)
291
+
292
+ def next_roll(self, value):
293
+ return self._c._post("/api/metadata/next/roll", value=value)
294
+
295
+ def reset_next(self):
296
+ return self._c._post("/api/metadata/next/reset")
297
+
298
+ # auto-increment / auto-reset flags
299
+ def auto_increment(self, field, value):
300
+ """field: 'slate' | 'take' | 'roll'."""
301
+ return self._c._post("/api/metadata/autoincrement/" + field, value=value)
302
+
303
+ def auto_reset(self, field, value):
304
+ """field: 'slate' | 'take' | 'roll' -- reset on scene change."""
305
+ return self._c._post("/api/metadata/autoreset/" + field, value=value)
306
+
307
+
308
+ class _Config(_Group):
309
+ def preroll(self, enabled):
310
+ return self._c._post("/api/config/preroll", value=enabled)
311
+
312
+ def preroll_full(self, enabled, size_seconds):
313
+ return self._c._post(
314
+ "/api/config/preroll/full", prerollEnabled=enabled, prerollSize=size_seconds
315
+ )
316
+
317
+ def meter_smoothness(self, value):
318
+ """value: 0.0 .. 1.0."""
319
+ return self._c._post("/api/config/smoothness", value=value)
320
+
321
+ def global_monitor(self, enabled):
322
+ return self._c._post("/api/config/monitor", value=enabled)
323
+
324
+ def playback_volume(self, value):
325
+ """value: 0 .. 100."""
326
+ return self._c._post("/api/playback/volume", value=value)
327
+
328
+ def matrix_cell(self, matrix, row, col, value=True):
329
+ """matrix: 'input' | 'output'. Rejected while recording."""
330
+ return self._c._post("/api/matrix/cell", matrix=matrix, row=row, col=col, value=value)
331
+
332
+
333
+ class _Soundboard(_Group):
334
+ def play(self, pad_index):
335
+ return self._c._post("/api/soundboard/play", padIndex=pad_index)
336
+
337
+ def stop(self, pad_index):
338
+ return self._c._post("/api/soundboard/stop", padIndex=pad_index)
339
+
340
+ def add_pad(self):
341
+ return self._c._post("/api/soundboard/pad/add")
342
+
343
+ def remove_pad(self, pad_index):
344
+ return self._c._post("/api/soundboard/pad/remove", padIndex=pad_index)
345
+
346
+ def move_pad(self, from_index, to_index):
347
+ return self._c._post(
348
+ "/api/soundboard/pad/move", fromIndex=from_index, toIndex=to_index
349
+ )
350
+
351
+ def title(self, pad_index, title):
352
+ return self._c._post("/api/soundboard/pad/title", padIndex=pad_index, title=title)
353
+
354
+ def color(self, pad_index, color):
355
+ return self._c._post("/api/soundboard/pad/color", padIndex=pad_index, color=color)
356
+
357
+ def fade_in(self, pad_index, value=True):
358
+ return self._c._post("/api/soundboard/pad/fadein", padIndex=pad_index, value=value)
359
+
360
+ def repeat(self, pad_index, value=True):
361
+ return self._c._post("/api/soundboard/pad/repeat", padIndex=pad_index, value=value)
362
+
363
+ def solo(self, pad_index, value=True):
364
+ return self._c._post("/api/soundboard/pad/solo", padIndex=pad_index, value=value)
365
+
366
+ def fade_out(self, pad_index, value=True):
367
+ return self._c._post("/api/soundboard/pad/fadeout", padIndex=pad_index, value=value)
368
+
369
+ def trigger_mode(self, pad_index, mode):
370
+ return self._c._post(
371
+ "/api/soundboard/pad/triggermode", padIndex=pad_index, triggerMode=mode
372
+ )
373
+
374
+ def routing_mode(self, pad_index, mode):
375
+ """mode: 0 auto, 1 stereo, 2 mono left, 3 mono right, 4 mono center, 5 custom."""
376
+ return self._c._post(
377
+ "/api/soundboard/pad/routingmode", padIndex=pad_index, routingMode=mode
378
+ )
379
+
380
+ def routing_init(self, pad_index):
381
+ return self._c._post("/api/soundboard/pad/routing/init", padIndex=pad_index)
382
+
383
+ def routing_cell(self, pad_index, src_channel, dst_channel, gain):
384
+ return self._c._post(
385
+ "/api/soundboard/pad/routing/cell", padIndex=pad_index,
386
+ srcChannel=src_channel, dstChannel=dst_channel, value=gain,
387
+ )
388
+
389
+
390
+ class _Playlist(_Group):
391
+ def play(self):
392
+ return self._c._post("/api/playlist/play")
393
+
394
+ def stop(self):
395
+ return self._c._post("/api/playlist/stop")
396
+
397
+ def loop(self, value=True):
398
+ return self._c._post("/api/playlist/loop", value=value)
399
+
400
+ def index(self, index):
401
+ return self._c._post("/api/playlist/index", index=index)
402
+
403
+ def add_row(self):
404
+ return self._c._post("/api/playlist/row/add")
405
+
406
+ def remove_row(self, row_index):
407
+ return self._c._post("/api/playlist/row/remove", rowIndex=row_index)
408
+
409
+ def move_row(self, from_index, to_index):
410
+ return self._c._post(
411
+ "/api/playlist/row/move", fromIndex=from_index, toIndex=to_index
412
+ )
413
+
414
+ def row_title(self, row_index, title):
415
+ return self._c._post("/api/playlist/row/title", rowIndex=row_index, title=title)
416
+
417
+ def row_color(self, row_index, color):
418
+ return self._c._post("/api/playlist/row/color", rowIndex=row_index, color=color)
419
+
420
+ def row_routing_mode(self, row_index, mode):
421
+ """mode: 0 auto, 1 stereo, 2 mono left, 3 mono right, 4 mono center, 5 custom."""
422
+ return self._c._post(
423
+ "/api/playlist/row/routingmode", rowIndex=row_index, routingMode=mode
424
+ )
425
+
426
+ def row_routing_init(self, row_index):
427
+ return self._c._post("/api/playlist/row/routing/init", rowIndex=row_index)
428
+
429
+ def row_routing_cell(self, row_index, src_channel, dst_channel, gain):
430
+ return self._c._post(
431
+ "/api/playlist/row/routing/cell", rowIndex=row_index,
432
+ srcChannel=src_channel, dstChannel=dst_channel, value=gain,
433
+ )
434
+
435
+
436
+ class _ExportFiles(_Group):
437
+ def add(self):
438
+ return self._c._post("/api/exportfiles/add")
439
+
440
+ def remove(self, file_index):
441
+ return self._c._post("/api/exportfiles/remove", fileIndex=file_index)
442
+
443
+ def toggle_track(self, file_index, track_index):
444
+ return self._c._post(
445
+ "/api/exportfiles/track/toggle", fileIndex=file_index, trackIndex=track_index
446
+ )
447
+
448
+ def display_resolved_names(self, value):
449
+ return self._c._post("/api/exportfiles/resolvednames", value=value)
450
+
451
+ def update(self, file_index, field, value):
452
+ """field: 'name' | 'folder' | 'fileFormat' | 'trackExportType' | 'selectAllTracks'."""
453
+ return self._c._post(
454
+ "/api/exportfiles/update", fileIndex=file_index, field=field, value=value
455
+ )
456
+
457
+
458
+ class _SoundReport(_Group):
459
+ def csv(self, folder=None):
460
+ """GET the sound report as CSV. Returns {folder, csv}."""
461
+ route = "/api/soundreport/csv"
462
+ if folder:
463
+ route += "?folder=" + urllib.parse.quote(folder)
464
+ return self._c._get(route)
465
+
466
+ def auto_save(self, value):
467
+ return self._c._post("/api/soundreport/autosave", value=value)
468
+
469
+ def edit_entry(self, row, field, value, track_index=None):
470
+ """field: 'timecode' | 'circled' | 'note' | 'project' | 'scene' | 'slate' |
471
+ 'take' | 'roll' | 'fps' | 'trackName' (trackName also needs track_index)."""
472
+ return self._c._post(
473
+ "/api/soundreport/entry", row=row, field=field, value=value,
474
+ trackIndex=track_index,
475
+ )
@@ -0,0 +1,40 @@
1
+ """Exceptions for the WildTrax /api client, mapped from HTTP status codes."""
2
+
3
+
4
+ class WildTraxError(Exception):
5
+ """Base class for all client errors."""
6
+
7
+
8
+ class ConnectionFailed(WildTraxError):
9
+ """Could not reach the server (host down, refused, timeout)."""
10
+
11
+
12
+ class PendingApprovalError(WildTraxError):
13
+ """The server returned 202 -- this client/IP is awaiting approval in the
14
+ desktop Session Manager. Approve it (and grant Write for write actions),
15
+ then retry. ``WildTrax.wait_for_approval()`` polls until this clears."""
16
+
17
+
18
+ class PermissionDenied(WildTraxError):
19
+ """403 -- the client is approved but lacks Write permission for this action."""
20
+
21
+
22
+ class NotFound(WildTraxError):
23
+ """404 -- unknown route, or a bad index (e.g. trackIndex out of range)."""
24
+
25
+
26
+ class RecordingInProgress(WildTraxError):
27
+ """409 -- the action is blocked while the server is recording (e.g. arming
28
+ a track, changing track count, editing routing)."""
29
+
30
+
31
+ class RateLimited(WildTraxError):
32
+ """429 -- transport command debounced; sent too quickly after the last one."""
33
+
34
+
35
+ class BadRequest(WildTraxError):
36
+ """400 -- malformed request body (invalid JSON)."""
37
+
38
+
39
+ class ServerError(WildTraxError):
40
+ """An unexpected status code from the server."""
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: wildtrax-client
3
+ Version: 0.1.0
4
+ Summary: Python client for the WildTrax 2027 HTTP /api control surface
5
+ Author: Halter Technical
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://wildtrax.io
8
+ Project-URL: Documentation, https://wildtrax.io/resources/python-client
9
+ Keywords: wildtrax,audio,recording,remote,osc,control
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Multimedia :: Sound/Audio
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # PyWildTrax
19
+
20
+ A small, dependency-free Python client for the **WildTrax 2027** HTTP `/api`
21
+ control surface. Drive recording, tracks, metadata, the soundboard, the playlist,
22
+ and more from scripts — the same actions the OSC protocol exposes, over plain HTTP.
23
+
24
+ Built on the Python standard library only (`urllib`) — no `requests`, no extra
25
+ installs, works anywhere Python 3.8+ runs.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install wildtrax-client
31
+ # or, from a checkout:
32
+ pip install -e .
33
+ ```
34
+
35
+ > Install as **`wildtrax-client`**, import as **`wildtrax_client`**:
36
+ > ```python
37
+ > from wildtrax_client import WildTrax
38
+ > ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from wildtrax_client import WildTrax
44
+
45
+ wt = WildTrax("192.168.1.155") # default HTTP port 5659
46
+ wt.wait_for_approval() # blocks until the host approves this device
47
+
48
+ wt.metadata.scene("SC07")
49
+ wt.transport.record()
50
+ # ...
51
+ wt.transport.stop()
52
+
53
+ print(wt.session_state())
54
+ ```
55
+
56
+ ## Approval model
57
+
58
+ WildTrax has no passwords — security is the host approving devices on the LAN.
59
+ The **first** request from a new device shows up as *pending* in the desktop
60
+ **Session Manager** and the client gets `202`. A human approves it once (and grants
61
+ **Write** for write actions like recording). After that, the device is remembered.
62
+
63
+ - `wt.wait_for_approval(timeout=120)` — poll until approved, or raise.
64
+ - Or catch `PendingApprovalError` yourself and retry.
65
+ - `wt.is_approved()` / `wt.can_write()` — check status without blocking.
66
+
67
+ The server must have a **fixed HTTP port** enabled (Session Manager → Advanced →
68
+ *Fixed network ports*) so the port is stable; otherwise it binds an ephemeral one.
69
+
70
+ ## API
71
+
72
+ Methods are grouped to mirror the `/api` route families:
73
+
74
+ | Namespace | Examples |
75
+ |---|---|
76
+ | `wt.transport` | `record()`, `stop()`, `false_start()` |
77
+ | `wt.tracks` | `arm(i)`, `mute(i, True)`, `pan(i, 2)`, `name(i, "Boom")`, `add()`, `remove(i)`, `set_count(n)` |
78
+ | `wt.metadata` | `scene("SC07")`, `take("T001")`, `note(...)`, `add_marker()`, `next_scene(...)`, `reset_next()`, `auto_increment("take", True)` |
79
+ | `wt.config` | `preroll(True)`, `meter_smoothness(0.5)`, `global_monitor(True)`, `playback_volume(80)`, `matrix_cell("input", 1, 1)` |
80
+ | `wt.soundboard` | `play(1)`, `stop(1)`, `add_pad()`, `title(1, "FX")`, `repeat(1, True)`, `trigger_mode(1, 1)`, `routing_mode(1, 5)`, `routing_cell(1, 1, 2, 1.0)` |
81
+ | `wt.playlist` | `play()`, `stop()`, `loop(True)`, `index(2)`, `add_row()`, `row_title(1, "Cue")`, `row_routing_mode(1, 0)` |
82
+ | `wt.export` | `add()`, `remove(i)`, `update(i, "name", "Mix")`, `toggle_track(f, t)` |
83
+ | `wt.report` | `csv()`, `auto_save(True)`, `edit_entry(3, "circled", True)` |
84
+
85
+ > **Indices are 1-based.** Every index argument (`i`, pad index, row index, file
86
+ > index) is 1-based, matching the WildTrax UI (Track 1, Input 1) and the OSC
87
+ > surface — `wt.tracks.mute(1)` mutes the track shown as "Track 1". The server
88
+ > converts to its 0-based models at the HTTP seam.
89
+
90
+ Readback: `wt.session_state()`, `wt.recording()`, `wt.get_tracks()`,
91
+ `wt.get_metadata()`, `wt.report.csv()`. Per-element reads (1-based, mirror the OSC
92
+ flat read-side): `wt.get_pad(i)`, `wt.get_playlist_row(i)`, `wt.get_export_file(i)`,
93
+ `wt.get_matrix(which)`, `wt.get_soundreport_entry(i)`, `wt.get_session_client(i)`.
94
+
95
+ > **Session administration** (approving/kicking clients, permissions, renaming the
96
+ > session) is **not** available remotely — it's reserved for the desktop host.
97
+
98
+ ## Errors
99
+
100
+ All raise subclasses of `WildTraxError`:
101
+
102
+ | Exception | Meaning (HTTP) |
103
+ |---|---|
104
+ | `ConnectionFailed` | host unreachable |
105
+ | `PendingApprovalError` | `202` — awaiting approval |
106
+ | `PermissionDenied` | `403` — approved but no Write |
107
+ | `NotFound` | `404` — bad route or index |
108
+ | `RecordingInProgress` | `409` — blocked while recording |
109
+ | `RateLimited` | `429` — transport command debounced |
110
+ | `BadRequest` | `400` — malformed body |
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,8 @@
1
+ wildtrax_client/__init__.py,sha256=al0iar9r6T9rBUA5Gsp3Uixe18xj9Sl0ikqSUdRCpHg,604
2
+ wildtrax_client/client.py,sha256=RfjP5Gi9lG05icydFtLj6Zx_8tPuMp39l3DQ2k4qsQc,17942
3
+ wildtrax_client/errors.py,sha256=KyHsEjMKUnjr6lA5dPdzaxjlMDIqAv8DraYUGF5RsMQ,1256
4
+ wildtrax_client-0.1.0.dist-info/licenses/LICENSE,sha256=FLBuiRlnxhLUv1a6jVh81kMGCXsRcoRW5GjbJxlAIR8,1073
5
+ wildtrax_client-0.1.0.dist-info/METADATA,sha256=gW6LTXjxi0LAB_KBhcdpSLzcqBPEmdb5KlMAm02lU9Q,4508
6
+ wildtrax_client-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ wildtrax_client-0.1.0.dist-info/top_level.txt,sha256=TJGlIN_ZAnVL6IKJ1fV4umQ-_Yh_eyV4V1E2WwpAkQw,16
8
+ wildtrax_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Halter Technical
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 @@
1
+ wildtrax_client