yellowdog-python-examples 7.20.2__tar.gz → 8.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {yellowdog_python_examples-7.20.2/yellowdog_python_examples.egg-info → yellowdog_python_examples-8.0.0}/PKG-INFO +2 -2
  2. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/README.md +25 -10
  3. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/requirements.txt +1 -1
  4. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_list.py +12 -0
  5. yellowdog_python_examples-8.0.0/yellowdog_cli/__init__.py +1 -0
  6. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/create.py +159 -49
  7. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/list.py +14 -7
  8. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/args.py +12 -0
  9. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/entity_utils.py +10 -5
  10. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/load_config.py +44 -34
  11. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/settings.py +4 -0
  12. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0/yellowdog_python_examples.egg-info}/PKG-INFO +2 -2
  13. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_python_examples.egg-info/requires.txt +1 -1
  14. yellowdog_python_examples-7.20.2/yellowdog_cli/__init__.py +0 -1
  15. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/LICENSE +0 -0
  16. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/PYPI_README.md +0 -0
  17. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/pyproject.toml +0 -0
  18. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/setup.cfg +0 -0
  19. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_create_remove.py +0 -0
  20. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_demos.py +0 -0
  21. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_dryruns.py +0 -0
  22. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_entrypoints.py +0 -0
  23. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_gui.py +0 -0
  24. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_objects.py +0 -0
  25. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/tests/test_variable_processing.py +0 -0
  26. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/abort.py +0 -0
  27. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/admin.py +0 -0
  28. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/boost.py +0 -0
  29. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/cancel.py +0 -0
  30. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/cloudwizard.py +0 -0
  31. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/compare.py +0 -0
  32. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/delete.py +0 -0
  33. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/download.py +0 -0
  34. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/follow.py +0 -0
  35. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/format_json.py +0 -0
  36. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/hold.py +0 -0
  37. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/instantiate.py +0 -0
  38. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/jsonnet2json.py +0 -0
  39. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/provision.py +0 -0
  40. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/remove.py +0 -0
  41. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/resize.py +0 -0
  42. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/show.py +0 -0
  43. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/shutdown.py +0 -0
  44. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/start.py +0 -0
  45. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/submit.py +0 -0
  46. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/terminate.py +0 -0
  47. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/upload.py +0 -0
  48. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/__init__.py +0 -0
  49. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/check_imports.py +0 -0
  50. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/cloudwizard_aws.py +0 -0
  51. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/cloudwizard_aws_types.py +0 -0
  52. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/cloudwizard_azure.py +0 -0
  53. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/cloudwizard_common.py +0 -0
  54. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/cloudwizard_gcp.py +0 -0
  55. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/compact_json.py +0 -0
  56. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/config_types.py +0 -0
  57. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/csv_data.py +0 -0
  58. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/follow_utils.py +0 -0
  59. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/interactive.py +0 -0
  60. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/items.py +0 -0
  61. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/load_resources.py +0 -0
  62. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/misc_utils.py +0 -0
  63. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/printing.py +0 -0
  64. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/property_names.py +0 -0
  65. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/provision_utils.py +0 -0
  66. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/rich_console_input_fixed.py +0 -0
  67. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/start_hold_common.py +0 -0
  68. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/submit_utils.py +0 -0
  69. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/type_check.py +0 -0
  70. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/upload_utils.py +0 -0
  71. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/validate_properties.py +0 -0
  72. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/variables.py +0 -0
  73. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/wrapper.py +0 -0
  74. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/utils/ydid_utils.py +0 -0
  75. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_cli/version.py +0 -0
  76. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_python_examples.egg-info/SOURCES.txt +0 -0
  77. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_python_examples.egg-info/dependency_links.txt +0 -0
  78. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_python_examples.egg-info/entry_points.txt +0 -0
  79. {yellowdog_python_examples-7.20.2 → yellowdog_python_examples-8.0.0}/yellowdog_python_examples.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yellowdog-python-examples
3
- Version: 7.20.2
3
+ Version: 8.0.0
4
4
  Summary: Python CLI commands using the YellowDog Python SDK
5
5
  Author-email: YellowDog Limited <support@yellowdog.co>
6
6
  License-Expression: Apache-2.0
@@ -26,7 +26,7 @@ Requires-Dist: requests
26
26
  Requires-Dist: rich==13.9.4
27
27
  Requires-Dist: tabulate>=0.9.0
28
28
  Requires-Dist: toml
29
- Requires-Dist: yellowdog-sdk>=11.2.0
29
+ Requires-Dist: yellowdog-sdk>=11.2.1
30
30
  Provides-Extra: jsonnet
31
31
  Requires-Dist: jsonnet; extra == "jsonnet"
32
32
  Provides-Extra: cloudwizard
@@ -395,7 +395,7 @@ The **environment variables** are as follows:
395
395
  - `YD_NAMESPACE`
396
396
  - `YD_TAG`
397
397
 
398
- When setting the value of the above properties, a property set on the command line takes precedence over one set via an environment variable, and both take precedence over a value set in a configuration file.
398
+ When setting the value of the above properties, a property set on the command line takes precedence over one set via the configuration file, and both take precedence over a value set in an environment variable.
399
399
 
400
400
  If all the required common properties are set using the command line or environment variables, then the entire `common` section of the TOML file can be omitted.
401
401
 
@@ -486,10 +486,10 @@ User-defined variable names must not include spaces, but are otherwise unconstra
486
486
  The precedence order for setting variables is:
487
487
 
488
488
  1. Command line
489
- 2. `YD_VAR` environment variables
490
- 3. General environment variables
491
- 4. Variables in a `.env` file
492
- 5. TOML file
489
+ 2. TOML configuration file
490
+ 3. `YD_VAR` environment variables
491
+ 4. General environment variables
492
+ 5. Variables in a `.env` file
493
493
 
494
494
  This method can also be used to override the default variables, e.g., setting `-v username="other-user"` will override the default `{{username}}` variable.
495
495
 
@@ -2427,16 +2427,31 @@ Namespace Policies are matched by their `namespace` property when using `yd-crea
2427
2427
 
2428
2428
  ## Groups
2429
2429
 
2430
- When creating and updating groups, a list of roles can can be supplied and the group will be created or updated with the roles specified. Roles can be identified by their names or YellowDog IDs.
2430
+ When creating and updating groups, a list of roles with their scopes can can be supplied and the group will be created or updated with the roles specified. Roles can be identified by their names or YellowDog IDs.
2431
2431
 
2432
2432
  Example:
2433
2433
 
2434
2434
  ```json
2435
2435
  {
2436
- "resource": "Group",
2437
- "name": "my-group",
2438
- "description": "Description of my group",
2439
- "roles" : ["work-viewer", "compute-viewer"]
2436
+ "resource": "Group",
2437
+ "name": "my-group",
2438
+ "description": "Description of my group",
2439
+ "roles": [
2440
+ {
2441
+ "role": {"name": "work-viewer"},
2442
+ "scope": {"global": true}
2443
+ },
2444
+ {
2445
+ "role": {"name": "work-manager"},
2446
+ "scope": {
2447
+ "global": false,
2448
+ "namespaces": [
2449
+ {"namespace": "namespace-1"},
2450
+ {"namespace": "namespace-2"}
2451
+ ]
2452
+ }
2453
+ }
2454
+ ]
2440
2455
  }
2441
2456
  ```
2442
2457
 
@@ -5,4 +5,4 @@ requests
5
5
  rich == 13.9.4
6
6
  tabulate >= 0.9.0
7
7
  toml
8
- yellowdog-sdk >= 11.2.0
8
+ yellowdog-sdk >= 11.2.1
@@ -57,3 +57,15 @@ class TestList:
57
57
  def test_allowances(self):
58
58
  result = shell("yd-list -A -n='' -t=''")
59
59
  assert result.exit_code == 0
60
+
61
+ def test_groups(self):
62
+ result = shell("yd-list --groups -n='' -t=''")
63
+ assert result.exit_code == 0
64
+
65
+ def test_roles(self):
66
+ result = shell("yd-list --roles -n='' -t=''")
67
+ assert result.exit_code == 0
68
+
69
+ def test_permissions(self):
70
+ result = shell("yd-list --permissions -n='' -t=''")
71
+ assert result.exit_code == 0
@@ -0,0 +1 @@
1
+ __version__ = "8.0.0"
@@ -5,8 +5,9 @@ A script to create or update YellowDog resources.
5
5
  """
6
6
 
7
7
  from copy import deepcopy
8
+ from dataclasses import dataclass
8
9
  from datetime import datetime
9
- from typing import Dict, List, Optional
10
+ from typing import Dict, List, Optional, Set
10
11
 
11
12
  import yellowdog_client.model as model
12
13
  from dateparser import parse as date_parse
@@ -16,6 +17,7 @@ from yellowdog_client.model import (
16
17
  AccountAllowance,
17
18
  AddApplicationResponse,
18
19
  AddConfiguredWorkerPoolResponse,
20
+ AddGroupRequest,
19
21
  AllowanceLimitEnforcement,
20
22
  AllowanceResetType,
21
23
  ApiKey,
@@ -25,6 +27,7 @@ from yellowdog_client.model import (
25
27
  CloudProvider,
26
28
  CreateNamespaceRequest,
27
29
  Group,
30
+ GroupRole,
28
31
  ImageOsType,
29
32
  InstanceStatus,
30
33
  InternalUser,
@@ -38,6 +41,7 @@ from yellowdog_client.model import (
38
41
  RoleScope,
39
42
  SourceAllowance,
40
43
  SourcesAllowance,
44
+ UpdateGroupRequest,
41
45
  User,
42
46
  )
43
47
  from yellowdog_client.model.exceptions import InvalidRequestException
@@ -78,6 +82,7 @@ from yellowdog_cli.utils.settings import (
78
82
  PROP_DESCRIPTION,
79
83
  PROP_EFFECTIVE_FROM,
80
84
  PROP_EFFECTIVE_UNTIL,
85
+ PROP_GLOBAL,
81
86
  PROP_GROUPS,
82
87
  PROP_ID,
83
88
  PROP_IMAGE,
@@ -86,12 +91,15 @@ from yellowdog_cli.utils.settings import (
86
91
  PROP_KEYRING_NAME,
87
92
  PROP_NAME,
88
93
  PROP_NAMESPACE,
94
+ PROP_NAMESPACES,
89
95
  PROP_OPTIONS,
90
96
  PROP_OS_TYPE,
91
97
  PROP_RANGE,
92
98
  PROP_REQUIREMENT_CREATED_FROM,
93
99
  PROP_RESOURCE,
100
+ PROP_ROLE,
94
101
  PROP_ROLES,
102
+ PROP_SCOPE,
95
103
  PROP_SOURCE,
96
104
  PROP_SOURCE_CREATED_FROM,
97
105
  PROP_SOURCES,
@@ -100,7 +108,6 @@ from yellowdog_cli.utils.settings import (
100
108
  PROP_UNITS,
101
109
  PROP_USERNAME,
102
110
  RN_ADD_APPLICATION_REQUEST,
103
- RN_ADD_GROUP_REQUEST,
104
111
  RN_ALLOWANCE,
105
112
  RN_APPLICATION,
106
113
  RN_CONFIGURED_POOL,
@@ -118,7 +125,6 @@ from yellowdog_cli.utils.settings import (
118
125
  RN_STORAGE_CONFIGURATION,
119
126
  RN_STRING_ATTRIBUTE_DEFINITION,
120
127
  RN_UPDATE_APPLICATION_REQUEST,
121
- RN_UPDATE_GROUP_REQUEST,
122
128
  )
123
129
  from yellowdog_cli.utils.wrapper import ARGS_PARSER, CLIENT, CONFIG_COMMON, main_wrapper
124
130
  from yellowdog_cli.utils.ydid_utils import YDIDType, get_ydid_type
@@ -938,69 +944,171 @@ def create_namespace_policy(resource: Dict):
938
944
  )
939
945
 
940
946
 
947
+ @dataclass
948
+ class RoleSpecification:
949
+ """
950
+ Class to represent a compact expression of a role.
951
+ """
952
+
953
+ id: str
954
+ name: str
955
+ global_: Optional[bool]
956
+ namespaces: Optional[Set[str]]
957
+
958
+
941
959
  def create_group(resource: Dict):
942
960
  """
943
- Create or update a group. Will also add or remove roles specified
944
- by their names or IDs.
961
+ Create or update a group. Will also add or remove scoped
962
+ roles specified by their names or IDs.
945
963
  """
946
964
  try:
947
965
  name = resource[PROP_NAME]
966
+ description = resource.get(PROP_DESCRIPTION)
948
967
  except KeyError as e:
949
968
  raise Exception(f"Expected property to be defined ({e})")
950
969
 
951
- roles: Optional[List[str]] = resource.pop(PROP_ROLES, None)
952
-
953
- if roles is not None:
954
- # Convert role names to IDs
955
- new_role_ids = set()
956
- for role_name in roles:
957
- role_id = get_role_id_by_name(CLIENT, role_name)
958
- if role_id is None:
959
- print_warning(f"Role '{role_name}' not found ... ignoring")
960
- else:
961
- new_role_ids.add(role_id)
962
-
963
- def update_roles(group: Group):
970
+ def get_updated_role_specifications() -> List[RoleSpecification]:
964
971
  """
965
- Helper function to add/remove roles from a group.
972
+ Helper function to generate the list of supplied role specifications.
966
973
  """
967
- if roles is None:
968
- return
969
-
970
- current_role_ids = {role.role.id for role in group.roles}
974
+ roles_input = resource.get(PROP_ROLES)
975
+ if roles_input is None:
976
+ return []
977
+
978
+ role_specifications = []
979
+ for role_item in roles_input:
980
+ # Get the role
981
+ role = role_item.get(PROP_ROLE)
982
+ if role is None:
983
+ raise Exception("Role must have 'role' property")
984
+
985
+ # Get the ID and name of the role
986
+ id_ = role.get(PROP_ID)
987
+ if id_ is None:
988
+ name_ = role.get(PROP_NAME)
989
+ if name_ is None:
990
+ raise Exception("Group role must have 'id' or 'name' specified")
991
+ id_ = get_role_id_by_name(CLIENT, name_)
992
+ else:
993
+ name_ = role.get(PROP_NAME)
994
+ if name_ is None:
995
+ name_ = get_role_name_by_id(CLIENT, id_)
996
+
997
+ # Get the scope of the role
998
+ scope = role_item.get(PROP_SCOPE)
999
+ if scope is None:
1000
+ raise Exception(f"Group role '{name_}' must have 'scope' specified")
1001
+ global_ = scope.get(PROP_GLOBAL)
1002
+ if global_ is None or global_ is False:
1003
+ namespaces_ = scope.get(PROP_NAMESPACES)
1004
+ if namespaces_ is None:
1005
+ raise Exception(
1006
+ f"Non-global group role '{name_}' must have 'namespaces' specified"
1007
+ )
1008
+ namespace_names = []
1009
+ for namespace_ in namespaces_:
1010
+ namespace_name = namespace_.get(PROP_NAMESPACE)
1011
+ if namespace_name is None:
1012
+ raise Exception(
1013
+ f"Namespace applied to role '{name_}' "
1014
+ "must have 'namespace' property"
1015
+ )
1016
+ namespace_names.append(namespace_name)
1017
+
1018
+ # Construct the role specification & add to the list
1019
+ if len(namespace_names) == 0:
1020
+ raise Exception(
1021
+ f"Non-global role '{name_}' must have at least one namespace scope"
1022
+ )
1023
+ role_specifications.append(
1024
+ RoleSpecification(
1025
+ id=id_,
1026
+ name=name_,
1027
+ global_=False,
1028
+ namespaces=set(namespace_names),
1029
+ )
1030
+ )
1031
+ else:
1032
+ role_specifications.append(
1033
+ RoleSpecification(id=id_, name=name_, global_=True, namespaces=None)
1034
+ )
971
1035
 
972
- if current_role_ids == new_role_ids:
973
- print_log("No Role additions or deletions required")
974
- return
1036
+ return role_specifications
975
1037
 
976
- role_ids_to_remove = current_role_ids - new_role_ids
977
- for role_id in role_ids_to_remove:
978
- CLIENT.account_client.remove_role_from_group(group.id, role_id)
979
- print_log(
980
- f"Removed Role '{get_role_name_by_id(CLIENT, role_id)}' "
981
- f"from Group ({role_id})"
1038
+ def add_or_update_roles(role_specifications: List[RoleSpecification]):
1039
+ """
1040
+ Helper function to add/update a list of roles.
1041
+ """
1042
+ for role_spec in role_specifications:
1043
+ CLIENT.account_client.add_role_to_group(
1044
+ group_id,
1045
+ role_spec.id,
1046
+ RoleScope(role_spec.global_, role_spec.namespaces),
982
1047
  )
1048
+ if role_spec.global_:
1049
+ print_log(f"Added/updated role '{role_spec.name}' with global scope")
1050
+ else:
1051
+ ns_list_quoted = [f"'{ns}'" for ns in role_spec.namespaces]
1052
+ print_log(
1053
+ f"Added/updated role '{role_spec.name}' scoped to "
1054
+ f"namespace(s): {', '.join(ns_list_quoted)}"
1055
+ )
983
1056
 
984
- role_ids_to_add = new_role_ids - current_role_ids
985
- for role_id in role_ids_to_add:
986
- CLIENT.account_client.add_role_to_group(
987
- group.id, role_id, RoleScope(global_=True)
1057
+ def remove_roles(role_specifications: List[RoleSpecification]):
1058
+ """
1059
+ Helper function to remove a list of roles.
1060
+ """
1061
+ for role_spec in role_specifications:
1062
+ CLIENT.account_client.remove_role_from_group(
1063
+ group_id,
1064
+ role_spec.id,
988
1065
  )
989
- print_log(
990
- f"Added Role '{get_role_name_by_id(CLIENT, role_id)}' "
991
- f"to Group ({role_id})"
1066
+ if role_spec.global_:
1067
+ print_log(f"Removed role '{role_spec.name}' with global scope")
1068
+ else:
1069
+ ns_list_quoted = [f"'{ns}'" for ns in role_spec.namespaces]
1070
+ print_log(
1071
+ f"Removed role '{role_spec.name}' scoped to "
1072
+ f"namespace(s): {', '.join(ns_list_quoted)}"
1073
+ )
1074
+
1075
+ def get_roles_to_remove(
1076
+ existing_roles: List[GroupRole], new_roles: List[RoleSpecification]
1077
+ ) -> List[RoleSpecification]:
1078
+ """
1079
+ Helper function to determine the roles to be removed.
1080
+ """
1081
+ existing_role_specifications = [
1082
+ RoleSpecification(
1083
+ id=role.role.id,
1084
+ name=role.role.name,
1085
+ global_=role.scope.global_,
1086
+ namespaces=(
1087
+ None
1088
+ if role.scope.namespaces is None
1089
+ else set([ns.namespace for ns in role.scope.namespaces])
1090
+ ),
992
1091
  )
1092
+ for role in existing_roles
1093
+ ]
1094
+ # Select roles to remove
1095
+ return [
1096
+ role_spec
1097
+ for role_spec in existing_role_specifications
1098
+ if role_spec.name not in [role_spec.name for role_spec in new_roles]
1099
+ ]
993
1100
 
994
- def add_group():
1101
+ def add_group() -> str:
995
1102
  """
996
- Helper function to add a new group and its roles.
1103
+ Helper function to add a new group.
1104
+ Return the ID of the newly created group.
997
1105
  """
998
1106
  group: Group = CLIENT.account_client.add_group(
999
- _get_model_object(RN_ADD_GROUP_REQUEST, resource)
1107
+ AddGroupRequest(name=name, description=description)
1000
1108
  )
1001
1109
  print_log(f"Created Group '{group.name}' ({group.id})")
1002
1110
  clear_group_caches()
1003
- update_roles(group)
1111
+ return group.id
1004
1112
 
1005
1113
  def update_group(group_id: str):
1006
1114
  """
@@ -1009,18 +1117,20 @@ def create_group(resource: Dict):
1009
1117
  """
1010
1118
  if not confirmed(f"Update Group '{name}' ({group_id})?"):
1011
1119
  return
1012
-
1013
1120
  group: Group = CLIENT.account_client.update_group(
1014
- group_id, _get_model_object(RN_UPDATE_GROUP_REQUEST, resource)
1121
+ group_id, UpdateGroupRequest(name=name, description=description)
1015
1122
  )
1016
1123
  print_log(f"Updated Group '{group.name}' ({group.id})")
1017
- update_roles(group)
1124
+ updated_role_specs = get_updated_role_specifications()
1125
+ remove_roles(get_roles_to_remove(group.roles, updated_role_specs))
1126
+ add_or_update_roles(updated_role_specs)
1018
1127
 
1019
1128
  # Main logic
1020
1129
  group_id = get_group_id_by_name(CLIENT, name)
1021
- if group_id is None:
1022
- add_group()
1023
- else:
1130
+ if group_id is None: # New group
1131
+ group_id = add_group()
1132
+ add_or_update_roles(get_updated_role_specifications())
1133
+ else: # Existing group
1024
1134
  update_group(group_id)
1025
1135
 
1026
1136
 
@@ -62,6 +62,7 @@ from yellowdog_cli.utils.entity_utils import (
62
62
  get_user_groups,
63
63
  get_worker_pools,
64
64
  list_matching_object_paths,
65
+ substitute_id_for_name_in_allowance,
65
66
  substitute_ids_for_names_in_crt,
66
67
  substitute_image_family_id_for_name_in_cst,
67
68
  )
@@ -807,14 +808,26 @@ def list_allowances():
807
808
  print_log("No Allowances to display")
808
809
  return
809
810
 
811
+ if ARGS_PARSER.ids_only:
812
+ for allowance in allowances:
813
+ print(allowance.id)
814
+ return
815
+
810
816
  if not ARGS_PARSER.details:
811
817
  print_numbered_object_list(CLIENT, allowances)
812
818
  return
813
819
 
814
820
  # Show details
821
+ if len(allowances) > 0 and ARGS_PARSER.substitute_ids:
822
+ print_log(
823
+ "Substituting Compute Requirement Template IDs with names (if applicable)"
824
+ )
815
825
  print_yd_object_list(
816
826
  [
817
- (allowance, {PROP_RESOURCE: RN_ALLOWANCE})
827
+ (
828
+ substitute_id_for_name_in_allowance(CLIENT, allowance),
829
+ {PROP_RESOURCE: RN_ALLOWANCE},
830
+ )
818
831
  for allowance in select(CLIENT, allowances)
819
832
  ]
820
833
  )
@@ -1010,12 +1023,6 @@ def list_groups():
1010
1023
 
1011
1024
  selected_groups = select(CLIENT, groups)
1012
1025
 
1013
- # If stripping IDs, just supply the list of role names;
1014
- # subverts the type
1015
- if ARGS_PARSER.strip_ids:
1016
- for group in selected_groups:
1017
- group.roles = [group_role.role.name for group_role in group.roles]
1018
-
1019
1026
  print_yd_object_list(
1020
1027
  [(group, {PROP_RESOURCE: RN_GROUP}) for group in selected_groups]
1021
1028
  )
@@ -127,6 +127,13 @@ class CLIParser:
127
127
  required=False,
128
128
  help="include the process ID of this CLI invocation alongside timestamp in logging messages",
129
129
  )
130
+ parser.add_argument(
131
+ "--no-config",
132
+ "--nc",
133
+ action="store_true",
134
+ required=False,
135
+ help="ignore the contents of any TOML configuration file (even if specified on the command line)",
136
+ )
130
137
 
131
138
  # Module-specific argument sets
132
139
  if not any(module in sys.argv[0] for module in ["compare"]):
@@ -1287,6 +1294,11 @@ class CLIParser:
1287
1294
  def print_pid(self) -> Optional[bool]:
1288
1295
  return self.args.print_pid
1289
1296
 
1297
+ @property
1298
+ @allow_missing_attribute
1299
+ def no_config(self) -> Optional[bool]:
1300
+ return self.args.no_config
1301
+
1290
1302
  @property
1291
1303
  @allow_missing_attribute
1292
1304
  def work_req_file(self) -> Optional[str]:
@@ -8,6 +8,7 @@ from typing import Callable, List, Optional, Tuple, Union
8
8
  from yellowdog_client import PlatformClient
9
9
  from yellowdog_client.common import SearchClient
10
10
  from yellowdog_client.model import (
11
+ AccountAllowance,
11
12
  AllowanceSearch,
12
13
  Application,
13
14
  ApplicationSearch,
@@ -635,11 +636,17 @@ def substitute_image_family_id_for_name_in_cst(
635
636
 
636
637
  def substitute_id_for_name_in_allowance(
637
638
  client: PlatformClient,
638
- allowance: Union[RequirementsAllowance, SourcesAllowance, SourceAllowance],
639
- ) -> Union[RequirementsAllowance, SourcesAllowance, SourceAllowance]:
639
+ allowance: Union[
640
+ AccountAllowance, RequirementsAllowance, SourcesAllowance, SourceAllowance
641
+ ],
642
+ ) -> Union[AccountAllowance, RequirementsAllowance, SourcesAllowance, SourceAllowance]:
640
643
  """
641
644
  Substitute IDs in Allowance objects.
642
645
  """
646
+
647
+ if not ARGS_PARSER.substitute_ids:
648
+ return allowance
649
+
643
650
  if isinstance(allowance, RequirementsAllowance):
644
651
  allowance.requirementCreatedFromId = _get_requirement_template_name_from_id(
645
652
  client, allowance.requirementCreatedFromId
@@ -650,9 +657,7 @@ def substitute_id_for_name_in_allowance(
650
657
  client, allowance.sourceCreatedFromId
651
658
  )
652
659
 
653
- elif isinstance(allowance, SourceAllowance):
654
- # No processing for source allowances
655
- pass
660
+ # No processing for other allowance types
656
661
 
657
662
  return allowance
658
663
 
@@ -59,7 +59,6 @@ for norm, alt in [
59
59
  ]:
60
60
  if os.getenv(norm) is None and os.getenv(alt) is not None:
61
61
  os.environ[norm] = os.getenv(alt)
62
- print_log(f"Setting environment variable '{norm}' using value of '{alt}'")
63
62
 
64
63
  # CLI > YD_CONF > 'config.toml'
65
64
  CONFIG_FILE = relpath(
@@ -68,42 +67,50 @@ CONFIG_FILE = relpath(
68
67
  else ARGS_PARSER.config_file
69
68
  )
70
69
 
71
- try:
72
- CONFIG_FILE_DIR = dirname(CONFIG_FILE)
73
- config_dir_abs = abspath(CONFIG_FILE_DIR)
74
- config_dir_short = Path(config_dir_abs).parts[-1]
75
- VARIABLE_SUBSTITUTIONS.update(
76
- {"config_dir_abs": config_dir_abs, "config_dir_name": config_dir_short}
77
- )
78
- print_log(f"Loading configuration data from: '{CONFIG_FILE}'")
79
- CONFIG_TOML: Dict = load_toml_file_with_variable_substitutions(CONFIG_FILE)
70
+ if ARGS_PARSER.no_config:
71
+ # Suppress use of any TOML config file
72
+ print_log(f"Configuration file ('{CONFIG_FILE}') ignored")
73
+ CONFIG_TOML = {COMMON_SECTION: {}}
74
+ CONFIG_FILE_DIR = os.getcwd()
75
+
76
+ else:
77
+ # Attempt to load configuration data from TOML file
80
78
  try:
81
- validate_properties(CONFIG_TOML, f"'{CONFIG_FILE}'")
82
- except Exception as e:
83
- print_error(e)
79
+ CONFIG_FILE_DIR = dirname(CONFIG_FILE)
80
+ config_dir_abs = abspath(CONFIG_FILE_DIR)
81
+ config_dir_short = Path(config_dir_abs).parts[-1]
82
+ VARIABLE_SUBSTITUTIONS.update(
83
+ {"config_dir_abs": config_dir_abs, "config_dir_name": config_dir_short}
84
+ )
85
+ print_log(f"Loading configuration data from: '{CONFIG_FILE}'")
86
+ CONFIG_TOML: Dict = load_toml_file_with_variable_substitutions(CONFIG_FILE)
87
+ try:
88
+ validate_properties(CONFIG_TOML, f"'{CONFIG_FILE}'")
89
+ except Exception as e:
90
+ print_error(e)
91
+ exit(1)
92
+
93
+ except FileNotFoundError as e:
94
+ if ARGS_PARSER.config_file is not None:
95
+ print_error(e)
96
+ exit(1)
97
+ # No config file, so create a stub config dictionary
98
+ print_log(
99
+ "No configuration file; expecting configuration data on command line "
100
+ "or in environment variables"
101
+ )
102
+ CONFIG_TOML = {COMMON_SECTION: {}}
103
+ CONFIG_FILE_DIR = os.getcwd()
104
+
105
+ except (PermissionError, TomlDecodeError) as e:
106
+ print_error(
107
+ f"Unable to load configuration data from '{CONFIG_FILE}': {e}",
108
+ )
84
109
  exit(1)
85
110
 
86
- except FileNotFoundError as e:
87
- if ARGS_PARSER.config_file is not None:
111
+ except Exception as e:
88
112
  print_error(e)
89
113
  exit(1)
90
- # No config file, so create a stub config dictionary
91
- print_log(
92
- "No configuration file; expecting configuration data on command line "
93
- "or in environment variables"
94
- )
95
- CONFIG_TOML = {COMMON_SECTION: {}}
96
- CONFIG_FILE_DIR = os.getcwd()
97
-
98
- except (PermissionError, TomlDecodeError) as e:
99
- print_error(
100
- f"Unable to load configuration data from '{CONFIG_FILE}': {e}",
101
- )
102
- exit(1)
103
-
104
- except Exception as e:
105
- print_error(e)
106
- exit(1)
107
114
 
108
115
 
109
116
  def load_config_common() -> ConfigCommon:
@@ -123,7 +130,7 @@ def load_config_common() -> ConfigCommon:
123
130
 
124
131
  # Replace common section properties with command line or
125
132
  # environment variable overrides. Precedence is:
126
- # command line > environment variable > config file
133
+ # command line > config file > environment variable
127
134
  for key_name, args_parser_value, env_var_name in [
128
135
  (KEY, ARGS_PARSER.key, YD_KEY),
129
136
  (SECRET, ARGS_PARSER.secret, YD_SECRET),
@@ -137,7 +144,10 @@ def load_config_common() -> ConfigCommon:
137
144
  f"Using '{key_name}' provided on command line "
138
145
  "(or automatically set)"
139
146
  )
140
- elif os.environ.get(env_var_name, None) is not None:
147
+ elif (
148
+ common_section.get(key_name, None) is None
149
+ and os.environ.get(env_var_name, None) is not None
150
+ ):
141
151
  common_section[key_name] = os.environ[env_var_name]
142
152
  print_log(f"Using '{key_name}' provided via the environment")
143
153
 
@@ -154,6 +154,7 @@ PROP_DELETABLE = "deletable"
154
154
  PROP_DESCRIPTION = "description"
155
155
  PROP_EFFECTIVE_FROM = "effectiveFrom"
156
156
  PROP_EFFECTIVE_UNTIL = "effectiveUntil"
157
+ PROP_GLOBAL = "global"
157
158
  PROP_GROUPS = "groups"
158
159
  PROP_ID = "id"
159
160
  PROP_IMAGE = "image"
@@ -164,13 +165,16 @@ PROP_KEYRING = "keyring"
164
165
  PROP_KEYRING_NAME = "keyringName"
165
166
  PROP_NAME = "name"
166
167
  PROP_NAMESPACE = "namespace"
168
+ PROP_NAMESPACES = "namespaces"
167
169
  PROP_OPTIONS = "options"
168
170
  PROP_OS_TYPE = "osType"
169
171
  PROP_RANGE = "range"
170
172
  PROP_REMAINING_HOURS = "remainingHours"
171
173
  PROP_REQUIREMENT_CREATED_FROM = "requirementCreatedFromId"
172
174
  PROP_RESOURCE = "resource"
175
+ PROP_ROLE = "role"
173
176
  PROP_ROLES = "roles"
177
+ PROP_SCOPE = "scope"
174
178
  PROP_SOURCE = "source"
175
179
  PROP_SOURCES = "sources"
176
180
  PROP_SOURCE_CREATED_FROM = "sourceCreatedFromId"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yellowdog-python-examples
3
- Version: 7.20.2
3
+ Version: 8.0.0
4
4
  Summary: Python CLI commands using the YellowDog Python SDK
5
5
  Author-email: YellowDog Limited <support@yellowdog.co>
6
6
  License-Expression: Apache-2.0
@@ -26,7 +26,7 @@ Requires-Dist: requests
26
26
  Requires-Dist: rich==13.9.4
27
27
  Requires-Dist: tabulate>=0.9.0
28
28
  Requires-Dist: toml
29
- Requires-Dist: yellowdog-sdk>=11.2.0
29
+ Requires-Dist: yellowdog-sdk>=11.2.1
30
30
  Provides-Extra: jsonnet
31
31
  Requires-Dist: jsonnet; extra == "jsonnet"
32
32
  Provides-Extra: cloudwizard
@@ -5,7 +5,7 @@ requests
5
5
  rich==13.9.4
6
6
  tabulate>=0.9.0
7
7
  toml
8
- yellowdog-sdk>=11.2.0
8
+ yellowdog-sdk>=11.2.1
9
9
 
10
10
  [cloudwizard]
11
11
  boto3
@@ -1 +0,0 @@
1
- __version__ = "7.20.2"