clear-skies 2.0.15__py3-none-any.whl → 2.0.16__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.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- {clear_skies-2.0.15.dist-info → clear_skies-2.0.16.dist-info}/METADATA +1 -1
- {clear_skies-2.0.15.dist-info → clear_skies-2.0.16.dist-info}/RECORD +8 -7
- clearskies/backends/api_backend.py +31 -19
- clearskies/columns/belongs_to_model.py +1 -1
- clearskies/functional/json.py +47 -0
- clearskies/secrets/akeyless.py +4 -24
- {clear_skies-2.0.15.dist-info → clear_skies-2.0.16.dist-info}/WHEEL +0 -0
- {clear_skies-2.0.15.dist-info → clear_skies-2.0.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -62,7 +62,7 @@ clearskies/autodoc/schema/password.py,sha256=hSXPvHoXEbG-KHQFxXubEarxSMg9Cki_n8t
|
|
|
62
62
|
clearskies/autodoc/schema/schema.py,sha256=zsJFtEyUCGGokYsnpcIfRVbeEwVTOWqE6VjTYyv8JZM,242
|
|
63
63
|
clearskies/autodoc/schema/string.py,sha256=GXRI-aU8PYmjfnhmfaOe4vq7JASb-aB4SiFeG-EmJY0,60
|
|
64
64
|
clearskies/backends/__init__.py,sha256=krBB3gHLP-lVYkm5ksdK7teEtouAHhL3EC__WGVnF4A,3157
|
|
65
|
-
clearskies/backends/api_backend.py,sha256=
|
|
65
|
+
clearskies/backends/api_backend.py,sha256=yOiPgLV4M4zuAYK3IikZlaCmJb0wUlzjwdo1zTQ24I4,54665
|
|
66
66
|
clearskies/backends/backend.py,sha256=XzFBsUP4lwX2c2_zeK4Hch1MlT3YJS1ffxolUHoJXqA,5797
|
|
67
67
|
clearskies/backends/cursor_backend.py,sha256=5YBO0EwxU_1YKIL3_fIbA93sh7NG4Qg8jpAMoIA5qiU,14139
|
|
68
68
|
clearskies/backends/memory_backend.py,sha256=H_2fib9EFdXsg3ZMMpC20DMHnrnYDbPgZnmiarbOhiM,33963
|
|
@@ -70,7 +70,7 @@ clearskies/backends/secrets_backend.py,sha256=f_C5oAAr3tgM4UqioYPWcASPe9DhySC_uL
|
|
|
70
70
|
clearskies/columns/__init__.py,sha256=a__-1hv-aMV3RU0Sd-BEkfItSJaG51EF2VeRQoTdka8,1990
|
|
71
71
|
clearskies/columns/audit.py,sha256=KABjKk3YdV6G_43OdkhFA_iGkmjP9Sb-m0Beh2_aXU4,7624
|
|
72
72
|
clearskies/columns/belongs_to_id.py,sha256=Ga3IpWlMy2_m8e5OK_iortNjHdMbF1RQF2tglWAzBVc,17575
|
|
73
|
-
clearskies/columns/belongs_to_model.py,sha256=
|
|
73
|
+
clearskies/columns/belongs_to_model.py,sha256=6DonRb1JSAlCjHnaSnZ92K9r0WrNNWhUIH_mN1tcRCE,5708
|
|
74
74
|
clearskies/columns/belongs_to_self.py,sha256=cmgjxq4SmokGO2UbE0VlcFRXack9KjPLzyE9L3fNang,3725
|
|
75
75
|
clearskies/columns/boolean.py,sha256=pdkO7dxqIYCHX95C_yGo5vyvH_D0fO8zs0XgRN50zMQ,3713
|
|
76
76
|
clearskies/columns/category_tree.py,sha256=hqMlYQjVRlsU0PsC_M3_WWKkkzqw8wM5Xo-D4Mp9tIc,10967
|
|
@@ -203,6 +203,7 @@ clearskies/exceptions/moved_permanently.py,sha256=fcgU_VBtAe8ZnbyNoNpXDcTQ8Utsjd
|
|
|
203
203
|
clearskies/exceptions/moved_temporarily.py,sha256=Pt3muYHASvgOC50wPmoul9hUfy3Ud_NPSGFxshNWbIk,110
|
|
204
204
|
clearskies/exceptions/not_found.py,sha256=_lZwovDrd18dUHDop5pF4mhexBPNr126xF2gOLA2-EA,36
|
|
205
205
|
clearskies/functional/__init__.py,sha256=yXnbX-pjW6MOaBTjIzogvSJZ6O9dbUAWOalhu0WSDiQ,106
|
|
206
|
+
clearskies/functional/json.py,sha256=gKyLl_DNfSv1XddTQH6HtdOjqrhUXaRs_uImTh0Lei0,1594
|
|
206
207
|
clearskies/functional/routing.py,sha256=tfIvP_Y29GTGr91_1ec3LSQFoTRwpkqU4BYHXPnBaXA,3685
|
|
207
208
|
clearskies/functional/string.py,sha256=ZnkOjx8nxqZq2TV0CIb-Kz4onGoyekTX_WkLJM6XTmM,3311
|
|
208
209
|
clearskies/functional/validations.py,sha256=cPYOTwWomlQrPvqPP_Jdlds7zZ5H9GABCP5pnGzC9T4,2821
|
|
@@ -222,7 +223,7 @@ clearskies/query/join.py,sha256=4lrDUQzck7klKY_VYkc4SVK95SVwyy3SVTvasnsAEyc,4713
|
|
|
222
223
|
clearskies/query/query.py,sha256=0XR3fNhOpDNJY0US2oseAS3p3Y0jxxVs86P6vWEvUcA,6063
|
|
223
224
|
clearskies/query/sort.py,sha256=c-EtIkjg3kLjwSTdXD7sfyx-mNUhAepUV-2izprh3iY,754
|
|
224
225
|
clearskies/secrets/__init__.py,sha256=G-A8YhCMlS_OdboSeKzCZp6iwfqwU4BPEnB5HvD88wY,142
|
|
225
|
-
clearskies/secrets/akeyless.py,sha256=
|
|
226
|
+
clearskies/secrets/akeyless.py,sha256=KFvehjTiWtl7YhOE3a99__F38N9IUQURNi-bkY7l8ZI,19348
|
|
226
227
|
clearskies/secrets/secrets.py,sha256=z9ouvwTwdyyOFmaCCWMRR6T9capRWFHswz563OA-JzE,1566
|
|
227
228
|
clearskies/secrets/additional_configs/__init__.py,sha256=cFCrbtKF5nuR061S2y1iKZp349x-y8Srdwe3VZbfSFU,1119
|
|
228
229
|
clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py,sha256=CnIiXLVQdUnUey3dbCTXuNNP7Mmw1gjjNjZiBtfgGto,2757
|
|
@@ -253,7 +254,7 @@ clearskies/validators/minimum_value.py,sha256=ZyG2S_-4lbbMbf7PUemEyAT-r4-gjJMUdE
|
|
|
253
254
|
clearskies/validators/required.py,sha256=GWxyexwj-K6DunZWNEnZxW6tQGAFd4oOCvQrW1s1K9k,1308
|
|
254
255
|
clearskies/validators/timedelta.py,sha256=DJ0pTm-SSUtjZ7phGoD6vjb086vXPzvLLijkU-jQlOs,1892
|
|
255
256
|
clearskies/validators/unique.py,sha256=X7qGv_BfskNJWnYCt6vDHbvpBiHym58yLjXk5ZnhAlg,975
|
|
256
|
-
clear_skies-2.0.
|
|
257
|
-
clear_skies-2.0.
|
|
258
|
-
clear_skies-2.0.
|
|
259
|
-
clear_skies-2.0.
|
|
257
|
+
clear_skies-2.0.16.dist-info/METADATA,sha256=DWJ3EZbyG0Ny3gKospCLkq5xOx2znEJK-HDk4EUsLDM,2114
|
|
258
|
+
clear_skies-2.0.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
259
|
+
clear_skies-2.0.16.dist-info/licenses/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
|
|
260
|
+
clear_skies-2.0.16.dist-info/RECORD,,
|
|
@@ -11,6 +11,7 @@ from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
|
11
11
|
from clearskies.autodoc.schema import String as AutoDocString
|
|
12
12
|
from clearskies.backends.backend import Backend
|
|
13
13
|
from clearskies.di import InjectableProperties, inject
|
|
14
|
+
from clearskies.functional import json as json_functional
|
|
14
15
|
from clearskies.functional import routing, string
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
@@ -550,7 +551,7 @@ class ApiBackend(configurable.Configurable, Backend, InjectableProperties):
|
|
|
550
551
|
}
|
|
551
552
|
```
|
|
552
553
|
"""
|
|
553
|
-
api_to_model_map = configs.
|
|
554
|
+
api_to_model_map = configs.AnyDict(default={})
|
|
554
555
|
|
|
555
556
|
"""
|
|
556
557
|
The name of the pagination parameter
|
|
@@ -589,7 +590,7 @@ class ApiBackend(configurable.Configurable, Backend, InjectableProperties):
|
|
|
589
590
|
authentication: Authentication | None = None,
|
|
590
591
|
model_casing: str = "snake_case",
|
|
591
592
|
api_casing: str = "snake_case",
|
|
592
|
-
api_to_model_map: dict[str, str] = {},
|
|
593
|
+
api_to_model_map: dict[str, str | list[str]] = {},
|
|
593
594
|
pagination_parameter_name: str = "start",
|
|
594
595
|
pagination_parameter_type: str = "str",
|
|
595
596
|
limit_parameter_name: str = "limit",
|
|
@@ -897,16 +898,16 @@ class ApiBackend(configurable.Configurable, Backend, InjectableProperties):
|
|
|
897
898
|
f"The response from a records request returned a variable of type {response_data.__class__.__name__}, which is just confusing. To do automatic introspection, I need a list or a dictionary. I'm afraid you'll have to extend the API backend and override the map_record_response method to deal with this."
|
|
898
899
|
)
|
|
899
900
|
|
|
900
|
-
for key, value in response_data.items():
|
|
901
|
-
if not isinstance(value, list):
|
|
902
|
-
continue
|
|
903
|
-
return self.map_records_response(value, query, query_data)
|
|
904
|
-
|
|
905
901
|
# a records request may only return a single record, so before we fail, let's check for that
|
|
906
902
|
record = self.check_dict_and_map_to_model(response_data, columns, query_data)
|
|
907
903
|
if record is not None:
|
|
908
904
|
return [record]
|
|
909
905
|
|
|
906
|
+
for key, value in response_data.items():
|
|
907
|
+
if not isinstance(value, list):
|
|
908
|
+
continue
|
|
909
|
+
return self.map_records_response(value, query, query_data)
|
|
910
|
+
|
|
910
911
|
raise ValueError(
|
|
911
912
|
"The response from a records request returned a dictionary, but none of the items in the dictionary was a list, so I don't know where to find the records. I only ever check one level deep in dictionaries. I'm afraid you'll have to extend the API backend and override the map_records_response method to deal with this."
|
|
912
913
|
)
|
|
@@ -958,27 +959,38 @@ class ApiBackend(configurable.Configurable, Backend, InjectableProperties):
|
|
|
958
959
|
map_keys = set(response_to_model_map.keys())
|
|
959
960
|
matching = response_keys.intersection(map_keys)
|
|
960
961
|
|
|
961
|
-
# if nothing matches then clearly this isn't what we're looking for: repeat on all the children
|
|
962
|
-
if not matching:
|
|
963
|
-
for key, value in response_data.items():
|
|
964
|
-
if not isinstance(value, dict):
|
|
965
|
-
continue
|
|
966
|
-
mapped = self.check_dict_and_map_to_model(value, columns)
|
|
967
|
-
if mapped:
|
|
968
|
-
return {**query_data, **mapped}
|
|
969
|
-
|
|
970
|
-
# no match anywhere :(
|
|
971
|
-
return None
|
|
972
|
-
|
|
973
962
|
# we may need to be smarter about whether or not we think we found a match, but for now let's
|
|
974
963
|
# ignore that possibility. If any columns match between the keys in our response dictionary and
|
|
975
964
|
# the keys that we are expecting to find data in, then just assume that we have found a record.
|
|
976
965
|
mapped = {response_to_model_map[key]: response_data[key] for key in matching}
|
|
977
966
|
|
|
967
|
+
for api_key, column_name in self.api_to_model_map.items():
|
|
968
|
+
if not "." in api_key:
|
|
969
|
+
continue
|
|
970
|
+
value = json_functional.get_nested_attribute(response_data, api_key)
|
|
971
|
+
if value is None:
|
|
972
|
+
continue
|
|
973
|
+
if isinstance(column_name, list):
|
|
974
|
+
for column in column_name:
|
|
975
|
+
mapped[column] = value
|
|
976
|
+
else:
|
|
977
|
+
mapped[column_name] = value
|
|
978
978
|
# finally, move over anything not mentioned in the map
|
|
979
979
|
for key in response_keys.difference(map_keys):
|
|
980
980
|
mapped[string.swap_casing(key, self.api_casing, self.model_casing)] = response_data[key]
|
|
981
981
|
|
|
982
|
+
# if nothing matches then clearly this isn't what we're looking for: repeat on all the children
|
|
983
|
+
if not mapped:
|
|
984
|
+
for key, value in response_data.items():
|
|
985
|
+
if not isinstance(value, dict):
|
|
986
|
+
continue
|
|
987
|
+
remapped = self.check_dict_and_map_to_model(value, columns)
|
|
988
|
+
if remapped:
|
|
989
|
+
return {**query_data, **remapped}
|
|
990
|
+
|
|
991
|
+
# no match anywhere :(
|
|
992
|
+
return None
|
|
993
|
+
|
|
982
994
|
return {**query_data, **mapped}
|
|
983
995
|
|
|
984
996
|
def build_response_to_model_map(self, columns: dict[str, Column]) -> dict[str, str]:
|
|
@@ -65,7 +65,7 @@ class BelongsToModel(Column):
|
|
|
65
65
|
parent_class = belongs_to_column.parent_model_class
|
|
66
66
|
parent_model = self.di.build(parent_class, cache=False)
|
|
67
67
|
if not parent_id:
|
|
68
|
-
return parent_model.
|
|
68
|
+
return parent_model.empty()
|
|
69
69
|
|
|
70
70
|
parent_id_column_name = parent_model.id_column_name
|
|
71
71
|
join_alias = belongs_to_column.join_table_alias()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Any, cast
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_nested_attribute(data: dict[str, Any] | str, attr_path: str) -> Any:
|
|
5
|
+
"""
|
|
6
|
+
Extract a nested attribute from JSON data using dot notation.
|
|
7
|
+
|
|
8
|
+
This function navigates through a nested JSON structure using a dot-separated path
|
|
9
|
+
to retrieve a specific attribute. If the input is a string, it will attempt to parse
|
|
10
|
+
it as JSON first.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
```
|
|
14
|
+
data = {"database": {"credentials": {"username": "admin", "password": "secret"}}}
|
|
15
|
+
username = get_nested_attribute(data, "database.credentials.username")
|
|
16
|
+
# Returns "admin"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
data: The JSON data as a dictionary or a JSON string
|
|
21
|
+
attr_path: The path to the attribute using dot notation (e.g., "database.username")
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The value at the specified path
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If the data cannot be parsed as JSON
|
|
28
|
+
KeyError: If the attribute path doesn't exist in the data
|
|
29
|
+
"""
|
|
30
|
+
keys = attr_path.split(".", 1)
|
|
31
|
+
if not isinstance(data, dict):
|
|
32
|
+
try:
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
data = json.loads(data)
|
|
36
|
+
except Exception:
|
|
37
|
+
raise ValueError(f"Could not parse data as JSON to get attribute '{attr_path}'")
|
|
38
|
+
|
|
39
|
+
# At this point, we know data is a dictionary
|
|
40
|
+
data_dict = cast(dict[str, Any], data) # Help type checker understand data is a dict
|
|
41
|
+
|
|
42
|
+
if len(keys) == 1:
|
|
43
|
+
if keys[0] not in data_dict:
|
|
44
|
+
raise KeyError(f"Data does not contain attribute '{attr_path}'")
|
|
45
|
+
return data_dict[keys[0]]
|
|
46
|
+
|
|
47
|
+
return get_nested_attribute(data_dict[keys[0]], keys[1])
|
clearskies/secrets/akeyless.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
|
-
import json
|
|
5
4
|
import logging
|
|
6
5
|
from types import ModuleType
|
|
7
6
|
from typing import TYPE_CHECKING, Any
|
|
@@ -9,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
9
8
|
from clearskies import configs, secrets
|
|
10
9
|
from clearskies.decorators import parameters_to_properties
|
|
11
10
|
from clearskies.di import inject
|
|
11
|
+
from clearskies.functional.json import get_nested_attribute
|
|
12
12
|
from clearskies.secrets.exceptions import PermissionsError
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -224,7 +224,7 @@ class Akeyless(secrets.Secrets):
|
|
|
224
224
|
raise KeyError(f"Secret '{path}' not found")
|
|
225
225
|
raise e
|
|
226
226
|
if json_attribute:
|
|
227
|
-
return
|
|
227
|
+
return get_nested_attribute(res[path], json_attribute) # type: ignore
|
|
228
228
|
return str(res[path])
|
|
229
229
|
|
|
230
230
|
def get_dynamic_secret(
|
|
@@ -249,7 +249,7 @@ class Akeyless(secrets.Secrets):
|
|
|
249
249
|
kwargs["args"] = args # type: ignore
|
|
250
250
|
res: dict[str, Any] = self.api.get_dynamic_secret_value(self.akeyless.GetDynamicSecretValue(**kwargs)) # type: ignore
|
|
251
251
|
if json_attribute:
|
|
252
|
-
return
|
|
252
|
+
return get_nested_attribute(res, json_attribute)
|
|
253
253
|
return res
|
|
254
254
|
|
|
255
255
|
def get_rotated_secret(
|
|
@@ -276,7 +276,7 @@ class Akeyless(secrets.Secrets):
|
|
|
276
276
|
|
|
277
277
|
res: dict[str, str] = self._api.get_rotated_secret_value(self.akeyless.GetRotatedSecretValue(**kwargs))["value"] # type: ignore
|
|
278
278
|
if json_attribute:
|
|
279
|
-
return
|
|
279
|
+
return get_nested_attribute(res, json_attribute)
|
|
280
280
|
return res
|
|
281
281
|
|
|
282
282
|
def describe_secret(self, path: str) -> Any:
|
|
@@ -470,26 +470,6 @@ class Akeyless(secrets.Secrets):
|
|
|
470
470
|
self.akeyless.DescribePermissions(token=self._get_token(), path=path, type=type)
|
|
471
471
|
).client_permissions # type: ignore
|
|
472
472
|
|
|
473
|
-
def _get_nested_attribute(self, data: dict[str, Any] | str, attr_path: str) -> Any:
|
|
474
|
-
"""
|
|
475
|
-
Extract a nested attribute from JSON data.
|
|
476
|
-
|
|
477
|
-
Parses the provided data as JSON if it's a string. Traverses the nested structure using
|
|
478
|
-
the dot-separated path (e.g., "database.username"). Raises ValueError if the data cannot
|
|
479
|
-
be parsed as JSON, or KeyError if the attribute path doesn't exist in the data.
|
|
480
|
-
"""
|
|
481
|
-
keys = attr_path.split(".", 1)
|
|
482
|
-
if not isinstance(data, dict):
|
|
483
|
-
try:
|
|
484
|
-
data = json.loads(data)
|
|
485
|
-
except Exception:
|
|
486
|
-
raise ValueError(f"Could not parse secret as JSON to get attribute '{attr_path}'")
|
|
487
|
-
if len(keys) == 1:
|
|
488
|
-
if not isinstance(data, dict) or keys[0] not in data:
|
|
489
|
-
raise KeyError(f"Secret does not contain attribute '{attr_path}'")
|
|
490
|
-
return data[keys[0]] # type: ignore
|
|
491
|
-
return self._get_nested_attribute(data[keys[0]], keys[1]) # type: ignore
|
|
492
|
-
|
|
493
473
|
|
|
494
474
|
class AkeylessSaml(Akeyless):
|
|
495
475
|
"""Convenience class for SAML authentication with Akeyless."""
|
|
File without changes
|
|
File without changes
|