python-doniclient 1.0.2__tar.gz → 1.0.4__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 (27) hide show
  1. python_doniclient-1.0.4/PKG-INFO +110 -0
  2. python_doniclient-1.0.4/README.md +90 -0
  3. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/osc/cli.py +193 -57
  4. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/osc/common.py +11 -0
  5. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/v1/resource_fields.py +0 -1
  6. python_doniclient-1.0.4/pyproject.toml +60 -0
  7. python_doniclient-1.0.4/python_doniclient.egg-info/PKG-INFO +110 -0
  8. python_doniclient-1.0.4/python_doniclient.egg-info/SOURCES.txt +16 -0
  9. python_doniclient-1.0.4/python_doniclient.egg-info/dependency_links.txt +1 -0
  10. python_doniclient-1.0.4/python_doniclient.egg-info/entry_points.txt +16 -0
  11. python_doniclient-1.0.4/python_doniclient.egg-info/requires.txt +4 -0
  12. python_doniclient-1.0.4/python_doniclient.egg-info/top_level.txt +1 -0
  13. python_doniclient-1.0.4/setup.cfg +4 -0
  14. python-doniclient-1.0.2/PKG-INFO +0 -33
  15. python-doniclient-1.0.2/README.md +0 -5
  16. python-doniclient-1.0.2/doniclient/tests/__init__.py +0 -0
  17. python-doniclient-1.0.2/doniclient/tests/osc/__init__.py +0 -0
  18. python-doniclient-1.0.2/doniclient/tests/osc/fakes.py +0 -57
  19. python-doniclient-1.0.2/doniclient/tests/osc/test_hardware.py +0 -130
  20. python-doniclient-1.0.2/doniclient/tests/v1/__init__.py +0 -0
  21. python-doniclient-1.0.2/pyproject.toml +0 -71
  22. python-doniclient-1.0.2/setup.py +0 -61
  23. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/__init__.py +0 -0
  24. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/osc/__init__.py +0 -0
  25. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/osc/availability.py +0 -0
  26. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/osc/plugin.py +0 -0
  27. {python-doniclient-1.0.2 → python_doniclient-1.0.4}/doniclient/v1/client.py +0 -0
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-doniclient
3
+ Version: 1.0.4
4
+ Summary: This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project
5
+ Author-email: Chameleon Cloud <contact@chameleoncloud.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/ChameleonCloud/python-doniclient
8
+ Classifier: Environment :: OpenStack
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: osc-lib
17
+ Requires-Dist: python-dateutil
18
+ Requires-Dist: PyYAML>=5.4.1
19
+ Requires-Dist: keystoneauth1
20
+
21
+ # ChameleonCloud Python Doniclient CLI
22
+
23
+ ![ChameleonCloud Logo](https://www.chameleoncloud.org/static/images/logo.png)
24
+
25
+ Welcome to the ChameleonCloud Python Doniclient CLI repository! This command-line interface (CLI) provides a convenient way to interact with ChameleonCloud's Doni hardware inventory project, enabling you to manage and automate various hardware-related tasks.
26
+
27
+ ## Table of Contents
28
+
29
+ - [Installation](#installation)
30
+ - [Authentication](#authentication)
31
+ - [Usage](#usage)
32
+ - [Testing](#testing)
33
+ - [Contributing](#contributing)
34
+
35
+ ## Installation
36
+
37
+ You can install the ChameleonCloud Python Doniclient CLI using pip:
38
+
39
+ ```bash
40
+ pip install python-doniclient
41
+ ```
42
+
43
+ ### Cython sources issue
44
+ if you encounter an error message citing cython sources, please run the following to downgrade cython to a compatible version and use PyYaml 6.0+
45
+ ```pip install "cython<3.0.0"```
46
+
47
+ ## Authentication
48
+ Environment variables are the primary authentication method. Please refer to the [documentation on OpenRC scripts](https://chameleoncloud.readthedocs.io/en/latest/technical/cli.html#the-openstack-rc-script) to learn more about how to download and source your authentication credentials for the CLI;
49
+
50
+ ## Usage
51
+ To use the CLI, you can run the `openstack hardware` command followed by the desired subcommand. `openstack hardware --help` shows all the subcommands available. Here's a basic usage examples:
52
+
53
+ ### List all hardware in the Doni database.
54
+
55
+ ```bash
56
+ openstack hardware list
57
+ ```
58
+
59
+ Options:
60
+
61
+ - --all: List hardware from all owners (requires admin rights).
62
+ - --long: Include all columns in the output.
63
+ - --worker-type <worker_type>: Filter by worker type.
64
+ - --worker-state <worker_state>: Filter by worker state (choices: PENDING, IN_PROGRESS, ERROR, STEADY).
65
+
66
+ For more details on specific commands and their options use --help or -h
67
+
68
+ ### Create hardware in the Doni database
69
+
70
+ ```bash
71
+ openstack hardware create --name <hardware_name> --hardware-type <hardware_type> --property <property_name>=<property_value>
72
+ ```
73
+
74
+ ### List specific hardware item in Doni using name or uuid.
75
+
76
+ ```bash
77
+ openstack hardware get <hardware_uuid>
78
+ ```
79
+
80
+ ```bash
81
+ openstack hardware get <hardware_name>
82
+ ```
83
+
84
+ ### Set properties and name of an existing hardware item.
85
+
86
+ ```bash
87
+ openstack hardware set <hardware_uuid> --name <new_hardware_name> --property <property_name>=<new_property_value>
88
+ ```
89
+
90
+ Sets new name and property for the item `<hardware_uuid>`
91
+
92
+ ```bash
93
+ openstack hardware set <hardware_name> --property <property_name>=<new_property_value>
94
+ ```
95
+
96
+ Sets property for the item `<hardware_name`
97
+
98
+ ### Unset properties of an existing hardware item.
99
+
100
+ ```bash
101
+ openstack hardware unset <hardware_uuid> --property <property_name>
102
+ ```
103
+
104
+ Unsets property `<property_name>` for the item `<hardware_uuid`
105
+
106
+ ```bash
107
+ openstack hardware update <hardware_name> --property <property_name>=<new_property_value>
108
+ ```
109
+
110
+ Unsets property `<property_name>` for the item `<hardware_name>`
@@ -0,0 +1,90 @@
1
+ # ChameleonCloud Python Doniclient CLI
2
+
3
+ ![ChameleonCloud Logo](https://www.chameleoncloud.org/static/images/logo.png)
4
+
5
+ Welcome to the ChameleonCloud Python Doniclient CLI repository! This command-line interface (CLI) provides a convenient way to interact with ChameleonCloud's Doni hardware inventory project, enabling you to manage and automate various hardware-related tasks.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Authentication](#authentication)
11
+ - [Usage](#usage)
12
+ - [Testing](#testing)
13
+ - [Contributing](#contributing)
14
+
15
+ ## Installation
16
+
17
+ You can install the ChameleonCloud Python Doniclient CLI using pip:
18
+
19
+ ```bash
20
+ pip install python-doniclient
21
+ ```
22
+
23
+ ### Cython sources issue
24
+ if you encounter an error message citing cython sources, please run the following to downgrade cython to a compatible version and use PyYaml 6.0+
25
+ ```pip install "cython<3.0.0"```
26
+
27
+ ## Authentication
28
+ Environment variables are the primary authentication method. Please refer to the [documentation on OpenRC scripts](https://chameleoncloud.readthedocs.io/en/latest/technical/cli.html#the-openstack-rc-script) to learn more about how to download and source your authentication credentials for the CLI;
29
+
30
+ ## Usage
31
+ To use the CLI, you can run the `openstack hardware` command followed by the desired subcommand. `openstack hardware --help` shows all the subcommands available. Here's a basic usage examples:
32
+
33
+ ### List all hardware in the Doni database.
34
+
35
+ ```bash
36
+ openstack hardware list
37
+ ```
38
+
39
+ Options:
40
+
41
+ - --all: List hardware from all owners (requires admin rights).
42
+ - --long: Include all columns in the output.
43
+ - --worker-type <worker_type>: Filter by worker type.
44
+ - --worker-state <worker_state>: Filter by worker state (choices: PENDING, IN_PROGRESS, ERROR, STEADY).
45
+
46
+ For more details on specific commands and their options use --help or -h
47
+
48
+ ### Create hardware in the Doni database
49
+
50
+ ```bash
51
+ openstack hardware create --name <hardware_name> --hardware-type <hardware_type> --property <property_name>=<property_value>
52
+ ```
53
+
54
+ ### List specific hardware item in Doni using name or uuid.
55
+
56
+ ```bash
57
+ openstack hardware get <hardware_uuid>
58
+ ```
59
+
60
+ ```bash
61
+ openstack hardware get <hardware_name>
62
+ ```
63
+
64
+ ### Set properties and name of an existing hardware item.
65
+
66
+ ```bash
67
+ openstack hardware set <hardware_uuid> --name <new_hardware_name> --property <property_name>=<new_property_value>
68
+ ```
69
+
70
+ Sets new name and property for the item `<hardware_uuid>`
71
+
72
+ ```bash
73
+ openstack hardware set <hardware_name> --property <property_name>=<new_property_value>
74
+ ```
75
+
76
+ Sets property for the item `<hardware_name`
77
+
78
+ ### Unset properties of an existing hardware item.
79
+
80
+ ```bash
81
+ openstack hardware unset <hardware_uuid> --property <property_name>
82
+ ```
83
+
84
+ Unsets property `<property_name>` for the item `<hardware_uuid`
85
+
86
+ ```bash
87
+ openstack hardware update <hardware_name> --property <property_name>=<new_property_value>
88
+ ```
89
+
90
+ Unsets property `<property_name>` for the item `<hardware_name>`
@@ -13,6 +13,7 @@ from osc_lib.command import command
13
13
  from doniclient.osc.common import (
14
14
  BaseParser,
15
15
  ExpandDotNotation,
16
+ ExpandDotNotationAndStoreTrue,
16
17
  HardwarePatchCommand,
17
18
  conditional_action,
18
19
  )
@@ -40,32 +41,98 @@ class ListHardware(BaseParser, command.Lister):
40
41
  help="Include all columns.",
41
42
  action="store_true",
42
43
  )
44
+ parser.add_argument(
45
+ "--worker-type",
46
+ metavar="<worker_type>",
47
+ help="Filter by worker type",
48
+ required=False,
49
+ default="",
50
+ )
51
+ parser.add_argument(
52
+ "--worker-state",
53
+ help="Filter by worker state",
54
+ choices=["PENDING", "IN_PROGRESS", "ERROR", "STEADY"],
55
+ required=False,
56
+ default="",
57
+ )
43
58
  return parser
44
59
 
60
+ def extract_workers_state(self, workers):
61
+ # Extract worker types and their corresponding states and last errors from workers list
62
+ worker_details = {}
63
+ for worker in workers:
64
+ worker_type = worker.get("worker_type")
65
+ state = worker.get("state")
66
+ last_error = worker.get("state_details", {}).get("last_error")
67
+ if worker_type and (state or last_error):
68
+ if worker_type not in worker_details:
69
+ worker_details[worker_type] = {}
70
+ if state:
71
+ worker_details[worker_type]["state"] = state
72
+ if last_error:
73
+ worker_details[worker_type]["last_error"] = last_error
74
+ return worker_details
75
+
45
76
  def take_action(self, parsed_args):
46
- """List all hw items in Doni."""
77
+ """List all hardware items in Doni."""
78
+
47
79
  hw_client = self.app.client_manager.inventory
48
80
  columns = res_fields.HARDWARE_RESOURCE.fields
49
- labels = res_fields.HARDWARE_RESOURCE.labels
81
+ labels = list(res_fields.HARDWARE_RESOURCE.labels) # Convert tuple to list
50
82
 
51
83
  if parsed_args.long:
52
84
  columns = res_fields.HARDWARE_DETAILED_RESOURCE.fields
53
- labels = res_fields.HARDWARE_DETAILED_RESOURCE.labels
85
+ labels = list(
86
+ res_fields.HARDWARE_DETAILED_RESOURCE.labels
87
+ ) # Convert tuple to list
54
88
 
89
+ # Fetch hardware data based on --all option
55
90
  if parsed_args.all:
56
91
  data = hw_client.export()
57
92
  else:
58
93
  data = hw_client.list()
59
94
 
60
- return (
61
- labels,
62
- (
63
- oscutils.get_dict_properties(
64
- s, columns, formatters={"Properties": oscutils.format_dict}
65
- )
66
- for s in data
67
- ),
68
- )
95
+ worker_types = []
96
+ output_data = []
97
+
98
+ # Extract filter values for better readability
99
+ worker_type_filter = parsed_args.worker_type
100
+ worker_state_filter = parsed_args.worker_state
101
+
102
+ for hardware in data:
103
+ workers = hardware.get("workers", [])
104
+ worker_details = self.extract_workers_state(workers)
105
+
106
+ # Apply worker type and state filters
107
+ if (worker_type_filter and worker_type_filter not in worker_details) or (
108
+ worker_state_filter
109
+ and all(worker_state_filter != details["state"] for details in worker_details.values())
110
+ ):
111
+ continue
112
+
113
+ output_item = oscutils.get_dict_properties(hardware, columns)
114
+
115
+ # Apply combined worker type and state filter
116
+ if worker_type_filter and worker_state_filter:
117
+ details = worker_details.get(worker_type_filter, {})
118
+ if worker_state_filter not in details["state"]:
119
+ continue
120
+
121
+ for worker_type, details in worker_details.items():
122
+ worker_state = details.get("state", "-")
123
+ worker_error = details.get("last_error", "-")
124
+ output_item = output_item + (worker_state,)
125
+ output_item = output_item + (worker_error,)
126
+
127
+ if worker_type not in worker_types:
128
+ worker_types.append(worker_type)
129
+ worker_types.append(worker_type + " last error")
130
+
131
+ output_data.append(output_item)
132
+
133
+ labels += worker_types # Add worker types to labels list
134
+ return labels, output_data
135
+
69
136
 
70
137
 
71
138
  class GetHardware(BaseParser, command.ShowOne):
@@ -93,7 +160,13 @@ class DeleteHardware(BaseParser):
93
160
  def take_action(self, parsed_args):
94
161
  hw_client = self.app.client_manager.inventory
95
162
  try:
96
- hw_client.delete(parsed_args.uuid)
163
+ data = oscutils.find_resource(hw_client, parsed_args.uuid)
164
+ except HttpError as ex:
165
+ LOG.error(ex.response.text)
166
+ raise ex
167
+ uuid = data["uuid"]
168
+ try:
169
+ hw_client.delete(uuid)
97
170
  except HttpError as ex:
98
171
  LOG.error(ex.response.text)
99
172
  raise ex
@@ -107,15 +180,20 @@ class SyncHardware(BaseParser):
107
180
  def take_action(self, parsed_args):
108
181
  hw_client = self.app.client_manager.inventory
109
182
  try:
110
- hw_client.sync(parsed_args.uuid)
183
+ data = oscutils.find_resource(hw_client, parsed_args.uuid)
184
+ except HttpError as ex:
185
+ LOG.error(ex.response.text)
186
+ raise ex
187
+ uuid = data["uuid"]
188
+ try:
189
+ hw_client.sync(uuid)
111
190
  except HttpError as ex:
112
191
  LOG.error(ex.response.text)
113
192
  raise ex
114
193
 
115
194
 
116
- def _add_prop_flag_group(parser, hardware_type, prop_flags):
195
+ def _add_prop_flag_group(parser, hardware_type, prop_flags, prog_name):
117
196
  """Register a mapping of flags for corresponding hardware properties.
118
-
119
197
  Args:
120
198
  parser (argparse.Parser): the parent parser
121
199
  group_name (str): the name of the new argument grouping. This is just used
@@ -124,21 +202,70 @@ def _add_prop_flag_group(parser, hardware_type, prop_flags):
124
202
  containing information about how the flag should be displayed and parsed.
125
203
  Importantly, the property name MUST match some field on the ``properties``
126
204
  dict on the target hardware type.
205
+ prog_name (str): command executed (openstack hardware <set/unset>)
127
206
  """
128
207
  # Only store this flag in the resulting args if the hardware_type is in effect.
129
208
  condition_fn = lambda args: args.hardware_type == hardware_type
130
209
  group = parser.add_argument_group(f"{hardware_type} properties")
131
210
  for prop_name, flag in prop_flags.items():
211
+ argument_params = {}
212
+ argument_params["type"] = flag.type
213
+ argument_params["default"] = flag.default
214
+ argument_params["metavar"] = f"<{flag.flag}>"
215
+ argument_params["dest"] = f"properties.{prop_name}"
216
+ argument_params["action"] = conditional_action(ExpandDotNotation, condition_fn)
217
+ if 'unset' in prog_name:
218
+ argument_params["nargs"] = 0
219
+ argument_params["type"] = None
220
+ argument_params["default"] = None
221
+ argument_params["metavar"] = None
222
+ argument_params["action"] = conditional_action(ExpandDotNotationAndStoreTrue, condition_fn)
132
223
  group.add_argument(
133
224
  f"--{flag.flag}",
134
- type=flag.type,
135
- default=flag.default,
136
- metavar=f"<{flag.flag}>",
137
- dest=f"properties.{prop_name}",
138
- action=conditional_action(ExpandDotNotation, condition_fn),
225
+ **argument_params
139
226
  )
140
227
 
141
228
 
229
+ class HardwarePropertyFlags:
230
+ baremetal_property_flags = {
231
+ "management_address": PropertyFlag("management_address", str, None),
232
+ "ipmi_username": PropertyFlag("ipmi_username", str, None),
233
+ "ipmi_password": PropertyFlag("ipmi_password", str, None),
234
+ "ipmi_terminal_port": PropertyFlag("ipmi_terminal_port", int, None),
235
+ "baremetal_deploy_kernel_image": PropertyFlag(
236
+ "deploy_kernel", str, None
237
+ ),
238
+ "baremetal_deploy_ramdisk_image": PropertyFlag(
239
+ "deploy_ramdisk", str, None
240
+ ),
241
+ # FIXME(jason): why is the flag named ironic_?
242
+ "baremetal_driver": PropertyFlag("ironic_driver", str, None),
243
+ "baremetal_resource_class": PropertyFlag(
244
+ "resource_class", str, "baremetal"
245
+ ),
246
+ "baremetal_capabilities": PropertyFlag(
247
+ "capabilities", json.loads, None
248
+ ),
249
+ "cpu_arch": PropertyFlag("cpu_arch", str, "x86_64"),
250
+ "node_type": PropertyFlag("blazar_node_type", str, None),
251
+ "su_factor": PropertyFlag("blazar_su_factor", float, 1.0),
252
+ "placement": PropertyFlag("placement", json.loads, None),
253
+ "interfaces": PropertyFlag("interfaces", json.loads, {}),
254
+ }
255
+ device_property_flags = {
256
+ "machine_name": PropertyFlag("machine-name", str, None),
257
+ "contact_email": PropertyFlag("contact-email", str, None),
258
+ "channels": PropertyFlag("channels", json.loads, None),
259
+ "application_credential_id": PropertyFlag(
260
+ "application-credential-id", str, None
261
+ ),
262
+ "application_credential_secret": PropertyFlag(
263
+ "application-credential-secret", str, None
264
+ ),
265
+ "local_egress": PropertyFlag("local-egress", str, None),
266
+ }
267
+
268
+
142
269
  class CreateOrUpdateParser(BaseParser):
143
270
  def get_parser(self, prog_name):
144
271
  parser = super().get_parser(prog_name)
@@ -162,47 +289,15 @@ class CreateOrUpdateParser(BaseParser):
162
289
  _add_prop_flag_group(
163
290
  parser,
164
291
  "baremetal",
165
- {
166
- "mgmt_addr": PropertyFlag("mgmt_addr", str, None),
167
- "ipmi_username": PropertyFlag("ipmi_username", str, None),
168
- "ipmi_password": PropertyFlag("ipmi_password", str, None),
169
- "ipmi_terminal_port": PropertyFlag("ipmi_terminal_port", int, None),
170
- "baremetal_deploy_kernel_image": PropertyFlag(
171
- "deploy_kernel", str, None
172
- ),
173
- "baremetal_deploy_ramdisk_image": PropertyFlag(
174
- "deploy_ramdisk", str, None
175
- ),
176
- # FIXME(jason): why is the flag named ironic_?
177
- "baremetal_driver": PropertyFlag("ironic_driver", str, None),
178
- "baremetal_resource_class": PropertyFlag(
179
- "resource_class", str, "baremetal"
180
- ),
181
- "baremetal_capabilities": PropertyFlag(
182
- "capabilities", json.loads, None
183
- ),
184
- "cpu_arch": PropertyFlag("cpu_arch", str, "x86_64"),
185
- "node_type": PropertyFlag("blazar_node_type", str, None),
186
- "su_factor": PropertyFlag("blazar_su_factor", float, 1.0),
187
- "placement": PropertyFlag("placement", json.loads, None),
188
- "interfaces": PropertyFlag("interfaces", json.loads, {}),
189
- },
292
+ HardwarePropertyFlags.baremetal_property_flags,
293
+ prog_name
190
294
  )
191
295
 
192
296
  _add_prop_flag_group(
193
297
  parser,
194
298
  "device",
195
- {
196
- "machine_name": PropertyFlag("machine-name", str, None),
197
- "contact_email": PropertyFlag("contact-email", str, None),
198
- "channels": PropertyFlag("channels", json.loads, None),
199
- "application_credential_id": PropertyFlag(
200
- "application-credential-id", str, None
201
- ),
202
- "application_credential_secret": PropertyFlag(
203
- "application-credential-secret", str, None
204
- ),
205
- },
299
+ HardwarePropertyFlags.device_property_flags,
300
+ prog_name
206
301
  )
207
302
 
208
303
  return parser
@@ -283,6 +378,21 @@ class UpdateHardware(CreateOrUpdateParser, HardwarePatchCommand):
283
378
 
284
379
  return patch
285
380
 
381
+ def take_action(self, parsed_args):
382
+ hw_client = self.app.client_manager.inventory
383
+ try:
384
+ data = oscutils.find_resource(hw_client, parsed_args.uuid)
385
+ except HttpError as ex:
386
+ LOG.error(ex.response.text)
387
+ raise ex
388
+ uuid = data["uuid"]
389
+ patch = self.get_patch(parsed_args)
390
+ try:
391
+ hw_client.update(uuid, patch)
392
+ except HttpError as ex:
393
+ LOG.error(ex.response.text)
394
+ raise ex
395
+
286
396
 
287
397
  class ImportHardware(BaseParser):
288
398
  def get_parser(self, prog_name):
@@ -312,3 +422,29 @@ class ImportHardware(BaseParser):
312
422
  raise ex
313
423
  else:
314
424
  LOG.debug(data)
425
+
426
+
427
+ class UnsetHardware(UpdateHardware):
428
+ """Unset properties of existing hardware item."""
429
+
430
+ needs_uuid = True
431
+
432
+ def get_patch(self, parsed_args):
433
+ patch = []
434
+
435
+ field_map = {
436
+ "name": "name",
437
+ }
438
+
439
+ for key, val in field_map.items():
440
+ arg = getattr(parsed_args, key, None)
441
+ if arg:
442
+ patch.append({"op": "remove", "path": f"/{val}", "value": arg})
443
+
444
+ try:
445
+ for key, val in parsed_args.properties.items():
446
+ patch.append({"op": "remove", "path": f"/properties/{key}", "value": val})
447
+ except AttributeError:
448
+ pass
449
+
450
+ return patch
@@ -63,6 +63,17 @@ class ExpandDotNotation(argparse.Action):
63
63
  getattr(namespace, group).update({dest: values})
64
64
 
65
65
 
66
+ class ExpandDotNotationAndStoreTrue(argparse.Action):
67
+ """Set property based on dest.key."""
68
+
69
+ def __call__(self, parser, namespace, values, option_string=None):
70
+ group, dest = self.dest.split(".", 2)
71
+
72
+ if not hasattr(namespace, group):
73
+ setattr(namespace, group, {})
74
+ getattr(namespace, group).update({dest: True})
75
+
76
+
66
77
  class BaseParser(command.Command):
67
78
  """Base Parser for use with Doni commands.
68
79
 
@@ -55,7 +55,6 @@ HARDWARE_RESOURCE = Resource(
55
55
  [
56
56
  "uuid",
57
57
  "name",
58
- "properties",
59
58
  ]
60
59
  )
61
60
 
@@ -0,0 +1,60 @@
1
+ [project]
2
+ name = "python-doniclient"
3
+ version = "1.0.4"
4
+ description = "This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project"
5
+ authors = [{ name = "Chameleon Cloud", email = "contact@chameleoncloud.org" }]
6
+ requires-python = ">=3.9"
7
+ readme = "README.md"
8
+ license = "Apache-2.0"
9
+ classifiers = [
10
+ "Environment :: OpenStack",
11
+ "Intended Audience :: Information Technology",
12
+ "Intended Audience :: System Administrators",
13
+ "Operating System :: POSIX :: Linux",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.9",
16
+
17
+ ]
18
+ dependencies = [
19
+ "osc-lib",
20
+ "python-dateutil",
21
+ "PyYAML>=5.4.1",
22
+ "keystoneauth1",
23
+ ]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/ChameleonCloud/python-doniclient"
27
+
28
+ [project.entry-points."openstack.cli.extension"]
29
+ inventory = "doniclient.osc.plugin"
30
+
31
+ [project.entry-points."openstack.inventory.v1"]
32
+ hardware_list = "doniclient.osc.cli:ListHardware"
33
+ hardware_import = "doniclient.osc.cli:ImportHardware"
34
+ hardware_show = "doniclient.osc.cli:GetHardware"
35
+ hardware_create = "doniclient.osc.cli:CreateHardware"
36
+ hardware_set = "doniclient.osc.cli:UpdateHardware"
37
+ hardware_unset = "doniclient.osc.cli:UnsetHardware"
38
+ hardware_delete = "doniclient.osc.cli:DeleteHardware"
39
+ hardware_sync = "doniclient.osc.cli:SyncHardware"
40
+ hardware_availability_list = "doniclient.osc.availability:ListHardwareAvailability"
41
+ hardware_availability_set = "doniclient.osc.availability:UpdateHardwareAvailability"
42
+ hardware_availability_add = "doniclient.osc.availability:AddHardwareAvailability"
43
+ hardware_availability_remove = "doniclient.osc.availability:RemoveHardwareAvailability"
44
+
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "oslotest",
49
+ "python-openstackclient",
50
+ "ruff",
51
+ "stestr",
52
+ ]
53
+
54
+ [build-system]
55
+ requires = ["setuptools>=64"]
56
+ build-backend = "setuptools.build_meta"
57
+
58
+ [tool.setuptools.packages.find]
59
+ include = ["doniclient*"]
60
+ exclude = ["doniclient.tests*"]
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-doniclient
3
+ Version: 1.0.4
4
+ Summary: This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project
5
+ Author-email: Chameleon Cloud <contact@chameleoncloud.org>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/ChameleonCloud/python-doniclient
8
+ Classifier: Environment :: OpenStack
9
+ Classifier: Intended Audience :: Information Technology
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: osc-lib
17
+ Requires-Dist: python-dateutil
18
+ Requires-Dist: PyYAML>=5.4.1
19
+ Requires-Dist: keystoneauth1
20
+
21
+ # ChameleonCloud Python Doniclient CLI
22
+
23
+ ![ChameleonCloud Logo](https://www.chameleoncloud.org/static/images/logo.png)
24
+
25
+ Welcome to the ChameleonCloud Python Doniclient CLI repository! This command-line interface (CLI) provides a convenient way to interact with ChameleonCloud's Doni hardware inventory project, enabling you to manage and automate various hardware-related tasks.
26
+
27
+ ## Table of Contents
28
+
29
+ - [Installation](#installation)
30
+ - [Authentication](#authentication)
31
+ - [Usage](#usage)
32
+ - [Testing](#testing)
33
+ - [Contributing](#contributing)
34
+
35
+ ## Installation
36
+
37
+ You can install the ChameleonCloud Python Doniclient CLI using pip:
38
+
39
+ ```bash
40
+ pip install python-doniclient
41
+ ```
42
+
43
+ ### Cython sources issue
44
+ if you encounter an error message citing cython sources, please run the following to downgrade cython to a compatible version and use PyYaml 6.0+
45
+ ```pip install "cython<3.0.0"```
46
+
47
+ ## Authentication
48
+ Environment variables are the primary authentication method. Please refer to the [documentation on OpenRC scripts](https://chameleoncloud.readthedocs.io/en/latest/technical/cli.html#the-openstack-rc-script) to learn more about how to download and source your authentication credentials for the CLI;
49
+
50
+ ## Usage
51
+ To use the CLI, you can run the `openstack hardware` command followed by the desired subcommand. `openstack hardware --help` shows all the subcommands available. Here's a basic usage examples:
52
+
53
+ ### List all hardware in the Doni database.
54
+
55
+ ```bash
56
+ openstack hardware list
57
+ ```
58
+
59
+ Options:
60
+
61
+ - --all: List hardware from all owners (requires admin rights).
62
+ - --long: Include all columns in the output.
63
+ - --worker-type <worker_type>: Filter by worker type.
64
+ - --worker-state <worker_state>: Filter by worker state (choices: PENDING, IN_PROGRESS, ERROR, STEADY).
65
+
66
+ For more details on specific commands and their options use --help or -h
67
+
68
+ ### Create hardware in the Doni database
69
+
70
+ ```bash
71
+ openstack hardware create --name <hardware_name> --hardware-type <hardware_type> --property <property_name>=<property_value>
72
+ ```
73
+
74
+ ### List specific hardware item in Doni using name or uuid.
75
+
76
+ ```bash
77
+ openstack hardware get <hardware_uuid>
78
+ ```
79
+
80
+ ```bash
81
+ openstack hardware get <hardware_name>
82
+ ```
83
+
84
+ ### Set properties and name of an existing hardware item.
85
+
86
+ ```bash
87
+ openstack hardware set <hardware_uuid> --name <new_hardware_name> --property <property_name>=<new_property_value>
88
+ ```
89
+
90
+ Sets new name and property for the item `<hardware_uuid>`
91
+
92
+ ```bash
93
+ openstack hardware set <hardware_name> --property <property_name>=<new_property_value>
94
+ ```
95
+
96
+ Sets property for the item `<hardware_name`
97
+
98
+ ### Unset properties of an existing hardware item.
99
+
100
+ ```bash
101
+ openstack hardware unset <hardware_uuid> --property <property_name>
102
+ ```
103
+
104
+ Unsets property `<property_name>` for the item `<hardware_uuid`
105
+
106
+ ```bash
107
+ openstack hardware update <hardware_name> --property <property_name>=<new_property_value>
108
+ ```
109
+
110
+ Unsets property `<property_name>` for the item `<hardware_name>`
@@ -0,0 +1,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ doniclient/__init__.py
4
+ doniclient/osc/__init__.py
5
+ doniclient/osc/availability.py
6
+ doniclient/osc/cli.py
7
+ doniclient/osc/common.py
8
+ doniclient/osc/plugin.py
9
+ doniclient/v1/client.py
10
+ doniclient/v1/resource_fields.py
11
+ python_doniclient.egg-info/PKG-INFO
12
+ python_doniclient.egg-info/SOURCES.txt
13
+ python_doniclient.egg-info/dependency_links.txt
14
+ python_doniclient.egg-info/entry_points.txt
15
+ python_doniclient.egg-info/requires.txt
16
+ python_doniclient.egg-info/top_level.txt
@@ -0,0 +1,16 @@
1
+ [openstack.cli.extension]
2
+ inventory = doniclient.osc.plugin
3
+
4
+ [openstack.inventory.v1]
5
+ hardware_availability_add = doniclient.osc.availability:AddHardwareAvailability
6
+ hardware_availability_list = doniclient.osc.availability:ListHardwareAvailability
7
+ hardware_availability_remove = doniclient.osc.availability:RemoveHardwareAvailability
8
+ hardware_availability_set = doniclient.osc.availability:UpdateHardwareAvailability
9
+ hardware_create = doniclient.osc.cli:CreateHardware
10
+ hardware_delete = doniclient.osc.cli:DeleteHardware
11
+ hardware_import = doniclient.osc.cli:ImportHardware
12
+ hardware_list = doniclient.osc.cli:ListHardware
13
+ hardware_set = doniclient.osc.cli:UpdateHardware
14
+ hardware_show = doniclient.osc.cli:GetHardware
15
+ hardware_sync = doniclient.osc.cli:SyncHardware
16
+ hardware_unset = doniclient.osc.cli:UnsetHardware
@@ -0,0 +1,4 @@
1
+ osc-lib
2
+ python-dateutil
3
+ PyYAML>=5.4.1
4
+ keystoneauth1
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -1,33 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: python-doniclient
3
- Version: 1.0.2
4
- Summary: This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project
5
- Home-page: https://github.com/ChameleonCloud/python-doniclient
6
- License: Apache-2.0
7
- Author: Chameleon Cloud
8
- Author-email: contact@chameleoncloud.org
9
- Requires-Python: >=3.7,<4.0
10
- Classifier: Environment :: OpenStack
11
- Classifier: Intended Audience :: Information Technology
12
- Classifier: Intended Audience :: System Administrators
13
- Classifier: License :: OSI Approved :: Apache Software License
14
- Classifier: Operating System :: POSIX :: Linux
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
- Classifier: Programming Language :: Python :: 3.7
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
- Requires-Dist: PyYAML (<=5.4.1)
21
- Requires-Dist: keystoneauth1 (<=4.4.0)
22
- Requires-Dist: osc-lib (<=2.4.2)
23
- Requires-Dist: python-dateutil (<=2.8.2)
24
- Requires-Dist: python-openstackclient[cli] (<=5.6.0)
25
- Project-URL: Repository, https://github.com/ChameleonCloud/python-doniclient
26
- Description-Content-Type: text/markdown
27
-
28
- # python-doniclient
29
-
30
- Client for Chameleon Registration Service
31
-
32
-
33
-
@@ -1,5 +0,0 @@
1
- # python-doniclient
2
-
3
- Client for Chameleon Registration Service
4
-
5
-
File without changes
File without changes
@@ -1,57 +0,0 @@
1
- """Define fake objects for Doni CLI tests."""
2
-
3
- import copy
4
- import datetime
5
- import uuid
6
- from unittest import mock
7
-
8
- from openstackclient.tests.unit import fakes, utils
9
-
10
- hardware_uuid = uuid.uuid4().hex
11
- hardware_name = "fake name"
12
- hardware_project_id = uuid.uuid4().hex
13
- hardware_created_at = datetime.datetime.utcnow().isoformat()
14
- hardware_updated_at = datetime.datetime.utcnow().isoformat()
15
- hardware_baremetal_type = "baremetal"
16
-
17
-
18
- class FakeHardware(object):
19
- @staticmethod
20
- def create_one_hardware(attrs=None):
21
- """Create a fake hw item."""
22
- attrs = attrs or {}
23
-
24
- hw_info = {
25
- "created_at": hardware_created_at,
26
- "hardware_type": hardware_baremetal_type,
27
- "name": hardware_name,
28
- "project_id": hardware_project_id,
29
- "properties": {},
30
- "updated_at": hardware_updated_at,
31
- "uuid": hardware_uuid,
32
- "workers": [],
33
- }
34
-
35
- hw_info.update(attrs)
36
- # TODO: client currently returns Dicts, not Resource objects.
37
- # hw_item = fakes.FakeResource(info=copy.deepcopy(hw_info), loaded=True)
38
- return hw_info
39
-
40
-
41
- class FakeHardwareClient(object):
42
- def __init__(self, **kwargs) -> None:
43
- self.hardware = mock.Mock()
44
- self.hardware.resource_class = fakes.FakeResource(None, {})
45
-
46
-
47
- class TestHardware(utils.TestCommand):
48
- def setUp(self):
49
- super(TestHardware, self).setUp()
50
-
51
- self.app.client_manager.auth_ref = mock.Mock(auth_token="TOKEN")
52
- self.app.client_manager.inventory = mock.Mock()
53
-
54
-
55
- class FakeHardwareResource(fakes.FakeResource):
56
- def get_keys(self):
57
- return {"property": "value"}
@@ -1,130 +0,0 @@
1
- import copy
2
- import json
3
- from unittest import mock
4
- import uuid
5
-
6
- from openstackclient.tests.unit import fakes, utils
7
-
8
- from doniclient.osc import cli as hardware_cli
9
- from doniclient.tests.osc import fakes as hardware_fakes
10
-
11
-
12
- FAKE_HARDWARE_UUID = hardware_fakes.hardware_uuid
13
-
14
- class TestHardware(hardware_fakes.TestHardware):
15
- def setUp(self):
16
- super(TestHardware, self).setUp()
17
-
18
- # Get a shortcut to the baremetal manager mock
19
- self.hardware_mock = self.app.client_manager.inventory
20
- self.hardware_mock.reset_mock()
21
-
22
-
23
- class TestHardwareShow(TestHardware):
24
- def setUp(self):
25
- super().setUp()
26
-
27
- self.hardware_mock.get_by_uuid.return_value = (
28
- hardware_fakes.FakeHardware.create_one_hardware()
29
- )
30
-
31
- self.cmd = hardware_cli.GetHardware(self.app, None)
32
-
33
- def test_hardware_show(self):
34
- arglist = [FAKE_HARDWARE_UUID]
35
- verifylist = []
36
-
37
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
38
-
39
- # DisplayCommandBase.take_action() returns two tuples
40
- columns, data = self.cmd.take_action(parsed_args)
41
-
42
- # Set expected values
43
- args = [FAKE_HARDWARE_UUID]
44
-
45
- self.hardware_mock.get_by_uuid.assert_called_with(*args)
46
-
47
- collist = (
48
- "created_at",
49
- "hardware_type",
50
- "name",
51
- "project_id",
52
- "properties",
53
- "updated_at",
54
- "uuid",
55
- "workers",
56
- )
57
- self.assertEqual(collist, columns)
58
-
59
- datalist = (
60
- hardware_fakes.hardware_created_at,
61
- hardware_fakes.hardware_baremetal_type,
62
- hardware_fakes.hardware_name,
63
- hardware_fakes.hardware_project_id,
64
- {},
65
- hardware_fakes.hardware_updated_at,
66
- hardware_fakes.hardware_uuid,
67
- [],
68
- )
69
- self.assertEqual(datalist, tuple(data))
70
-
71
-
72
- class TestHardwareList(TestHardware):
73
- def setUp(self):
74
- super().setUp()
75
-
76
- self.hardware_mock.list.return_value = list(
77
- hardware_fakes.FakeHardware.create_one_hardware()
78
- )
79
-
80
- self.cmd = hardware_cli.ListHardware(self.app, None)
81
-
82
- def test_hardware_list(self):
83
- arglist = []
84
- verifylist = []
85
-
86
- parsed_args = self.check_parser(self.cmd, arglist, verifylist)
87
-
88
- # DisplayCommandBase.take_action() returns two tuples
89
- columns, data = self.cmd.take_action(parsed_args)
90
-
91
- # Set expected values
92
- args = []
93
-
94
- self.hardware_mock.list.assert_called_with(*args)
95
-
96
-
97
- class TestHardwareCreate(TestHardware):
98
- def setUp(self):
99
- super().setUp()
100
- self.cmd = hardware_cli.CreateHardware(self.app, None)
101
-
102
-
103
- class TestHardwareDelete(TestHardware):
104
- def setUp(self):
105
- super().setUp()
106
- self.cmd = hardware_cli.DeleteHardware(self.app, None)
107
-
108
-
109
- class TestHardwareSet(TestHardware):
110
- def setUp(self):
111
- super().setUp()
112
- self.cmd = hardware_cli.UpdateHardware(self.app, None)
113
-
114
- def test_hardware_update(self):
115
- self.hardware_mock.update.return_value = hardware_fakes.FakeHardware.create_one_hardware()
116
-
117
- fake_mgmt_address = "fake-mgmt_addr"
118
- arglist = [FAKE_HARDWARE_UUID, "--mgmt_addr", fake_mgmt_address]
119
- parsed_args = self.check_parser(self.cmd, arglist, [])
120
- assert parsed_args.properties == {"mgmt_addr": fake_mgmt_address}
121
-
122
- self.cmd.take_action(parsed_args)
123
- self.hardware_mock.update.assert_called_with(FAKE_HARDWARE_UUID, [{
124
- "op": "add", "path": "/properties/mgmt_addr", "value": fake_mgmt_address
125
- }])
126
-
127
- class TestHardwareSync(TestHardware):
128
- def setUp(self):
129
- super().setUp()
130
- self.cmd = hardware_cli.SyncHardware(self.app, None)
File without changes
@@ -1,71 +0,0 @@
1
- [tool.black]
2
- # Set line length explicitly.
3
- # See https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length for more details
4
- line-length = 88
5
-
6
- [tool.pytest.ini_options]
7
- log_cli = true
8
- log_cli_level = "INFO"
9
- log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
10
- log_cli_date_format = "%Y-%m-%d %H:%M:%S"
11
- filterwarnings = ["ignore::DeprecationWarning"]
12
-
13
- [tool.isort]
14
- known_first_party = "doniclient"
15
-
16
- [tool.poetry]
17
- name = "python-doniclient"
18
- version = "1.0.2"
19
- description = "This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project"
20
- license = "Apache-2.0"
21
- authors = ["Chameleon Cloud <contact@chameleoncloud.org>"]
22
- readme = "README.md"
23
- repository = "https://github.com/ChameleonCloud/python-doniclient"
24
-
25
- classifiers = [
26
- "Environment :: OpenStack",
27
- "Intended Audience :: Information Technology",
28
- "Intended Audience :: System Administrators",
29
- "Operating System :: POSIX :: Linux",
30
- ]
31
-
32
- packages = [{ include = "doniclient" }]
33
-
34
- [tool.poetry.dependencies]
35
- python = "^3.7"
36
- osc-lib = "<=2.4.2"
37
- python-openstackclient = { version = "<=5.6.0", extras = ["cli"] }
38
- python-dateutil = "<=2.8.2"
39
- PyYAML = "<=5.4.1"
40
- keystoneauth1 = "<=4.4.0"
41
-
42
- [tool.poetry.dev-dependencies]
43
- pytest = "^6.2"
44
- black = { version = "^21", allow-prereleases = true }
45
- flake8 = "^3.9.0"
46
- flake8-bugbear = "^21.4.1"
47
- pytest-mock = "^3.5.1"
48
- isort = "^5.8.0"
49
- pydocstyle = "^6.0.0"
50
- oslotest = "^4.4.1"
51
- stestr = "^3.2.1"
52
-
53
- [build-system]
54
- requires = ["poetry-core>=1.0.0"]
55
- build-backend = "poetry.core.masonry.api"
56
-
57
- [tool.poetry.plugins."openstack.cli.extension"]
58
- inventory = "doniclient.osc.plugin"
59
-
60
- [tool.poetry.plugins."openstack.inventory.v1"]
61
- hardware_list = "doniclient.osc.cli:ListHardware"
62
- hardware_import = "doniclient.osc.cli:ImportHardware"
63
- hardware_show = "doniclient.osc.cli:GetHardware"
64
- hardware_create = "doniclient.osc.cli:CreateHardware"
65
- hardware_set = "doniclient.osc.cli:UpdateHardware"
66
- hardware_delete = "doniclient.osc.cli:DeleteHardware"
67
- hardware_sync = "doniclient.osc.cli:SyncHardware"
68
- hardware_availability_list = "doniclient.osc.availability:ListHardwareAvailability"
69
- hardware_availability_set = "doniclient.osc.availability:UpdateHardwareAvailability"
70
- hardware_availability_add = "doniclient.osc.availability:AddHardwareAvailability"
71
- hardware_availability_remove = "doniclient.osc.availability:RemoveHardwareAvailability"
@@ -1,61 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['doniclient',
6
- 'doniclient.osc',
7
- 'doniclient.tests',
8
- 'doniclient.tests.osc',
9
- 'doniclient.tests.v1',
10
- 'doniclient.v1']
11
-
12
- package_data = \
13
- {'': ['*']}
14
-
15
- install_requires = \
16
- ['PyYAML<=5.4.1',
17
- 'keystoneauth1<=4.4.0',
18
- 'osc-lib<=2.4.2',
19
- 'python-dateutil<=2.8.2',
20
- 'python-openstackclient[cli]<=5.6.0']
21
-
22
- entry_points = \
23
- {'openstack.cli.extension': ['inventory = doniclient.osc.plugin'],
24
- 'openstack.inventory.v1': ['hardware_availability_add = '
25
- 'doniclient.osc.availability:AddHardwareAvailability',
26
- 'hardware_availability_list = '
27
- 'doniclient.osc.availability:ListHardwareAvailability',
28
- 'hardware_availability_remove = '
29
- 'doniclient.osc.availability:RemoveHardwareAvailability',
30
- 'hardware_availability_set = '
31
- 'doniclient.osc.availability:UpdateHardwareAvailability',
32
- 'hardware_create = '
33
- 'doniclient.osc.cli:CreateHardware',
34
- 'hardware_delete = '
35
- 'doniclient.osc.cli:DeleteHardware',
36
- 'hardware_import = '
37
- 'doniclient.osc.cli:ImportHardware',
38
- 'hardware_list = doniclient.osc.cli:ListHardware',
39
- 'hardware_set = doniclient.osc.cli:UpdateHardware',
40
- 'hardware_show = doniclient.osc.cli:GetHardware',
41
- 'hardware_sync = doniclient.osc.cli:SyncHardware']}
42
-
43
- setup_kwargs = {
44
- 'name': 'python-doniclient',
45
- 'version': '1.0.2',
46
- 'description': 'This is a plugin for the openstack commandline client, to enable support for the Doni hardware inventory project',
47
- 'long_description': '# python-doniclient\n\nClient for Chameleon Registration Service\n\n\n',
48
- 'author': 'Chameleon Cloud',
49
- 'author_email': 'contact@chameleoncloud.org',
50
- 'maintainer': None,
51
- 'maintainer_email': None,
52
- 'url': 'https://github.com/ChameleonCloud/python-doniclient',
53
- 'packages': packages,
54
- 'package_data': package_data,
55
- 'install_requires': install_requires,
56
- 'entry_points': entry_points,
57
- 'python_requires': '>=3.7,<4.0',
58
- }
59
-
60
-
61
- setup(**setup_kwargs)