python-chi 1.2.2__tar.gz → 1.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. python_chi-1.2.3/ChangeLog +7 -0
  2. {python_chi-1.2.2 → python_chi-1.2.3}/PKG-INFO +3 -1
  3. {python_chi-1.2.2 → python_chi-1.2.3}/chi/container.py +9 -1
  4. {python_chi-1.2.2 → python_chi-1.2.3}/chi/context.py +71 -0
  5. {python_chi-1.2.2 → python_chi-1.2.3}/chi/hardware.py +183 -0
  6. {python_chi-1.2.2 → python_chi-1.2.3}/chi/lease.py +267 -3
  7. {python_chi-1.2.2 → python_chi-1.2.3}/chi/network.py +0 -43
  8. {python_chi-1.2.2 → python_chi-1.2.3}/chi/server.py +0 -1
  9. {python_chi-1.2.2 → python_chi-1.2.3}/chi/share.py +0 -11
  10. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/PKG-INFO +3 -1
  11. python_chi-1.2.3/python_chi.egg-info/pbr.json +1 -0
  12. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/requires.txt +2 -0
  13. {python_chi-1.2.2 → python_chi-1.2.3}/requirements.txt +3 -0
  14. python_chi-1.2.3/setup.py +5 -0
  15. python_chi-1.2.2/ChangeLog +0 -7
  16. python_chi-1.2.2/python_chi.egg-info/pbr.json +0 -1
  17. python_chi-1.2.2/setup.py +0 -5
  18. {python_chi-1.2.2 → python_chi-1.2.3}/.github/CODEOWNERS +0 -0
  19. {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/linting.yml +0 -0
  20. {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/pypi-publish.yml +0 -0
  21. {python_chi-1.2.2 → python_chi-1.2.3}/.github/workflows/test.yml +0 -0
  22. {python_chi-1.2.2 → python_chi-1.2.3}/.mailmap +0 -0
  23. {python_chi-1.2.2 → python_chi-1.2.3}/.readthedocs.yml +0 -0
  24. {python_chi-1.2.2 → python_chi-1.2.3}/AUTHORS +0 -0
  25. {python_chi-1.2.2 → python_chi-1.2.3}/DEVELOPMENT.rst +0 -0
  26. {python_chi-1.2.2 → python_chi-1.2.3}/LICENSE +0 -0
  27. {python_chi-1.2.2 → python_chi-1.2.3}/Makefile +0 -0
  28. {python_chi-1.2.2 → python_chi-1.2.3}/README.rst +0 -0
  29. {python_chi-1.2.2 → python_chi-1.2.3}/chi/__init__.py +0 -0
  30. {python_chi-1.2.2 → python_chi-1.2.3}/chi/clients.py +0 -0
  31. {python_chi-1.2.2 → python_chi-1.2.3}/chi/exception.py +0 -0
  32. {python_chi-1.2.2 → python_chi-1.2.3}/chi/image.py +0 -0
  33. {python_chi-1.2.2 → python_chi-1.2.3}/chi/jupyterhub.py +0 -0
  34. {python_chi-1.2.2 → python_chi-1.2.3}/chi/keypair.py +0 -0
  35. {python_chi-1.2.2 → python_chi-1.2.3}/chi/magic.py +0 -0
  36. {python_chi-1.2.2 → python_chi-1.2.3}/chi/ssh.py +0 -0
  37. {python_chi-1.2.2 → python_chi-1.2.3}/chi/storage.py +0 -0
  38. {python_chi-1.2.2 → python_chi-1.2.3}/chi/util.py +0 -0
  39. {python_chi-1.2.2 → python_chi-1.2.3}/docs/__init__.py +0 -0
  40. {python_chi-1.2.2 → python_chi-1.2.3}/docs/_templates/page.html +0 -0
  41. {python_chi-1.2.2 → python_chi-1.2.3}/docs/conf.py +0 -0
  42. {python_chi-1.2.2 → python_chi-1.2.3}/docs/examples.rst +0 -0
  43. {python_chi-1.2.2 → python_chi-1.2.3}/docs/generate_notebook.py +0 -0
  44. {python_chi-1.2.2 → python_chi-1.2.3}/docs/index.rst +0 -0
  45. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/clients.rst +0 -0
  46. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/container.rst +0 -0
  47. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/context.rst +0 -0
  48. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/exception.rst +0 -0
  49. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/hardware.rst +0 -0
  50. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/image.rst +0 -0
  51. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/lease.rst +0 -0
  52. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/magic.rst +0 -0
  53. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/network.rst +0 -0
  54. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/server.rst +0 -0
  55. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/share.rst +0 -0
  56. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/ssh.rst +0 -0
  57. {python_chi-1.2.2 → python_chi-1.2.3}/docs/modules/storage.rst +0 -0
  58. {python_chi-1.2.2 → python_chi-1.2.3}/docs/requirements.txt +0 -0
  59. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/SOURCES.txt +0 -0
  60. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/dependency_links.txt +0 -0
  61. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/not-zip-safe +0 -0
  62. {python_chi-1.2.2 → python_chi-1.2.3}/python_chi.egg-info/top_level.txt +0 -0
  63. {python_chi-1.2.2 → python_chi-1.2.3}/ruff.toml +0 -0
  64. {python_chi-1.2.2 → python_chi-1.2.3}/setup.cfg +0 -0
  65. {python_chi-1.2.2 → python_chi-1.2.3}/test-requirements.txt +0 -0
  66. {python_chi-1.2.2 → python_chi-1.2.3}/tests/__init__.py +0 -0
  67. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_container.py +0 -0
  68. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_context.py +0 -0
  69. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_lease.py +0 -0
  70. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_network.py +0 -0
  71. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_server.py +0 -0
  72. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_share.py +0 -0
  73. {python_chi-1.2.2 → python_chi-1.2.3}/tests/test_ssh.py +0 -0
  74. {python_chi-1.2.2 → python_chi-1.2.3}/tox.ini +0 -0
@@ -0,0 +1,7 @@
1
+ CHANGES
2
+ =======
3
+
4
+ v1.2.3
5
+ ------
6
+
7
+ * Fix formatting and tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-chi
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Helper library for Chameleon Infrastructure (CHI) testbed
5
5
  Home-page: https://www.chameleoncloud.org
6
6
  Author: University of Chicago
@@ -27,9 +27,11 @@ Requires-Dist: python-novaclient
27
27
  Requires-Dist: python-swiftclient
28
28
  Requires-Dist: python-zunclient
29
29
  Requires-Dist: ipython
30
+ Requires-Dist: ipydatagrid
30
31
  Requires-Dist: ipywidgets
31
32
  Requires-Dist: networkx
32
33
  Requires-Dist: matplotlib
34
+ Requires-Dist: pandas
33
35
  Dynamic: author
34
36
  Dynamic: author-email
35
37
  Dynamic: classifier
@@ -17,7 +17,7 @@ import logging
17
17
  import os
18
18
  import tarfile
19
19
  import time
20
- from typing import List, Optional, Tuple
20
+ from typing import Dict, List, Optional, Tuple
21
21
 
22
22
  from IPython.display import HTML, display
23
23
  from packaging.version import Version
@@ -62,6 +62,8 @@ class Container:
62
62
  id (str): The ID of the container.
63
63
  created_at (str): The timestamp when the container was created.
64
64
  status (str): The current status of the container.
65
+ environment (Dict[str, str]): A dictionary of environment variables for the container.
66
+ device_profiles (List[str]): A list of device profiles to be configured on the container.
65
67
  """
66
68
 
67
69
  def __init__(
@@ -75,6 +77,8 @@ class Container:
75
77
  runtime: str = None,
76
78
  command: List[str] = None,
77
79
  workdir: str = None,
80
+ environment: Dict[str, str] = {},
81
+ device_profiles: List[str] = [],
78
82
  ):
79
83
  self.name = name
80
84
  self.image_ref = image_ref
@@ -88,6 +92,8 @@ class Container:
88
92
  self._status = None
89
93
  self.command = command
90
94
  self.workdir = workdir
95
+ self.environment = environment
96
+ self.device_profiles = device_profiles
91
97
 
92
98
  @classmethod
93
99
  def from_zun_container(cls, zun_container):
@@ -152,6 +158,8 @@ class Container:
152
158
  start=self.start,
153
159
  start_timeout=self.start_timeout,
154
160
  runtime=self.runtime,
161
+ environment=self.environment,
162
+ device_profiles=self.device_profiles,
155
163
  **kwargs,
156
164
  )
157
165
 
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import re
3
4
  import sys
4
5
  import time
5
6
  from typing import List, Optional
@@ -72,6 +73,7 @@ deprecated_extra_opts = {
72
73
  _auth_plugin = None
73
74
  _session = None
74
75
  _sites = {}
76
+ _lease_id = None
75
77
 
76
78
  version = "1.1"
77
79
 
@@ -441,6 +443,75 @@ def choose_site(default: str = None) -> None:
441
443
  print("Choose site feature is only available in an ipynb environment.")
442
444
 
443
445
 
446
+ def use_lease_id(lease_id: str) -> None:
447
+ """
448
+ Sets the current lease ID to use in the global context.
449
+
450
+ This configures the lease so it can be stored for ease
451
+ of restoring suspended sessions. Further lease validation,
452
+ visualizations, and selectors are available in the lease module.
453
+
454
+ Args:
455
+ lease_id (str): The ID of the lease to use.
456
+ """
457
+ global _lease_id
458
+
459
+ if not re.fullmatch(r"[A-Za-z0-9\-]+", lease_id):
460
+ raise CHIValueError(
461
+ f'Lease ID "{lease_id}" is invalid. It must contain only letters, numbers, and hyphens with no spaces or special characters.'
462
+ )
463
+
464
+ _lease_id = lease_id
465
+
466
+ print(f"Now using lease with ID {lease_id}.")
467
+
468
+
469
+ def get_lease_id():
470
+ """
471
+ Returns the currently active lease ID, if one has been set.
472
+
473
+ Returns:
474
+ str or None: The lease ID currently in use, or None if no lease has been selected.
475
+ """
476
+ if _lease_id is None:
477
+ print("No lease ID has been set. Use `use_lease_id()` to select one.")
478
+ return _lease_id
479
+
480
+
481
+ def get_project_name(project_id: Optional[str] = None) -> str:
482
+ """
483
+ Returns the name of a project by ID, or the current project name if no ID is given.
484
+
485
+ Args:
486
+ project_id (str, optional): The ID of the project. If None, uses the current session project.
487
+
488
+ Returns:
489
+ str: The name of the project.
490
+
491
+ Raises:
492
+ ResourceError: If the project cannot be found or the request fails.
493
+ """
494
+ keystone_session = session()
495
+ keystone_client = KeystoneClient(
496
+ session=keystone_session,
497
+ interface=getattr(keystone_session, "interface", None),
498
+ region_name=getattr(keystone_session, "region_name", None),
499
+ )
500
+
501
+ try:
502
+ if project_id:
503
+ project = keystone_client.projects.get(project_id)
504
+ else:
505
+ current_id = keystone_session.get_project_id()
506
+ project = keystone_client.projects.get(current_id)
507
+ except keystone_exceptions.NotFound:
508
+ raise ResourceError("Project not found.")
509
+ except keystone_exceptions.Unauthorized:
510
+ raise ResourceError("Failed to retrieve project. Check your credentials.")
511
+
512
+ return project.name
513
+
514
+
444
515
  def list_projects(show: str = None) -> List[str]:
445
516
  """
446
517
  Retrieves a list of projects associated with the current user.
@@ -5,7 +5,11 @@ from dataclasses import dataclass
5
5
  from datetime import datetime, timedelta, timezone
6
6
  from typing import List, Optional, Set, Tuple
7
7
 
8
+ import pandas as pd
8
9
  import requests
10
+ from ipydatagrid import DataGrid, Expr, TextRenderer
11
+ from IPython.display import display
12
+ from ipywidgets import HTML, Box, Layout
9
13
 
10
14
  from chi import exception
11
15
 
@@ -98,6 +102,101 @@ class Node:
98
102
  blazarclient.host.get_allocation(host_id), minimum_hours
99
103
  )
100
104
 
105
+ def _ipython_display_(self):
106
+ """
107
+ Displays information about the node. This function is called passively by the Jupyter display system.
108
+ """
109
+
110
+ layout = Layout(padding="4px 10px")
111
+ style = {
112
+ "description_width": "initial",
113
+ "background": "#d3d3d3",
114
+ "white_space": "nowrap",
115
+ }
116
+
117
+ reservable_style = {
118
+ "description_width": "initial",
119
+ "background": " #a2d9fe",
120
+ "white_space": "nowrap",
121
+ }
122
+
123
+ if not self.reservable:
124
+ reservable_style["background"] = "#f69084"
125
+
126
+ children = [
127
+ HTML(f"<b>Node Name:</b> {self.name}", style=style, layout=layout),
128
+ HTML(f"<b>Site:</b> {self.site}", style=style, layout=layout),
129
+ HTML(f"<b>Type:</b> {self.type}", style=style, layout=layout),
130
+ ]
131
+ if getattr(self, "cpu", False) and "clock_speed" in self.cpu:
132
+ children.append(
133
+ HTML(
134
+ f"<b>Clock Speed:</b> {self.cpu['clock_speed'] / 1e9:.2f} GHz",
135
+ style=style,
136
+ layout=layout,
137
+ )
138
+ )
139
+
140
+ if (
141
+ getattr(self, "main_memory", False)
142
+ and "humanized_ram_size" in self.main_memory
143
+ ):
144
+ children.append(
145
+ HTML(
146
+ f"<b>RAM:</b> {self.main_memory['humanized_ram_size']}",
147
+ style=style,
148
+ layout=layout,
149
+ )
150
+ )
151
+
152
+ if getattr(self, "gpu", False) and "gpu" in self.gpu and self.gpu["gpu"]:
153
+ if "gpu_count" in self.gpu:
154
+ children.append(
155
+ HTML(
156
+ f"<b>GPU Count:</b> {self.gpu['gpu_count']}",
157
+ style=style,
158
+ layout=layout,
159
+ )
160
+ )
161
+ else:
162
+ children.append(HTML("<b>GPU:</b> True", style=style, layout=layout))
163
+ if "gpu_model" in self.gpu:
164
+ children.append(
165
+ HTML(
166
+ f"<b>GPU Model:</b> {self.gpu['gpu_model']}",
167
+ style=style,
168
+ layout=layout,
169
+ )
170
+ )
171
+ else:
172
+ children.append(HTML("<b>GPU Count:</b> 0", style=style, layout=layout))
173
+
174
+ if (
175
+ getattr(self, "storage_devices", False)
176
+ and len(self.storage_devices) > 0
177
+ and "humanized_size" in self.storage_devices[0]
178
+ ):
179
+ children.append(
180
+ HTML(
181
+ f"<b>Storage Size:</b> {self.storage_devices[0]['humanized_size']}",
182
+ style=style,
183
+ layout=layout,
184
+ )
185
+ )
186
+
187
+ if getattr(self, "reservable", False):
188
+ children.append(
189
+ HTML(
190
+ f"<b>Reservable:</b> {'Yes' if self.reservable else 'No'}",
191
+ style=reservable_style,
192
+ layout=layout,
193
+ )
194
+ )
195
+
196
+ box = Box(children=children)
197
+ box.layout = Layout(flex_flow="row wrap")
198
+ display(box)
199
+
101
200
 
102
201
  def _call_api(endpoint):
103
202
  url = "{0}/{1}.{2}".format(RESOURCE_API_URL, endpoint, "json")
@@ -260,6 +359,90 @@ def get_node_types() -> List[str]:
260
359
  return list(set(node_types))
261
360
 
262
361
 
362
+ def _reservable_color(cell):
363
+ return "#a2d9fe" if cell.value else "#f69084"
364
+
365
+
366
+ def _gpu_background_color(cell):
367
+ return "#d3d3d3" if not cell.value else None
368
+
369
+
370
+ def show_nodes(nodes: Optional[List[Node]] = None) -> None:
371
+ """
372
+ Display a sortable, filterable table of available nodes.
373
+
374
+ Args:
375
+ nodes (Optional[List[Node]], optional): A list of Node objects to display.
376
+ If not provided, defaults to the output of hardware.get_nodes().
377
+
378
+ Returns:
379
+ None
380
+ """
381
+
382
+ def estimate_column_width(df, column, char_px=7, padding=0):
383
+ if column not in df.columns:
384
+ raise ValueError(f"Column '{column}' not found in DataFrame.")
385
+ max_chars = df[column].astype(str).map(len).max()
386
+ return max(max_chars * char_px + padding, 80)
387
+
388
+ if not nodes:
389
+ nodes = get_nodes()
390
+
391
+ rows = []
392
+ for n in nodes:
393
+ rows.append(
394
+ {
395
+ "Node Name": n.name,
396
+ "Type": n.type,
397
+ "Clock Speed (GHz)": round(n.cpu.get("clock_speed", 0) / 1e9, 2),
398
+ "RAM": n.main_memory.get("humanized_ram_size", "N/A"),
399
+ "GPU Model": (n.gpu or {}).get("gpu_model") or "",
400
+ "GPU Count": (n.gpu or {}).get("gpu_count") or "",
401
+ "Storage Size": n.storage_devices[0].get("humanized_size", "N/A")
402
+ if n.storage_devices
403
+ else "N/A",
404
+ "Site": n.site,
405
+ "Reservable": bool(n.reservable),
406
+ }
407
+ )
408
+
409
+ df = pd.DataFrame(rows)
410
+ renderers = {
411
+ "Reservable": TextRenderer(
412
+ text_color="black",
413
+ background_color=Expr(_reservable_color),
414
+ ),
415
+ "GPU Model": TextRenderer(
416
+ background_color=Expr(_gpu_background_color),
417
+ ),
418
+ "GPU Count": TextRenderer(
419
+ background_color=Expr(_gpu_background_color),
420
+ ),
421
+ }
422
+
423
+ grid = DataGrid(
424
+ df,
425
+ layout=Layout(height="400px"),
426
+ selection_mode="row",
427
+ renderers=renderers,
428
+ column_widths={
429
+ "Node Name": int(estimate_column_width(df, "Node Name")),
430
+ "Site": int(estimate_column_width(df, "Site")),
431
+ "Type": int(estimate_column_width(df, "Type")),
432
+ "RAM": int(estimate_column_width(df, "RAM")),
433
+ "Storage Size": int(estimate_column_width(df, "Storage Size")),
434
+ "Clock Speed (GHz)": 55,
435
+ "GPU Model": 90,
436
+ "GPU Count": 30,
437
+ "key": 30,
438
+ "Reservable": 55,
439
+ },
440
+ df=pd.DataFrame(rows),
441
+ )
442
+
443
+ display(grid)
444
+
445
+
263
446
  @dataclass
264
447
  class Device:
265
448
  """
@@ -1,20 +1,23 @@
1
1
  import json
2
2
  import logging
3
3
  import numbers
4
+ import os
4
5
  import re
5
6
  import time
6
7
  from datetime import datetime, timedelta
7
8
  from typing import TYPE_CHECKING, List, Optional, Union
8
9
 
10
+ import pandas
9
11
  from blazarclient.exception import BlazarClientException
12
+ from ipydatagrid import DataGrid, Expr, TextRenderer
10
13
  from IPython.display import display
11
- from ipywidgets import HTML
14
+ from ipywidgets import HTML, Box, Layout
12
15
  from packaging.version import Version
13
16
 
14
17
  from chi import context, server, util
15
18
 
16
- from .clients import blazar
17
- from .context import _is_ipynb
19
+ from .clients import blazar, connection
20
+ from .context import _is_ipynb, get_project_name
18
21
  from .exception import CHIValueError, ResourceError, ServiceError
19
22
  from .hardware import Device, Node
20
23
  from .network import PUBLIC_NETWORK, get_network_id, list_floating_ips
@@ -259,6 +262,135 @@ class Lease:
259
262
 
260
263
  # self.events = lease_json.get('events', [])
261
264
 
265
+ def _ipython_display_(self):
266
+ """
267
+ Displays a styled summary of the lease when run in a Jupyter notebook.
268
+
269
+ This method is called automatically by the Jupyter display system when
270
+ an instance of the Lease object is the final expression in a cell.
271
+ It presents key lease attributes using ipywidgets for readability.
272
+ """
273
+ layout = Layout(padding="4px 10px")
274
+ style = {
275
+ "description_width": "initial",
276
+ "background": "#d3d3d3",
277
+ "white_space": "nowrap",
278
+ }
279
+
280
+ status_style = style.copy()
281
+ status_colors = {
282
+ "ACTIVE": "#a2d9fe",
283
+ "PENDING": "#ffe599",
284
+ "TERMINATED": "#f69084",
285
+ }
286
+ if self.status:
287
+ status_style["background"] = status_colors.get(self.status, "#d3d3d3")
288
+
289
+ children = [
290
+ # HTML(f"<b>Lease ID:</b> {self.id}", style=style, layout=layout),
291
+ HTML(f"<b>Status:</b> {self.status}", style=status_style, layout=layout),
292
+ HTML(f"<b>Name:</b> {self.name}", style=style, layout=layout),
293
+ ]
294
+
295
+ if self.start_date:
296
+ children.append(
297
+ HTML(
298
+ f"<b>Start:</b> {self.start_date.strftime('%Y-%m-%d %H:%M')}",
299
+ style=style,
300
+ layout=layout,
301
+ )
302
+ )
303
+ if self.end_date:
304
+ children.append(
305
+ HTML(
306
+ f"<b>End:</b> {self.end_date.strftime('%Y-%m-%d %H:%M')}",
307
+ style=style,
308
+ layout=layout,
309
+ )
310
+ )
311
+
312
+ remaining = None
313
+ if self.end_date and datetime.now() < self.end_date:
314
+ remaining = self.end_date - datetime.now()
315
+
316
+ if remaining:
317
+ days = remaining.days
318
+ hours = remaining.seconds // 3600
319
+ minutes = (remaining.seconds % 3600) // 60
320
+ children.append(
321
+ HTML(
322
+ f"<b>Remaining:</b> {days:02d}d {hours:02d}h {minutes:02d}m",
323
+ style=style,
324
+ layout=layout,
325
+ )
326
+ )
327
+
328
+ # Reservations
329
+ children.append(
330
+ HTML(
331
+ f"<b>Node Reservations:</b> {len(self.node_reservations)}",
332
+ style=style,
333
+ layout=layout,
334
+ )
335
+ )
336
+ children.append(
337
+ HTML(
338
+ f"<b>FIP Reservations:</b> {len(self.fip_reservations)}",
339
+ style=style,
340
+ layout=layout,
341
+ )
342
+ )
343
+ children.append(
344
+ HTML(
345
+ f"<b>Device Reservations:</b> {len(self.device_reservations)}",
346
+ style=style,
347
+ layout=layout,
348
+ )
349
+ )
350
+
351
+ if self.project_id:
352
+ try:
353
+ project_name = context.get_project_name(self.project_id)
354
+ children.append(
355
+ HTML(
356
+ f"<b>Project Name:</b> {project_name}",
357
+ style=style,
358
+ layout=layout,
359
+ )
360
+ )
361
+ except ResourceError:
362
+ children.append(
363
+ HTML(
364
+ f"<b>Project ID:</b> {self.project_id}",
365
+ style=style,
366
+ layout=layout,
367
+ )
368
+ )
369
+ if self.user_id:
370
+ user_id = connection().get_user_id()
371
+ if self.user_id == user_id:
372
+ label = os.getenv("OS_USERNAME")
373
+ children.append(
374
+ HTML(f"<b>User Name:</b> {label}", style=style, layout=layout)
375
+ )
376
+ else:
377
+ label = self.user_id # [:8] # or just show a truncated ID
378
+ children.append(
379
+ HTML(f"<b>User ID:</b> {label}", style=style, layout=layout)
380
+ )
381
+ if self.created_at:
382
+ children.append(
383
+ HTML(
384
+ f"<b>Created At:</b> {self.created_at.strftime('%Y-%m-%d %H:%M')}",
385
+ style=style,
386
+ layout=layout,
387
+ )
388
+ )
389
+
390
+ box = Box(children=children)
391
+ box.layout = Layout(flex_flow="row wrap")
392
+ display(box)
393
+
262
394
  def add_device_reservation(
263
395
  self,
264
396
  amount: int = None,
@@ -1079,6 +1211,138 @@ def list_leases() -> List[Lease]:
1079
1211
  return leases
1080
1212
 
1081
1213
 
1214
+ def _status_color(cell):
1215
+ return (
1216
+ "#a2d9fe"
1217
+ if cell.value == "2-ACTIVE"
1218
+ else (
1219
+ "#ffe599"
1220
+ if cell.value == "1-PENDING"
1221
+ else ("#f69084" if cell.value == "3-TERMINATED" else "#e0e0e0")
1222
+ )
1223
+ )
1224
+
1225
+
1226
+ def show_leases() -> DataGrid:
1227
+ """
1228
+ Displays a table of the user's leases in an interactive, sortable format.
1229
+
1230
+ Uses an ipydatagrid to present key lease attributes such as ID, name, status,
1231
+ duration, and reservation counts. The grid supports sorting, filtering, and
1232
+ scrolling for easy exploration of lease state.
1233
+
1234
+ Returns:
1235
+ DataGrid: An ipydatagrid widget displaying the leases.
1236
+ """
1237
+
1238
+ def estimate_column_width(df, column, char_px=7, padding=0):
1239
+ if column not in df.columns:
1240
+ raise ValueError(f"Column '{column}' not found in DataFrame.")
1241
+ max_chars = df[column].astype(str).map(len).max()
1242
+ return max(max_chars * char_px + padding, 80)
1243
+
1244
+ leases = list_leases()
1245
+
1246
+ rows = []
1247
+ for lease in leases:
1248
+ try:
1249
+ project_name = get_project_name(lease.project_id)
1250
+ except ResourceError:
1251
+ project_name = lease.project_id[:8] if lease.project_id else "Unknown"
1252
+
1253
+ if lease.user_id == connection().current_user_id:
1254
+ user_label = os.getenv("OS_USERNAME")
1255
+ else:
1256
+ user_label = lease.user_id if lease.user_id else "Unknown"
1257
+
1258
+ if lease.start_date and lease.end_date:
1259
+ duration_hrs = round(
1260
+ (lease.end_date - lease.start_date).total_seconds() / 3600, 1
1261
+ )
1262
+ else:
1263
+ duration_hrs = "N/A"
1264
+
1265
+ # Inside your row-building loop:
1266
+ if lease.end_date and lease.end_date > datetime.now():
1267
+ remaining_td = lease.end_date - datetime.now()
1268
+ remaining_str = (
1269
+ f"{remaining_td.days:02d}d {(remaining_td.seconds // 3600):02d}h"
1270
+ )
1271
+ elif lease.end_date and lease.end_date <= datetime.now():
1272
+ remaining_str = "Expired"
1273
+ else:
1274
+ remaining_str = "N/A"
1275
+
1276
+ # prepending status with numeric makes it possible to character sort
1277
+ # since ipydatagrid does not allow custom sort functions
1278
+ status_order = {
1279
+ "PENDING": "1-PENDING",
1280
+ "ACTIVE": "2-ACTIVE",
1281
+ "TERMINATED": "3-TERMINATED",
1282
+ }
1283
+
1284
+ rows.append(
1285
+ {
1286
+ "Name": lease.name,
1287
+ "Status": status_order.get(lease.status, f"4-{lease.status}"),
1288
+ "User": user_label,
1289
+ "Project": project_name,
1290
+ "Start": lease.start_date.strftime("%Y-%m-%d %H:%M")
1291
+ if lease.start_date
1292
+ else "",
1293
+ "End": lease.end_date.strftime("%Y-%m-%d %H:%M")
1294
+ if lease.end_date
1295
+ else "",
1296
+ "Remaining": remaining_str,
1297
+ "Total Hours": duration_hrs,
1298
+ "# Nodes": len(lease.node_reservations),
1299
+ "# FIPs": len(lease.fip_reservations),
1300
+ "Created": lease.created_at.strftime("%Y-%m-%d %H:%M")
1301
+ if lease.created_at
1302
+ else "",
1303
+ "Lease ID": lease.id,
1304
+ "_is_user_lease": 0
1305
+ if lease.user_id == connection().current_user_id
1306
+ else 1,
1307
+ }
1308
+ )
1309
+
1310
+ df = pandas.DataFrame(rows)
1311
+ df = pandas.DataFrame(rows)
1312
+ df = df.sort_values(by=["_is_user_lease", "Status", "Created"])
1313
+ df = df.drop(columns=["_is_user_lease"])
1314
+
1315
+ renderers = {
1316
+ "Status": TextRenderer(
1317
+ background_color=Expr(_status_color),
1318
+ text_color="black",
1319
+ ),
1320
+ }
1321
+
1322
+ display(
1323
+ DataGrid(
1324
+ df,
1325
+ layout={"height": "400px", "width": "100%"},
1326
+ column_widths={
1327
+ "key": 30,
1328
+ "Name": int(estimate_column_width(df, "Name")),
1329
+ "Status": 120,
1330
+ "Remaining": 80,
1331
+ "Total Hours": 50,
1332
+ "# Nodes": 30,
1333
+ "# FIPs": 30,
1334
+ "Project": 100,
1335
+ "User": 75,
1336
+ "Start": 95,
1337
+ "End": 95,
1338
+ "Created": 95,
1339
+ "Lease ID": 30,
1340
+ },
1341
+ renderers=renderers,
1342
+ )
1343
+ )
1344
+
1345
+
1082
1346
  def _get_lease_from_blazar(ref: str):
1083
1347
  blazar_client = blazar()
1084
1348
 
@@ -3,49 +3,6 @@ from neutronclient.common.exceptions import NotFound
3
3
  from .clients import neutron
4
4
  from .exception import CHIValueError, ResourceError
5
5
 
6
- __all__ = [
7
- "get_network",
8
- "get_network_id",
9
- "create_network",
10
- "delete_network",
11
- "update_network",
12
- "list_networks",
13
- "get_subnet",
14
- "get_subnet_id",
15
- "create_subnet",
16
- "delete_subnet",
17
- "update_subnet",
18
- "list_subnets",
19
- "get_port",
20
- "get_port_id",
21
- "create_port",
22
- "update_port",
23
- "delete_port",
24
- "list_ports",
25
- "get_router",
26
- "get_router_id",
27
- "create_router",
28
- "delete_router",
29
- "update_router",
30
- "list_routers",
31
- "add_route_to_router",
32
- "add_routes_to_router",
33
- "remove_route_from_router",
34
- "remove_routes_from_router",
35
- "remove_all_routes_from_router",
36
- "add_port_to_router",
37
- "add_port_to_router_by_name",
38
- "add_subnet_to_router",
39
- "add_subnet_to_router_by_name",
40
- "remove_subnet_from_router",
41
- "remove_port_from_router",
42
- "get_free_floating_ip",
43
- "get_floating_ip",
44
- "list_floating_ips",
45
- "bind_floating_ip",
46
- "nuke_network",
47
- ]
48
-
49
6
  PUBLIC_NETWORK = "public"
50
7
 
51
8
 
@@ -644,7 +644,6 @@ class Server:
644
644
 
645
645
  Args:
646
646
  volume_id (str): The volume to attach.
647
- mount_location (str, optional): The mount location of the volume. Defaults to "/mnt/volume".
648
647
  """
649
648
  nova().volumes.create_server_volume(self.id, volume_id)
650
649
 
@@ -3,17 +3,6 @@ from manilaclient.exceptions import NotFound
3
3
  from .clients import manila
4
4
  from .exception import CHIValueError, ResourceError
5
5
 
6
- __all__ = [
7
- "create_share",
8
- "delete_share",
9
- "extend_share",
10
- "get_access_rules",
11
- "get_share",
12
- "get_share_id",
13
- "list_shares",
14
- "shrink_share",
15
- ]
16
-
17
6
 
18
7
  def _get_default_share_type_id():
19
8
  # we only support one share type - cephfsnfstype
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-chi
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Helper library for Chameleon Infrastructure (CHI) testbed
5
5
  Home-page: https://www.chameleoncloud.org
6
6
  Author: University of Chicago
@@ -27,9 +27,11 @@ Requires-Dist: python-novaclient
27
27
  Requires-Dist: python-swiftclient
28
28
  Requires-Dist: python-zunclient
29
29
  Requires-Dist: ipython
30
+ Requires-Dist: ipydatagrid
30
31
  Requires-Dist: ipywidgets
31
32
  Requires-Dist: networkx
32
33
  Requires-Dist: matplotlib
34
+ Requires-Dist: pandas
33
35
  Dynamic: author
34
36
  Dynamic: author-email
35
37
  Dynamic: classifier
@@ -0,0 +1 @@
1
+ {"git_version": "3ff8f3d", "is_release": false}
@@ -11,6 +11,8 @@ python-novaclient
11
11
  python-swiftclient
12
12
  python-zunclient
13
13
  ipython
14
+ ipydatagrid
14
15
  ipywidgets
15
16
  networkx
16
17
  matplotlib
18
+ pandas
@@ -12,6 +12,9 @@ python-novaclient
12
12
  python-swiftclient
13
13
  python-zunclient
14
14
  ipython
15
+ ipydatagrid
15
16
  ipywidgets
16
17
  networkx
17
18
  matplotlib
19
+ pandas
20
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env python
2
+
3
+ from setuptools import setup
4
+
5
+ setup(setup_requires=["pbr"], pbr=True, version="v1.2.3")
@@ -1,7 +0,0 @@
1
- CHANGES
2
- =======
3
-
4
- v1.2.2
5
- ------
6
-
7
- * Fix nova API version
@@ -1 +0,0 @@
1
- {"git_version": "e454aa3", "is_release": false}
python_chi-1.2.2/setup.py DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- from setuptools import setup
4
-
5
- setup(setup_requires=["pbr"], pbr=True, version="v1.2.2")
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
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes