peak-sdk 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

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 (50) hide show
  1. peak/_metadata.py +58 -3
  2. peak/_version.py +1 -1
  3. peak/cli/cli.py +4 -2
  4. peak/cli/helpers.py +2 -0
  5. peak/cli/press/blocks/specs.py +2 -2
  6. peak/cli/press/specs.py +4 -2
  7. peak/cli/resources/alerts/__init__.py +35 -0
  8. peak/cli/resources/alerts/emails.py +360 -0
  9. peak/cli/resources/images.py +1 -1
  10. peak/cli/resources/services.py +23 -0
  11. peak/cli/resources/users.py +71 -0
  12. peak/cli/resources/workflows.py +77 -15
  13. peak/cli/ruff.toml +5 -3
  14. peak/compression.py +2 -2
  15. peak/exceptions.py +4 -6
  16. peak/handler.py +3 -5
  17. peak/helpers.py +35 -9
  18. peak/output.py +2 -2
  19. peak/press/apps.py +8 -16
  20. peak/press/blocks.py +8 -16
  21. peak/press/deployments.py +2 -4
  22. peak/press/specs.py +12 -14
  23. peak/resources/__init__.py +3 -2
  24. peak/resources/alerts.py +309 -0
  25. peak/resources/artifacts.py +2 -4
  26. peak/resources/images.py +8 -14
  27. peak/resources/services.py +7 -6
  28. peak/resources/users.py +93 -0
  29. peak/resources/webapps.py +3 -5
  30. peak/resources/workflows.py +106 -16
  31. peak/sample_yaml/resources/emails/send_email.yaml +15 -0
  32. peak/sample_yaml/resources/services/create_or_update_service.yaml +1 -0
  33. peak/sample_yaml/resources/services/create_service.yaml +1 -0
  34. peak/sample_yaml/resources/services/update_service.yaml +1 -0
  35. peak/sample_yaml/resources/workflows/create_or_update_workflow.yaml +28 -0
  36. peak/sample_yaml/resources/workflows/create_workflow.yaml +10 -0
  37. peak/sample_yaml/resources/workflows/patch_workflow.yaml +28 -0
  38. peak/sample_yaml/resources/workflows/update_workflow.yaml +28 -0
  39. peak/session.py +7 -4
  40. peak/telemetry.py +1 -1
  41. peak/template.py +6 -4
  42. peak/tools/logging/__init__.py +26 -268
  43. peak/tools/logging/log_level.py +35 -3
  44. peak/tools/logging/logger.py +389 -0
  45. peak/validators.py +34 -2
  46. {peak_sdk-1.4.0.dist-info → peak_sdk-1.6.0.dist-info}/METADATA +11 -12
  47. {peak_sdk-1.4.0.dist-info → peak_sdk-1.6.0.dist-info}/RECORD +50 -43
  48. {peak_sdk-1.4.0.dist-info → peak_sdk-1.6.0.dist-info}/WHEEL +1 -1
  49. {peak_sdk-1.4.0.dist-info → peak_sdk-1.6.0.dist-info}/LICENSE +0 -0
  50. {peak_sdk-1.4.0.dist-info → peak_sdk-1.6.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,71 @@
1
+ #
2
+ # # Copyright © 2024 Peak AI Limited. or its affiliates. All Rights Reserved.
3
+ # #
4
+ # # Licensed under the Apache License, Version 2.0 (the "License"). You
5
+ # # may not use this file except in compliance with the License. A copy of
6
+ # # the License is located at:
7
+ # #
8
+ # # https://github.com/PeakBI/peak-sdk/blob/main/LICENSE
9
+ # #
10
+ # # or in the "license" file accompanying this file. This file is
11
+ # # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12
+ # # ANY KIND, either express or implied. See the License for the specific
13
+ # # language governing permissions and limitations under the License.
14
+ # #
15
+ # # This file is part of the peak-sdk.
16
+ # # see (https://github.com/PeakBI/peak-sdk)
17
+ # #
18
+ # # You should have received a copy of the APACHE LICENSE, VERSION 2.0
19
+ # # along with this program. If not, see <https://apache.org/licenses/LICENSE-2.0>
20
+ #
21
+ """Peak Users commands."""
22
+ from typing import Optional
23
+
24
+ import typer
25
+ from peak import Session
26
+ from peak.cli.args import OUTPUT_TYPES, PAGING
27
+ from peak.constants import OutputTypesNoTable
28
+ from peak.output import Writer
29
+ from peak.resources import users
30
+
31
+ app = typer.Typer(
32
+ help="User management and permission checking.",
33
+ short_help="Manage User Permissions.",
34
+ )
35
+
36
+ _FEATURE = typer.Option(..., help="The feature path to check permissions for, e.g., 'PRICING.GUARDRAILS'.")
37
+ _ACTION = typer.Option(..., help="The action to check for the feature path, e.g., 'read' or 'write'.")
38
+ _AUTH_TOKEN = typer.Option(
39
+ None,
40
+ help="Authentication token for the user. If not provided, the token from the environment will be used.",
41
+ )
42
+
43
+
44
+ @app.command(short_help="Check user permission for a specific feature.", options_metavar="check_permission")
45
+ def check_permission(
46
+ ctx: typer.Context,
47
+ feature: str = _FEATURE,
48
+ action: str = _ACTION,
49
+ auth_token: Optional[str] = _AUTH_TOKEN,
50
+ paging: Optional[bool] = PAGING, # noqa: ARG001
51
+ output_type: Optional[OutputTypesNoTable] = OUTPUT_TYPES, # noqa: ARG001
52
+ ) -> None:
53
+ """Check if the user has the specified permission for the given feature path.
54
+
55
+ \b
56
+ 📝 ***Example usage:***
57
+ ```bash
58
+ peak user check-permission --feature "PRICING.GUARDRAILS" --action "read" --auth-token <your-auth-token>
59
+ ```
60
+
61
+ \b
62
+ 🆗 ***Response:***
63
+ True if the user has the permission, False otherwise.
64
+ """
65
+ user_session = Session(auth_token=auth_token)
66
+ user_client = users.get_client(session=user_session)
67
+ writer: Writer = ctx.obj["writer"]
68
+
69
+ with writer.pager():
70
+ response = user_client.check_permissions({feature: action})
71
+ writer.write(response.get(feature, False))
@@ -82,6 +82,16 @@ _CLEAR_IMAGE_CACHE = typer.Option(None, help="Whether to clear image cache on wo
82
82
 
83
83
  _STEP_NAMES = typer.Option(None, help="List of step names to be updated.")
84
84
 
85
+ _EXECUTION_STATUS = typer.Option(
86
+ None,
87
+ help="List of workflow execution statuses. Valid values are `Success`, `Running`, `Stopped`, `Stopping` and `Failed`.",
88
+ )
89
+
90
+ _COUNT = typer.Option(
91
+ None,
92
+ help="Number of workflow executions required in the provided time range or 90 days. If not provided, all executions are returned.",
93
+ )
94
+
85
95
 
86
96
  @app.command(short_help="Create a new workflow.", options_metavar="create_workflow")
87
97
  def create(
@@ -110,12 +120,24 @@ def create(
110
120
  - events (map):
111
121
  success (boolean | required: false): Whether to call event on success.
112
122
  fail (boolean | required: false): Whether to call event on success.
113
- runtimeExceeded (int | required: false): The runtime after which event is called.
114
- user (string | required: false): User to be notified.
115
- webhook (map | required: false):
123
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
124
+ user (string): User to be notified.
125
+ - events (map):
126
+ success (boolean | required: false): Whether to call event on success.
127
+ fail (boolean | required: false): Whether to call event on success.
128
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
129
+ webhook (map):
116
130
  name (string): Name of the webhook.
117
131
  url (string): URL of the webhook.
118
132
  payload (string): Webhook payload.
133
+ - events (map):
134
+ success (boolean | required: false): Whether to call event on success.
135
+ fail (boolean | required: false): Whether to call event on success.
136
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
137
+ email (map):
138
+ name (string): Name of the email watcher.
139
+ recipients (map):
140
+ to (list(str)): List of email addresses to send the email to. Email can be sent only to the users who are added in the tenant.
119
141
  retryOptions (map | required: false): # Workflow level retry options which will be applied to all steps.
120
142
  duration (int | required: false): Duration in seconds after which the step is retried if it fails. Default is 5 seconds and maximum is 120 seconds.
121
143
  exitCodes (list(int) | required: false): List of exit codes for which the step is retried. If not provided, the step is retried for all exit codes.
@@ -222,12 +244,24 @@ def update(
222
244
  - events (map):
223
245
  success (boolean | required: false): Whether to call event on success.
224
246
  fail (boolean | required: false): Whether to call event on success.
225
- runtimeExceeded (int | required: false): The runtime after which event is called.
226
- user (string | required: false): User to be notified.
227
- webhook (map | required: false):
247
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
248
+ user (string): User to be notified.
249
+ - events (map):
250
+ success (boolean | required: false): Whether to call event on success.
251
+ fail (boolean | required: false): Whether to call event on success.
252
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
253
+ webhook (map):
228
254
  name (string): Name of the webhook.
229
255
  url (string): URL of the webhook.
230
256
  payload (string): Webhook payload.
257
+ - events (map):
258
+ success (boolean | required: false): Whether to call event on success.
259
+ fail (boolean | required: false): Whether to call event on success.
260
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
261
+ email (map):
262
+ name (string): Name of the email watcher.
263
+ recipients (map):
264
+ to (list(str)): List of email addresses to send the email to. Email can be sent only to the users who are added in the tenant.
231
265
  retryOptions (map | required: false): # Workflow level retry options which will be applied to all steps.
232
266
  duration (int | required: false): Duration in seconds after which the step is retried if it fails. Default is 5 seconds and maximum is 120 seconds.
233
267
  exitCodes (list(int) | required: false): List of exit codes for which the step is retried. If not provided, the step is retried for all exit codes.
@@ -334,12 +368,24 @@ def create_or_update(
334
368
  - events (map):
335
369
  success (boolean | required: false): Whether to call event on success.
336
370
  fail (boolean | required: false): Whether to call event on success.
337
- runtimeExceeded (int | required: false): The runtime after which event is called.
338
- user (string | required: false): User to be notified.
339
- webhook (map | required: false):
371
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
372
+ user (string): User to be notified.
373
+ - events (map):
374
+ success (boolean | required: false): Whether to call event on success.
375
+ fail (boolean | required: false): Whether to call event on success.
376
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
377
+ webhook (map):
340
378
  name (string): Name of the webhook.
341
379
  url (string): URL of the webhook.
342
380
  payload (string): Webhook payload.
381
+ - events (map):
382
+ success (boolean | required: false): Whether to call event on success.
383
+ fail (boolean | required: false): Whether to call event on success.
384
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
385
+ email (map):
386
+ name (string): Name of the email watcher.
387
+ recipients (map):
388
+ to (list(str)): List of email addresses to send the email to. Email can be sent only to the users who are added in the tenant.
343
389
  retryOptions (map | required: false): # Workflow level retry options which will be applied to all steps.
344
390
  duration (int | required: false): Duration in seconds after which the step is retried if it fails. Default is 5 seconds and maximum is 120 seconds.
345
391
  exitCodes (list(int) | required: false): List of exit codes for which the step is retried. If not provided, the step is retried for all exit codes.
@@ -476,12 +522,24 @@ def patch(
476
522
  - events (map):
477
523
  success (boolean | required: false): Whether to call event on success.
478
524
  fail (boolean | required: false): Whether to call event on success.
479
- runtimeExceeded (int | required: false): The runtime after which event is called.
480
- user (string | required: false): User to be notified.
481
- webhook (map | required: false):
525
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
526
+ user (string): User to be notified.
527
+ - events (map):
528
+ success (boolean | required: false): Whether to call event on success.
529
+ fail (boolean | required: false): Whether to call event on success.
530
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
531
+ webhook (map):
482
532
  name (string): Name of the webhook.
483
533
  url (string): URL of the webhook.
484
534
  payload (string): Webhook payload.
535
+ - events (map):
536
+ success (boolean | required: false): Whether to call event on success.
537
+ fail (boolean | required: false): Whether to call event on success.
538
+ runtimeExceeded (int | required: false): The runtime in minutes after which event is called.
539
+ email (map):
540
+ name (string): Name of the email watcher.
541
+ recipients (map):
542
+ to (list(str)): List of email addresses to send the email to. Email can be sent only to the users who are added in the tenant.
485
543
  retryOptions (map | required: false): # Workflow level retry options which will be applied to all steps.
486
544
  duration (int | required: false): Duration in seconds after which the step is retried if it fails. Default is 5 seconds and maximum is 120 seconds.
487
545
  exitCodes (list(int) | required: false): List of exit codes for which the step is retried. If not provided, the step is retried for all exit codes.
@@ -599,7 +657,7 @@ def list_workflows(
599
657
  \b
600
658
  📝 ***Example usage:***<br/>
601
659
  ```bash
602
- peak workflows list --page-size 10 --page-number 1 --workflow_status "Draft" --last_execution_status "Success" --last_modified_by "abc@peak.ai" --name "test"
660
+ peak workflows list --page-size 10 --page-number 1 --workflow-status "Draft" --last-execution-status "Success" --last-modified-by "abc@peak.ai" --name "test"
603
661
  ```
604
662
 
605
663
  \b
@@ -847,6 +905,8 @@ def list_executions(
847
905
  workflow_id: int = _WORKFLOW_ID,
848
906
  date_from: Optional[str] = args.DATE_FROM,
849
907
  date_to: Optional[str] = args.DATE_TO,
908
+ status: Optional[List[str]] = _EXECUTION_STATUS,
909
+ count: Optional[int] = _COUNT,
850
910
  page_size: Optional[int] = args.PAGE_SIZE,
851
911
  page_number: Optional[int] = args.PAGE_NUMBER,
852
912
  paging: Optional[bool] = PAGING, # noqa: ARG001
@@ -857,7 +917,7 @@ def list_executions(
857
917
  \b
858
918
  📝 ***Example usage:***<br/>
859
919
  ```bash
860
- peak workflows list-executions 9999 --page-size 10 --page-number 1
920
+ peak workflows list-executions 9999 --page-size 10 --page-number 1 --status "Running" --status "Failed"
861
921
  ```
862
922
 
863
923
  \b
@@ -882,6 +942,8 @@ def list_executions(
882
942
  workflow_id=workflow_id,
883
943
  date_from=date_from,
884
944
  date_to=date_to,
945
+ status=parse_list_of_strings(status),
946
+ count=count,
885
947
  page_size=page_size,
886
948
  page_number=page_number,
887
949
  return_iterator=False,
@@ -974,7 +1036,7 @@ def get_execution_logs(
974
1036
  writer.write(response)
975
1037
  break
976
1038
 
977
- next_token = response["nextToken"] if "nextToken" in response else None
1039
+ next_token = response.get("nextToken", None)
978
1040
 
979
1041
 
980
1042
  @app.command(short_help="Get workflow execution details.", options_metavar="get_execution_details")
peak/cli/ruff.toml CHANGED
@@ -1,11 +1,13 @@
1
1
  extend = '../../pyproject.toml'
2
2
  src = ["."]
3
- ignore = [
3
+ lint.ignore = [
4
4
  "UP007", # typer is not PEP-585 and PEP-604 compliant
5
5
  "D208",
6
6
  "FBT001",
7
7
  "FBT002",
8
8
  "FBT003",
9
- "RSE102"
9
+ "RSE102",
10
+ "FA100"
11
+ # TODO: remove this once we are supporting > 3.10 properly
10
12
  ]
11
- unfixable = []
13
+ lint.unfixable = []
peak/compression.py CHANGED
@@ -65,7 +65,7 @@ def compress(path: str, ignore_files: Optional[list[str]] = None) -> Iterator[te
65
65
  raise exceptions.FileLimitExceededException(constants.MAX_ARTIFACT_SIZE_MB)
66
66
 
67
67
  # include directories as API backend need directories explicitly included
68
- relative_root_path = Path(".")
68
+ relative_root_path = Path()
69
69
  if relative_root_path in parent_directories:
70
70
  parent_directories.remove(relative_root_path)
71
71
  for directory in parent_directories:
@@ -192,7 +192,7 @@ def _build_files_dict(files: Iterable[str]) -> dict[str, dict]: # type: ignore[
192
192
  """
193
193
  files_dict: dict[str, dict] = {} # type: ignore[type-arg]
194
194
  for f in files:
195
- components = os.path.normpath(f).split(os.sep)
195
+ components = Path(os.path.normpath(f)).parts
196
196
  current_dir = files_dict
197
197
  for directory in components[:-1]:
198
198
  if directory not in current_dir:
peak/exceptions.py CHANGED
@@ -29,13 +29,11 @@ from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type
29
29
  class PeakBaseException(Exception):
30
30
  """Base exception class for the Peak SDK."""
31
31
 
32
- ...
33
-
34
32
 
35
33
  class HttpExceptionsRegistryMeta(type):
36
34
  """Registry metaclass."""
37
35
 
38
- REGISTRY: Dict[int, Any] = defaultdict(lambda: Exception)
36
+ REGISTRY: ClassVar[Dict[int, Any]] = defaultdict(lambda: Exception)
39
37
 
40
38
  def __new__(
41
39
  cls: "Type[HttpExceptionsRegistryMeta]",
@@ -58,7 +56,7 @@ class HttpExceptionsRegistryMeta(type):
58
56
  HttpExceptionsRegistryMeta: the child class itself, forward annotated for type checking
59
57
  """
60
58
  new_cls: "HttpExceptionsRegistryMeta" = type.__new__(cls, name, bases, attrs)
61
- status_code: Optional[int] = attrs.get("STATUS_CODE", None)
59
+ status_code: Optional[int] = attrs.get("STATUS_CODE")
62
60
  if status_code:
63
61
  cls.REGISTRY[status_code] = new_cls
64
62
  return new_cls
@@ -148,11 +146,11 @@ class MissingEnvironmentVariableException(PeakBaseException):
148
146
  class FileLimitExceededException(PeakBaseException):
149
147
  """Limits on the file are exceeded."""
150
148
 
151
- def __init__(self, max_size: int | float, *, message: str = "", units: str = "MB") -> None:
149
+ def __init__(self, max_size: float, *, message: str = "", units: str = "MB") -> None:
152
150
  """Throw exception with custom message.
153
151
 
154
152
  Args:
155
- max_size (int): Maximum size of the file.
153
+ max_size (float): Maximum size of the file.
156
154
  message (str): Additional message to add to exception.
157
155
  units (str): Units of the maximum size.
158
156
  """
peak/handler.py CHANGED
@@ -51,7 +51,7 @@ writer = Writer(ignore_debug_mode=True)
51
51
  class HandlerRegistryMeta(type):
52
52
  """Metaclass for registering all types of Handler classes."""
53
53
 
54
- REGISTRY: Dict[ContentType, BaseHandler] = {}
54
+ REGISTRY: ClassVar[Dict[ContentType, BaseHandler]] = {}
55
55
 
56
56
  def __new__(
57
57
  cls: "Type[HandlerRegistryMeta]",
@@ -79,7 +79,7 @@ class HandlerRegistryMeta(type):
79
79
  error_invalid_content_type: str = f"Invalid content type for {name} handler"
80
80
  new_cls: "HandlerRegistryMeta" = type.__new__(cls, name, bases, attrs)
81
81
 
82
- content_type: Optional[ContentType] = attrs.get("CONTENT_TYPE", None)
82
+ content_type: Optional[ContentType] = attrs.get("CONTENT_TYPE")
83
83
  try:
84
84
  if content_type and ContentType(content_type):
85
85
  cls.REGISTRY[content_type] = new_cls()
@@ -92,14 +92,12 @@ class HandlerRegistryMeta(type):
92
92
  class _CombinedMeta(HandlerRegistryMeta, ABCMeta):
93
93
  """Utility class for combining multiple meta-classes."""
94
94
 
95
- ...
96
-
97
95
 
98
96
  class AuthRetrySession(requests.Session):
99
97
  """Session with extra sugar attached."""
100
98
 
101
99
  # used in testing, so we can modify the backoff_factor etc. to speed up the tests
102
- _DEFAULT_RETRY_CONFIG: Dict[str, Any] = {
100
+ _DEFAULT_RETRY_CONFIG: ClassVar[Dict[str, Any]] = {
103
101
  "backoff_factor": 2,
104
102
  "total": 5,
105
103
  "status_forcelist": [500, 502, 503, 504],
peak/helpers.py CHANGED
@@ -44,7 +44,7 @@ def parse_body_for_multipart_request(body: Dict[str, Any]) -> Dict[str, str]:
44
44
  Returns:
45
45
  Dict[str, str]: the parsed object
46
46
  """
47
- return {key: (value if type(value) == str else json.dumps(value)) for (key, value) in body.items()}
47
+ return {key: (value if isinstance(value, str) else json.dumps(value)) for (key, value) in body.items()}
48
48
 
49
49
 
50
50
  def remove_keys(body: Dict[str, Any], keys: List[str]) -> Dict[str, Any]:
@@ -76,8 +76,7 @@ def get_base_domain(stage: str, subdomain: Optional[str] = "service") -> str:
76
76
  stage = "dev"
77
77
 
78
78
  domain: str = f"https://{subdomain}.{stage}.peak.ai"
79
- domain = domain.replace("..", ".") # for prod domain
80
- return domain
79
+ return domain.replace("..", ".") # for prod domain
81
80
 
82
81
 
83
82
  def parse_list_of_strings(param: List[str] | None) -> List[str] | None:
@@ -113,7 +112,7 @@ def snake_case_to_lower_camel_case(snake_case_string: str) -> str:
113
112
 
114
113
 
115
114
  def variables_to_dict(*args: Any, frame: FrameType | None = None) -> Dict[str, str]:
116
- """Converts arbitary variables to a dictonary.
115
+ """Converts arbitrary variables to a dictionary.
117
116
 
118
117
  Args:
119
118
  args (str|int): tuple of string|int variables
@@ -143,11 +142,11 @@ def combine_dictionaries(
143
142
  dict2: Dict[str, Any],
144
143
  nested_keys_to_skip: Optional[List[str]] = [], # noqa: B006
145
144
  ) -> Dict[str, Any]:
146
- """Combines two dictonaries. Values for second dictonary have higer precedence.
145
+ """Combines two dictionaries. Values for second dictionary have higher precedence.
147
146
 
148
147
  Args:
149
148
  dict1 (Dict[str, Any]): dictionary 1
150
- dict2 (Dict[str, Any]): dictonary 2
149
+ dict2 (Dict[str, Any]): dictionary 2
151
150
  nested_keys_to_skip (List[str] | None): Keys for which nested combining is not required.
152
151
 
153
152
  Returns:
@@ -158,7 +157,7 @@ def combine_dictionaries(
158
157
 
159
158
  combined_dict = dict(dict1)
160
159
  for key in dict2:
161
- if key in combined_dict and type(combined_dict[key]) is dict and key not in (nested_keys_to_skip or []):
160
+ if key in combined_dict and isinstance(combined_dict[key], dict) and key not in (nested_keys_to_skip or []):
162
161
  combined_dict[key] = combine_dictionaries(combined_dict[key], dict2[key])
163
162
  else:
164
163
  combined_dict[key] = dict2[key]
@@ -178,12 +177,12 @@ def map_user_options(
178
177
  dict_type_keys (List[str]): List of keys which have json type values
179
178
 
180
179
  Returns:
181
- Dict[str, str]: Mappe dictionary
180
+ Dict[str, str]: Mapped dictionary
182
181
  """
183
182
  result: Dict[str, Any] = {}
184
183
  for key in user_options:
185
184
  if key in mapping:
186
- nested_dict = result[mapping[key]] if mapping[key] in result else {}
185
+ nested_dict = result.get(mapping[key], {})
187
186
  nested_dict[key] = json.loads(user_options[key]) if key in dict_type_keys else user_options[key]
188
187
  result[mapping[key]] = nested_dict
189
188
  else:
@@ -239,3 +238,30 @@ def download_logs_helper(
239
238
  if config.SOURCE == Sources.CLI:
240
239
  writer = Writer()
241
240
  writer.write(f"\nLog contents have been saved to: {file_name}")
241
+
242
+
243
+ def search_action(current_path: str, current_dict: Dict[str, Any], action: str) -> Dict[str, bool]:
244
+ """Search for a specified action within a nested dictionary structure and check if deeper levels exist.
245
+
246
+ Args:
247
+ current_path (str): The dot-separated path representing the feature hierarchy.
248
+ current_dict (Dict[str, Any]): The nested dictionary representing the permissions structure.
249
+ action (str): The action to search for (e.g., "read" or "write").
250
+
251
+ Returns:
252
+ bool: A dictionary with keys 'has_permission' indicating if the action is found and
253
+ 'deeper_levels' indicating if there are deeper levels that need specification.
254
+ """
255
+ keys = current_path.split(".")
256
+ for key in keys:
257
+ if key in current_dict:
258
+ current_dict = current_dict[key]
259
+ else:
260
+ return {"has_permission": False, "deeper_levels": False}
261
+
262
+ if "actions" in current_dict:
263
+ actions = current_dict["actions"]
264
+ has_permission = "*" in actions or action in actions or ("write" in actions and action == "read")
265
+ return {"has_permission": has_permission, "deeper_levels": False}
266
+
267
+ return {"has_permission": False, "deeper_levels": True}
peak/output.py CHANGED
@@ -105,7 +105,7 @@ class Writer:
105
105
  title = params["title"]
106
106
 
107
107
  if "subheader_key" in params and params["subheader_key"] in data:
108
- subheader_title = "Total" if "subheader_title" not in params else params["subheader_title"]
108
+ subheader_title = params.get("subheader_title", "Total")
109
109
  title = f'{title} ({subheader_title} = {data[params["subheader_key"]]})'
110
110
 
111
111
  table = Table(
@@ -135,7 +135,7 @@ class Writer:
135
135
  elif "parser" in key_details[1]:
136
136
  v = key_details[1]["parser"](v)
137
137
 
138
- if isinstance(v, (dict, list)): # noqa: UP038
138
+ if isinstance(v, (dict, list)):
139
139
  parsed_value = json_highlighter(Text(json.dumps(v, indent=2), overflow="fold"))
140
140
  else:
141
141
  parsed_value = Text(str(v), overflow="fold")
peak/press/apps.py CHANGED
@@ -48,8 +48,7 @@ class App(BaseClient):
48
48
  page_number: Optional[int] = None,
49
49
  *,
50
50
  return_iterator: Literal[False],
51
- ) -> Dict[str, Any]:
52
- ...
51
+ ) -> Dict[str, Any]: ...
53
52
 
54
53
  @overload
55
54
  def list_specs(
@@ -64,8 +63,7 @@ class App(BaseClient):
64
63
  page_number: Optional[int] = None,
65
64
  *,
66
65
  return_iterator: Literal[True] = True,
67
- ) -> Iterator[Dict[str, Any]]:
68
- ...
66
+ ) -> Iterator[Dict[str, Any]]: ...
69
67
 
70
68
  def list_specs(
71
69
  self,
@@ -440,8 +438,7 @@ class App(BaseClient):
440
438
  page_number: Optional[int] = None,
441
439
  *,
442
440
  return_iterator: Literal[False],
443
- ) -> Dict[str, Any]:
444
- ...
441
+ ) -> Dict[str, Any]: ...
445
442
 
446
443
  @overload
447
444
  def list_spec_releases(
@@ -452,8 +449,7 @@ class App(BaseClient):
452
449
  page_number: Optional[int] = None,
453
450
  *,
454
451
  return_iterator: Literal[True] = True,
455
- ) -> Iterator[Dict[str, Any]]:
456
- ...
452
+ ) -> Iterator[Dict[str, Any]]: ...
457
453
 
458
454
  def list_spec_releases(
459
455
  self,
@@ -595,8 +591,7 @@ class App(BaseClient):
595
591
  page_number: Optional[int] = None,
596
592
  *,
597
593
  return_iterator: Literal[False],
598
- ) -> Dict[str, Any]:
599
- ...
594
+ ) -> Dict[str, Any]: ...
600
595
 
601
596
  @overload
602
597
  def list_deployments(
@@ -608,8 +603,7 @@ class App(BaseClient):
608
603
  page_number: Optional[int] = None,
609
604
  *,
610
605
  return_iterator: Literal[True] = True,
611
- ) -> Iterator[Dict[str, Any]]:
612
- ...
606
+ ) -> Iterator[Dict[str, Any]]: ...
613
607
 
614
608
  def list_deployments(
615
609
  self,
@@ -844,8 +838,7 @@ class App(BaseClient):
844
838
  page_number: Optional[int] = None,
845
839
  *,
846
840
  return_iterator: Literal[False],
847
- ) -> Dict[str, Any]:
848
- ...
841
+ ) -> Dict[str, Any]: ...
849
842
 
850
843
  @overload
851
844
  def list_deployment_revisions(
@@ -857,8 +850,7 @@ class App(BaseClient):
857
850
  page_number: Optional[int] = None,
858
851
  *,
859
852
  return_iterator: Literal[True] = True,
860
- ) -> Iterator[Dict[str, Any]]:
861
- ...
853
+ ) -> Iterator[Dict[str, Any]]: ...
862
854
 
863
855
  def list_deployment_revisions(
864
856
  self,
peak/press/blocks.py CHANGED
@@ -52,8 +52,7 @@ class Block(BaseClient):
52
52
  page_number: Optional[int] = None,
53
53
  *,
54
54
  return_iterator: Literal[False],
55
- ) -> Dict[str, Any]:
56
- ...
55
+ ) -> Dict[str, Any]: ...
57
56
 
58
57
  @overload
59
58
  def list_specs(
@@ -68,8 +67,7 @@ class Block(BaseClient):
68
67
  page_number: Optional[int] = None,
69
68
  *,
70
69
  return_iterator: Literal[True] = True,
71
- ) -> Iterator[Dict[str, Any]]:
72
- ...
70
+ ) -> Iterator[Dict[str, Any]]: ...
73
71
 
74
72
  def list_specs(
75
73
  self,
@@ -582,8 +580,7 @@ class Block(BaseClient):
582
580
  page_number: Optional[int] = None,
583
581
  *,
584
582
  return_iterator: Literal[False],
585
- ) -> Dict[str, Any]:
586
- ...
583
+ ) -> Dict[str, Any]: ...
587
584
 
588
585
  @overload
589
586
  def list_spec_releases(
@@ -594,8 +591,7 @@ class Block(BaseClient):
594
591
  page_number: Optional[int] = None,
595
592
  *,
596
593
  return_iterator: Literal[True] = True,
597
- ) -> Iterator[Dict[str, Any]]:
598
- ...
594
+ ) -> Iterator[Dict[str, Any]]: ...
599
595
 
600
596
  def list_spec_releases(
601
597
  self,
@@ -774,8 +770,7 @@ class Block(BaseClient):
774
770
  page_number: Optional[int] = None,
775
771
  *,
776
772
  return_iterator: Literal[False],
777
- ) -> Dict[str, Any]:
778
- ...
773
+ ) -> Dict[str, Any]: ...
779
774
 
780
775
  @overload
781
776
  def list_deployments(
@@ -788,8 +783,7 @@ class Block(BaseClient):
788
783
  page_number: Optional[int] = None,
789
784
  *,
790
785
  return_iterator: Literal[True] = True,
791
- ) -> Iterator[Dict[str, Any]]:
792
- ...
786
+ ) -> Iterator[Dict[str, Any]]: ...
793
787
 
794
788
  def list_deployments(
795
789
  self,
@@ -1070,8 +1064,7 @@ class Block(BaseClient):
1070
1064
  page_number: Optional[int] = None,
1071
1065
  *,
1072
1066
  return_iterator: Literal[False],
1073
- ) -> Dict[str, Any]:
1074
- ...
1067
+ ) -> Dict[str, Any]: ...
1075
1068
 
1076
1069
  @overload
1077
1070
  def list_deployment_revisions(
@@ -1083,8 +1076,7 @@ class Block(BaseClient):
1083
1076
  page_number: Optional[int] = None,
1084
1077
  *,
1085
1078
  return_iterator: Literal[True] = True,
1086
- ) -> Iterator[Dict[str, Any]]:
1087
- ...
1079
+ ) -> Iterator[Dict[str, Any]]: ...
1088
1080
 
1089
1081
  def list_deployment_revisions(
1090
1082
  self,
peak/press/deployments.py CHANGED
@@ -44,8 +44,7 @@ class Deployment(BaseClient):
44
44
  page_number: Optional[int] = None,
45
45
  *,
46
46
  return_iterator: Literal[False],
47
- ) -> Dict[str, Any]:
48
- ...
47
+ ) -> Dict[str, Any]: ...
49
48
 
50
49
  @overload
51
50
  def list_deployments(
@@ -58,8 +57,7 @@ class Deployment(BaseClient):
58
57
  page_number: Optional[int] = None,
59
58
  *,
60
59
  return_iterator: Literal[True] = True,
61
- ) -> Iterator[Dict[str, Any]]:
62
- ...
60
+ ) -> Iterator[Dict[str, Any]]: ...
63
61
 
64
62
  def list_deployments(
65
63
  self,