motion-intelligence 0.4.0__tar.gz → 0.5.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.
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/PKG-INFO +1 -1
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/__init__.py +6 -2
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/cli.py +200 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/client.py +154 -1
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/PKG-INFO +1 -1
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/pyproject.toml +1 -1
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/README.md +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/batch.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/categories.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/config.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/dockerfile.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/predictor.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/profile.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/server.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion/weight_upload.py +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/SOURCES.txt +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/dependency_links.txt +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/entry_points.txt +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/requires.txt +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/top_level.txt +0 -0
- {motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: motion-intelligence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Author, build & deploy Python models for the Motion inference platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/devathub9/Motion-packages
|
|
6
6
|
Project-URL: Repository, https://github.com/devathub9/Motion-packages
|
|
@@ -8,11 +8,15 @@ Callers use the client to run a deployed model:
|
|
|
8
8
|
|
|
9
9
|
from motion import MotionClient
|
|
10
10
|
"""
|
|
11
|
-
from .client import
|
|
11
|
+
from .client import (
|
|
12
|
+
MotionClient, MotionJobFailedError, MotionTimeoutError,
|
|
13
|
+
MotionWorkflowRunFailedError, MotionWorkflowRunTimeoutError,
|
|
14
|
+
)
|
|
12
15
|
from .predictor import BasePredictor, Input, Path
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
15
18
|
"BasePredictor", "Input", "Path",
|
|
16
19
|
"MotionClient", "MotionJobFailedError", "MotionTimeoutError",
|
|
20
|
+
"MotionWorkflowRunFailedError", "MotionWorkflowRunTimeoutError",
|
|
17
21
|
]
|
|
18
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.5.0"
|
|
@@ -471,6 +471,170 @@ def _extract_input_default(func_node, arg_name: str) -> dict:
|
|
|
471
471
|
return {}
|
|
472
472
|
|
|
473
473
|
|
|
474
|
+
def _parse_kv_list(pairs):
|
|
475
|
+
"""['fps=24', 'steps=20'] -> {'fps': '24', 'steps': '20'}."""
|
|
476
|
+
out = {}
|
|
477
|
+
for item in pairs or []:
|
|
478
|
+
if "=" not in item:
|
|
479
|
+
print(_c(f" ✗ invalid '{item}' — expected KEY=VALUE", "31"))
|
|
480
|
+
raise SystemExit(1)
|
|
481
|
+
key, _, value = item.partition("=")
|
|
482
|
+
out[key] = value
|
|
483
|
+
return out
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _print_result(result):
|
|
487
|
+
for artifact in result.get("artifacts") or []:
|
|
488
|
+
url = artifact.get("url") if isinstance(artifact, dict) else artifact
|
|
489
|
+
if url:
|
|
490
|
+
print(f" artifact: {url}")
|
|
491
|
+
if result.get("output_url"):
|
|
492
|
+
print(f" output: {result['output_url']}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def cmd_run_model(args):
|
|
496
|
+
from .client import MotionClient
|
|
497
|
+
|
|
498
|
+
print(_c(f"▶ motion run model ({args.model_code})"))
|
|
499
|
+
try:
|
|
500
|
+
client = MotionClient()
|
|
501
|
+
except ValueError as e:
|
|
502
|
+
print(_c(f" ✗ {e}", "31"))
|
|
503
|
+
return 1
|
|
504
|
+
|
|
505
|
+
outputs = args.outputs.split(",") if args.outputs else None
|
|
506
|
+
try:
|
|
507
|
+
submitted = client.submit_job(
|
|
508
|
+
args.model_code, args.file, task=args.task or "",
|
|
509
|
+
outputs=outputs, params=_parse_kv_list(args.param),
|
|
510
|
+
)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
print(_c(f" ✗ submit failed: {e}", "31"))
|
|
513
|
+
return 1
|
|
514
|
+
print(f" ✓ submitted job_id={submitted['job_id']}")
|
|
515
|
+
|
|
516
|
+
if args.no_wait:
|
|
517
|
+
print(f" poll: GET /api/v1/motion/jobs/{submitted['job_id']}")
|
|
518
|
+
return 0
|
|
519
|
+
|
|
520
|
+
print(" → waiting for the job to finish …")
|
|
521
|
+
try:
|
|
522
|
+
result = client.wait_for_job(
|
|
523
|
+
submitted["job_id"], on_poll=lambda j: print(f" {j.get('status')}…"),
|
|
524
|
+
)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
print(_c(f" ✗ {e}", "31"))
|
|
527
|
+
return 1
|
|
528
|
+
|
|
529
|
+
print(_c("\n✓ job completed", "32"))
|
|
530
|
+
_print_result(result)
|
|
531
|
+
return 0
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def cmd_run_workflow(args):
|
|
535
|
+
from .client import MotionClient
|
|
536
|
+
|
|
537
|
+
print(_c(f"▶ motion run workflow ({args.workflow_id})"))
|
|
538
|
+
try:
|
|
539
|
+
client = MotionClient()
|
|
540
|
+
except ValueError as e:
|
|
541
|
+
print(_c(f" ✗ {e}", "31"))
|
|
542
|
+
return 1
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
started = client.run_workflow(
|
|
546
|
+
args.workflow_id, file=args.file, texts=_parse_kv_list(args.text),
|
|
547
|
+
)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(_c(f" ✗ run failed: {e}", "31"))
|
|
550
|
+
return 1
|
|
551
|
+
|
|
552
|
+
run_id = started.get("run_id")
|
|
553
|
+
job_id = started.get("job_id")
|
|
554
|
+
print(f" ✓ started {'run_id=' + run_id if run_id else 'job_id=' + job_id}")
|
|
555
|
+
|
|
556
|
+
if args.no_wait:
|
|
557
|
+
return 0
|
|
558
|
+
|
|
559
|
+
print(" → waiting for the workflow to finish …")
|
|
560
|
+
try:
|
|
561
|
+
if run_id:
|
|
562
|
+
result = client.wait_for_workflow_run(
|
|
563
|
+
run_id,
|
|
564
|
+
on_poll=lambda r: print(
|
|
565
|
+
f" step {r.get('current_step', 0) + 1}/{r.get('step_count', 1)} ({r.get('status')})…"
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
else:
|
|
569
|
+
result = client.wait_for_job(job_id, on_poll=lambda j: print(f" {j.get('status')}…"))
|
|
570
|
+
except Exception as e:
|
|
571
|
+
print(_c(f" ✗ {e}", "31"))
|
|
572
|
+
return 1
|
|
573
|
+
|
|
574
|
+
print(_c("\n✓ workflow completed", "32"))
|
|
575
|
+
_print_result(result)
|
|
576
|
+
return 0
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def cmd_workflow_deploy(args):
|
|
580
|
+
from .client import MotionClient
|
|
581
|
+
|
|
582
|
+
print(_c(f"▶ motion workflow deploy ({args.workflow_id})"))
|
|
583
|
+
try:
|
|
584
|
+
client = MotionClient()
|
|
585
|
+
result = client.deploy_workflow(args.workflow_id, notes=args.notes)
|
|
586
|
+
except Exception as e:
|
|
587
|
+
print(_c(f" ✗ {e}", "31"))
|
|
588
|
+
return 1
|
|
589
|
+
|
|
590
|
+
print(_c(f"\n✓ deployed v{result['version']}", "32"))
|
|
591
|
+
if result.get("notes"):
|
|
592
|
+
print(f" notes: {result['notes']}")
|
|
593
|
+
print(f" deployed_at: {result['deployed_at']}")
|
|
594
|
+
print(" API-key calls to this workflow's /run now use this version.")
|
|
595
|
+
return 0
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def cmd_workflow_deployments(args):
|
|
599
|
+
from .client import MotionClient
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
client = MotionClient()
|
|
603
|
+
result = client.list_workflow_deployments(args.workflow_id)
|
|
604
|
+
except Exception as e:
|
|
605
|
+
print(_c(f"✗ {e}", "31"))
|
|
606
|
+
return 1
|
|
607
|
+
|
|
608
|
+
deployments = result.get("deployments", [])
|
|
609
|
+
if not deployments:
|
|
610
|
+
print("No deployments yet — run `motion workflow deploy <workflow_id>` first.")
|
|
611
|
+
return 0
|
|
612
|
+
|
|
613
|
+
print(_c(f"▶ deploy history ({args.workflow_id})"))
|
|
614
|
+
for d in deployments:
|
|
615
|
+
marker = _c(" (current)", "32") if d.get("is_current") else ""
|
|
616
|
+
rollback_note = f" [rollback of v{d['rollback_of_version']}]" if d.get("rollback_of_version") else ""
|
|
617
|
+
print(f" v{d['version']}{marker} {d['deployed_at']}{rollback_note}")
|
|
618
|
+
if d.get("notes"):
|
|
619
|
+
print(f" {d['notes']}")
|
|
620
|
+
return 0
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def cmd_workflow_rollback(args):
|
|
624
|
+
from .client import MotionClient
|
|
625
|
+
|
|
626
|
+
print(_c(f"▶ motion workflow rollback ({args.workflow_id} → v{args.version})"))
|
|
627
|
+
try:
|
|
628
|
+
client = MotionClient()
|
|
629
|
+
result = client.rollback_workflow(args.workflow_id, args.version)
|
|
630
|
+
except Exception as e:
|
|
631
|
+
print(_c(f" ✗ {e}", "31"))
|
|
632
|
+
return 1
|
|
633
|
+
|
|
634
|
+
print(_c(f"\n✓ rolled back — now live as v{result['version']}", "32"))
|
|
635
|
+
return 0
|
|
636
|
+
|
|
637
|
+
|
|
474
638
|
def main(argv=None):
|
|
475
639
|
p = argparse.ArgumentParser(prog="motion", description="Build & deploy models for Motion inference")
|
|
476
640
|
sub = p.add_subparsers(dest="command", required=True)
|
|
@@ -517,6 +681,42 @@ def main(argv=None):
|
|
|
517
681
|
)
|
|
518
682
|
d.set_defaults(func=cmd_deploy)
|
|
519
683
|
|
|
684
|
+
r = sub.add_parser("run", help="run a deployed model or workflow and wait for the result")
|
|
685
|
+
run_sub = r.add_subparsers(dest="run_target", required=True)
|
|
686
|
+
|
|
687
|
+
rm = run_sub.add_parser("model", help="run a deployed model")
|
|
688
|
+
rm.add_argument("model_code", help="e.g. jay/pose-estimation")
|
|
689
|
+
rm.add_argument("file", help="local path to the input file")
|
|
690
|
+
rm.add_argument("--task", default="", help="task/prompt text, if the model takes one")
|
|
691
|
+
rm.add_argument("--param", action="append", metavar="KEY=VALUE", help="extra predict() param (repeatable)")
|
|
692
|
+
rm.add_argument("--outputs", help="comma-separated output names to request")
|
|
693
|
+
rm.add_argument("--no-wait", action="store_true", help="submit and exit, don't wait for completion")
|
|
694
|
+
rm.set_defaults(func=cmd_run_model)
|
|
695
|
+
|
|
696
|
+
rw = run_sub.add_parser("workflow", help="run a deployed workflow (see `motion workflow deploy` first)")
|
|
697
|
+
rw.add_argument("workflow_id")
|
|
698
|
+
rw.add_argument("file", nargs="?", default=None, help="local path to the input file, if the workflow needs one")
|
|
699
|
+
rw.add_argument("--text", action="append", metavar="KEY=VALUE", help="named text input (repeatable)")
|
|
700
|
+
rw.add_argument("--no-wait", action="store_true", help="start and exit, don't wait for completion")
|
|
701
|
+
rw.set_defaults(func=cmd_run_workflow)
|
|
702
|
+
|
|
703
|
+
w = sub.add_parser("workflow", help="deploy, version, and roll back a workflow")
|
|
704
|
+
workflow_sub = w.add_subparsers(dest="workflow_action", required=True)
|
|
705
|
+
|
|
706
|
+
wd = workflow_sub.add_parser("deploy", help="deploy the current draft as a new live version")
|
|
707
|
+
wd.add_argument("workflow_id")
|
|
708
|
+
wd.add_argument("--notes", help="optional changelog note for this deploy")
|
|
709
|
+
wd.set_defaults(func=cmd_workflow_deploy)
|
|
710
|
+
|
|
711
|
+
wl = workflow_sub.add_parser("deployments", help="list a workflow's deploy/version history")
|
|
712
|
+
wl.add_argument("workflow_id")
|
|
713
|
+
wl.set_defaults(func=cmd_workflow_deployments)
|
|
714
|
+
|
|
715
|
+
wb = workflow_sub.add_parser("rollback", help="roll back to an earlier deployed version")
|
|
716
|
+
wb.add_argument("workflow_id")
|
|
717
|
+
wb.add_argument("version", type=int)
|
|
718
|
+
wb.set_defaults(func=cmd_workflow_rollback)
|
|
719
|
+
|
|
520
720
|
args = p.parse_args(argv)
|
|
521
721
|
return args.func(args)
|
|
522
722
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""MotionClient — call a deployed model from Python, the consumer
|
|
1
|
+
"""MotionClient — call a deployed model or workflow from Python, the consumer
|
|
2
|
+
side of `motion`.
|
|
2
3
|
|
|
3
4
|
from motion import MotionClient
|
|
4
5
|
|
|
@@ -7,6 +8,10 @@
|
|
|
7
8
|
result = client.wait_for_job(job["job_id"])
|
|
8
9
|
print(result["artifacts"])
|
|
9
10
|
|
|
11
|
+
# A workflow built in the dashboard, once deployed there (or via
|
|
12
|
+
# client.deploy_workflow(...)):
|
|
13
|
+
result = client.process_workflow(workflow_id, "video.mp4")
|
|
14
|
+
|
|
10
15
|
`motion build` / `motion deploy` (cli.py) and `BasePredictor` (predictor.py) are for
|
|
11
16
|
authoring and shipping a model; this module is for running one that's already
|
|
12
17
|
deployed — same package, the other side of the counter.
|
|
@@ -40,6 +45,19 @@ class MotionTimeoutError(TimeoutError):
|
|
|
40
45
|
super().__init__(f"job {job_id} did not finish within {timeout}s")
|
|
41
46
|
|
|
42
47
|
|
|
48
|
+
class MotionWorkflowRunFailedError(RuntimeError):
|
|
49
|
+
def __init__(self, run_id: str, error_message: str | None):
|
|
50
|
+
self.run_id = run_id
|
|
51
|
+
self.error_message = error_message
|
|
52
|
+
super().__init__(f"workflow run {run_id} failed: {error_message or 'no error message'}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MotionWorkflowRunTimeoutError(TimeoutError):
|
|
56
|
+
def __init__(self, run_id: str, timeout: float):
|
|
57
|
+
self.run_id = run_id
|
|
58
|
+
super().__init__(f"workflow run {run_id} did not finish within {timeout}s")
|
|
59
|
+
|
|
60
|
+
|
|
43
61
|
class MotionClient:
|
|
44
62
|
"""Thin wrapper over the Motion inference API.
|
|
45
63
|
|
|
@@ -162,8 +180,139 @@ class MotionClient:
|
|
|
162
180
|
return submitted
|
|
163
181
|
return self.wait_for_job(submitted["job_id"], poll_interval=poll_interval, timeout=timeout)
|
|
164
182
|
|
|
183
|
+
# ─── Workflows ───────────────────────────────────────────────────────────
|
|
184
|
+
# A workflow is built visually in the dashboard, then deployed (see
|
|
185
|
+
# deploy_workflow below) — running it here always executes that deployed
|
|
186
|
+
# snapshot, never a draft still being edited.
|
|
187
|
+
|
|
188
|
+
def run_workflow(
|
|
189
|
+
self,
|
|
190
|
+
workflow_id: str,
|
|
191
|
+
file: str | bytes | BinaryIO | None = None,
|
|
192
|
+
files: dict[str, str | bytes | BinaryIO] | None = None,
|
|
193
|
+
text: str | None = None,
|
|
194
|
+
texts: dict[str, str] | None = None,
|
|
195
|
+
) -> dict:
|
|
196
|
+
"""Run a deployed workflow. Returns `{"job_id": ...}` for a single-model
|
|
197
|
+
workflow — poll with `wait_for_job` — or `{"run_id": ...}` for a
|
|
198
|
+
multi-model pipeline — poll with `wait_for_workflow_run`. See
|
|
199
|
+
`process_workflow` for the version that waits for you either way.
|
|
200
|
+
|
|
201
|
+
`file`/`text` cover the common single-input case; `files`/`texts` (each
|
|
202
|
+
a dict keyed by the workflow's input name) cover workflows with more
|
|
203
|
+
than one named input — one file per name.
|
|
204
|
+
"""
|
|
205
|
+
if not workflow_id or not workflow_id.strip():
|
|
206
|
+
raise ValueError("`workflow_id` must be a non-empty string")
|
|
207
|
+
|
|
208
|
+
data: dict[str, str] = dict(texts or {})
|
|
209
|
+
if text is not None and "text" not in data:
|
|
210
|
+
data["text"] = text
|
|
211
|
+
|
|
212
|
+
opened: list[Any] = []
|
|
213
|
+
try:
|
|
214
|
+
multipart_files = {}
|
|
215
|
+
for key, f in (files or {}).items():
|
|
216
|
+
multipart_files[key] = self._prepare_file(f, opened)
|
|
217
|
+
if file is not None and "file" not in multipart_files:
|
|
218
|
+
multipart_files["file"] = self._prepare_file(file, opened)
|
|
219
|
+
|
|
220
|
+
if not multipart_files and not data:
|
|
221
|
+
raise ValueError("Provide at least one input file (file/files) or text (text/texts).")
|
|
222
|
+
|
|
223
|
+
return self._post(
|
|
224
|
+
f"{API_PREFIX}/workflow/{workflow_id.strip()}/run", data=data, files=multipart_files,
|
|
225
|
+
)
|
|
226
|
+
finally:
|
|
227
|
+
for f in opened:
|
|
228
|
+
f.close()
|
|
229
|
+
|
|
230
|
+
def get_workflow_run(self, run_id: str) -> dict:
|
|
231
|
+
return self._get(f"{API_PREFIX}/workflow/runs/{run_id}")
|
|
232
|
+
|
|
233
|
+
def wait_for_workflow_run(
|
|
234
|
+
self,
|
|
235
|
+
run_id: str,
|
|
236
|
+
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
|
237
|
+
timeout: float = _DEFAULT_WAIT_TIMEOUT,
|
|
238
|
+
on_poll=None,
|
|
239
|
+
) -> dict:
|
|
240
|
+
"""Poll `get_workflow_run` until the run reaches `completed` or `failed`."""
|
|
241
|
+
deadline = time.monotonic() + timeout
|
|
242
|
+
while True:
|
|
243
|
+
run = self.get_workflow_run(run_id)
|
|
244
|
+
if on_poll is not None:
|
|
245
|
+
on_poll(run)
|
|
246
|
+
if run.get("status") == "completed":
|
|
247
|
+
return run
|
|
248
|
+
if run.get("status") == "failed":
|
|
249
|
+
raise MotionWorkflowRunFailedError(run_id, run.get("error_message"))
|
|
250
|
+
remaining = deadline - time.monotonic()
|
|
251
|
+
if remaining <= 0:
|
|
252
|
+
raise MotionWorkflowRunTimeoutError(run_id, timeout)
|
|
253
|
+
time.sleep(min(poll_interval, remaining))
|
|
254
|
+
|
|
255
|
+
def process_workflow(
|
|
256
|
+
self,
|
|
257
|
+
workflow_id: str,
|
|
258
|
+
file: str | bytes | BinaryIO | None = None,
|
|
259
|
+
files: dict[str, str | bytes | BinaryIO] | None = None,
|
|
260
|
+
text: str | None = None,
|
|
261
|
+
texts: dict[str, str] | None = None,
|
|
262
|
+
wait: bool = True,
|
|
263
|
+
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
|
264
|
+
timeout: float = _DEFAULT_WAIT_TIMEOUT,
|
|
265
|
+
) -> dict:
|
|
266
|
+
"""`run_workflow` + wait for it in one call — the fire-and-get-result path.
|
|
267
|
+
|
|
268
|
+
Waits on whichever the run started — `run_id` for a multi-model
|
|
269
|
+
pipeline, `job_id` for a single model — so callers don't need to know
|
|
270
|
+
which ahead of time.
|
|
271
|
+
"""
|
|
272
|
+
started = self.run_workflow(workflow_id, file=file, files=files, text=text, texts=texts)
|
|
273
|
+
if not wait:
|
|
274
|
+
return started
|
|
275
|
+
if started.get("run_id"):
|
|
276
|
+
return self.wait_for_workflow_run(started["run_id"], poll_interval=poll_interval, timeout=timeout)
|
|
277
|
+
return self.wait_for_job(started["job_id"], poll_interval=poll_interval, timeout=timeout)
|
|
278
|
+
|
|
279
|
+
# ─── Workflow deployments ────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
def deploy_workflow(self, workflow_id: str, notes: str | None = None) -> dict:
|
|
282
|
+
"""Snapshot the workflow's current draft (as last saved in the dashboard)
|
|
283
|
+
as a new live version. `run_workflow`/`process_workflow` execute this
|
|
284
|
+
snapshot until the workflow is redeployed or rolled back."""
|
|
285
|
+
body = {"notes": notes} if notes is not None else {}
|
|
286
|
+
return self._post_json(f"{API_PREFIX}/workflow/{workflow_id}/deploy", json=body)
|
|
287
|
+
|
|
288
|
+
def list_workflow_deployments(self, workflow_id: str) -> dict:
|
|
289
|
+
"""Version history: `{"deployments": [...]}`, newest first."""
|
|
290
|
+
return self._get(f"{API_PREFIX}/workflow/{workflow_id}/deployments")
|
|
291
|
+
|
|
292
|
+
def rollback_workflow(self, workflow_id: str, version: int) -> dict:
|
|
293
|
+
"""Restore an earlier deployed version as a brand-new version (history
|
|
294
|
+
is never rewritten)."""
|
|
295
|
+
return self._post_json(f"{API_PREFIX}/workflow/{workflow_id}/deployments/{version}/rollback")
|
|
296
|
+
|
|
165
297
|
# ─── HTTP layer ──────────────────────────────────────────────────────────
|
|
166
298
|
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _prepare_file(file: str | bytes | BinaryIO, opened: list[Any]) -> tuple[str, Any, str]:
|
|
301
|
+
"""Normalize a path/bytes/file-object into a `requests`-style file tuple.
|
|
302
|
+
Any opened file handle is appended to `opened` so the caller can close it."""
|
|
303
|
+
if isinstance(file, (str, os.PathLike)):
|
|
304
|
+
obj: Any = open(file, "rb")
|
|
305
|
+
opened.append(obj)
|
|
306
|
+
name = os.path.basename(str(file))
|
|
307
|
+
elif isinstance(file, (bytes, bytearray)):
|
|
308
|
+
obj = file
|
|
309
|
+
name = "input"
|
|
310
|
+
else:
|
|
311
|
+
obj = file
|
|
312
|
+
name = getattr(file, "name", "input")
|
|
313
|
+
mime = mimetypes.guess_type(name)[0] or "application/octet-stream"
|
|
314
|
+
return name, obj, mime
|
|
315
|
+
|
|
167
316
|
def _get(self, path: str, params: dict | None = None) -> Any:
|
|
168
317
|
res = self._session.get(f"{self.base_url}{path}", params=params, timeout=self.timeout)
|
|
169
318
|
return self._handle(res)
|
|
@@ -172,6 +321,10 @@ class MotionClient:
|
|
|
172
321
|
res = self._session.post(f"{self.base_url}{path}", data=data, files=files, timeout=self.timeout)
|
|
173
322
|
return self._handle(res)
|
|
174
323
|
|
|
324
|
+
def _post_json(self, path: str, json: dict | None = None) -> Any:
|
|
325
|
+
res = self._session.post(f"{self.base_url}{path}", json=json, timeout=self.timeout)
|
|
326
|
+
return self._handle(res)
|
|
327
|
+
|
|
175
328
|
@staticmethod
|
|
176
329
|
def _handle(res: requests.Response) -> Any:
|
|
177
330
|
if not res.ok:
|
{motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: motion-intelligence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Author, build & deploy Python models for the Motion inference platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/devathub9/Motion-packages
|
|
6
6
|
Project-URL: Repository, https://github.com/devathub9/Motion-packages
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/requires.txt
RENAMED
|
File without changes
|
{motion_intelligence-0.4.0 → motion_intelligence-0.5.0}/motion_intelligence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|