lookout-cli 4.2.1__tar.gz → 5.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/PKG-INFO +3 -1
  2. lookout_cli-5.0.1/lookout_cli/groups/model.py +222 -0
  3. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/PKG-INFO +3 -1
  4. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/requires.txt +2 -0
  5. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/pyproject.toml +3 -1
  6. lookout_cli-4.2.1/lookout_cli/groups/model.py +0 -112
  7. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/README.md +0 -0
  8. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/__init__.py +0 -0
  9. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/auth_helpers.py +0 -0
  10. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/banner.py +0 -0
  11. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/cli.py +0 -0
  12. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.cache.yaml +0 -0
  13. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.dev.yaml +0 -0
  14. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.jetson.yaml +0 -0
  15. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.network-host.yaml +0 -0
  16. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.network-proxy.yaml +0 -0
  17. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.prod.yaml +0 -0
  18. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.x86.yaml +0 -0
  19. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker/docker-compose.yaml +0 -0
  20. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/docker_helpers.py +0 -0
  21. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/groups/__init__.py +0 -0
  22. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/groups/base.py +0 -0
  23. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/groups/setup.py +0 -0
  24. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/helpers.py +0 -0
  25. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli/test/lookout_cli_test.py +0 -0
  26. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/SOURCES.txt +0 -0
  27. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/dependency_links.txt +0 -0
  28. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/entry_points.txt +0 -0
  29. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/top_level.txt +0 -0
  30. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/lookout_cli.egg-info/zip-safe +0 -0
  31. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/setup.cfg +0 -0
  32. {lookout_cli-4.2.1 → lookout_cli-5.0.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lookout_cli
3
- Version: 4.2.1
3
+ Version: 5.0.1
4
4
  Summary: A CLI for interacting with Lookout+
5
5
  Author-email: Greenroom Robotics <team@greenroomrobotics.com>
6
6
  Maintainer-email: David Revay <david.revay@greenroomrobotics.com>
@@ -22,6 +22,8 @@ Requires-Dist: python-on-whales
22
22
  Requires-Dist: PyYAML
23
23
  Requires-Dist: bcrypt
24
24
  Requires-Dist: mlflow-skinny<3,>=2.19.0
25
+ Requires-Dist: azure-identity<2,>=1.16
26
+ Requires-Dist: azure-storage-blob<13,>=12.19
25
27
  Requires-Dist: tqdm
26
28
 
27
29
  # Lookout CLI
@@ -0,0 +1,222 @@
1
+ import shutil
2
+ import tempfile
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from lookout_cli.groups.base import config_dir_option, set_initial_env
10
+ from lookout_cli.helpers import make_dir_set_permission
11
+ from lookout_config import get_config_io
12
+
13
+ MODEL_NAME = "rtdetrv2"
14
+ DEFAULT_ALIAS = "champion"
15
+ DEFAULT_MLFLOW_URL = "http://gr-nuc-visionai:4242"
16
+ DEFAULT_AZURE_ACCOUNT = "stgrmlmodelsprod"
17
+ DEFAULT_AZURE_CONTAINER = "general"
18
+
19
+
20
+ class Source(str, Enum):
21
+ """Where to pull models from. MLflow (over Tailscale) is being deprecated in
22
+ favour of Azure Blob (the ML platform's published-models account)."""
23
+
24
+ MLFLOW = "mlflow"
25
+ AZURE = "azure"
26
+
27
+
28
+ class SensorType(str, Enum):
29
+ RGB = "rgb"
30
+ IR = "ir"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Artifact:
35
+ """A model file: ``suffix`` is appended to the model base name to form the
36
+ output file; ``mlflow_path`` is where it lives within an MLflow run."""
37
+
38
+ suffix: str
39
+ mlflow_path: str
40
+ required: bool = False
41
+
42
+
43
+ ARTIFACTS = (
44
+ Artifact(".onnx", "onnx_model_fixed/model.onnx", required=True),
45
+ Artifact(".int8.onnx", "onnx_model_int8/model.onnx"),
46
+ Artifact(".calib", "calibration/int8.calib"),
47
+ )
48
+
49
+
50
+ def _enum_choices(enum: type[Enum]) -> list[str]:
51
+ return [member.value for member in enum]
52
+
53
+
54
+ def _save(path: Path) -> None:
55
+ click.echo(click.style(f"Downloaded {path.name}", fg="green"))
56
+
57
+
58
+ class Backend(ABC):
59
+ """Downloads RT-DETR models into a directory. Subclasses implement
60
+ :meth:`download` for a single model; the base drives the per-sensor loop."""
61
+
62
+ def download_all(self, sensors: list[SensorType], dest: Path) -> None:
63
+ failures: list[str] = []
64
+ for sensor in sensors:
65
+ model_name = f"{MODEL_NAME}.{sensor.value}"
66
+ try:
67
+ self.download(model_name, dest)
68
+ except Exception as e:
69
+ failures.append(model_name)
70
+ click.echo(click.style(f"Failed to download {model_name}: {e}", fg="red"))
71
+ if failures:
72
+ raise click.ClickException(f"Failed to download: {', '.join(failures)}")
73
+
74
+ @abstractmethod
75
+ def download(self, model_name: str, dest: Path) -> None:
76
+ raise NotImplementedError
77
+
78
+
79
+ class MlflowBackend(Backend):
80
+ """MLflow registry, reached over Tailscale (deprecated)."""
81
+
82
+ def __init__(self, alias: str, url: str) -> None:
83
+ import mlflow
84
+ from mlflow.tracking import MlflowClient
85
+
86
+ mlflow.set_tracking_uri(url)
87
+ self._client = MlflowClient()
88
+ self._alias = alias
89
+
90
+ def download(self, model_name: str, dest: Path) -> None:
91
+ version = self._client.get_model_version_by_alias(model_name, self._alias)
92
+ base_name = f"{model_name}.v{version.version}.{self._alias}"
93
+ click.echo(click.style(f"Downloading {base_name} to {dest}"))
94
+
95
+ with tempfile.TemporaryDirectory() as temp_dir:
96
+ for artifact in ARTIFACTS:
97
+ parent = str(Path(artifact.mlflow_path).parent)
98
+ if not any(
99
+ a.path == artifact.mlflow_path
100
+ for a in self._client.list_artifacts(version.run_id, parent)
101
+ ):
102
+ if artifact.required:
103
+ raise FileNotFoundError(artifact.mlflow_path)
104
+ click.echo(click.style(f"Skipping missing {artifact.suffix}", fg="yellow"))
105
+ continue
106
+ local = self._client.download_artifacts(
107
+ version.run_id, artifact.mlflow_path, temp_dir
108
+ )
109
+ shutil.move(local, output := dest / f"{base_name}{artifact.suffix}")
110
+ _save(output)
111
+
112
+
113
+ class AzureBackend(Backend):
114
+ """Azure published-models blob account.
115
+
116
+ Auth via ``DefaultAzureCredential`` (``az login`` locally, VM managed
117
+ identity in prod). Blobs live under a ``{model_name}/{alias}/`` prefix and
118
+ keep their versioned basenames, so the result matches the MLflow backend.
119
+ """
120
+
121
+ def __init__(self, alias: str, account: str, container: str) -> None:
122
+ from azure.identity import DefaultAzureCredential
123
+ from azure.storage.blob import ContainerClient
124
+
125
+ self._client = ContainerClient(
126
+ account_url=f"https://{account}.blob.core.windows.net",
127
+ container_name=container,
128
+ credential=DefaultAzureCredential(),
129
+ )
130
+ self._alias = alias
131
+ self._location = f"{account}/{container}"
132
+
133
+ def download(self, model_name: str, dest: Path) -> None:
134
+ prefix = f"{model_name}/{self._alias}/"
135
+ blobs = list(self._client.list_blobs(name_starts_with=prefix))
136
+ # Match the MLflow contract: a prefix with only stale/optional blobs (no
137
+ # required model artefact) is not a successful download.
138
+ if not any(
139
+ blob.name.endswith(".onnx") and not blob.name.endswith(".int8.onnx") for blob in blobs
140
+ ):
141
+ raise FileNotFoundError(f"no model artefact under {self._location}/{prefix}")
142
+
143
+ click.echo(click.style(f"Downloading {prefix} from {self._location} to {dest}"))
144
+ for blob in blobs:
145
+ with open(output := dest / Path(blob.name).name, "wb") as f:
146
+ self._client.download_blob(blob.name).readinto(f)
147
+ _save(output)
148
+
149
+
150
+ @click.group(help="ONNX model management commands")
151
+ def model() -> None:
152
+ pass
153
+
154
+
155
+ @click.command(name="download")
156
+ @click.option(
157
+ "--type",
158
+ "model_type",
159
+ type=click.Choice(["all", *_enum_choices(SensorType)]),
160
+ default="all",
161
+ show_default=True,
162
+ help="Sensor model(s) to download.",
163
+ )
164
+ @click.option(
165
+ "--alias",
166
+ default=DEFAULT_ALIAS,
167
+ show_default=True,
168
+ help="Alias of model to download.",
169
+ )
170
+ @click.option(
171
+ "--source",
172
+ type=click.Choice(_enum_choices(Source)),
173
+ default=Source.MLFLOW.value,
174
+ show_default=True,
175
+ help="Where to download models from.",
176
+ )
177
+ @click.option(
178
+ "--account",
179
+ default=DEFAULT_AZURE_ACCOUNT,
180
+ show_default=True,
181
+ help="Azure storage account (azure source only).",
182
+ )
183
+ @click.option(
184
+ "--container",
185
+ default=DEFAULT_AZURE_CONTAINER,
186
+ show_default=True,
187
+ help="Azure container (azure source only).",
188
+ )
189
+ @click.option(
190
+ "--mlflow-url",
191
+ default=DEFAULT_MLFLOW_URL,
192
+ show_default=True,
193
+ help="MLflow tracking URL (mlflow source only).",
194
+ )
195
+ @config_dir_option
196
+ def download(
197
+ model_type: str,
198
+ alias: str,
199
+ source: str,
200
+ account: str,
201
+ container: str,
202
+ mlflow_url: str,
203
+ config_dir: str,
204
+ ) -> None:
205
+ """Download ONNX models from MLflow (Tailscale) or Azure Blob."""
206
+ set_initial_env(config_dir)
207
+
208
+ dest = Path(get_config_io().read().models_directory).expanduser()
209
+ make_dir_set_permission(dest)
210
+
211
+ sensors = list(SensorType) if model_type == "all" else [SensorType(model_type)]
212
+ backend: Backend = (
213
+ AzureBackend(alias, account, container)
214
+ if Source(source) is Source.AZURE
215
+ else MlflowBackend(alias, mlflow_url)
216
+ )
217
+
218
+ backend.download_all(sensors, dest)
219
+ click.echo(click.style("Download complete.", fg="green"))
220
+
221
+
222
+ model.add_command(download)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lookout_cli
3
- Version: 4.2.1
3
+ Version: 5.0.1
4
4
  Summary: A CLI for interacting with Lookout+
5
5
  Author-email: Greenroom Robotics <team@greenroomrobotics.com>
6
6
  Maintainer-email: David Revay <david.revay@greenroomrobotics.com>
@@ -22,6 +22,8 @@ Requires-Dist: python-on-whales
22
22
  Requires-Dist: PyYAML
23
23
  Requires-Dist: bcrypt
24
24
  Requires-Dist: mlflow-skinny<3,>=2.19.0
25
+ Requires-Dist: azure-identity<2,>=1.16
26
+ Requires-Dist: azure-storage-blob<13,>=12.19
25
27
  Requires-Dist: tqdm
26
28
 
27
29
  # Lookout CLI
@@ -7,4 +7,6 @@ python-on-whales
7
7
  PyYAML
8
8
  bcrypt
9
9
  mlflow-skinny<3,>=2.19.0
10
+ azure-identity<2,>=1.16
11
+ azure-storage-blob<13,>=12.19
10
12
  tqdm
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lookout_cli"
7
- version = "4.2.1"
7
+ version = "5.0.1"
8
8
  description = "A CLI for interacting with Lookout+"
9
9
  readme = "README.md"
10
10
  license = {text = "Copyright (C) 2025, Greenroom Robotics"}
@@ -32,6 +32,8 @@ dependencies = [
32
32
  "PyYAML",
33
33
  "bcrypt",
34
34
  "mlflow-skinny>=2.19.0,<3",
35
+ "azure-identity>=1.16,<2",
36
+ "azure-storage-blob>=12.19,<13",
35
37
  "tqdm"
36
38
  ]
37
39
 
@@ -1,112 +0,0 @@
1
- import shutil
2
- import tempfile
3
- from pathlib import Path
4
-
5
- import click
6
- import mlflow
7
- from lookout_cli.groups.base import config_dir_option, set_initial_env
8
- from lookout_cli.helpers import make_dir_set_permission
9
- from lookout_config import get_config_io
10
- from mlflow.tracking import MlflowClient
11
-
12
- MLFLOW_URL = "http://gr-nuc-visionai:4242"
13
- MODEL_NAME = "rtdetrv2"
14
-
15
-
16
- def download_artifact(
17
- client: MlflowClient, run_id: str, temp_dir: str, output_path: Path, artifact_path: str
18
- ):
19
- local_path = client.download_artifacts(
20
- run_id=run_id,
21
- path=artifact_path,
22
- dst_path=temp_dir,
23
- )
24
- shutil.move(local_path, output_path)
25
- click.echo(click.style(f"Downloaded {output_path.name}", fg="green"))
26
-
27
-
28
- def download_calib(client: MlflowClient, run_id: str, temp_dir: str, output_path: Path):
29
- artifact_path = "calibration/int8.calib"
30
- artifacts = client.list_artifacts(run_id, path=str(Path(artifact_path).parent))
31
- if not any(artifact.path == artifact_path for artifact in artifacts):
32
- click.echo(click.style("No int8 calibration file found", fg="red"))
33
- return
34
-
35
- download_artifact(client, run_id, temp_dir, output_path, artifact_path)
36
-
37
-
38
- def download_onnx(client: MlflowClient, run_id: str, temp_dir: str, output_path: Path):
39
- artifact_path = "onnx_model_fixed/model.onnx"
40
- download_artifact(client, run_id, temp_dir, output_path, artifact_path)
41
-
42
-
43
- def download_onnx_int8(client: MlflowClient, run_id: str, temp_dir: str, output_path: Path):
44
- artifact_path = "onnx_model_int8/model.onnx"
45
- artifacts = client.list_artifacts(run_id, path=str(Path(artifact_path).parent))
46
- if not any(artifact.path == artifact_path for artifact in artifacts):
47
- click.echo(click.style("No int8 ONNX model found", fg="red"))
48
- return
49
-
50
- download_artifact(client, run_id, temp_dir, output_path, artifact_path)
51
-
52
-
53
- @click.group(help="ONNX model management commands")
54
- def model():
55
- pass
56
-
57
-
58
- @click.command(name="download")
59
- @click.option(
60
- "--type",
61
- "model_type",
62
- type=click.Choice(["all", "rgb", "ir"]),
63
- default="all",
64
- show_default=True,
65
- help="Type of model to download.",
66
- )
67
- @click.option(
68
- "--alias",
69
- type=str,
70
- default="champion",
71
- show_default=True,
72
- help="Alias of model to download.",
73
- )
74
- @config_dir_option
75
- def download(model_type: str, alias: str, config_dir: str):
76
- """Download ONNX models from MLflow."""
77
- set_initial_env(config_dir)
78
-
79
- config = get_config_io().read()
80
- models_directory = Path(config.models_directory).expanduser()
81
- make_dir_set_permission(models_directory)
82
-
83
- mlflow.set_tracking_uri(MLFLOW_URL)
84
- client = MlflowClient()
85
-
86
- types_to_download: list[str] = ["rgb", "ir"] if model_type == "all" else [model_type]
87
-
88
- for sensor_type in types_to_download:
89
- model_name = f"{MODEL_NAME}.{sensor_type}"
90
-
91
- try:
92
- model_version = client.get_model_version_by_alias(model_name, alias)
93
- run_id = model_version.run_id
94
- version = model_version.version
95
- base_name = f"{model_name}.v{version}.{alias}"
96
-
97
- click.echo(click.style(f"Downloading {base_name} to {models_directory}"))
98
-
99
- with tempfile.TemporaryDirectory() as temp_dir:
100
- download_onnx(client, run_id, temp_dir, models_directory / f"{base_name}.onnx")
101
- download_onnx_int8(
102
- client, run_id, temp_dir, models_directory / f"{base_name}.int8.onnx"
103
- )
104
- download_calib(client, run_id, temp_dir, models_directory / f"{base_name}.calib")
105
-
106
- except Exception as e:
107
- click.echo(click.style(f"Failed to download {model_name}: {e}", fg="red"))
108
-
109
- click.echo(click.style("Download complete.", fg="green"))
110
-
111
-
112
- model.add_command(download)
File without changes
File without changes
File without changes