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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear-skies
3
- Version: 2.0.15
3
+ Version: 2.0.16
4
4
  Summary: A framework for building backends in the cloud
5
5
  Project-URL: Documentation, https://clearskies.io/
6
6
  Project-URL: Repository, https://github.com/clearskies-py/clearskies
@@ -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=4xcAymjkAgIA_9H68owe2Y9D4omxRnq6tnp_0RTIEhc,54133
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=nZ-yOs5CJx_WoMQ2wLLQfwSTfuF5vantEL0DFUgwC4Q,5714
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=k9j-GbAGo67J4cH6VQDVYLZJodRcJfNmwLRaBOE2vgg,20335
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.15.dist-info/METADATA,sha256=iKhiL7xgkQDyCY83fzUEE9iUSOBIRLCH83nPoKUHOgo,2114
257
- clear_skies-2.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
258
- clear_skies-2.0.15.dist-info/licenses/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
259
- clear_skies-2.0.15.dist-info/RECORD,,
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.StringDict(default={})
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.empty_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])
@@ -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 self._get_nested_attribute(res[path], json_attribute) # type: ignore
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 self._get_nested_attribute(res, json_attribute)
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 self._get_nested_attribute(res, json_attribute)
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."""