clear-skies 2.0.27__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 (270) hide show
  1. clear_skies-2.0.27.dist-info/METADATA +78 -0
  2. clear_skies-2.0.27.dist-info/RECORD +270 -0
  3. clear_skies-2.0.27.dist-info/WHEEL +4 -0
  4. clear_skies-2.0.27.dist-info/licenses/LICENSE +7 -0
  5. clearskies/__init__.py +69 -0
  6. clearskies/action.py +7 -0
  7. clearskies/authentication/__init__.py +15 -0
  8. clearskies/authentication/authentication.py +44 -0
  9. clearskies/authentication/authorization.py +23 -0
  10. clearskies/authentication/authorization_pass_through.py +22 -0
  11. clearskies/authentication/jwks.py +165 -0
  12. clearskies/authentication/public.py +5 -0
  13. clearskies/authentication/secret_bearer.py +551 -0
  14. clearskies/autodoc/__init__.py +8 -0
  15. clearskies/autodoc/formats/__init__.py +5 -0
  16. clearskies/autodoc/formats/oai3_json/__init__.py +7 -0
  17. clearskies/autodoc/formats/oai3_json/oai3_json.py +87 -0
  18. clearskies/autodoc/formats/oai3_json/oai3_schema_resolver.py +15 -0
  19. clearskies/autodoc/formats/oai3_json/parameter.py +35 -0
  20. clearskies/autodoc/formats/oai3_json/request.py +68 -0
  21. clearskies/autodoc/formats/oai3_json/response.py +28 -0
  22. clearskies/autodoc/formats/oai3_json/schema/__init__.py +11 -0
  23. clearskies/autodoc/formats/oai3_json/schema/array.py +9 -0
  24. clearskies/autodoc/formats/oai3_json/schema/default.py +13 -0
  25. clearskies/autodoc/formats/oai3_json/schema/enum.py +7 -0
  26. clearskies/autodoc/formats/oai3_json/schema/object.py +35 -0
  27. clearskies/autodoc/formats/oai3_json/test.json +1985 -0
  28. clearskies/autodoc/py.typed +0 -0
  29. clearskies/autodoc/request/__init__.py +15 -0
  30. clearskies/autodoc/request/header.py +6 -0
  31. clearskies/autodoc/request/json_body.py +6 -0
  32. clearskies/autodoc/request/parameter.py +8 -0
  33. clearskies/autodoc/request/request.py +47 -0
  34. clearskies/autodoc/request/url_parameter.py +6 -0
  35. clearskies/autodoc/request/url_path.py +6 -0
  36. clearskies/autodoc/response/__init__.py +5 -0
  37. clearskies/autodoc/response/response.py +9 -0
  38. clearskies/autodoc/schema/__init__.py +31 -0
  39. clearskies/autodoc/schema/array.py +10 -0
  40. clearskies/autodoc/schema/base64.py +8 -0
  41. clearskies/autodoc/schema/boolean.py +5 -0
  42. clearskies/autodoc/schema/date.py +5 -0
  43. clearskies/autodoc/schema/datetime.py +5 -0
  44. clearskies/autodoc/schema/double.py +5 -0
  45. clearskies/autodoc/schema/enum.py +17 -0
  46. clearskies/autodoc/schema/integer.py +6 -0
  47. clearskies/autodoc/schema/long.py +5 -0
  48. clearskies/autodoc/schema/number.py +6 -0
  49. clearskies/autodoc/schema/object.py +13 -0
  50. clearskies/autodoc/schema/password.py +5 -0
  51. clearskies/autodoc/schema/schema.py +11 -0
  52. clearskies/autodoc/schema/string.py +5 -0
  53. clearskies/backends/__init__.py +67 -0
  54. clearskies/backends/api_backend.py +1194 -0
  55. clearskies/backends/backend.py +137 -0
  56. clearskies/backends/cursor_backend.py +339 -0
  57. clearskies/backends/graphql_backend.py +977 -0
  58. clearskies/backends/memory_backend.py +794 -0
  59. clearskies/backends/secrets_backend.py +100 -0
  60. clearskies/clients/__init__.py +5 -0
  61. clearskies/clients/graphql_client.py +182 -0
  62. clearskies/column.py +1221 -0
  63. clearskies/columns/__init__.py +71 -0
  64. clearskies/columns/audit.py +306 -0
  65. clearskies/columns/belongs_to_id.py +478 -0
  66. clearskies/columns/belongs_to_model.py +145 -0
  67. clearskies/columns/belongs_to_self.py +109 -0
  68. clearskies/columns/boolean.py +110 -0
  69. clearskies/columns/category_tree.py +274 -0
  70. clearskies/columns/category_tree_ancestors.py +51 -0
  71. clearskies/columns/category_tree_children.py +125 -0
  72. clearskies/columns/category_tree_descendants.py +48 -0
  73. clearskies/columns/created.py +92 -0
  74. clearskies/columns/created_by_authorization_data.py +114 -0
  75. clearskies/columns/created_by_header.py +103 -0
  76. clearskies/columns/created_by_ip.py +90 -0
  77. clearskies/columns/created_by_routing_data.py +102 -0
  78. clearskies/columns/created_by_user_agent.py +89 -0
  79. clearskies/columns/date.py +232 -0
  80. clearskies/columns/datetime.py +284 -0
  81. clearskies/columns/email.py +78 -0
  82. clearskies/columns/float.py +149 -0
  83. clearskies/columns/has_many.py +552 -0
  84. clearskies/columns/has_many_self.py +62 -0
  85. clearskies/columns/has_one.py +21 -0
  86. clearskies/columns/integer.py +158 -0
  87. clearskies/columns/json.py +126 -0
  88. clearskies/columns/many_to_many_ids.py +335 -0
  89. clearskies/columns/many_to_many_ids_with_data.py +281 -0
  90. clearskies/columns/many_to_many_models.py +163 -0
  91. clearskies/columns/many_to_many_pivots.py +132 -0
  92. clearskies/columns/phone.py +162 -0
  93. clearskies/columns/select.py +95 -0
  94. clearskies/columns/string.py +102 -0
  95. clearskies/columns/timestamp.py +164 -0
  96. clearskies/columns/updated.py +107 -0
  97. clearskies/columns/uuid.py +83 -0
  98. clearskies/configs/README.md +105 -0
  99. clearskies/configs/__init__.py +170 -0
  100. clearskies/configs/actions.py +43 -0
  101. clearskies/configs/any.py +15 -0
  102. clearskies/configs/any_dict.py +24 -0
  103. clearskies/configs/any_dict_or_callable.py +25 -0
  104. clearskies/configs/authentication.py +23 -0
  105. clearskies/configs/authorization.py +23 -0
  106. clearskies/configs/boolean.py +18 -0
  107. clearskies/configs/boolean_or_callable.py +20 -0
  108. clearskies/configs/callable_config.py +20 -0
  109. clearskies/configs/columns.py +34 -0
  110. clearskies/configs/conditions.py +30 -0
  111. clearskies/configs/config.py +26 -0
  112. clearskies/configs/datetime.py +20 -0
  113. clearskies/configs/datetime_or_callable.py +21 -0
  114. clearskies/configs/email.py +10 -0
  115. clearskies/configs/email_list.py +17 -0
  116. clearskies/configs/email_list_or_callable.py +17 -0
  117. clearskies/configs/email_or_email_list_or_callable.py +59 -0
  118. clearskies/configs/endpoint.py +23 -0
  119. clearskies/configs/endpoint_list.py +29 -0
  120. clearskies/configs/float.py +18 -0
  121. clearskies/configs/float_or_callable.py +20 -0
  122. clearskies/configs/headers.py +28 -0
  123. clearskies/configs/integer.py +18 -0
  124. clearskies/configs/integer_or_callable.py +20 -0
  125. clearskies/configs/joins.py +30 -0
  126. clearskies/configs/list_any_dict.py +32 -0
  127. clearskies/configs/list_any_dict_or_callable.py +33 -0
  128. clearskies/configs/model_class.py +35 -0
  129. clearskies/configs/model_column.py +67 -0
  130. clearskies/configs/model_columns.py +58 -0
  131. clearskies/configs/model_destination_name.py +26 -0
  132. clearskies/configs/model_to_id_column.py +45 -0
  133. clearskies/configs/readable_model_column.py +11 -0
  134. clearskies/configs/readable_model_columns.py +11 -0
  135. clearskies/configs/schema.py +23 -0
  136. clearskies/configs/searchable_model_columns.py +11 -0
  137. clearskies/configs/security_headers.py +39 -0
  138. clearskies/configs/select.py +28 -0
  139. clearskies/configs/select_list.py +49 -0
  140. clearskies/configs/string.py +31 -0
  141. clearskies/configs/string_dict.py +34 -0
  142. clearskies/configs/string_list.py +47 -0
  143. clearskies/configs/string_list_or_callable.py +48 -0
  144. clearskies/configs/string_or_callable.py +18 -0
  145. clearskies/configs/timedelta.py +20 -0
  146. clearskies/configs/timezone.py +20 -0
  147. clearskies/configs/url.py +25 -0
  148. clearskies/configs/validators.py +45 -0
  149. clearskies/configs/writeable_model_column.py +11 -0
  150. clearskies/configs/writeable_model_columns.py +11 -0
  151. clearskies/configurable.py +78 -0
  152. clearskies/contexts/__init__.py +11 -0
  153. clearskies/contexts/cli.py +130 -0
  154. clearskies/contexts/context.py +99 -0
  155. clearskies/contexts/wsgi.py +79 -0
  156. clearskies/contexts/wsgi_ref.py +87 -0
  157. clearskies/cursors/__init__.py +10 -0
  158. clearskies/cursors/cursor.py +161 -0
  159. clearskies/cursors/from_environment/__init__.py +5 -0
  160. clearskies/cursors/from_environment/mysql.py +51 -0
  161. clearskies/cursors/from_environment/postgresql.py +49 -0
  162. clearskies/cursors/from_environment/sqlite.py +35 -0
  163. clearskies/cursors/mysql.py +61 -0
  164. clearskies/cursors/postgresql.py +61 -0
  165. clearskies/cursors/sqlite.py +62 -0
  166. clearskies/decorators.py +33 -0
  167. clearskies/decorators.pyi +10 -0
  168. clearskies/di/__init__.py +15 -0
  169. clearskies/di/additional_config.py +130 -0
  170. clearskies/di/additional_config_auto_import.py +17 -0
  171. clearskies/di/di.py +948 -0
  172. clearskies/di/inject/__init__.py +25 -0
  173. clearskies/di/inject/akeyless_sdk.py +16 -0
  174. clearskies/di/inject/by_class.py +24 -0
  175. clearskies/di/inject/by_name.py +22 -0
  176. clearskies/di/inject/di.py +16 -0
  177. clearskies/di/inject/environment.py +15 -0
  178. clearskies/di/inject/input_output.py +19 -0
  179. clearskies/di/inject/logger.py +16 -0
  180. clearskies/di/inject/now.py +16 -0
  181. clearskies/di/inject/requests.py +16 -0
  182. clearskies/di/inject/secrets.py +15 -0
  183. clearskies/di/inject/utcnow.py +16 -0
  184. clearskies/di/inject/uuid.py +16 -0
  185. clearskies/di/injectable.py +32 -0
  186. clearskies/di/injectable_properties.py +131 -0
  187. clearskies/end.py +219 -0
  188. clearskies/endpoint.py +1303 -0
  189. clearskies/endpoint_group.py +333 -0
  190. clearskies/endpoints/__init__.py +25 -0
  191. clearskies/endpoints/advanced_search.py +519 -0
  192. clearskies/endpoints/callable.py +382 -0
  193. clearskies/endpoints/create.py +201 -0
  194. clearskies/endpoints/delete.py +133 -0
  195. clearskies/endpoints/get.py +267 -0
  196. clearskies/endpoints/health_check.py +181 -0
  197. clearskies/endpoints/list.py +567 -0
  198. clearskies/endpoints/restful_api.py +417 -0
  199. clearskies/endpoints/schema.py +185 -0
  200. clearskies/endpoints/simple_search.py +279 -0
  201. clearskies/endpoints/update.py +188 -0
  202. clearskies/environment.py +106 -0
  203. clearskies/exceptions/__init__.py +19 -0
  204. clearskies/exceptions/authentication.py +2 -0
  205. clearskies/exceptions/authorization.py +2 -0
  206. clearskies/exceptions/client_error.py +2 -0
  207. clearskies/exceptions/input_errors.py +4 -0
  208. clearskies/exceptions/missing_dependency.py +2 -0
  209. clearskies/exceptions/moved_permanently.py +3 -0
  210. clearskies/exceptions/moved_temporarily.py +3 -0
  211. clearskies/exceptions/not_found.py +2 -0
  212. clearskies/functional/__init__.py +7 -0
  213. clearskies/functional/json.py +47 -0
  214. clearskies/functional/routing.py +92 -0
  215. clearskies/functional/string.py +112 -0
  216. clearskies/functional/validations.py +76 -0
  217. clearskies/input_outputs/__init__.py +13 -0
  218. clearskies/input_outputs/cli.py +157 -0
  219. clearskies/input_outputs/exceptions/__init__.py +7 -0
  220. clearskies/input_outputs/exceptions/cli_input_error.py +2 -0
  221. clearskies/input_outputs/exceptions/cli_not_found.py +2 -0
  222. clearskies/input_outputs/headers.py +54 -0
  223. clearskies/input_outputs/input_output.py +116 -0
  224. clearskies/input_outputs/programmatic.py +62 -0
  225. clearskies/input_outputs/py.typed +0 -0
  226. clearskies/input_outputs/wsgi.py +80 -0
  227. clearskies/loggable.py +19 -0
  228. clearskies/model.py +2039 -0
  229. clearskies/py.typed +0 -0
  230. clearskies/query/__init__.py +12 -0
  231. clearskies/query/condition.py +228 -0
  232. clearskies/query/join.py +136 -0
  233. clearskies/query/query.py +195 -0
  234. clearskies/query/sort.py +27 -0
  235. clearskies/schema.py +82 -0
  236. clearskies/secrets/__init__.py +7 -0
  237. clearskies/secrets/additional_configs/__init__.py +32 -0
  238. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py +61 -0
  239. clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +160 -0
  240. clearskies/secrets/akeyless.py +507 -0
  241. clearskies/secrets/exceptions/__init__.py +7 -0
  242. clearskies/secrets/exceptions/not_found_error.py +2 -0
  243. clearskies/secrets/exceptions/permissions_error.py +2 -0
  244. clearskies/secrets/secrets.py +39 -0
  245. clearskies/security_header.py +17 -0
  246. clearskies/security_headers/__init__.py +11 -0
  247. clearskies/security_headers/cache_control.py +68 -0
  248. clearskies/security_headers/cors.py +51 -0
  249. clearskies/security_headers/csp.py +95 -0
  250. clearskies/security_headers/hsts.py +23 -0
  251. clearskies/security_headers/x_content_type_options.py +0 -0
  252. clearskies/security_headers/x_frame_options.py +0 -0
  253. clearskies/typing.py +11 -0
  254. clearskies/validator.py +36 -0
  255. clearskies/validators/__init__.py +33 -0
  256. clearskies/validators/after_column.py +61 -0
  257. clearskies/validators/before_column.py +15 -0
  258. clearskies/validators/in_the_future.py +29 -0
  259. clearskies/validators/in_the_future_at_least.py +13 -0
  260. clearskies/validators/in_the_future_at_most.py +12 -0
  261. clearskies/validators/in_the_past.py +29 -0
  262. clearskies/validators/in_the_past_at_least.py +12 -0
  263. clearskies/validators/in_the_past_at_most.py +12 -0
  264. clearskies/validators/maximum_length.py +25 -0
  265. clearskies/validators/maximum_value.py +28 -0
  266. clearskies/validators/minimum_length.py +25 -0
  267. clearskies/validators/minimum_value.py +28 -0
  268. clearskies/validators/required.py +32 -0
  269. clearskies/validators/timedelta.py +58 -0
  270. clearskies/validators/unique.py +28 -0
