clear-skies 2.0.13__py3-none-any.whl → 2.0.15__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.

Files changed (35) hide show
  1. {clear_skies-2.0.13.dist-info → clear_skies-2.0.15.dist-info}/METADATA +1 -1
  2. {clear_skies-2.0.13.dist-info → clear_skies-2.0.15.dist-info}/RECORD +34 -32
  3. clearskies/authentication/jwks.py +1 -1
  4. clearskies/autodoc/formats/oai3_json/request.py +1 -1
  5. clearskies/backends/secrets_backend.py +0 -1
  6. clearskies/columns/updated.py +0 -1
  7. clearskies/configs/__init__.py +0 -2
  8. clearskies/configs/email_list_or_callable.py +0 -2
  9. clearskies/configs/headers.py +1 -3
  10. clearskies/contexts/context.py +1 -2
  11. clearskies/contexts/wsgi.py +0 -2
  12. clearskies/contexts/wsgi_ref.py +0 -1
  13. clearskies/di/di.py +2 -3
  14. clearskies/di/inject/akeyless_sdk.py +15 -0
  15. clearskies/di/inject/input_output.py +0 -2
  16. clearskies/di/test_module/__init__.py +3 -0
  17. clearskies/di/test_module/module_class.py +0 -3
  18. clearskies/endpoint.py +1 -1
  19. clearskies/input_outputs/exceptions/__init__.py +5 -0
  20. clearskies/input_outputs/input_output.py +0 -1
  21. clearskies/input_outputs/wsgi.py +1 -1
  22. clearskies/model.py +0 -1
  23. clearskies/secrets/akeyless.py +377 -37
  24. clearskies/secrets/exceptions/__init__.py +7 -1
  25. clearskies/secrets/exceptions/not_found_error.py +2 -0
  26. clearskies/secrets/exceptions/permissions_error.py +2 -0
  27. clearskies/secrets/secrets.py +0 -5
  28. clearskies/validators/__init__.py +1 -1
  29. clearskies/validators/maximum_value.py +0 -1
  30. clearskies/validators/minimum_length.py +0 -1
  31. clearskies/validators/minimum_value.py +0 -1
  32. clearskies/validators/unique.py +0 -1
  33. clearskies/secrets/exceptions/not_found.py +0 -2
  34. {clear_skies-2.0.13.dist-info → clear_skies-2.0.15.dist-info}/WHEEL +0 -0
  35. {clear_skies-2.0.13.dist-info → clear_skies-2.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear-skies
3
- Version: 2.0.13
3
+ Version: 2.0.15
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
@@ -5,10 +5,10 @@ clearskies/configurable.py,sha256=gfX9o5qgMqc8lSzrdcUyR3b6zdOvg9ccYBQ0VvZjXkk,30
5
5
  clearskies/decorators.py,sha256=lcXJC4xI-hRyKDSH8yklcWjOrIL9k2Qks1xP2lmyDm0,1024
6
6
  clearskies/decorators.pyi,sha256=qSRzPJYYoV9trEsh_V1BxP2HdMz0G2gOs4FV8f5hvyU,388
7
7
  clearskies/end.py,sha256=mEPdBB4lh9zmAKTbsffCh-mQMZGIVohriWjm-952EzA,10629
8
- clearskies/endpoint.py,sha256=z4ys46jZC1rFSAfwdc-eScUDThcJ4mILRCQMcqvAX68,47960
8
+ clearskies/endpoint.py,sha256=Q_ovFPRB_8dRm2bU-yDHtXqeAVRVenILMD4f2wrtaKU,47947
9
9
  clearskies/endpoint_group.py,sha256=qsxQpt1i6IRvQqhQkt_udoVj0TMdoIOccvKyEBaeaEg,12343
10
10
  clearskies/environment.py,sha256=BbNgQH3uuCTxoHeLB9O1_HsPTaJL5aiXqY6hZ9aj-jo,3739
11
- clearskies/model.py,sha256=1cCx1KcAOK6_MHBdgtWdu4lZMYuPQaWHSrXsdW4v7V8,77545
11
+ clearskies/model.py,sha256=OBPIatlGpyoiIG0krdUy10icmouz6jLhdr81FtyH9Zo,77535
12
12
  clearskies/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  clearskies/schema.py,sha256=9S0RuFBiydsDy4kATy2BBsXJ9aWHILW0ja_q9F4kxb0,2872
14
14
  clearskies/security_header.py,sha256=_RsQEJahVTIB8m8IIgQgYk8NhX_6QJqRJpQDh2NETNo,467
@@ -19,7 +19,7 @@ clearskies/authentication/__init__.py,sha256=MxXNCWhKbha7TPu_MAyssfTdOCEDSNBwzsh
19
19
  clearskies/authentication/authentication.py,sha256=B1I8udvtGfEdiUrqUM8V3YT_Cxema9eDOSlnXjgC35s,1230
20
20
  clearskies/authentication/authorization.py,sha256=87eRORyR7RpyB5YnLm6zqn4JQjTUIA4psfTjTE-oSSg,805
21
21
  clearskies/authentication/authorization_pass_through.py,sha256=EEQ-JJbBVLAoRefrNFAWBicoJO5k3B4gZQc2rQAwIBk,556
22
- clearskies/authentication/jwks.py,sha256=_-peGZC9vDcIxuPXFObvIGX_GA04aOfewVNngrdbCDA,4857
22
+ clearskies/authentication/jwks.py,sha256=slAx3xbTDiIWx0Cp7fn4z7_iDfGMWZZ-YAHHX5Imae0,4852
23
23
  clearskies/authentication/public.py,sha256=GSNl4X4fnB25BZoXSo8nqL0iZCa9rzfvPfIsJ2Y34B4,84
24
24
  clearskies/authentication/secret_bearer.py,sha256=OVLi9kJNIGF1Cyl4nOxDxuuctZCVBP2L-bT2Og8qCHE,17377
25
25
  clearskies/autodoc/__init__.py,sha256=JRUAmd0he8iGlgiZvxewLMIXJqnOFEdvlaKAtHpC2lo,124
@@ -29,7 +29,7 @@ clearskies/autodoc/formats/oai3_json/__init__.py,sha256=tbbOZWkX9ZWj4BM3arHrlmqy
29
29
  clearskies/autodoc/formats/oai3_json/oai3_json.py,sha256=-LViw86zpPAgAEnC42KuobccvFATRg-j8ZEd8BiTYFA,2964
30
30
  clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py,sha256=5gifJ36dyWEeYtfjHVbbTpPBo0BB1Gqxt0Y3sxTiHJA,509
31
31
  clearskies/autodoc/formats/oai3_json/parameter.py,sha256=h9bHXxqJhUc7NRQCZF6RnIPuaY9BA6V_xzYNnsplXlc,1110
32
- clearskies/autodoc/formats/oai3_json/request.py,sha256=WHEWQvhuhkw82V4YS6GwDN4y8Qy7QDSn9awJEhwLmWU,2939
32
+ clearskies/autodoc/formats/oai3_json/request.py,sha256=RM6PgdZNHVP6iH9oeOfSPwyp-v0k3MhUEOT8SLxORd0,2932
33
33
  clearskies/autodoc/formats/oai3_json/response.py,sha256=ou06oFQtaAR70qIvIoAe4QzohWiACzhCwlW73oM5Dvg,778
34
34
  clearskies/autodoc/formats/oai3_json/test.json,sha256=DdMvSUbaTEAyWtX_RDxEFR82kK9pCCTeFUghQwHjB3s,59848
35
35
  clearskies/autodoc/formats/oai3_json/schema/__init__.py,sha256=VJiDueMdGfJoVQVt9SXIE_-uPghSXtCzpTXuqPaaiRw,173
@@ -66,7 +66,7 @@ clearskies/backends/api_backend.py,sha256=4xcAymjkAgIA_9H68owe2Y9D4omxRnq6tnp_0R
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
69
- clearskies/backends/secrets_backend.py,sha256=6YVx154vhFLt_Fz0dz8wjQiVbaqBzPoUlfsYXU-9h7s,4044
69
+ clearskies/backends/secrets_backend.py,sha256=f_C5oAAr3tgM4UqioYPWcASPe9DhySC_uLWYGgMEPpY,3987
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
@@ -100,10 +100,10 @@ clearskies/columns/phone.py,sha256=AVNeNXm_A_CjD4W-QeJtsKHMIlMQc_DbpKFDxJKw9H4,5
100
100
  clearskies/columns/select.py,sha256=Huef3sp9JP2DRzXgNBurZfY4lsKn0RU4WmAj689HlWw,2714
101
101
  clearskies/columns/string.py,sha256=Vyl1eb6RjW8q0a4AMqHI01vBLao9f7fkowNUumNoPqU,2594
102
102
  clearskies/columns/timestamp.py,sha256=nIEbvg1YqKPjtEeFyYRnPPPSYB_lYIIUUm8zNoE8dE8,5810
103
- clearskies/columns/updated.py,sha256=BFan3JnO2IEbryTGC2rR_RD1ruDcev49owPBYyjjSVI,3340
103
+ clearskies/columns/updated.py,sha256=qE9CGwTwURGp9hr4zQAW8inpp-Xz2leWsIiHHcG1Ao4,3324
104
104
  clearskies/columns/uuid.py,sha256=QE1UKHA9zt7bYd59rYRmzIHSonq9-sZeQbAIQtoJC54,2265
105
105
  clearskies/configs/README.md,sha256=4-r2FvkrBTrKkcKgJ5O3Qj1RZ7nnkknenKze4XYlQDs,4827
106
- clearskies/configs/__init__.py,sha256=dCuGWN6eI7r9hW8d33R4nCehEMdgEpvJU1TQ-NWVK6M,5401
106
+ clearskies/configs/__init__.py,sha256=DwpoecIXXFzQMHCi38pohYHSbw7KIb6z0hSkus2HKGg,5385
107
107
  clearskies/configs/actions.py,sha256=5OwrokU8uTSSQ_6SvL94SOP5JVkvRRFGAXNcCdMt8aQ,1388
108
108
  clearskies/configs/any.py,sha256=QSLOO955Lbg8NqYeyxdQGXTTyfFc5P036LORUa7avHc,388
109
109
  clearskies/configs/any_dict.py,sha256=LECTIcCF22JXbAaZdWSmKh9UShRuRt8PydNCuj05ijI,907
@@ -120,13 +120,13 @@ clearskies/configs/datetime.py,sha256=QJxqRyFkb0YByHmaH_Rd_geUiyvBpk5xdBdRDj2827
120
120
  clearskies/configs/datetime_or_callable.py,sha256=hp6ay-958DWoPy8Ryf1VS4R5NPKoF9LceiUhSB7I_wg,835
121
121
  clearskies/configs/email.py,sha256=BbXGvZnO9wwT8LHexAq0ibwDNLVvv_8yUJEOThzJVE4,317
122
122
  clearskies/configs/email_list.py,sha256=cTgdQTz4HYkqbdTEHPy8LcLjPGqFWY5TOIH8cHbfXXI,582
123
- clearskies/configs/email_list_or_callable.py,sha256=jowDly4ZKLOYJ73aJ8VoNX338jzqTZyIcY91ANvrgt0,679
123
+ clearskies/configs/email_list_or_callable.py,sha256=iApQZzHEy6Pn5XiZzRn2gbHkRipIdLVqXzmwkjiMdmI,650
124
124
  clearskies/configs/email_or_email_list_or_callable.py,sha256=tFdAsI1sTDMOheb3h8nQhIVxnl9xzZPwZtKjYEHjkKM,2470
125
125
  clearskies/configs/endpoint.py,sha256=NEDthtdPzejAot7yzUjC01PvQJR9BIyk5YnWXCi1w9I,759
126
126
  clearskies/configs/endpoint_list.py,sha256=HS1IO2HX8Y0W5ZZU7_Yk_6p5a8mo93an5Q2KAkOl9ZU,1202
127
127
  clearskies/configs/float.py,sha256=7HwDMf-3VfrZxTmXwpnwDgXyzujUBpz1xPi1StdjB1Q,621
128
128
  clearskies/configs/float_or_callable.py,sha256=9IiylL4Gg9_7HJOrYxbUPuLGE03TwKsAMsxfB7Lh2yY,744
129
- clearskies/configs/headers.py,sha256=D3bo3jE8x5DA1vLeo9ewnfIBHcRbr0JavOY-SQyFeVQ,986
129
+ clearskies/configs/headers.py,sha256=MY_oYm4tI5_9h635E8Yo_hj2id5IqfztqtwRZVdYaYo,976
130
130
  clearskies/configs/integer.py,sha256=lw05RhD8ZkHO_tYJLtCUHZ_Ucz2Mf7C0Z5wVOsMqgq0,620
131
131
  clearskies/configs/integer_or_callable.py,sha256=acCUP2ZSfSMPyID2LzIOQ-dlgARpxEktgr7KFWMtGbk,739
132
132
  clearskies/configs/joins.py,sha256=i1QmxFwkpL-pnzVPh3mrHpwrvzsjt9nH5pMSj4J1TT4,944
@@ -157,28 +157,29 @@ clearskies/configs/writeable_model_column.py,sha256=22pMoOAwYjvhHBcFrBb6Imtuoahb
157
157
  clearskies/configs/writeable_model_columns.py,sha256=vQFh5w6ToC8SDi_GaEvy33csp2CTFIroAfOLigHjFKo,359
158
158
  clearskies/contexts/__init__.py,sha256=f7XVUq2UKlDH6fjmcUWk6lbe9p_OaGpZ5ZjM6CuwTGQ,247
159
159
  clearskies/contexts/cli.py,sha256=cuGWoyRhHlO_Ba6Dozg3sGob1VZoI4TBFOLu-2Udabk,2838
160
- clearskies/contexts/context.py,sha256=rT-d0v5wiVqRNz_bsitTBXYEy0555-H2xr9TpETf-og,3676
161
- clearskies/contexts/wsgi.py,sha256=Wrf0B9WxoMPe71jn2fATsSYyS_Ego3VKFGjuI6KliWs,3165
162
- clearskies/contexts/wsgi_ref.py,sha256=q78gyeQS3KJ80NsozJkKb0ag00_6uwPK1yj5wxUM3JA,2818
160
+ clearskies/contexts/context.py,sha256=RFIJbpP32iv0NuvepCNldrFRYMWTbGssM_nduBDJLh8,3640
161
+ clearskies/contexts/wsgi.py,sha256=gS6eJxtDsae2sFNP3v7lhFqK5KfVTHjs9uGYqKS63Mw,3131
162
+ clearskies/contexts/wsgi_ref.py,sha256=aFJ40hqAwvYcftXESMvJz5W58c2qWK3BUYeksEMi5KU,2770
163
163
  clearskies/di/__init__.py,sha256=Ab8GNv9ZksnCABq8n2gCcyLEAXD-5-kX4O8PweTJIFs,474
164
164
  clearskies/di/additional_config.py,sha256=65INxw8aqTZQsyaKPj-aQmd6FBe4_4DwibXGgWYBy14,5139
165
165
  clearskies/di/additional_config_auto_import.py,sha256=XYw0Kcnp6hp-ee-c0YjiATwJvRb2E82xk9PuoX9dGRY,758
166
- clearskies/di/di.py,sha256=mW7Tk6-9pI5khq9h_A2kXJ3XDytwC6JdLX3TcLxU2q0,46348
166
+ clearskies/di/di.py,sha256=gjyj5fudA9M2JB1_kl_GTn2yH4SaF6M32kR7Lz5Hfz4,46395
167
167
  clearskies/di/injectable.py,sha256=TTgqhx494470I61-88BUQmHmevfat-wXVseKl8pQOEk,852
168
168
  clearskies/di/injectable_properties.py,sha256=yJP0J7l7tjG2soyXtrfDgktE7M8tQHaP-55Cmtq0b7M,6466
169
169
  clearskies/di/inject/__init__.py,sha256=plEkWId-VyhvqX5KM2HhdCqi7_ZJzPmFz69cPAo812Y,643
170
+ clearskies/di/inject/akeyless_sdk.py,sha256=eCV5KkALAAwNnHVMdAQ0hlYGA2U_sJfHn3vBxkngZh8,417
170
171
  clearskies/di/inject/by_class.py,sha256=1wn08Ahne1nJ-ddRRE4i70U2XwCvU_khG2U0iEGoaVE,832
171
172
  clearskies/di/inject/by_name.py,sha256=Vgt_4NdptIVNrpVawGz-ZZfqp1MQLnvlqAZmjsX0WqI,675
172
173
  clearskies/di/inject/di.py,sha256=wuJU7u3AKPYoymQVcIR8m7t4pVIEtMvsN1RzujMqGfk,311
173
174
  clearskies/di/inject/environment.py,sha256=gQ1Tf2OVpcBNB3ciTv44rc7sKZM3pIOkbCFsCj0ELEI,469
174
- clearskies/di/inject/input_output.py,sha256=URb2qt8F-0QJ1zQpGdkrJxrZVNyF7S21EhhE5x2Irys,517
175
+ clearskies/di/inject/input_output.py,sha256=bpHuqomGmpHzQwgekbX8kYxeUfEPEaOCX4xp4ryXOa8,500
175
176
  clearskies/di/inject/now.py,sha256=Kp4xkRfOKAf4vl0hwo0PspKbfDxbhfptMm1rrXgc__0,395
176
177
  clearskies/di/inject/requests.py,sha256=ZXeEihj76dli4sf-WfW-sEZ30Y63eUqnP4OPChbovt4,403
177
178
  clearskies/di/inject/secrets.py,sha256=WDcu2Fg9QuBkIbl1lhT1j7L1JxOfl4oipJPLD3H_wyM,445
178
179
  clearskies/di/inject/utcnow.py,sha256=ugXgCerskwM9Pmc9OsdUKG4shbsYE-5c0YebUFx7ngQ,401
179
180
  clearskies/di/inject/uuid.py,sha256=GwMKqY-k7iDgc3QuOOe80aUejoU9PfFFyxZjl6Vinjc,408
180
- clearskies/di/test_module/__init__.py,sha256=7YHQF7JHP0FdI7GdEGANSZ_t1EISQYhUNm1wqOg0NKw,88
181
- clearskies/di/test_module/module_class.py,sha256=I_-wnMuHfbsvti-7d2Z4bXnr6deo__uvww9nds9qrlE,46
181
+ clearskies/di/test_module/__init__.py,sha256=UiuP3b8dbatcqOsVGyZ9vEPiNpr-IKJkp6PDXetHTvM,140
182
+ clearskies/di/test_module/module_class.py,sha256=cBF95A9616CTSx3P5GhR8-3201ttxIudYaWjxZD6apE,28
182
183
  clearskies/di/test_module/another_module/__init__.py,sha256=8SRmHPDepLKGWTUSc1ucDF6U8mJPsNDsBDmBQCpzPWo,35
183
184
  clearskies/endpoints/__init__.py,sha256=payWNEivwmxsT-pWqNkzHisSbyNjB7oq7_I5QIQL_XA,750
184
185
  clearskies/endpoints/advanced_search.py,sha256=FXbY1-VBf9qtsYAvXaqxLpR1FNUFhfB5EhhIx6fVDHk,21397
@@ -208,11 +209,11 @@ clearskies/functional/validations.py,sha256=cPYOTwWomlQrPvqPP_Jdlds7zZ5H9GABCP5p
208
209
  clearskies/input_outputs/__init__.py,sha256=9qeKJULw3MQ3zqkgBZice5d7qqRgsP3y-wkhWO2Y9vM,362
209
210
  clearskies/input_outputs/cli.py,sha256=t7uWqLi6VI3i_zuyoKLdIq3vUwr19lQZoJmuAxVEvgg,5741
210
211
  clearskies/input_outputs/headers.py,sha256=AnyqI64kploPX7qiBfQCD9w8b2FYWVIuwaXVafbpIiM,2085
211
- clearskies/input_outputs/input_output.py,sha256=tJQVN3U3MX_jpwsXJ-g-K1cdqQwyuSarTjo3JOp7zQQ,5154
212
+ clearskies/input_outputs/input_output.py,sha256=iaWi6B4o2hvNqL6CmXIc1Fv4UJNfLVLn9uNDuqmnDA0,5120
212
213
  clearskies/input_outputs/programmatic.py,sha256=OCRq0M42cKZKgk4YAfJyTWo3T4jNRmnGmVr7zCTovpg,1658
213
214
  clearskies/input_outputs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
214
- clearskies/input_outputs/wsgi.py,sha256=wcqZUu8zuKj9V1ur2oNlYqgcI_sC5LJEieZxvDVMMFU,2783
215
- clearskies/input_outputs/exceptions/__init__.py,sha256=3KiM3KaMYEKoToqCCQ4_no2n0W5ROqeBC0sI2Ix4P6w,82
215
+ clearskies/input_outputs/wsgi.py,sha256=qqU_3KeVt5oeZvqygQ2lSxkcmTCsGX7z0R8Zx1DVhag,2783
216
+ clearskies/input_outputs/exceptions/__init__.py,sha256=ONAtr46SecnF96TJkjWWJfVfSXWbJrOaAnkN-f6fKs4,137
216
217
  clearskies/input_outputs/exceptions/cli_input_error.py,sha256=kOFU8aLTLmeTL_AKDshxMu8_ufildg6p8ndhE1xHfb0,41
217
218
  clearskies/input_outputs/exceptions/cli_not_found.py,sha256=JBBuZA9ZwdkPhd3a0qaGgEPQrxh1fehy4R3ZaV2gWXU,39
218
219
  clearskies/query/__init__.py,sha256=ISF80_cG3rd574sRTdKKPxAdlSjtQh_ClXKKs_MSSSo,277
@@ -221,13 +222,14 @@ clearskies/query/join.py,sha256=4lrDUQzck7klKY_VYkc4SVK95SVwyy3SVTvasnsAEyc,4713
221
222
  clearskies/query/query.py,sha256=0XR3fNhOpDNJY0US2oseAS3p3Y0jxxVs86P6vWEvUcA,6063
222
223
  clearskies/query/sort.py,sha256=c-EtIkjg3kLjwSTdXD7sfyx-mNUhAepUV-2izprh3iY,754
223
224
  clearskies/secrets/__init__.py,sha256=G-A8YhCMlS_OdboSeKzCZp6iwfqwU4BPEnB5HvD88wY,142
224
- clearskies/secrets/akeyless.py,sha256=4SwnVNzMAijZtzR0Q25dizEw-q7nbS4G5s0CoGyc-G0,7219
225
- clearskies/secrets/secrets.py,sha256=9sYrI0PmxXAzyDCfylOmb8svXqPc9IefaWKaBPHrjxE,1815
225
+ clearskies/secrets/akeyless.py,sha256=k9j-GbAGo67J4cH6VQDVYLZJodRcJfNmwLRaBOE2vgg,20335
226
+ clearskies/secrets/secrets.py,sha256=z9ouvwTwdyyOFmaCCWMRR6T9capRWFHswz563OA-JzE,1566
226
227
  clearskies/secrets/additional_configs/__init__.py,sha256=cFCrbtKF5nuR061S2y1iKZp349x-y8Srdwe3VZbfSFU,1119
227
228
  clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py,sha256=CnIiXLVQdUnUey3dbCTXuNNP7Mmw1gjjNjZiBtfgGto,2757
228
229
  clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py,sha256=N8ruxrTNhvYlp3cYXq6V78KPPr4n40LM7QoXHvD8IZg,6235
229
- clearskies/secrets/exceptions/__init__.py,sha256=j-SLHD-DL0CT4cZXibD9kXHk63JEl_UKX6xL_nq1EfE,32
230
- clearskies/secrets/exceptions/not_found.py,sha256=_lZwovDrd18dUHDop5pF4mhexBPNr126xF2gOLA2-EA,36
230
+ clearskies/secrets/exceptions/__init__.py,sha256=HtcwEuKzu3tRLq3FeXUN0UhaEGqukL5vntuBbX47DzM,209
231
+ clearskies/secrets/exceptions/not_found_error.py,sha256=9NT1g4Q94PRteYl9coiLVbqbHTlkrd-C-KQKG5clz9Q,41
232
+ clearskies/secrets/exceptions/permissions_error.py,sha256=6oxKMDLZLmoZZFB9PMZxWQgsaQXoX2V6qWn-LAckHt0,44
231
233
  clearskies/security_headers/__init__.py,sha256=JUpc4Y8dNBinDQAkP7OOABR7N787-lRR23boSWmY6Us,285
232
234
  clearskies/security_headers/cache_control.py,sha256=gD1070vxwDJXQi8R3qIIR6hsOpVaaEV3-I52Up1DfM4,2143
233
235
  clearskies/security_headers/cors.py,sha256=RqqakKH13JXCp0ybkf7YFGEKDR5Xg7a1lfk47AAI7CU,1778
@@ -235,7 +237,7 @@ clearskies/security_headers/csp.py,sha256=hms81Jw9VWzDRWdk5P7o3oq7LOOElpuGwhnIpw
235
237
  clearskies/security_headers/hsts.py,sha256=BeNrxxxrIWNxbF2Rb1fmOgmbF3rp1i5ve7iXh3Qr7q4,723
236
238
  clearskies/security_headers/x_content_type_options.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
237
239
  clearskies/security_headers/x_frame_options.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
238
- clearskies/validators/__init__.py,sha256=Hz0L5Y7EkeK4Px6WcdQ3ku6VqUGpLHX0ZWwCpT1ts1U,1248
240
+ clearskies/validators/__init__.py,sha256=v7TK6PlSjJxIuhx_aCoocBUCF-8DxNWWpYmoR7uOe6I,1247
239
241
  clearskies/validators/after_column.py,sha256=GqZTdwNlkkLYD5_UJExih5aOksMBKJ4Trvr56PhBPsk,2309
240
242
  clearskies/validators/before_column.py,sha256=jv5sUCeTwkkn-d1AFGaJccZdthc0o6rQASBT_vLHZbE,564
241
243
  clearskies/validators/in_the_future.py,sha256=xx2Du7iTsXWth5KWdyjnYb7iOYpFjjPxSZ1p69Ay54A,901
@@ -245,13 +247,13 @@ clearskies/validators/in_the_past.py,sha256=8eb79Q9feHL5rLsQdoOYIEsHegUiXI4zkH5S
245
247
  clearskies/validators/in_the_past_at_least.py,sha256=FT8rfs2-5Sd933Uql6-pwFRvvcPPXWq8hpobtkGr2z0,395
246
248
  clearskies/validators/in_the_past_at_most.py,sha256=AFJoIlA3xeoJtWKh-2-PG6A97_FMHwt4Yk3G9TRNLzA,393
247
249
  clearskies/validators/maximum_length.py,sha256=VxKvonpF58ng5icGt2d2BC2dWjXtNfnynjr95LZVYHg,843
248
- clearskies/validators/maximum_value.py,sha256=gqWRESL7rfPa4XYY2iDwjAu8NuVrtEwMXXMlY6rV_uk,871
249
- clearskies/validators/minimum_length.py,sha256=j-LHWLKXf3gF7yD9-RgNLXNm2gHj_-ZvGUxyCdRKPrw,860
250
- clearskies/validators/minimum_value.py,sha256=NDLcG6xCemlv3kfr-RiUaM3x2INS1GJGMB9RoOezZ7k,872
250
+ clearskies/validators/maximum_value.py,sha256=icdUN1AbT5c5SXpxDRks3954GRIUqu0FdpT6TeZFT2Q,855
251
+ clearskies/validators/minimum_length.py,sha256=2ukRKH35gdnjz9VezclIFCf3JtbN7iCbLSjlPNdaOyY,844
252
+ clearskies/validators/minimum_value.py,sha256=ZyG2S_-4lbbMbf7PUemEyAT-r4-gjJMUdEWccMam9H0,856
251
253
  clearskies/validators/required.py,sha256=GWxyexwj-K6DunZWNEnZxW6tQGAFd4oOCvQrW1s1K9k,1308
252
254
  clearskies/validators/timedelta.py,sha256=DJ0pTm-SSUtjZ7phGoD6vjb086vXPzvLLijkU-jQlOs,1892
253
- clearskies/validators/unique.py,sha256=GFEQOMYRIO9pSGHHj6zf1GdnJ0UM7Dm4ZO4uGn19BZo,991
254
- clear_skies-2.0.13.dist-info/METADATA,sha256=-vCQH5UBp10JkiJslrdn99Gg4FAvuEjusoEU-lypGkY,2114
255
- clear_skies-2.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
256
- clear_skies-2.0.13.dist-info/licenses/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
257
- clear_skies-2.0.13.dist-info/RECORD,,
255
+ 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,,
@@ -106,7 +106,7 @@ class Jwks(Authentication, di.InjectableProperties):
106
106
 
107
107
  def validate_jwt(self, raw_jwt):
108
108
  try:
109
- from jwcrypto import jwk, jws, jwt # type: ignore
109
+ from jwcrypto import jwk, jwt # type: ignore
110
110
  from jwcrypto.common import JWException # type: ignore
111
111
  except:
112
112
  raise ValueError(
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from ...schema import Array, Object
3
+ from ...schema import Object
4
4
  from .parameter import Parameter
5
5
  from .response import Response
6
6
 
@@ -7,7 +7,6 @@ from clearskies.query import Condition, Query
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from clearskies import Model
10
- from clearskies.authentication import Authentication
11
10
 
12
11
 
13
12
  class SecretsBackend(Backend):
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from clearskies import configs, decorators
@@ -63,8 +63,6 @@ class MyConfigurableClass(configs.Configurable):
63
63
 
64
64
  """
65
65
 
66
- import inspect
67
-
68
66
  from .actions import Actions
69
67
  from .any import Any
70
68
  from .any_dict import AnyDict
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable
4
-
5
3
  from clearskies.configs import string_list_or_callable
6
4
 
7
5
 
@@ -9,9 +9,7 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class Headers(config.Config):
12
- """
13
- This is for a configuration that should be an instance of type clearskies.input_outputs.Headers.
14
- """
12
+ """This is for a configuration that should be an instance of type clearskies.input_outputs.Headers."""
15
13
 
16
14
  def __set__(self, instance, value: headers.Headers):
17
15
  if value is None:
@@ -7,8 +7,7 @@ from typing import TYPE_CHECKING, Any, Callable
7
7
  from clearskies import exceptions
8
8
  from clearskies.di import Di
9
9
  from clearskies.di.additional_config import AdditionalConfig
10
- from clearskies.input_outputs import InputOutput
11
- from clearskies.input_outputs import Programmatic
10
+ from clearskies.input_outputs import InputOutput, Programmatic
12
11
 
13
12
  if TYPE_CHECKING:
14
13
  from clearskies.endpoint import Endpoint
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
4
-
5
3
  from clearskies.contexts.context import Context
6
4
  from clearskies.input_outputs import Wsgi as WsgiInputOutput
7
5
 
@@ -4,7 +4,6 @@ import datetime
4
4
  from types import ModuleType
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
  from wsgiref.simple_server import make_server
7
- from wsgiref.util import setup_testing_defaults
8
7
 
9
8
  from clearskies.contexts.context import Context
10
9
  from clearskies.input_outputs import Wsgi as WsgiInputOutput
clearskies/di/di.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import datetime
2
2
  import inspect
3
3
  import os
4
- import re
5
- import sys
6
4
  from types import ModuleType
7
5
  from typing import Any, Callable
8
6
 
@@ -591,6 +589,7 @@ class Di:
591
589
  4. The Di class itself if it has a matching `provide_[name]` function (aka the builtins)
592
590
  """
593
591
  if name in self._prepared and cache:
592
+ self.inject_properties(self._prepared[name].__class__)
594
593
  return self._prepared[name]
595
594
 
596
595
  if name in self._bindings:
@@ -985,7 +984,7 @@ class Di:
985
984
  def provide_global_table_prefix(self):
986
985
  return ""
987
986
 
988
- def provide_akeyles_sdk(self):
987
+ def provide_akeyless_sdk(self):
989
988
  import akeyless # type: ignore[import-untyped]
990
989
 
991
990
  return akeyless
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from types import ModuleType
4
+
5
+ from clearskies.di.injectable import Injectable
6
+
7
+
8
+ class AkeylessSDK(Injectable):
9
+ def __init__(self, cache: bool = True):
10
+ self.cache = cache
11
+
12
+ def __get__(self, instance, parent) -> ModuleType:
13
+ if instance is None:
14
+ return self # type: ignore
15
+ return self._di.build_from_name("akeyless_sdk", cache=self.cache)
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- import requests
6
-
7
5
  from clearskies.di.injectable import Injectable
8
6
 
9
7
  if TYPE_CHECKING:
@@ -4,3 +4,6 @@ from .module_class import ModuleClass
4
4
 
5
5
  class Hi:
6
6
  pass
7
+
8
+
9
+ __all__ = ["another_module", "ModuleClass", "Hi"]
@@ -1,5 +1,2 @@
1
- import datetime
2
-
3
-
4
1
  class ModuleClass:
5
2
  pass
clearskies/endpoint.py CHANGED
@@ -10,7 +10,7 @@ from clearskies.authentication import Authentication, Authorization, Public
10
10
  from clearskies.autodoc import schema
11
11
  from clearskies.autodoc.request import Parameter, Request
12
12
  from clearskies.autodoc.response import Response
13
- from clearskies.functional import routing, string, validations
13
+ from clearskies.functional import routing, string
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from clearskies import Column, Model, SecurityHeader
@@ -1,2 +1,7 @@
1
1
  from .cli_input_error import CLIInputError
2
2
  from .cli_not_found import CLINotFound
3
+
4
+ __all__ = [
5
+ "CLIInputError",
6
+ "CLINotFound",
7
+ ]
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import json
4
4
  from abc import ABC, abstractmethod
5
5
  from typing import TYPE_CHECKING, Any
6
- from urllib.parse import parse_qs
7
6
 
8
7
  from clearskies import configs, configurable
9
8
 
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
- from urllib.parse import parse_qs
3
2
 
4
3
  import json
5
4
  from typing import Callable
5
+ from urllib.parse import parse_qs
6
6
 
7
7
  from clearskies.input_outputs.headers import Headers
8
8
  from clearskies.input_outputs.input_output import InputOutput
clearskies/model.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from abc import abstractmethod
5
4
  from typing import TYPE_CHECKING, Any, Callable, Iterator, Self
6
5
 
@@ -1,111 +1,370 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- from typing import Any
4
+ import json
5
+ import logging
6
+ from types import ModuleType
7
+ from typing import TYPE_CHECKING, Any
5
8
 
6
9
  from clearskies import configs, secrets
10
+ from clearskies.decorators import parameters_to_properties
7
11
  from clearskies.di import inject
12
+ from clearskies.secrets.exceptions import PermissionsError
13
+
14
+ if TYPE_CHECKING:
15
+ from akeyless import ListItemsOutput, V2Api
8
16
 
9
17
 
10
18
  class Akeyless(secrets.Secrets):
19
+ """
20
+ Backend for managing secrets using the Akeyless Vault.
21
+
22
+ This class provides integration with Akeyless vault services, allowing you to store, retrieve,
23
+ and manage secrets. It supports different types of secrets (static, dynamic, rotated) and
24
+ includes authentication mechanisms for AWS IAM, SAML, and JWT.
25
+ """
26
+
27
+ """
28
+ HTTP client for making API requests
29
+ """
11
30
  requests = inject.Requests()
31
+
32
+ """
33
+ Environment configuration for retrieving environment variables
34
+ """
12
35
  environment = inject.Environment()
13
- akeyless = inject.ByName("akeyless_sdk")
14
36
 
37
+ """
38
+ The Akeyless SDK module injected by the dependency injection system
39
+ """
40
+ akeyless: ModuleType = inject.ByName("akeyless_sdk") # type: ignore
41
+
42
+ """
43
+ The access ID for the Akeyless service
44
+
45
+ This must match the pattern p-[0-9a-zA-Z]+ (e.g., "p-abc123")
46
+ """
15
47
  access_id = configs.String(required=True, regexp=r"^p-[\d\w]+$")
48
+
49
+ """
50
+ The authentication method to use
51
+
52
+ Must be one of "aws_iam", "saml", or "jwt"
53
+ """
16
54
  access_type = configs.Select(["aws_iam", "saml", "jwt"], required=True)
55
+
56
+ """
57
+ The Akeyless API host to connect to
58
+
59
+ Defaults to "https://api.akeyless.io"
60
+ """
17
61
  api_host = configs.String(default="https://api.akeyless.io")
18
- profile = configs.String(regexp=r"^[\d\w\-]+$")
19
-
20
- _token_refresh: datetime.datetime = None # type: ignore
21
- _token: str = ""
22
- _api: Any = None
23
-
24
- def __init__(self, access_id: str, access_type: str, jwt_env_key: str = "", api_host: str = "", profile: str = ""):
25
- self.access_id = access_id
26
- self.access_type = access_type
27
- self.jwt_env_key = jwt_env_key
28
- self.api_host = api_host
29
- self.profile = profile
30
- if self.access_type == "jwt" and not self.jwt_env_key:
31
- raise ValueError("When using the JWT access type for Akeyless you must provide jwt_env_key")
32
62
 
63
+ """
64
+ The environment variable key that contains the JWT when using JWT authentication
65
+
66
+ This is required when access_type is "jwt"
67
+ """
68
+ jwt_env_key = configs.String(required=False)
69
+
70
+ """
71
+ The SAML profile name when using SAML authentication
72
+
73
+ Must match the pattern [0-9a-zA-Z-]+ if provided
74
+ """
75
+ profile = configs.String(regexp=r"^[\d\w-]+$")
76
+
77
+ """
78
+ Whether to automatically guess the secret type
79
+
80
+ When enabled, the system will check the secret type (static, dynamic, rotated)
81
+ and call the appropriate method to retrieve it.
82
+ """
83
+ auto_guess_type = configs.Boolean(default=False)
84
+
85
+ """
86
+ When the current token expires
87
+ """
88
+ _token_refresh: datetime.datetime # type: ignore
89
+
90
+ """
91
+ The current authentication token
92
+ """
93
+ _token: str
94
+
95
+ """
96
+ The configured V2Api client
97
+ """
98
+ _api: V2Api
99
+
100
+ @parameters_to_properties
101
+ def __init__(
102
+ self,
103
+ access_id: str,
104
+ access_type: str,
105
+ jwt_env_key: str | None = None,
106
+ api_host: str | None = None,
107
+ profile: str | None = None,
108
+ auto_guess_type: bool = False,
109
+ ):
110
+ """
111
+ Initialize the Akeyless backend with the specified configuration.
112
+
113
+ The access_id must be provided and follow the format p-[0-9a-zA-Z]+. The access_type must be
114
+ one of "aws_iam", "saml", or "jwt". If using JWT authentication, jwt_env_key must be provided.
115
+ """
33
116
  self.finalize_and_validate_configuration()
117
+ self.logger = logging.getLogger(self.__class__.__name__)
118
+
119
+ def configure(self) -> None:
120
+ """
121
+ Perform additional configuration validation.
122
+
123
+ Ensures that when using JWT authentication, the jwt_env_key is provided. Raises ValueError
124
+ if access_type is "jwt" and jwt_env_key is not provided.
125
+ """
126
+ if self.access_type == "jwt" and not self.jwt_env_key:
127
+ raise ValueError("When using the JWT access type for Akeyless you must provide jwt_env_key")
34
128
 
35
129
  @property
36
- def api(self) -> Any:
37
- if self._api is None:
130
+ def api(self) -> V2Api:
131
+ """
132
+ Get the configured V2Api client.
133
+
134
+ Creates a new API client if one doesn't exist yet, using the configured api_host.
135
+ """
136
+ if not hasattr(self, "_api"):
38
137
  configuration = self.akeyless.Configuration(host=self.api_host)
39
138
  self._api = self.akeyless.V2Api(self.akeyless.ApiClient(configuration))
40
139
  return self._api
41
140
 
42
141
  def create(self, path: str, value: Any) -> bool:
142
+ """
143
+ Create a new secret at the given path.
144
+
145
+ Checks permissions before creating the secret and raises PermissionsError if the user doesn't
146
+ have write permission for the path. The value is converted to a string before storage.
147
+ """
148
+ if not "write" in self.describe_permissions(path):
149
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
150
+
43
151
  res = self.api.create_secret(self.akeyless.CreateSecret(name=path, value=str(value), token=self._get_token()))
44
152
  return True
45
153
 
46
- def get(self, path: str, silent_if_not_found: bool = False) -> str:
154
+ def get(
155
+ self,
156
+ path: str,
157
+ silent_if_not_found: bool = False,
158
+ json_attribute: str | None = None,
159
+ args: dict[str, Any] | None = None,
160
+ ) -> str:
161
+ """
162
+ Get the secret at the given path.
163
+
164
+ When auto_guess_type is enabled, this method automatically determines if the secret is static,
165
+ dynamic, or rotated and calls the appropriate method to retrieve it. If silent_if_not_found is
166
+ True, returns an empty string when the secret is not found. If json_attribute is provided,
167
+ treats the secret as JSON and returns the specified attribute.
168
+ """
169
+ if not self.auto_guess_type:
170
+ return self.get_static_secret(path, silent_if_not_found=silent_if_not_found, json_attribute=json_attribute)
171
+
47
172
  try:
48
- res = self._api.get_secret_value(self.akeyless.GetSecretValue(names=[path], token=self._get_token()))
173
+ secret = self.describe_secret(path)
174
+ except Exception as e:
175
+ if e.status == 404: # type: ignore
176
+ if silent_if_not_found:
177
+ return ""
178
+ raise e
179
+ else:
180
+ raise ValueError(
181
+ f"describe-secret call failed for path {path}: perhaps a permissions issue? Akeless says {e}"
182
+ )
183
+
184
+ self.logger.debug(f"Auto-detected secret type '{secret.item_type}' for secret '{path}'")
185
+ match secret.item_type.lower():
186
+ case "dynamic_secret":
187
+ return str(
188
+ self.get_dynamic_secret(
189
+ path,
190
+ json_attribute=json_attribute,
191
+ args=args,
192
+ )
193
+ )
194
+ case "rotated_secret":
195
+ return str(self.get_rotated_secret(path, json_attribute=json_attribute, args=args))
196
+ case "static_secret":
197
+ return self.get_static_secret(
198
+ path, json_attribute=json_attribute, silent_if_not_found=silent_if_not_found
199
+ )
200
+ case _:
201
+ raise ValueError(f"Unsupported secret type for auto-detection: '{secret.item_type}'")
202
+
203
+ def get_static_secret(self, path: str, silent_if_not_found: bool = False, json_attribute: str | None = None) -> str:
204
+ """
205
+ Get a static secret from the given path.
206
+
207
+ Checks permissions before retrieving the secret and raises PermissionsError if the user doesn't
208
+ have read permission. If silent_if_not_found is True, returns an empty string when the secret
209
+ is not found. If json_attribute is provided, treats the secret as JSON and returns the specified attribute.
210
+ """
211
+ if not "read" in self.describe_permissions(path):
212
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
213
+
214
+ try:
215
+ res: dict[str, object] = self.api.get_secret_value( # type: ignore
216
+ self.akeyless.GetSecretValue(
217
+ names=[path], token=self._get_token(), json=True if json_attribute else False
218
+ )
219
+ )
49
220
  except Exception as e:
50
221
  if e.status == 404: # type: ignore
51
222
  if silent_if_not_found:
52
223
  return ""
53
224
  raise KeyError(f"Secret '{path}' not found")
54
225
  raise e
55
- return res[path]
226
+ if json_attribute:
227
+ return self._get_nested_attribute(res[path], json_attribute) # type: ignore
228
+ return str(res[path])
229
+
230
+ def get_dynamic_secret(
231
+ self, path: str, json_attribute: str | None = None, args: dict[str, Any] | None = None
232
+ ) -> Any:
233
+ """
234
+ Get a dynamic secret from the given path.
235
+
236
+ Dynamic secrets are generated on-demand, such as database credentials. Checks permissions
237
+ before retrieving the secret and raises PermissionsError if the user doesn't have read
238
+ permission. If json_attribute is provided, treats the result as JSON and returns the
239
+ specified attribute.
240
+ """
241
+ if not "read" in self.describe_permissions(path):
242
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
56
243
 
57
- def get_dynamic_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
58
244
  kwargs = {
59
245
  "name": path,
60
246
  "token": self._get_token(),
61
247
  }
62
248
  if args:
63
249
  kwargs["args"] = args # type: ignore
250
+ res: dict[str, Any] = self.api.get_dynamic_secret_value(self.akeyless.GetDynamicSecretValue(**kwargs)) # type: ignore
251
+ if json_attribute:
252
+ return self._get_nested_attribute(res, json_attribute)
253
+ return res
64
254
 
65
- return self._api.get_dynamic_secret_value(self.akeyless.GetDynamicSecretValue(**kwargs))
255
+ def get_rotated_secret(
256
+ self, path: str, json_attribute: str | None = None, args: dict[str, Any] | None = None
257
+ ) -> Any:
258
+ """
259
+ Get a rotated secret from the given path.
260
+
261
+ Rotated secrets are automatically replaced on a schedule. Checks permissions before
262
+ retrieving the secret and raises PermissionsError if the user doesn't have read
263
+ permission. If json_attribute is provided, treats the result as JSON and returns the
264
+ specified attribute.
265
+ """
266
+ if not "read" in self.describe_permissions(path):
267
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
66
268
 
67
- def get_rotated_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
68
269
  kwargs = {
69
270
  "names": path,
70
271
  "token": self._get_token(),
272
+ "json": True if json_attribute else False,
71
273
  }
72
274
  if args:
73
275
  kwargs["args"] = args # type: ignore
74
276
 
75
- res = self._api.get_rotated_secret_value(self.akeyless.GetRotatedSecretValue(**kwargs))
277
+ res: dict[str, str] = self._api.get_rotated_secret_value(self.akeyless.GetRotatedSecretValue(**kwargs))["value"] # type: ignore
278
+ if json_attribute:
279
+ return self._get_nested_attribute(res, json_attribute)
76
280
  return res
77
281
 
282
+ def describe_secret(self, path: str) -> Any:
283
+ """
284
+ Get metadata about a secret.
285
+
286
+ Checks permissions before retrieving metadata and raises PermissionsError if the user
287
+ doesn't have read permission for the path.
288
+ """
289
+ if not "read" in self.describe_permissions(path):
290
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
291
+
292
+ return self.api.describe_item(self.akeyless.DescribeItem(name=path, token=self._get_token()))
293
+
78
294
  def list_secrets(self, path: str) -> list[Any]:
79
- res = self._api.list_items(self.akeyless.ListItems(path=path, token=self._get_token()))
295
+ """
296
+ List all secrets at the given path.
297
+
298
+ Checks permissions before listing secrets and raises PermissionsError if the user doesn't
299
+ have list permission for the path. Returns an empty list if no secrets are found.
300
+ """
301
+ if not "list" in self.describe_permissions(path):
302
+ raise PermissionsError(f"You do not have permission the secrets in '{path}'")
303
+
304
+ res: ListItemsOutput = self.api.list_items( # type: ignore
305
+ self.akeyless.ListItems(
306
+ path=path,
307
+ token=self._get_token(),
308
+ )
309
+ )
80
310
  if not res.items:
81
311
  return []
82
312
 
83
313
  return [item.item_name for item in res.items]
84
314
 
85
315
  def update(self, path: str, value: Any) -> None:
86
- res = self._api.update_secret_val(
316
+ """
317
+ Update an existing secret.
318
+
319
+ Checks permissions before updating the secret and raises PermissionsError if the user
320
+ doesn't have write permission for the path. The value is converted to a string before storage.
321
+ """
322
+ if not "write" in self.describe_permissions(path):
323
+ raise PermissionsError(f"You do not have permission the secret '{path}'")
324
+
325
+ res = self.api.update_secret_val(
87
326
  self.akeyless.UpdateSecretVal(name=path, value=str(value), token=self._get_token())
88
327
  )
89
328
 
90
329
  def upsert(self, path: str, value: Any) -> None:
330
+ """
331
+ Create or update a secret.
332
+
333
+ This method attempts to update an existing secret, and if that fails, it tries to create
334
+ a new one. The value is converted to a string before storage.
335
+ """
91
336
  try:
92
337
  self.update(path, value)
93
338
  except Exception as e:
94
339
  self.create(path, value)
95
340
 
96
341
  def list_sub_folders(self, main_folder: str) -> list[str]:
97
- """Return the list of secrets/sub folders in the given folder."""
98
- items = self._api.list_items(self.akeyless.ListItems(path=main_folder, token=self._get_token()))
342
+ """
343
+ Return the list of secrets/sub folders in the given folder.
344
+
345
+ Checks permissions before listing subfolders and raises PermissionsError if the user doesn't
346
+ have list permission for the path. Returns the relative subfolder names without the parent path.
347
+ """
348
+ if not "list" in self.describe_permissions(main_folder):
349
+ raise PermissionsError(f"You do not have permission to list sub folders in '{main_folder}'")
350
+
351
+ items = self.api.list_items(self.akeyless.ListItems(path=main_folder, token=self._get_token()))
99
352
 
100
353
  # akeyless will return the absolute path and end in a slash but we only want the folder name
101
354
  main_folder_string_len = len(main_folder)
102
- return [sub_folder[main_folder_string_len:-1] for sub_folder in items.folders]
355
+ return [sub_folder[main_folder_string_len:-1] for sub_folder in items.folders] # type: ignore
103
356
 
104
357
  def get_ssh_certificate(self, cert_issuer: str, cert_username: str, path_to_public_file: str) -> Any:
358
+ """
359
+ Get an SSH certificate from Akeyless.
360
+
361
+ Reads the public key from the specified file path and requests a certificate for the given
362
+ username and issuer from Akeyless.
363
+ """
105
364
  with open(path_to_public_file, "r") as fp:
106
365
  public_key = fp.read()
107
366
 
108
- res = self._api.get_ssh_certificate(
367
+ res = self.api.get_ssh_certificate(
109
368
  self.akeyless.GetSSHCertificate(
110
369
  cert_username=cert_username,
111
370
  cert_issuer_name=cert_issuer,
@@ -114,11 +373,22 @@ class Akeyless(secrets.Secrets):
114
373
  )
115
374
  )
116
375
 
117
- return res.data
376
+ return res.data # type: ignore
118
377
 
119
378
  def _get_token(self) -> str:
379
+ """
380
+ Get an authentication token for Akeyless API calls.
381
+
382
+ Returns a cached token if available and not expired (within 10 seconds), otherwise obtains
383
+ a new one using the configured authentication method. Tokens are valid for about an hour,
384
+ but we set the refresh time to 30 minutes to be safe.
385
+ """
120
386
  # AKeyless tokens live for an hour
121
- if self._token is not None and (self._token_refresh - datetime.datetime.now()).total_seconds() > 10:
387
+ if (
388
+ hasattr(self, "_token_refresh")
389
+ and hasattr(self, "_token")
390
+ and (self._token_refresh - datetime.datetime.now()).total_seconds() > 10
391
+ ):
122
392
  return self._token
123
393
 
124
394
  auth_method_name = f"auth_{self.access_type}"
@@ -130,14 +400,26 @@ class Akeyless(secrets.Secrets):
130
400
  return self._token
131
401
 
132
402
  def auth_aws_iam(self):
403
+ """
404
+ Authenticate using AWS IAM.
405
+
406
+ Uses the akeyless_cloud_id package to generate a cloud ID and authenticates with Akeyless
407
+ using the configured access_id.
408
+ """
133
409
  from akeyless_cloud_id import CloudId # type: ignore
134
410
 
135
- res = self._api.auth(
411
+ res = self.api.auth(
136
412
  self.akeyless.Auth(access_id=self.access_id, access_type="aws_iam", cloud_id=CloudId().generate())
137
413
  )
138
- return res.token
414
+ return res.token # type: ignore
139
415
 
140
416
  def auth_saml(self):
417
+ """
418
+ Authenticate using SAML.
419
+
420
+ Uses the akeyless CLI to generate credentials and then retrieves a token either directly
421
+ from the credentials file or by making an API call to convert the credentials to a token.
422
+ """
141
423
  import json
142
424
  import os
143
425
  from pathlib import Path
@@ -161,27 +443,85 @@ class Akeyless(secrets.Secrets):
161
443
  return response.json()["token"]
162
444
 
163
445
  def auth_jwt(self):
446
+ """
447
+ Authenticate using JWT.
448
+
449
+ Retrieves the JWT from the environment variable specified by jwt_env_key and authenticates
450
+ with Akeyless. Raises ValueError if jwt_env_key is not specified.
451
+ """
164
452
  if not self.jwt_env_key:
165
453
  raise ValueError(
166
454
  "To use AKeyless JWT Auth, "
167
455
  "you must specify the name of the ENV key to load the JWT from when configuring AKeyless"
168
456
  )
169
- res = self._api.auth(
457
+ res = self.api.auth(
170
458
  self.akeyless.Auth(access_id=self.access_id, access_type="jwt", jwt=self.environment.get(self.jwt_env_key))
171
459
  )
172
- return res.token
460
+ return res.token # type: ignore
461
+
462
+ def describe_permissions(self, path: str, type: str = "item") -> list[str]:
463
+ """
464
+ List permissions for a path.
465
+
466
+ Returns a list of permission strings (e.g., "read", "write", "list") that the current
467
+ authentication token has for the specified path.
468
+ """
469
+ return self.api.describe_permissions(
470
+ self.akeyless.DescribePermissions(token=self._get_token(), path=path, type=type)
471
+ ).client_permissions # type: ignore
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
173
492
 
174
493
 
175
494
  class AkeylessSaml(Akeyless):
495
+ """Convenience class for SAML authentication with Akeyless."""
496
+
176
497
  def __init__(self, access_id: str, api_host: str = "", profile: str = ""):
498
+ """
499
+ Initialize with SAML authentication.
500
+
501
+ Sets access_type to "saml" and passes the remaining parameters to the parent class.
502
+ """
177
503
  return super().__init__(access_id, "saml", api_host=api_host, profile=profile)
178
504
 
179
505
 
180
506
  class AkeylessJwt(Akeyless):
507
+ """Convenience class for JWT authentication with Akeyless."""
508
+
181
509
  def __init__(self, access_id: str, jwt_env_key: str = "", api_host: str = "", profile: str = ""):
510
+ """
511
+ Initialize with JWT authentication.
512
+
513
+ Sets access_type to "jwt" and passes the remaining parameters to the parent class.
514
+ """
182
515
  return super().__init__(access_id, "jwt", jwt_env_key=jwt_env_key, api_host=api_host, profile=profile)
183
516
 
184
517
 
185
518
  class AkeylessAwsIam(Akeyless):
519
+ """Convenience class for AWS IAM authentication with Akeyless."""
520
+
186
521
  def __init__(self, access_id: str, api_host: str = ""):
522
+ """
523
+ Initialize with AWS IAM authentication.
524
+
525
+ Sets access_type to "aws_iam" and passes the remaining parameters to the parent class.
526
+ """
187
527
  return super().__init__(access_id, "aws_iam", api_host=api_host)
@@ -1 +1,7 @@
1
- from .not_found import NotFound
1
+ from clearskies.secrets.exceptions.not_found_error import NotFoundError
2
+ from clearskies.secrets.exceptions.permissions_error import PermissionsError
3
+
4
+ __all__ = [
5
+ "NotFoundError",
6
+ "PermissionsError",
7
+ ]
@@ -0,0 +1,2 @@
1
+ class NotFoundError(Exception):
2
+ pass
@@ -0,0 +1,2 @@
1
+ class PermissionsError(Exception):
2
+ pass
@@ -18,11 +18,6 @@ class Secrets(ABC, clearskies.configurable.Configurable, InjectableProperties):
18
18
  "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
19
19
  )
20
20
 
21
- def get_dynamic_secret(self, path: str, args: dict[str, Any] | None = None) -> Any:
22
- raise NotImplementedError(
23
- "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
24
- )
25
-
26
21
  def list_secrets(self, path: str) -> list[Any]:
27
22
  raise NotImplementedError(
28
23
  "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."
@@ -25,8 +25,8 @@ __all__ = [
25
25
  "InThePastAtMost",
26
26
  "MaximumLength",
27
27
  "MaximumValue",
28
+ "MinimumValue",
28
29
  "MinimumLength",
29
- "MaximumLength",
30
30
  "Required",
31
31
  "Timedelta",
32
32
  "Unique",
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from clearskies import configs
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from clearskies import configs
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from clearskies import configs
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from clearskies.validator import Validator
@@ -1,2 +0,0 @@
1
- class NotFound(Exception):
2
- pass