aioli-sdk 0.2.1.dev0__tar.gz → 0.2.1.dev2__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 (87) hide show
  1. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/PKG-INFO +1 -1
  2. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/__version__.py +1 -1
  3. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/deployment.py +91 -6
  4. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/model.py +79 -60
  5. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/registry.py +4 -23
  6. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/test/test_cli.py +88 -10
  7. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/util.py +43 -1
  8. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/PKG-INFO +1 -1
  9. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/__init__.py +1 -1
  10. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/authentication_api.py +1 -1
  11. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/deployments_api.py +1 -1
  12. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/information_api.py +1 -1
  13. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/packaged_models_api.py +1 -1
  14. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/registries_api.py +1 -1
  15. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/roles_api.py +1 -1
  16. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/templates_api.py +1 -1
  17. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/users_api.py +1 -1
  18. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api_client.py +1 -1
  19. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/configuration.py +2 -2
  20. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/exceptions.py +1 -1
  21. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/__init__.py +1 -1
  22. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/auto_scaling_template.py +1 -1
  23. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/autoscaling.py +3 -13
  24. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/configuration_resources.py +1 -1
  25. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/deployment.py +3 -13
  26. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/deployment_model_version.py +1 -1
  27. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/deployment_request.py +1 -1
  28. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/deployment_state.py +3 -13
  29. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/error_response.py +1 -1
  30. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/event_info.py +1 -1
  31. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/failure_info.py +1 -1
  32. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/login_request.py +1 -1
  33. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/login_response.py +1 -1
  34. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/model_auth_token.py +1 -1
  35. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/observability.py +1 -1
  36. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/packaged_model.py +4 -14
  37. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/packaged_model_request.py +3 -13
  38. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/resource_profile.py +1 -1
  39. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/resources_template.py +1 -1
  40. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/role.py +1 -1
  41. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/role_assignment.py +3 -10
  42. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/role_assignments.py +1 -1
  43. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/security.py +1 -1
  44. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/success_response.py +1 -1
  45. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/trained_model_registry.py +8 -15
  46. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/trained_model_registry_request.py +8 -15
  47. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/user.py +1 -1
  48. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/user_patch_request.py +1 -1
  49. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/models/user_request.py +3 -13
  50. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/rest.py +1 -1
  51. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/setup.py +1 -1
  52. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/README.md +0 -0
  53. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/__init__.py +0 -0
  54. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/__init__.py +0 -0
  55. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/__main__.py +0 -0
  56. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/_util.py +0 -0
  57. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/cli.py +0 -0
  58. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/errors.py +0 -0
  59. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/render.py +0 -0
  60. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/role.py +0 -0
  61. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/sso.py +0 -0
  62. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/test/conftest.py +0 -0
  63. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/user.py +0 -0
  64. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/cli/version.py +0 -0
  65. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/__init__.py +0 -0
  66. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/__init__.py +0 -0
  67. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/_util.py +0 -0
  68. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/authentication.py +0 -0
  69. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/certs.py +0 -0
  70. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/errors.py +0 -0
  71. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/api/request.py +0 -0
  72. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/check.py +0 -0
  73. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/constants.py +0 -0
  74. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/declarative_argparse.py +0 -0
  75. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/common/requests.py +0 -0
  76. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli/util.py +0 -0
  77. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/SOURCES.txt +0 -0
  78. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/dependency_links.txt +0 -0
  79. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/entry_points.txt +0 -0
  80. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/not-zip-safe +0 -0
  81. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/requires.txt +0 -0
  82. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aioli_sdk.egg-info/top_level.txt +0 -0
  83. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api/__init__.py +0 -0
  84. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/api_response.py +0 -0
  85. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/aiolirest/py.typed +0 -0
  86. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/pyproject.toml +0 -0
  87. {aioli-sdk-0.2.1.dev0 → aioli_sdk-0.2.1.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aioli-sdk
3
- Version: 0.2.1.dev0
3
+ Version: 0.2.1.dev2
4
4
  Summary: Aioli (AI OnLine Inference), a platform for deploying AI models at scale.
5
5
  Home-page: https://github.com/determined-ai/aioli
6
6
  Author: HPE AI Solutions
@@ -1,2 +1,2 @@
1
1
  # © Copyright 2024 Hewlett Packard Enterprise Development LP
2
- __version__ = "0.2.1-dev0"
2
+ __version__ = "0.2.1-dev2"
@@ -9,18 +9,35 @@ from aioli.common import api
9
9
  from aioli.common.api import authentication
10
10
  from aioli.common.api.errors import NotFoundException
11
11
  from aioli.common.declarative_argparse import Arg, ArgsDescription, Cmd, Group
12
+ from aioli.common.util import (
13
+ construct_arguments,
14
+ construct_environment,
15
+ launch_dashboard,
16
+ )
12
17
  from aiolirest.models.autoscaling import Autoscaling
13
18
  from aiolirest.models.deployment import Deployment, DeploymentState
14
19
  from aiolirest.models.deployment_request import DeploymentRequest
20
+ from aiolirest.models.event_info import EventInfo
15
21
  from aiolirest.models.security import Security
16
22
 
17
23
 
24
+ @authentication.required
25
+ def dashboard(args: Namespace) -> None:
26
+ with cli.setup_session(args) as session:
27
+ api_instance = aiolirest.DeploymentsApi(session)
28
+
29
+ deployment: Deployment = lookup_deployment(args.name, api_instance)
30
+
31
+ observability = api_instance.deployments_id_observability_get(deployment.id)
32
+ launch_dashboard(args, observability.dashboard_url)
33
+
34
+
18
35
  @authentication.required
19
36
  def show_deployment(args: Namespace) -> None:
20
37
  with cli.setup_session(args) as session:
21
38
  api_instance = aiolirest.DeploymentsApi(session)
22
39
 
23
- deployment = lookup_deployment(args.name, api_instance)
40
+ deployment: Deployment = lookup_deployment(args.name, api_instance)
24
41
 
25
42
  # For a more useful display, replace the model ID with its name
26
43
  packaged_models_api = aiolirest.PackagedModelsApi(session)
@@ -59,8 +76,6 @@ def format_json(response: List[Deployment], model_api: aiolirest.PackagedModelsA
59
76
  # Use model name instead of id
60
77
  model = model_api.models_id_get(d.model)
61
78
  m_dict["model"] = model.name
62
- # Remove pop as part of INF-503
63
- m_dict.pop("service", None)
64
79
  m_dict.pop("clusterName", None)
65
80
  deps.append(m_dict)
66
81
 
@@ -117,6 +132,7 @@ def format_deployments(
117
132
  def create(args: Namespace) -> None:
118
133
  with cli.setup_session(args) as session:
119
134
  api_instance = aiolirest.DeploymentsApi(session)
135
+
120
136
  sec = Security(authenticationRequired=False)
121
137
  if args.authentication_required is not None:
122
138
  val = args.authentication_required.lower() == "true"
@@ -142,6 +158,8 @@ def create(args: Namespace) -> None:
142
158
  namespace=args.namespace,
143
159
  autoScaling=auto,
144
160
  canaryTrafficPercent=args.canary_traffic_percent,
161
+ environment=construct_environment(args),
162
+ arguments=construct_arguments(args),
145
163
  )
146
164
  api_instance.deployments_post(r)
147
165
 
@@ -201,6 +219,12 @@ def update(args: Namespace) -> None:
201
219
  val = args.authentication_required.lower() == "true"
202
220
  request.security.authentication_required = val
203
221
 
222
+ if args.env is not None:
223
+ request.environment = construct_environment(args)
224
+
225
+ if args.arg is not None:
226
+ request.arguments = construct_arguments(args)
227
+
204
228
  headers = {"Content-Type": "application/json"}
205
229
  assert found.id is not None
206
230
  api_instance.deployments_id_put(found.id, request, _headers=headers)
@@ -215,6 +239,32 @@ def delete_deployment(args: Namespace) -> None:
215
239
  api_instance.deployments_id_delete(found.id)
216
240
 
217
241
 
242
+ @authentication.required
243
+ def get_deployment_events(args: Namespace) -> None:
244
+ def format_events(event: EventInfo) -> List[Any]:
245
+ result = [
246
+ event.reason,
247
+ event.message,
248
+ event.time,
249
+ event.event_type,
250
+ ]
251
+ return result
252
+
253
+ with cli.setup_session(args) as session:
254
+ api_instance = aiolirest.DeploymentsApi(session)
255
+ found = lookup_deployment(args.name, api_instance)
256
+ assert found.id is not None
257
+ events = api_instance.deployments_id_events_get(found.id)
258
+ headers = [
259
+ "Reason",
260
+ "Message",
261
+ "Time",
262
+ "Event Type",
263
+ ]
264
+ values = [format_events(r) for r in events]
265
+ render.tabulate_or_csv(headers, values, args.csv)
266
+
267
+
218
268
  common_deployment_args: ArgsDescription = [
219
269
  Arg(
220
270
  "--authentication-required",
@@ -235,10 +285,24 @@ common_deployment_args: ArgsDescription = [
235
285
  type=int,
236
286
  default=100,
237
287
  ),
288
+ Arg(
289
+ "-a",
290
+ "--arg",
291
+ help="Argument to be added to the service command line. "
292
+ "If specifying an argument that starts with a '-', use the form --arg=<your-argument>",
293
+ action="append",
294
+ ),
295
+ Arg(
296
+ "-e",
297
+ "--env",
298
+ help="Specifies an environment variable & value as name=value, "
299
+ "to be passed to the launched container",
300
+ action="append",
301
+ ),
238
302
  ]
239
303
 
240
304
  main_cmd = Cmd(
241
- "d|eployment",
305
+ "d|eployment|s",
242
306
  None,
243
307
  "manage trained deployments",
244
308
  [
@@ -262,12 +326,24 @@ main_cmd = Cmd(
262
326
  Arg(
263
327
  "name",
264
328
  help="The name of the deployment. Must begin with a letter, but may contain "
265
- "letters, numbers, underscore, and hyphen",
329
+ "letters, numbers, and hyphen",
266
330
  ),
267
331
  Arg("--model", help="Model to be deployed", required="true"),
268
332
  ]
269
333
  + common_deployment_args,
270
334
  ),
335
+ # dashboard command.
336
+ Cmd(
337
+ "dashboard",
338
+ dashboard,
339
+ "launch the deployment dashboard",
340
+ [
341
+ Arg(
342
+ "name",
343
+ help="The name of the deployment.",
344
+ ),
345
+ ],
346
+ ),
271
347
  # Show command.
272
348
  Cmd(
273
349
  "show",
@@ -294,7 +370,7 @@ main_cmd = Cmd(
294
370
  Arg(
295
371
  "--name",
296
372
  help="The new name of the deployment. Must begin with a letter, but may "
297
- "contain letters, numbers, underscore, and hyphen",
373
+ "contain letters, numbers, and hyphen",
298
374
  ),
299
375
  Arg("--model", help="Model to be deployed", required="true"),
300
376
  ]
@@ -308,6 +384,15 @@ main_cmd = Cmd(
308
384
  Arg("name", help="The name of the deployment"),
309
385
  ],
310
386
  ),
387
+ Cmd(
388
+ "event|s",
389
+ get_deployment_events,
390
+ "get deployment events",
391
+ [
392
+ Arg("name", help="The name of the deployment"),
393
+ Arg("--csv", action="store_true", help="print as CSV"),
394
+ ],
395
+ ),
311
396
  ],
312
397
  )
313
398
 
@@ -1,6 +1,6 @@
1
1
  # © Copyright 2023-2024 Hewlett Packard Enterprise Development LP
2
2
  from argparse import Namespace
3
- from typing import Any, Dict, List
3
+ from typing import Any, List
4
4
 
5
5
  from pydantic import StrictInt
6
6
 
@@ -12,6 +12,11 @@ from aioli.common import api
12
12
  from aioli.common.api import authentication
13
13
  from aioli.common.api.errors import NotFoundException, VersionRequiredException
14
14
  from aioli.common.declarative_argparse import Arg, ArgsDescription, Cmd, Group
15
+ from aioli.common.util import (
16
+ construct_arguments,
17
+ construct_environment,
18
+ launch_dashboard,
19
+ )
15
20
  from aiolirest.models.configuration_resources import ConfigurationResources
16
21
  from aiolirest.models.deployment_model_version import DeploymentModelVersion
17
22
  from aiolirest.models.packaged_model import PackagedModel
@@ -74,32 +79,6 @@ def format_models(
74
79
  render.tabulate_or_csv(headers, values, args.csv)
75
80
 
76
81
 
77
- def construct_environment(args: Namespace) -> Dict[str, str]:
78
- environment: Dict[str, str] = {}
79
- if args.env is None:
80
- return environment
81
-
82
- for entry in args.env:
83
- # split to name & value
84
- the_split = entry.split("=", maxsplit=1)
85
- name: str = the_split[0]
86
- value: str = ""
87
- if len(the_split) > 1:
88
- value = the_split[1]
89
- environment[name] = value
90
- return environment
91
-
92
-
93
- def construct_arguments(args: Namespace) -> List[str]:
94
- arguments: List[str] = []
95
- if args.arg is None:
96
- return arguments
97
-
98
- for entry in args.arg:
99
- arguments.append(entry.strip())
100
- return arguments
101
-
102
-
103
82
  @authentication.required
104
83
  def create(args: Namespace) -> None:
105
84
  with cli.setup_session(args) as session:
@@ -125,40 +104,72 @@ def create(args: Namespace) -> None:
125
104
  api_instance.models_post(r)
126
105
 
127
106
 
128
- def lookup_model(name: str, api: aiolirest.PackagedModelsApi) -> PackagedModel:
129
- model = None
130
- for r in api.models_get():
107
+ def lookup_model(name: str, args: Namespace, api: aiolirest.PackagedModelsApi) -> PackagedModel:
108
+ # From the database, get the model record. If the model exists in multiple versions,
109
+ # then sufficient version information must be part of the request.
110
+ model_count = 0
111
+ models: List[PackagedModel] = api.models_get()
112
+ for r in models:
131
113
  if r.name == name:
132
- if model is not None:
133
- raise VersionRequiredException(
134
- f"please specify model version as {name} matches more than one model"
135
- )
136
114
  model = r
115
+ model_count += 1
137
116
 
138
- if model is None:
139
- raise NotFoundException(f"model {name} not found")
140
- return model
117
+ if model_count == 1:
118
+ return model # Found a single version of the specified model
119
+ if model_count > 1 and not args.version:
120
+ raise_version_required(name)
141
121
 
122
+ if model_count == 0:
123
+ # The specified model does not exist; extract the version suffix if any
124
+ split_on = ".v"
125
+ split = name.lower().rsplit(split_on, 1)
142
126
 
143
- def lookup_model_and_version(
144
- name: str, version: int, api: aiolirest.PackagedModelsApi
145
- ) -> PackagedModel:
146
- for r in api.models_get():
147
- if r.name == name and r.version == StrictInt(version):
127
+ if len(split) != 1:
128
+ version = split[1] # without the ".v"
129
+ # Extract name as everything to the left of the last ".v"
130
+ name = name[0 : len(name) - len(version) - len(split_on)]
131
+
132
+ # if there is an explicit version specified, then use that
133
+ if args.version:
134
+ version = args.version
135
+ if version is None:
136
+ raise_version_required(name)
137
+
138
+ return lookup_model_and_version(name, version, models)
139
+
140
+
141
+ def raise_version_required(name: str) -> None:
142
+ raise VersionRequiredException(
143
+ f"please specify model version as {name} matches more than one model"
144
+ )
145
+
146
+
147
+ def lookup_model_and_version(name: str, version: str, models: List[PackagedModel]) -> PackagedModel:
148
+ # The version may optionally be expressed with a prefix of 'v'
149
+ version_no_prefix: str = version.lstrip("vV")
150
+ for r in models:
151
+ if r.name == name and r.version == StrictInt(version_no_prefix):
148
152
  return r
149
153
  raise NotFoundException(f"model {name} version {version} not found")
150
154
 
151
155
 
152
156
  @authentication.required
153
- def show_model(args: Namespace) -> None:
157
+ def dashboard(args: Namespace) -> None:
154
158
  with cli.setup_session(args) as session:
155
159
  api_instance = aiolirest.PackagedModelsApi(session)
156
160
 
157
- if args.version:
158
- model = lookup_model_and_version(args.name, args.version, api_instance)
159
- else:
160
- model = lookup_model(args.name, api_instance)
161
+ model = lookup_model(args.name, args, api_instance)
161
162
 
163
+ observability = api_instance.models_id_observability_get(model.id)
164
+ launch_dashboard(args, observability.dashboard_url)
165
+
166
+
167
+ @authentication.required
168
+ def show_model(args: Namespace) -> None:
169
+ with cli.setup_session(args) as session:
170
+ api_instance = aiolirest.PackagedModelsApi(session)
171
+
172
+ model = lookup_model(args.name, args, api_instance)
162
173
  registries_api = aiolirest.RegistriesApi(session)
163
174
 
164
175
  rname = lookup_registry_name_by_id(model.registry, registries_api)
@@ -176,10 +187,7 @@ def show_model(args: Namespace) -> None:
176
187
  def update(args: Namespace) -> None:
177
188
  with cli.setup_session(args) as session:
178
189
  api_instance = aiolirest.PackagedModelsApi(session)
179
- if args.version:
180
- found = lookup_model_and_version(args.modelname, args.version, api_instance)
181
- else:
182
- found = lookup_model(args.modelname, api_instance)
190
+ found = lookup_model(args.modelname, args, api_instance)
183
191
  request = PackagedModelRequest(
184
192
  description=found.description,
185
193
  image=found.image,
@@ -255,7 +263,7 @@ def update(args: Namespace) -> None:
255
263
  def delete_model(args: Namespace) -> None:
256
264
  with cli.setup_session(args) as session:
257
265
  api_instance = aiolirest.PackagedModelsApi(session)
258
- found = lookup_model_and_version(args.name, args.version, api_instance)
266
+ found = lookup_model_and_version(args.name, args.version, api_instance.models_get())
259
267
 
260
268
  assert found.id is not None
261
269
  api_instance.models_id_delete(found.id)
@@ -265,7 +273,7 @@ def delete_model(args: Namespace) -> None:
265
273
  def auth_token(args: Namespace) -> None:
266
274
  with cli.setup_session(args) as session:
267
275
  api_instance = aiolirest.PackagedModelsApi(session)
268
- found = lookup_model(args.name, api_instance)
276
+ found = lookup_model(args.name, args, api_instance)
269
277
  assert found.id is not None
270
278
  response = api_instance.models_id_token_get(found.id)
271
279
  t = response.to_dict()
@@ -276,10 +284,7 @@ def auth_token(args: Namespace) -> None:
276
284
  def list_versions(args: Namespace) -> None:
277
285
  with cli.setup_session(args) as session:
278
286
  api_instance = aiolirest.PackagedModelsApi(session)
279
- if args.version:
280
- found = lookup_model_and_version(args.name, args.version, api_instance)
281
- else:
282
- found = lookup_model(args.name, api_instance)
287
+ found = lookup_model(args.name, args, api_instance)
283
288
  assert found.id is not None
284
289
  response = api_instance.models_versions_get(found.id)
285
290
 
@@ -336,7 +341,7 @@ common_model_args: ArgsDescription = [
336
341
  ]
337
342
 
338
343
  main_cmd = Cmd(
339
- "m|odel",
344
+ "m|odel|s",
340
345
  None,
341
346
  "manage packaged models",
342
347
  [
@@ -360,12 +365,25 @@ main_cmd = Cmd(
360
365
  Arg(
361
366
  "name",
362
367
  help="The name of the packaged model. Must begin with a letter, but may "
363
- "contain letters, numbers, underscore, and hyphen",
368
+ "contain letters, numbers, and hyphen",
364
369
  ),
365
370
  Arg("--image", help="Docker container image servicing the packaged model"),
366
371
  ]
367
372
  + common_model_args,
368
373
  ),
374
+ # dashboard command.
375
+ Cmd(
376
+ "dashboard",
377
+ dashboard,
378
+ "launch the packaged model dashboard",
379
+ [
380
+ Arg(
381
+ "name",
382
+ help="The name of the packaged model.",
383
+ ),
384
+ Arg("--version", help="The packaged model version to show"),
385
+ ],
386
+ ),
369
387
  # Show command.
370
388
  Cmd(
371
389
  "show",
@@ -393,7 +411,7 @@ main_cmd = Cmd(
393
411
  Arg(
394
412
  "--name",
395
413
  help="The new name of the packaged model. Must begin with a letter, but may "
396
- "contain letters, numbers, underscore, and hyphen",
414
+ "contain letters, numbers, and hyphen",
397
415
  ),
398
416
  Arg("--image", help="Docker container image servicing the packaged model"),
399
417
  Arg("--version", help="The packaged model version to update"),
@@ -415,6 +433,7 @@ main_cmd = Cmd(
415
433
  "get packaged model auth token",
416
434
  [
417
435
  Arg("name", help="The name of the packaged model"),
436
+ Arg("--version", help="The version of the packaged model"),
418
437
  ],
419
438
  ),
420
439
  Cmd(
@@ -1,6 +1,6 @@
1
1
  # © Copyright 2023-2024 Hewlett Packard Enterprise Development LP
2
2
  from argparse import Namespace
3
- from typing import Any, List, Optional, Union
3
+ from typing import Any, List, Optional
4
4
 
5
5
  import aiolirest
6
6
  from aioli import cli
@@ -11,8 +11,6 @@ from aioli.common.declarative_argparse import Arg, Cmd, Group
11
11
  from aiolirest.models.trained_model_registry import TrainedModelRegistry
12
12
  from aiolirest.models.trained_model_registry_request import TrainedModelRegistryRequest
13
13
 
14
- from .errors import CliError
15
-
16
14
  # Avoid reporting BrokenPipeError when piping `tabulate` output through
17
15
  # a filter like `head`.
18
16
  FLUSH = False
@@ -67,24 +65,11 @@ def format_deployment(response: List[TrainedModelRegistry], args: Namespace) ->
67
65
  render.tabulate_or_csv(headers, values, args.csv)
68
66
 
69
67
 
70
- def _validate_type_enum(reg_type: Any) -> Union[None, str]:
71
- if reg_type not in ("s3", "path", "http", "openllm", "ngc"):
72
- return (
73
- "Registry type must be one of enum values (s3, path, "
74
- + f"http, openllm, ngc), provided '{reg_type}'"
75
- )
76
- return None
77
-
78
-
79
68
  @authentication.required
80
69
  def create(args: Namespace) -> None:
81
70
  with cli.setup_session(args) as session:
82
71
  api_instance = aiolirest.RegistriesApi(session)
83
72
 
84
- err = _validate_type_enum(args.type)
85
- if err:
86
- raise CliError(err)
87
-
88
73
  r = TrainedModelRegistryRequest(
89
74
  name=args.name,
90
75
  accessKey=args.access_key,
@@ -153,10 +138,6 @@ def update(args: Namespace) -> None:
153
138
  request.name = args.name
154
139
 
155
140
  if args.type is not None:
156
- err = _validate_type_enum(args.type)
157
- if err:
158
- raise CliError(err)
159
-
160
141
  request.type = args.type
161
142
 
162
143
  if args.access_key is not None:
@@ -186,7 +167,7 @@ def delete_registry(args: Namespace) -> None:
186
167
 
187
168
 
188
169
  main_cmd = Cmd(
189
- "r|egistry",
170
+ "registries r|egistry",
190
171
  None,
191
172
  "manage packaged model registries",
192
173
  [
@@ -212,7 +193,7 @@ main_cmd = Cmd(
212
193
  Arg(
213
194
  "name",
214
195
  help="The name of the model registry. Must begin with a letter, but may "
215
- "contain letters, numbers, underscore, and hyphen",
196
+ "contain letters, numbers, and hyphen",
216
197
  ),
217
198
  Arg("--type", help="The type of this model registry", required="true"),
218
199
  Arg("--bucket", help="S3 Bucket name"),
@@ -247,7 +228,7 @@ main_cmd = Cmd(
247
228
  Arg(
248
229
  "--name",
249
230
  help="The new name of the model registry. Must begin with a letter, but may "
250
- "contain letters, numbers, underscore, and hyphen",
231
+ "contain letters, numbers, and hyphen",
251
232
  ),
252
233
  Arg("--type", help="The type of this model registry"),
253
234
  Arg("--bucket", help="S3 Bucket name"),
@@ -1,4 +1,4 @@
1
- # © Copyright 2023 Hewlett Packard Enterprise Development LP
1
+ # © Copyright 2024 Hewlett Packard Enterprise Development LP
2
2
  import csv
3
3
  import os
4
4
  import subprocess
@@ -95,15 +95,15 @@ class TestCli:
95
95
  )
96
96
  assert proc.returncode == 1
97
97
  expected = (
98
- "Registry type must be one of enum values "
99
- + "(s3, path, http, openllm, ngc), provided 'bob'"
98
+ "Failed to create a registry: registry type must be one of the values "
99
+ + "(s3, http, openllm, ngc), provided 'bob'"
100
100
  )
101
101
  assert proc.stderr.decode("utf-8").find(expected) == 0
102
102
 
103
103
  # List the newly created registry entry and test for expected values
104
104
  expected = (
105
105
  "bento-registry | s3 | minioadmin | demo-bento-registry | "
106
- "minioadmin | http://10.30.89.14:30008/"
106
+ "********** | http://10.30.89.14:30008/"
107
107
  )
108
108
  actual = subprocess.check_output(["aioli", "registry", "list"]).decode("utf-8")
109
109
  assert (actual.find(expected)) > 0
@@ -120,7 +120,7 @@ class TestCli:
120
120
  assert row["Type"] == "s3", "Expected s3"
121
121
  assert row["Access Key"] == "minioadmin", "Expected minioadmin"
122
122
  assert row["Bucket"] == "demo-bento-registry", "Expected demo-bento-registry"
123
- assert row["Secret Key"] == "minioadmin", "Expected minioadmin"
123
+ assert row["Secret Key"] == "**********", "Expected **********"
124
124
  assert (
125
125
  row["Endpoint URL"] == "http://10.30.89.14:30008/"
126
126
  ), "Expected http://10.30.89.14:30008/"
@@ -134,7 +134,7 @@ class TestCli:
134
134
  ' "bucket": "demo-bento-registry",\n'
135
135
  ' "endpointUrl": "http://10.30.89.14:30008/",\n'
136
136
  ' "name": "bento-registry",\n'
137
- ' "secretKey": "minioadmin",\n'
137
+ ' "secretKey": "**********",\n'
138
138
  ' "type": "s3"\n'
139
139
  ' }\n' # noqa: Q000
140
140
  ']\n' # noqa: Q000
@@ -168,7 +168,7 @@ class TestCli:
168
168
  ).decode("utf-8")
169
169
  expected = (
170
170
  "bento-registry1 | s3 | minioadmin | demo-bento-registry | "
171
- "minioadmin | http://10.30.89.14:30008/"
171
+ "********** | http://10.30.89.14:30008/"
172
172
  )
173
173
  actual = subprocess.check_output(["aioli", "registry", "list"]).decode("utf-8")
174
174
  assert (actual.find(expected)) > 0
@@ -187,8 +187,8 @@ class TestCli:
187
187
  )
188
188
  assert proc.returncode == 1
189
189
  expected = (
190
- "Registry type must be one of enum values "
191
- + "(s3, path, http, openllm, ngc), provided 'bob'"
190
+ "Failed to modify a registry: registry type must be one of the values "
191
+ + "(s3, http, openllm, ngc), provided 'bob'"
192
192
  )
193
193
  assert proc.stderr.decode("utf-8").find(expected) == 0
194
194
 
@@ -252,6 +252,49 @@ class TestCli:
252
252
  ), "Expected fictional.registry.example/imagename"
253
253
  assert row["Registry"] == "bento-registry1", "Expected bento-registry1"
254
254
 
255
+ # Exercise various means of specifying the model version, using the show command as a vehicle.
256
+ def test_model_versions(self, setup_login: None) -> None:
257
+ yaml_output = subprocess.check_output(["aioli", "model", "show", "iris-tf-keras"]).decode(
258
+ "utf-8"
259
+ )
260
+ temp_file: str = "/tmp/test-cli-m.yaml"
261
+
262
+ # Demonstrate that we have valid yaml
263
+ with open(temp_file, "w") as file:
264
+ file.write(yaml_output)
265
+ with open(temp_file, newline="") as file:
266
+ from ruamel.yaml import YAML
267
+
268
+ yaml = YAML(typ="safe")
269
+ model = yaml.load(file)
270
+
271
+ assert model["version"] == 1, "Validity check -- version == 1"
272
+
273
+ yaml_output2 = subprocess.check_output(
274
+ ["aioli", "model", "show", "iris-tf-keras.v1"]
275
+ ).decode("utf-8")
276
+ assert yaml_output == yaml_output2, "Expect the same output using .v1"
277
+
278
+ yaml_output2 = subprocess.check_output(
279
+ ["aioli", "model", "show", "iris-tf-keras.V1"]
280
+ ).decode("utf-8")
281
+ assert yaml_output == yaml_output2, "Expect the same output using .V1"
282
+
283
+ yaml_output2 = subprocess.check_output(
284
+ ["aioli", "model", "show", "iris-tf-keras", "--version", "1"]
285
+ ).decode("utf-8")
286
+ assert yaml_output == yaml_output2, "Expect the same output using --version 1"
287
+
288
+ yaml_output2 = subprocess.check_output(
289
+ ["aioli", "model", "show", "iris-tf-keras", "--version", "v1"]
290
+ ).decode("utf-8")
291
+ assert yaml_output == yaml_output2, "Expect the same output using --version v1"
292
+
293
+ yaml_output2 = subprocess.check_output(
294
+ ["aioli", "model", "show", "iris-tf-keras", "--version", "V1"]
295
+ ).decode("utf-8")
296
+ assert yaml_output == yaml_output2, "Expect the same output using --version V1"
297
+
255
298
  # fmt: off
256
299
  def test_model_list_json(self, setup_login: None) -> None:
257
300
  expected = (
@@ -388,10 +431,11 @@ class TestCli:
388
431
  assert (actual.find(expected)) > 0
389
432
 
390
433
  # Create a fourth with a new name of the model using the second version
434
+ # (specifying the version with the optional v{n} format)
391
435
  assert (
392
436
  os.system(
393
437
  "aioli model update iris-tf-keras --name iris-tf-keras-v4 "
394
- "--version 2 "
438
+ "--version v2 "
395
439
  "--registry bento-registry1 "
396
440
  "--url s3://demo-bento-registry/iris-tf-keras_updated_v3 "
397
441
  "--image fictional.registry.example/updated_imagename "
@@ -519,6 +563,8 @@ class TestCli:
519
563
  "--autoscaling-target 1 "
520
564
  "--autoscaling-metric concurrency "
521
565
  "--canary-traffic-percent 20 "
566
+ "-a='--debug' "
567
+ "-e MODS=SOME "
522
568
  "iris-tf-keras-deployment-2"
523
569
  )
524
570
  == 0
@@ -548,6 +594,38 @@ class TestCli:
548
594
  actual = filtered_result.strip()
549
595
  assert actual == expected
550
596
 
597
+ def test_deployment_update_args_environment(self, setup_login: None) -> None:
598
+ # Update the deployments arguments and environment and check that changes
599
+ # were made.
600
+ assert (
601
+ os.system(
602
+ "aioli deployment update --model iris-tf-keras "
603
+ "-a='--updated' "
604
+ "-e MODS=UPDATED "
605
+ "--env OTHER=VALUE "
606
+ "iris-tf-keras-deployment-2"
607
+ )
608
+ == 0
609
+ )
610
+
611
+ result = subprocess.check_output(
612
+ ["aioli", "deployment", "show", "iris-tf-keras-deployment-2"]
613
+ ).decode("utf-8")
614
+ result_list = result.split("\n")
615
+ args_found = False
616
+ other_found = False
617
+ for line in result_list:
618
+ if "- --updated" in line:
619
+ args_found = True
620
+ if "MODS: UPDATED" in line:
621
+ mods_found = True
622
+ if "OTHER: VALUE" in line:
623
+ other_found = True
624
+
625
+ assert args_found
626
+ assert mods_found
627
+ assert other_found
628
+
551
629
  def test_deployment_delete(self, setup_login: None) -> None:
552
630
  assert os.system("aioli deployment delete iris-tf-keras-deployment") == 0
553
631
  assert os.system("aioli deployment delete iris-tf-keras-deployment-2") == 0