@@ -0,0 +1,32 @@
1
+ from .mysql_connection_dynamic_producer import MySQLConnectionDynamicProducer
2
+ from .mysql_connection_dynamic_producer_via_ssh_cert_bastion import MySQLConnectionDynamicProducerViaSSHCertBastion
3
+
4
+
5
+ def mysql_connection_dynamic_producer(producer_name=None, database_host=None, database_name=None):
6
+ return MySQLConnectionDynamicProducer(
7
+ producer_name=producer_name,
8
+ database_host=database_host,
9
+ database_name=database_name,
10
+ )
11
+
12
+
13
+ def mysql_connection_dynamic_producer_via_ssh_cert_bastion(
14
+ producer_name=None,
15
+ bastion_host=None,
16
+ bastion_username=None,
17
+ public_key_file_path=None,
18
+ cert_issuer_name=None,
19
+ database_host=None,
20
+ database_name=None,
21
+ local_proxy_port=None,
22
+ ):
23
+ return MySQLConnectionDynamicProducerViaSSHCertBastion(
24
+ producer_name=producer_name,
25
+ bastion_host=bastion_host,
26
+ bastion_username=bastion_username,
27
+ cert_issuer_name=cert_issuer_name,
28
+ public_key_file_path=public_key_file_path,
29
+ database_host=database_host,
30
+ database_name=database_name,
31
+ local_proxy_port=local_proxy_port,
32
+ )
@@ -0,0 +1,61 @@
1
+ import clearskies.di
2
+
3
+
4
+ class MySQLConnectionDynamicProducer(clearskies.di.additional_config.AdditionalConfig):
5
+ _producer_name = None
6
+ _database_host = None
7
+ _database_name = None
8
+
9
+ def __init__(self, producer_name=None, database_host=None, database_name=None):
10
+ self._producer_name = producer_name
11
+ self._database_host = database_host
12
+ self._database_name = database_name
13
+
14
+ def provide_connection_details(self, environment, secrets):
15
+ if not secrets:
16
+ raise ValueError(
17
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
18
+ but AKeyless itself wasn't configured. \
19
+ Try setting the AKeyless auth method via clearskies.secrets.akeyless_[jwt|saml|aws_iam]_auth()"
20
+ )
21
+
22
+ producer_name = (
23
+ self._producer_name
24
+ if self._producer_name is not None
25
+ else environment.get("akeyless_mysql_dynamic_producer", silent=True)
26
+ )
27
+ if not producer_name:
28
+ raise ValueError(
29
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
30
+ but I wasn't told the path to the dynamic producer. \
31
+ This can be set in an environment variable named 'akeyless_mysql_dynamic_producer'\
32
+ or it can be set in the configuration via the producer_name kwarg."
33
+ )
34
+ database_name = (
35
+ self._database_name if self._database_name is not None else environment.get("db_database", silent=True)
36
+ )
37
+ if not database_name:
38
+ raise ValueError(
39
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
40
+ but I wasn't told the name of the database. \
41
+ This can be set in an environment variable named 'db_database' \
42
+ or it can be set in the configuration via the database_name kwarg."
43
+ )
44
+ database_host = (
45
+ self._database_host if self._database_host is not None else environment.get("db_host", silent=True)
46
+ )
47
+ if not database_host:
48
+ raise ValueError(
49
+ "I was asked to connect to a database via an AKeyless dynamic producer, \
50
+ but I wasn't told the host name of the database. \
51
+ This can be set in an environment variable named 'db_host' \
52
+ or it can be set in the configuration via the database_host kwarg."
53
+ )
54
+ credentials = secrets.get_dynamic_secret(producer_name)
55
+
56
+ return {
57
+ "username": credentials["user"],
58
+ "password": credentials["password"],
59
+ "host": database_host,
60
+ "database": database_name,
61
+ }
@@ -0,0 +1,160 @@
1
+ import os
2
+ import socket
3
+ import subprocess
4
+ import time
5
+ from pathlib import Path
6
+
7
+ import clearskies.di
8
+
9
+
10
+ class MySQLConnectionDynamicProducerViaSSHCertBastion(clearskies.di.additional_config.AdditionalConfig):
11
+ _config = None
12
+
13
+ def __init__(
14
+ self,
15
+ producer_name=None,
16
+ bastion_host=None,
17
+ bastion_username=None,
18
+ public_key_file_path=None,
19
+ local_proxy_port=None,
20
+ cert_issuer_name=None,
21
+ database_host=None,
22
+ database_name=None,
23
+ ):
24
+ # not using kwargs because I want the argument list to be explicit
25
+ self.config = {
26
+ "producer_name": producer_name,
27
+ "bastion_host": bastion_host,
28
+ "bastion_username": bastion_username,
29
+ "public_key_file_path": public_key_file_path,
30
+ "local_proxy_port": local_proxy_port,
31
+ "cert_issuer_name": cert_issuer_name,
32
+ "database_host": database_host,
33
+ "database_name": database_name,
34
+ }
35
+
36
+ def provide_connection_details(self, environment, secrets):
37
+ if not secrets:
38
+ raise ValueError(
39
+ "I was asked to connect to a database via an AKeyless dynamic producer but AKeyless itself wasn't configured. Try setting the AKeyless auth method via clearskies.secrets.akeyless_[jwt|saml|aws_iam]_auth()"
40
+ )
41
+
42
+ home = str(Path.home())
43
+ default_public_key_file_path = f"{home}/.ssh/id_rsa.pub"
44
+
45
+ producer_name = self._fetch_config(environment, "producer_name", "akeyless_mysql_dynamic_producer")
46
+ bastion_host = self._get_bastion_host(environment)
47
+ bastion_username = self._fetch_config(environment, "bastion_username", "akeyless_mysql_bastion_username")
48
+ public_key_file_path = self._fetch_config(
49
+ environment,
50
+ "public_key_file_path",
51
+ "akeyless_mysql_bastion_public_key_file_path",
52
+ default=default_public_key_file_path,
53
+ )
54
+ cert_issuer_name = self._fetch_config(
55
+ environment, "cert_issuer_name", "akeyless_mysql_bastion_cert_issuer_name"
56
+ )
57
+ local_proxy_port = self._fetch_config(
58
+ environment, "local_proxy_port", "akeyless_mysql_bastion_local_proxy_port", default=8888
59
+ )
60
+ database_host = self._fetch_config(environment, "database_host", "db_host")
61
+ database_name = self._fetch_config(environment, "database_name", "db_database")
62
+
63
+ # Create the SSH tunnel (yeah, it's obnoxious)
64
+ self._create_tunnel(
65
+ secrets,
66
+ bastion_host,
67
+ bastion_username,
68
+ local_proxy_port,
69
+ cert_issuer_name,
70
+ public_key_file_path,
71
+ database_host,
72
+ )
73
+
74
+ # and now we can fetch credentials
75
+ credentials = secrets.get_dynamic_secret(producer_name)
76
+
77
+ return {
78
+ "username": credentials["user"],
79
+ "password": credentials["password"],
80
+ "host": "127.0.0.1",
81
+ "database": database_name,
82
+ "port": local_proxy_port,
83
+ }
84
+
85
+ def _get_bastion_host(self, environment):
86
+ return self._fetch_config(environment, "bastion_host", "akeyless_mysql_bastion_host")
87
+
88
+ def _fetch_config(self, environment, config_key_name, environment_key_name, default=None):
89
+ if self.config[config_key_name]:
90
+ return self.config[config_key_name]
91
+ from_environment = environment.get(environment_key_name, silent=True)
92
+ if from_environment:
93
+ return from_environment
94
+ if default is not None:
95
+ return default
96
+ raise ValueError(
97
+ f"I was asked to connect to a database via an AKeyless dynamic producer through an SSH bastion"
98
+ "with certificate auth, but I wasn't given a required configuration value: '{config_key_name}'."
99
+ "This can be set in the call to "
100
+ "`clearskies.backends.akeyless.mysql_connection_dynamic_producer_via_ssh_cert_bastion()` by providing the "
101
+ "'{config_key_name}' argument, or by setting an environment variable named '{environment_key_name}'."
102
+ )
103
+
104
+ def _create_tunnel(
105
+ self,
106
+ secrets,
107
+ bastion_host,
108
+ bastion_username,
109
+ local_proxy_port,
110
+ cert_issuer_name,
111
+ public_key_file_path,
112
+ database_host,
113
+ ):
114
+ # first see if the socket is already open, since we don't close it.
115
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
116
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
117
+ if result == 0:
118
+ sock.close()
119
+ return
120
+
121
+ if not os.path.isfile(public_key_file_path):
122
+ raise ValueError(
123
+ f"I was asked to connect to AKeyless SSH with the public key file in '{public_key_file_path}',"
124
+ "but this file does not exist"
125
+ )
126
+
127
+ ssh_certificate = secrets.get_ssh_certificate(cert_issuer_name, bastion_username, public_key_file_path)
128
+
129
+ # We need to write the certificate out to the standard location that SSH expects it so that SSH can find it.
130
+ # I haven't found a good library for doing this in Python, so I'm relying on the ssh command
131
+ home = str(Path.home())
132
+ with open(f"{home}/.ssh/id_rsa-cert.pub", "w") as fp:
133
+ fp.write(ssh_certificate)
134
+
135
+ # and now we can do this thing.
136
+ tunnel_command = [
137
+ "ssh",
138
+ "-o",
139
+ "ConnectTimeout=2",
140
+ "-N",
141
+ "-L",
142
+ f"{local_proxy_port}:{database_host}:3306",
143
+ "-p",
144
+ "22",
145
+ f"{bastion_username}@{bastion_host}",
146
+ ]
147
+ subprocess.Popen(tunnel_command)
148
+ connected = False
149
+ attempts = 0
150
+ while not connected and attempts < 6:
151
+ attempts += 1
152
+ time.sleep(0.5)
153
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
154
+ result = sock.connect_ex(("127.0.0.1", local_proxy_port))
155
+ if result == 0:
156
+ connected = True
157
+ if not connected:
158
+ raise ValueError(
159
+ "Failed to open SSH tunnel. The following command was used: \n" + " ".join(tunnel_command)
160
+ )