aws-advanced-python-wrapper 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/PKG-INFO +4 -2
  2. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/README.md +3 -1
  3. aws_advanced_python_wrapper-1.2.0/aws_advanced_python_wrapper/allowed_and_blocked_hosts.py +29 -0
  4. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/aurora_connection_tracker_plugin.py +6 -7
  5. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/aurora_initial_connection_strategy_plugin.py +3 -3
  6. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/aws_secrets_manager_plugin.py +16 -15
  7. aws_advanced_python_wrapper-1.2.0/aws_advanced_python_wrapper/custom_endpoint_plugin.py +344 -0
  8. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/driver_dialect_codes.py +0 -1
  9. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/driver_info.py +1 -1
  10. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/failover_plugin.py +13 -6
  11. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/fastest_response_strategy_plugin.py +3 -1
  12. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/federated_plugin.py +7 -1
  13. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/host_list_provider.py +6 -3
  14. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/iam_plugin.py +8 -2
  15. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/mysql_driver_dialect.py +12 -2
  16. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/okta_plugin.py +7 -1
  17. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/plugin_service.py +52 -10
  18. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/read_write_splitting_plugin.py +5 -0
  19. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/reader_failover_handler.py +1 -1
  20. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/resources/aws_advanced_python_wrapper_messages.properties +21 -3
  21. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/sql_alchemy_connection_provider.py +4 -0
  22. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/stale_dns_plugin.py +12 -2
  23. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/iam_utils.py +0 -19
  24. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/mysql_exception_handler.py +2 -2
  25. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/properties.py +22 -0
  26. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/rdsutils.py +137 -74
  27. aws_advanced_python_wrapper-1.2.0/aws_advanced_python_wrapper/utils/region_utils.py +58 -0
  28. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/sliding_expiration_cache.py +20 -10
  29. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/writer_failover_handler.py +2 -2
  30. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/pyproject.toml +1 -1
  31. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/CONTRIBUTING.md +0 -0
  32. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/LICENSE +0 -0
  33. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/__init__.py +0 -0
  34. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/connect_time_plugin.py +0 -0
  35. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/connection_provider.py +0 -0
  36. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/credentials_provider_factory.py +0 -0
  37. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/database_dialect.py +0 -0
  38. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/default_plugin.py +0 -0
  39. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/developer_plugin.py +0 -0
  40. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/driver_configuration_profiles.py +0 -0
  41. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/driver_dialect.py +0 -0
  42. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/driver_dialect_manager.py +0 -0
  43. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/errors.py +0 -0
  44. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/exception_handling.py +0 -0
  45. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/execute_time_plugin.py +0 -0
  46. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/failover_result.py +0 -0
  47. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/host_availability.py +0 -0
  48. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/host_monitoring_plugin.py +0 -0
  49. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/host_selector.py +0 -0
  50. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/hostinfo.py +0 -0
  51. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/pep249.py +0 -0
  52. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/pg_driver_dialect.py +0 -0
  53. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/plugin.py +0 -0
  54. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/sqlalchemy_driver_dialect.py +0 -0
  55. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/states/__init__.py +0 -0
  56. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/states/session_state.py +0 -0
  57. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/states/session_state_service.py +0 -0
  58. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/__init__.py +0 -0
  59. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/atomic.py +0 -0
  60. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/cache_map.py +0 -0
  61. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/concurrent.py +0 -0
  62. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/decorators.py +0 -0
  63. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/failover_mode.py +0 -0
  64. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/log.py +0 -0
  65. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/messages.py +0 -0
  66. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/notifications.py +0 -0
  67. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/pg_exception_handler.py +0 -0
  68. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/rds_url_type.py +0 -0
  69. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/saml_utils.py +0 -0
  70. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/telemetry/default_telemetry_factory.py +0 -0
  71. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/telemetry/null_telemetry.py +0 -0
  72. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/telemetry/open_telemetry.py +0 -0
  73. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/telemetry/telemetry.py +0 -0
  74. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/telemetry/xray_telemetry.py +0 -0
  75. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/utils/utils.py +0 -0
  76. {aws_advanced_python_wrapper-1.1.0 → aws_advanced_python_wrapper-1.2.0}/aws_advanced_python_wrapper/wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aws-advanced-python-wrapper
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Amazon Web Services (AWS) Advanced Python Wrapper
5
5
  Home-page: https://github.com/awslabs/aws-advanced-python-wrapper
6
6
  License: Apache-2.0
@@ -257,12 +257,14 @@ This `aws-advanced-python-wrapper` is being tested against the following Communi
257
257
 
258
258
  | Database | Versions |
259
259
  |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
260
- | MySQL | 8.3.0 |
260
+ | MySQL | 8.4.0 |
261
261
  | PostgreSQL | 16.2 |
262
262
  | Aurora MySQL | - LTS version, see [here](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Updates.Versions.html#AuroraMySQL.Updates.LTS) for more details. <br><br> - Latest release, as shown on [this page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.30Updates.html). |
263
263
  | Aurora PostgreSQL | - LTS version, see [here](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.LTS.html) for more details. <br><br> - Latest release, as shown on [this page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/AuroraPostgreSQL.Updates.html).) |
264
264
 
265
265
  The `aws-advanced-python-wrapper` is compatible with MySQL 5.7 and MySQL 8.0 as per MySQL Connector/Python.
266
+ > [!WARNING]\
267
+ > Due to recent internal changes with the `v9.0.0` MySQL Connector/Python driver in regards to connection handling, the AWS Advanced Python Wrapper is not recommended for usage with `v9.0.0`. The AWS Advanced Python Wrapper will be updated in the future for `v9.0.0` compatibility with the community driver.
266
268
 
267
269
  ## License
268
270
 
@@ -222,12 +222,14 @@ This `aws-advanced-python-wrapper` is being tested against the following Communi
222
222
 
223
223
  | Database | Versions |
224
224
  |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
225
- | MySQL | 8.3.0 |
225
+ | MySQL | 8.4.0 |
226
226
  | PostgreSQL | 16.2 |
227
227
  | Aurora MySQL | - LTS version, see [here](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Updates.Versions.html#AuroraMySQL.Updates.LTS) for more details. <br><br> - Latest release, as shown on [this page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.30Updates.html). |
228
228
  | Aurora PostgreSQL | - LTS version, see [here](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Updates.LTS.html) for more details. <br><br> - Latest release, as shown on [this page](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/AuroraPostgreSQL.Updates.html).) |
229
229
 
230
230
  The `aws-advanced-python-wrapper` is compatible with MySQL 5.7 and MySQL 8.0 as per MySQL Connector/Python.
231
+ > [!WARNING]\
232
+ > Due to recent internal changes with the `v9.0.0` MySQL Connector/Python driver in regards to connection handling, the AWS Advanced Python Wrapper is not recommended for usage with `v9.0.0`. The AWS Advanced Python Wrapper will be updated in the future for `v9.0.0` compatibility with the community driver.
231
233
 
232
234
  ## License
233
235
 
@@ -0,0 +1,29 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Optional, Set
16
+
17
+
18
+ class AllowedAndBlockedHosts:
19
+ def __init__(self, allowed_host_ids: Optional[Set[str]], blocked_host_ids: Optional[Set[str]]):
20
+ self._allowed_host_ids = None if not allowed_host_ids else allowed_host_ids
21
+ self._blocked_host_ids = None if not blocked_host_ids else blocked_host_ids
22
+
23
+ @property
24
+ def allowed_host_ids(self) -> Optional[Set[str]]:
25
+ return self._allowed_host_ids
26
+
27
+ @property
28
+ def blocked_host_ids(self) -> Optional[Set[str]]:
29
+ return self._blocked_host_ids
@@ -50,13 +50,12 @@ class OpenedConnectionTracker:
50
50
  """
51
51
 
52
52
  aliases: FrozenSet[str] = host_info.as_aliases()
53
- host: str = host_info.as_alias()
54
53
 
55
- if self._rds_utils.is_rds_instance(host):
56
- self._track_connection(host, conn)
54
+ if self._rds_utils.is_rds_instance(host_info.host):
55
+ self._track_connection(host_info.as_alias(), conn)
57
56
  return
58
57
 
59
- instance_endpoint: Optional[str] = next((alias for alias in aliases if self._rds_utils.is_rds_instance(alias)),
58
+ instance_endpoint: Optional[str] = next((alias for alias in aliases if self._rds_utils.is_rds_instance(self._rds_utils.remove_port(alias))),
60
59
  None)
61
60
  if not instance_endpoint:
62
61
  logger.debug("OpenedConnectionTracker.UnableToPopulateOpenedConnectionSet")
@@ -82,7 +81,7 @@ class OpenedConnectionTracker:
82
81
  return
83
82
 
84
83
  for instance in host:
85
- if instance is not None and self._rds_utils.is_rds_instance(instance):
84
+ if instance is not None and self._rds_utils.is_rds_instance(self._rds_utils.remove_port(instance)):
86
85
  instance_endpoint = instance
87
86
  break
88
87
 
@@ -202,7 +201,7 @@ class AuroraConnectionTrackerPlugin(Plugin):
202
201
 
203
202
  def execute(self, target: object, method_name: str, execute_func: Callable, *args: Any, **kwargs: Any) -> Any:
204
203
  if self._current_writer is None or self._need_update_current_writer:
205
- self._current_writer = self._get_writer(self._plugin_service.hosts)
204
+ self._current_writer = self._get_writer(self._plugin_service.all_hosts)
206
205
  self._need_update_current_writer = False
207
206
 
208
207
  try:
@@ -210,7 +209,7 @@ class AuroraConnectionTrackerPlugin(Plugin):
210
209
 
211
210
  except Exception as e:
212
211
  # Check that e is a FailoverError and that the writer has changed
213
- if isinstance(e, FailoverError) and self._get_writer(self._plugin_service.hosts) != self._current_writer:
212
+ if isinstance(e, FailoverError) and self._get_writer(self._plugin_service.all_hosts) != self._current_writer:
214
213
  self._tracker.invalidate_all_connections(host_info=self._current_writer)
215
214
  self._tracker.log_opened_connections()
216
215
  self._need_update_current_writer = True
@@ -198,7 +198,7 @@ class AuroraInitialConnectionStrategyPlugin(Plugin):
198
198
  sleep(delay_ms / 1000)
199
199
 
200
200
  def _get_writer(self) -> Optional[HostInfo]:
201
- for host in self._plugin_service.hosts:
201
+ for host in self._plugin_service.all_hosts:
202
202
  if host.role == HostRole.WRITER:
203
203
  return host
204
204
 
@@ -225,10 +225,10 @@ class AuroraInitialConnectionStrategyPlugin(Plugin):
225
225
  init_host_provider_func(props)
226
226
 
227
227
  def _has_no_readers(self) -> bool:
228
- if len(self._plugin_service.hosts) == 0:
228
+ if len(self._plugin_service.all_hosts) == 0:
229
229
  return False
230
230
 
231
- for host in self._plugin_service.hosts:
231
+ for host in self._plugin_service.all_hosts:
232
232
  if host.role == HostRole.READER:
233
233
  return False
234
234
 
@@ -35,6 +35,7 @@ from aws_advanced_python_wrapper.utils.log import Logger
35
35
  from aws_advanced_python_wrapper.utils.messages import Messages
36
36
  from aws_advanced_python_wrapper.utils.properties import (Properties,
37
37
  WrapperProperties)
38
+ from aws_advanced_python_wrapper.utils.region_utils import RegionUtils
38
39
  from aws_advanced_python_wrapper.utils.telemetry.telemetry import \
39
40
  TelemetryTraceLevel
40
41
 
@@ -63,6 +64,7 @@ class AwsSecretsManagerPlugin(Plugin):
63
64
  Messages.get_formatted("AwsSecretsManagerPlugin.MissingRequiredConfigParameter",
64
65
  WrapperProperties.SECRETS_MANAGER_SECRET_ID.name))
65
66
 
67
+ self._region_utils = RegionUtils()
66
68
  region: str = self._get_rds_region(secret_id, props)
67
69
 
68
70
  secrets_endpoint = WrapperProperties.SECRETS_MANAGER_ENDPOINT.get(props)
@@ -194,23 +196,22 @@ class AwsSecretsManagerPlugin(Plugin):
194
196
  WrapperProperties.PASSWORD.set(properties, self._secret.password)
195
197
 
196
198
  def _get_rds_region(self, secret_id: str, props: Properties) -> str:
197
- region: Optional[str] = props.get(WrapperProperties.SECRETS_MANAGER_REGION.name)
198
- if not region:
199
- match = search(self._SECRETS_ARN_PATTERN, secret_id)
200
- if match:
201
- region = match.group("region")
202
- else:
203
- raise AwsWrapperError(
204
- Messages.get_formatted("AwsSecretsManagerPlugin.MissingRequiredConfigParameter",
205
- WrapperProperties.SECRETS_MANAGER_REGION.name))
206
-
207
199
  session = self._session if self._session else boto3.Session()
208
- if region not in session.get_available_regions("rds"):
209
- exception_message = "AwsSdk.UnsupportedRegion"
210
- logger.debug(exception_message, region)
211
- raise AwsWrapperError(Messages.get_formatted(exception_message, region))
200
+ region = self._region_utils.get_region(props, WrapperProperties.SECRETS_MANAGER_REGION.name, session=session)
201
+
202
+ if region:
203
+ return region
212
204
 
213
- return region
205
+ match = search(self._SECRETS_ARN_PATTERN, secret_id)
206
+ if match:
207
+ region = match.group("region")
208
+
209
+ if region:
210
+ return self._region_utils.verify_region(region)
211
+ else:
212
+ raise AwsWrapperError(
213
+ Messages.get_formatted("AwsSecretsManagerPlugin.MissingRequiredConfigParameter",
214
+ WrapperProperties.SECRETS_MANAGER_REGION.name))
214
215
 
215
216
 
216
217
  class AwsSecretsManagerPluginFactory(PluginFactory):
@@ -0,0 +1,344 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License").
4
+ # You may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from threading import Event, Thread
18
+ from time import perf_counter_ns, sleep
19
+ from typing import (TYPE_CHECKING, Any, Callable, ClassVar, Dict, List,
20
+ Optional, Set, Union, cast)
21
+
22
+ from aws_advanced_python_wrapper.allowed_and_blocked_hosts import \
23
+ AllowedAndBlockedHosts
24
+ from aws_advanced_python_wrapper.errors import AwsWrapperError
25
+ from aws_advanced_python_wrapper.utils.cache_map import CacheMap
26
+ from aws_advanced_python_wrapper.utils.messages import Messages
27
+ from aws_advanced_python_wrapper.utils.region_utils import RegionUtils
28
+
29
+ if TYPE_CHECKING:
30
+ from aws_advanced_python_wrapper.driver_dialect import DriverDialect
31
+ from aws_advanced_python_wrapper.hostinfo import HostInfo
32
+ from aws_advanced_python_wrapper.pep249 import Connection
33
+ from aws_advanced_python_wrapper.plugin_service import PluginService
34
+ from aws_advanced_python_wrapper.utils.properties import Properties
35
+
36
+ from enum import Enum
37
+
38
+ from boto3 import Session
39
+
40
+ from aws_advanced_python_wrapper.plugin import Plugin, PluginFactory
41
+ from aws_advanced_python_wrapper.utils.log import Logger
42
+ from aws_advanced_python_wrapper.utils.properties import WrapperProperties
43
+ from aws_advanced_python_wrapper.utils.rdsutils import RdsUtils
44
+ from aws_advanced_python_wrapper.utils.sliding_expiration_cache import \
45
+ SlidingExpirationCacheWithCleanupThread
46
+ from aws_advanced_python_wrapper.utils.telemetry.telemetry import (
47
+ TelemetryCounter, TelemetryFactory)
48
+
49
+ logger = Logger(__name__)
50
+
51
+
52
+ class CustomEndpointRoleType(Enum):
53
+ """
54
+ Enum representing the possible roles of instances specified by a custom endpoint. Note that, currently, it is not
55
+ possible to create a WRITER custom endpoint.
56
+ """
57
+ ANY = "ANY"
58
+ READER = "READER"
59
+
60
+ @classmethod
61
+ def from_string(cls, value):
62
+ return CustomEndpointRoleType(value)
63
+
64
+
65
+ class CustomEndpointInfo:
66
+ def __init__(self,
67
+ endpoint_id: str,
68
+ cluster_id: str,
69
+ endpoint: str,
70
+ role_type: CustomEndpointRoleType,
71
+ static_members: Optional[Set[str]],
72
+ excluded_members: Optional[Set[str]]):
73
+ self.endpoint_id = endpoint_id
74
+ self.cluster_id = cluster_id
75
+ self.endpoint = endpoint
76
+ self.role_type = role_type
77
+ self.static_members = None if not static_members else static_members
78
+ self.excluded_members = None if not excluded_members else excluded_members
79
+
80
+ @classmethod
81
+ def from_db_cluster_endpoint(cls, endpoint_response_info: Dict[str, Union[str, List[str]]]):
82
+ return CustomEndpointInfo(
83
+ str(endpoint_response_info.get("DBClusterEndpointIdentifier")),
84
+ str(endpoint_response_info.get("DBClusterIdentifier")),
85
+ str(endpoint_response_info.get("Endpoint")),
86
+ CustomEndpointRoleType.from_string(str(endpoint_response_info.get("CustomEndpointType"))),
87
+ set(cast('List[str]', endpoint_response_info.get("StaticMembers"))),
88
+ set(cast('List[str]', endpoint_response_info.get("ExcludedMembers")))
89
+ )
90
+
91
+ def __eq__(self, other: object):
92
+ if self is object:
93
+ return True
94
+ if not isinstance(other, CustomEndpointInfo):
95
+ return False
96
+
97
+ return self.endpoint_id == other.endpoint_id \
98
+ and self.cluster_id == other.cluster_id \
99
+ and self.endpoint == other.endpoint \
100
+ and self.role_type == other.role_type \
101
+ and self.static_members == other.static_members \
102
+ and self.excluded_members == other.excluded_members
103
+
104
+ def __hash__(self):
105
+ return hash((self.endpoint_id, self.cluster_id, self.endpoint, self.role_type))
106
+
107
+ def __str__(self):
108
+ return (f"CustomEndpointInfo[endpoint={self.endpoint}, cluster_id={self.cluster_id}, "
109
+ f"role_type={self.role_type}, endpoint_id={self.endpoint_id}, static_members={self.static_members}, "
110
+ f"excluded_members={self.excluded_members}]")
111
+
112
+
113
+ class CustomEndpointMonitor:
114
+ """
115
+ A custom endpoint monitor. This class uses a background thread to monitor a given custom endpoint for custom
116
+ endpoint information and future changes to the custom endpoint.
117
+ """
118
+ _CUSTOM_ENDPOINT_INFO_EXPIRATION_NS: ClassVar[int] = 5 * 60_000_000_000 # 5 minutes
119
+ # Keys are custom endpoint URLs, values are information objects for the associated custom endpoint.
120
+ _custom_endpoint_info_cache: ClassVar[CacheMap[str, CustomEndpointInfo]] = CacheMap()
121
+
122
+ def __init__(self,
123
+ plugin_service: PluginService,
124
+ custom_endpoint_host_info: HostInfo,
125
+ endpoint_id: str,
126
+ region: str,
127
+ refresh_rate_ns: int,
128
+ session: Optional[Session] = None):
129
+ self._plugin_service = plugin_service
130
+ self._custom_endpoint_host_info = custom_endpoint_host_info
131
+ self._endpoint_id = endpoint_id
132
+ self._region = region
133
+ self._refresh_rate_ns = refresh_rate_ns
134
+ self._session = session if session else Session()
135
+ self._client = self._session.client('rds', region_name=region)
136
+
137
+ self._stop_event = Event()
138
+ telemetry_factory = self._plugin_service.get_telemetry_factory()
139
+ self._info_changed_counter = telemetry_factory.create_counter("customEndpoint.infoChanged.counter")
140
+
141
+ self._thread = Thread(daemon=True, name="CustomEndpointMonitorThread", target=self._run)
142
+ self._thread.start()
143
+
144
+ def _run(self):
145
+ logger.debug("CustomEndpointMonitor.StartingMonitor", self._custom_endpoint_host_info.host)
146
+
147
+ try:
148
+ while not self._stop_event.is_set():
149
+ try:
150
+ start_ns = perf_counter_ns()
151
+
152
+ response = self._client.describe_db_cluster_endpoints(
153
+ DBClusterEndpointIdentifier=self._endpoint_id,
154
+ Filters=[
155
+ {
156
+ "Name": "db-cluster-endpoint-type",
157
+ "Values": ["custom"]
158
+ }
159
+ ]
160
+ )
161
+
162
+ endpoints = response["DBClusterEndpoints"]
163
+ if len(endpoints) != 1:
164
+ endpoint_hostnames = [endpoint["Endpoint"] for endpoint in endpoints]
165
+ logger.warning(
166
+ "CustomEndpointMonitor.UnexpectedNumberOfEndpoints",
167
+ self._endpoint_id,
168
+ self._region,
169
+ len(endpoints),
170
+ endpoint_hostnames)
171
+
172
+ sleep(self._refresh_rate_ns / 1_000_000_000)
173
+ continue
174
+
175
+ endpoint_info = CustomEndpointInfo.from_db_cluster_endpoint(endpoints[0])
176
+ cached_info = \
177
+ CustomEndpointMonitor._custom_endpoint_info_cache.get(self._custom_endpoint_host_info.host)
178
+ if cached_info is not None and cached_info == endpoint_info:
179
+ elapsed_time = perf_counter_ns() - start_ns
180
+ sleep_duration = max(0, self._refresh_rate_ns - elapsed_time)
181
+ sleep(sleep_duration / 1_000_000_000)
182
+ continue
183
+
184
+ logger.debug(
185
+ "CustomEndpointMonitor.DetectedChangeInCustomEndpointInfo",
186
+ self._custom_endpoint_host_info.host, endpoint_info)
187
+
188
+ # The custom endpoint info has changed, so we need to update the set of allowed/blocked hosts.
189
+ hosts = AllowedAndBlockedHosts(endpoint_info.static_members, endpoint_info.excluded_members)
190
+ self._plugin_service.allowed_and_blocked_hosts = hosts
191
+ CustomEndpointMonitor._custom_endpoint_info_cache.put(
192
+ self._custom_endpoint_host_info.host,
193
+ endpoint_info,
194
+ CustomEndpointMonitor._CUSTOM_ENDPOINT_INFO_EXPIRATION_NS)
195
+ self._info_changed_counter.inc()
196
+
197
+ elapsed_time = perf_counter_ns() - start_ns
198
+ sleep_duration = max(0, self._refresh_rate_ns - elapsed_time)
199
+ sleep(sleep_duration / 1_000_000_000)
200
+ continue
201
+ except InterruptedError as e:
202
+ raise e
203
+ except Exception as e:
204
+ # If the exception is not an InterruptedError, log it and continue monitoring.
205
+ logger.error("CustomEndpointMonitor.Exception", self._custom_endpoint_host_info.host, e)
206
+ except InterruptedError:
207
+ logger.info("CustomEndpointMonitor.Interrupted", self._custom_endpoint_host_info.host)
208
+ finally:
209
+ CustomEndpointMonitor._custom_endpoint_info_cache.remove(self._custom_endpoint_host_info.host)
210
+ self._stop_event.set()
211
+ self._client.close()
212
+ logger.debug("CustomEndpointMonitor.StoppedMonitor", self._custom_endpoint_host_info.host)
213
+
214
+ def has_custom_endpoint_info(self):
215
+ return CustomEndpointMonitor._custom_endpoint_info_cache.get(self._custom_endpoint_host_info.host) is not None
216
+
217
+ def close(self):
218
+ logger.debug("CustomEndpointMonitor.StoppingMonitor", self._custom_endpoint_host_info.host)
219
+ CustomEndpointMonitor._custom_endpoint_info_cache.remove(self._custom_endpoint_host_info.host)
220
+ self._stop_event.set()
221
+
222
+
223
+ class CustomEndpointPlugin(Plugin):
224
+ """
225
+ A plugin that analyzes custom endpoints for custom endpoint information and custom endpoint changes, such as adding
226
+ or removing an instance in the custom endpoint.
227
+ """
228
+ _SUBSCRIBED_METHODS: ClassVar[Set[str]] = {"connect"}
229
+ _CACHE_CLEANUP_RATE_NS: ClassVar[int] = 6 * 10 ^ 10 # 1 minute
230
+ _monitors: ClassVar[SlidingExpirationCacheWithCleanupThread[str, CustomEndpointMonitor]] = \
231
+ SlidingExpirationCacheWithCleanupThread(_CACHE_CLEANUP_RATE_NS,
232
+ should_dispose_func=lambda _: True,
233
+ item_disposal_func=lambda monitor: monitor.close())
234
+
235
+ def __init__(self, plugin_service: PluginService, props: Properties):
236
+ self._plugin_service = plugin_service
237
+ self._props = props
238
+
239
+ self._should_wait_for_info: bool = WrapperProperties.WAIT_FOR_CUSTOM_ENDPOINT_INFO.get_bool(self._props)
240
+ self._wait_for_info_timeout_ms: int = WrapperProperties.WAIT_FOR_CUSTOM_ENDPOINT_INFO_TIMEOUT_MS.get_int(self._props)
241
+ self._idle_monitor_expiration_ms: int = \
242
+ WrapperProperties.CUSTOM_ENDPOINT_IDLE_MONITOR_EXPIRATION_MS.get_int(self._props)
243
+
244
+ self._rds_utils = RdsUtils()
245
+ self._region_utils = RegionUtils()
246
+ self._region: Optional[str] = None
247
+ self._custom_endpoint_host_info: Optional[HostInfo] = None
248
+ self._custom_endpoint_id: Optional[str] = None
249
+ telemetry_factory: TelemetryFactory = self._plugin_service.get_telemetry_factory()
250
+ self._wait_for_info_counter: TelemetryCounter = telemetry_factory.create_counter("customEndpoint.waitForInfo.counter")
251
+
252
+ CustomEndpointPlugin._SUBSCRIBED_METHODS.update(self._plugin_service.network_bound_methods)
253
+
254
+ @property
255
+ def subscribed_methods(self) -> Set[str]:
256
+ return CustomEndpointPlugin._SUBSCRIBED_METHODS
257
+
258
+ def connect(
259
+ self,
260
+ target_driver_func: Callable,
261
+ driver_dialect: DriverDialect,
262
+ host_info: HostInfo,
263
+ props: Properties,
264
+ is_initial_connection: bool,
265
+ connect_func: Callable) -> Connection:
266
+ if not self._rds_utils.is_rds_custom_cluster_dns(host_info.host):
267
+ return connect_func()
268
+
269
+ self._custom_endpoint_host_info = host_info
270
+ logger.debug("CustomEndpointPlugin.ConnectionRequestToCustomEndpoint", host_info.host)
271
+
272
+ self._custom_endpoint_id = self._rds_utils.get_cluster_id(host_info.host)
273
+ if not self._custom_endpoint_id:
274
+ raise AwsWrapperError(
275
+ Messages.get_formatted(
276
+ "CustomEndpointPlugin.ErrorParsingEndpointIdentifier", self._custom_endpoint_host_info.host))
277
+
278
+ hostname = self._custom_endpoint_host_info.host
279
+ self._region = self._region_utils.get_region_from_hostname(hostname)
280
+ if not self._region:
281
+ error_message = "RdsUtils.UnsupportedHostname"
282
+ logger.debug(error_message, hostname)
283
+ raise AwsWrapperError(Messages.get_formatted(error_message, hostname))
284
+
285
+ monitor = self._create_monitor_if_absent(props)
286
+ if self._should_wait_for_info:
287
+ self._wait_for_info(monitor)
288
+
289
+ return connect_func()
290
+
291
+ def _create_monitor_if_absent(self, props: Properties) -> CustomEndpointMonitor:
292
+ host_info = cast('HostInfo', self._custom_endpoint_host_info)
293
+ endpoint_id = cast('str', self._custom_endpoint_id)
294
+ region = cast('str', self._region)
295
+ monitor = CustomEndpointPlugin._monitors.compute_if_absent(
296
+ host_info.host,
297
+ lambda key: CustomEndpointMonitor(
298
+ self._plugin_service,
299
+ host_info,
300
+ endpoint_id,
301
+ region,
302
+ WrapperProperties.CUSTOM_ENDPOINT_INFO_REFRESH_RATE_MS.get_int(props) * 1_000_000),
303
+ self._idle_monitor_expiration_ms * 1_000_000)
304
+
305
+ return cast('CustomEndpointMonitor', monitor)
306
+
307
+ def _wait_for_info(self, monitor: CustomEndpointMonitor):
308
+ has_info = monitor.has_custom_endpoint_info()
309
+ if has_info:
310
+ return
311
+
312
+ self._wait_for_info_counter.inc()
313
+ host_info = cast('HostInfo', self._custom_endpoint_host_info)
314
+ hostname = host_info.host
315
+ logger.debug("CustomEndpointPlugin.WaitingForCustomEndpointInfo", hostname, self._wait_for_info_timeout_ms)
316
+ wait_for_info_timeout_ns = perf_counter_ns() + self._wait_for_info_timeout_ms * 1_000_000
317
+
318
+ try:
319
+ while not has_info and perf_counter_ns() < wait_for_info_timeout_ns:
320
+ sleep(0.1)
321
+ has_info = monitor.has_custom_endpoint_info()
322
+ except InterruptedError:
323
+ raise AwsWrapperError(Messages.get_formatted("CustomEndpointPlugin.InterruptedThread", hostname))
324
+
325
+ if not has_info:
326
+ raise AwsWrapperError(
327
+ Messages.get_formatted(
328
+ "CustomEndpointPlugin.TimedOutWaitingForCustomEndpointInfo",
329
+ self._wait_for_info_timeout_ms, hostname))
330
+
331
+ def execute(self, target: type, method_name: str, execute_func: Callable, *args: Any, **kwargs: Any) -> Any:
332
+ if self._custom_endpoint_host_info is None:
333
+ return execute_func()
334
+
335
+ monitor = self._create_monitor_if_absent(self._props)
336
+ if self._should_wait_for_info:
337
+ self._wait_for_info(monitor)
338
+
339
+ return execute_func()
340
+
341
+
342
+ class CustomEndpointPluginFactory(PluginFactory):
343
+ def get_instance(self, plugin_service: PluginService, props: Properties) -> Plugin:
344
+ return CustomEndpointPlugin(plugin_service, props)
@@ -13,7 +13,6 @@
13
13
  # limitations under the License.
14
14
 
15
15
  class DriverDialectCodes:
16
- SQLALCHEMY = "sqlalchemy"
17
16
  PSYCOPG = "psycopg"
18
17
  MYSQL_CONNECTOR_PYTHON = "mysql-connector-python"
19
18
  GENERIC = "generic"
@@ -15,4 +15,4 @@
15
15
 
16
16
  class DriverInfo:
17
17
  DRIVER_NAME = "aws_advanced_python_wrapper"
18
- DRIVER_VERSION = "1.0.0-rc"
18
+ DRIVER_VERSION = "1.2.0"
@@ -25,6 +25,7 @@ if TYPE_CHECKING:
25
25
 
26
26
  from typing import Any, Callable, Dict, Optional, Set
27
27
 
28
+ from aws_advanced_python_wrapper import LogUtils
28
29
  from aws_advanced_python_wrapper.errors import (
29
30
  AwsWrapperError, FailoverFailedError, FailoverSuccessError,
30
31
  TransactionResolutionUnknownError)
@@ -328,17 +329,23 @@ class FailoverPlugin(Plugin):
328
329
  try:
329
330
  logger.info("FailoverPlugin.StartWriterFailover")
330
331
 
331
- result: WriterFailoverResult = self._writer_failover_handler.failover(self._plugin_service.hosts)
332
-
332
+ result: WriterFailoverResult = self._writer_failover_handler.failover(self._plugin_service.all_hosts)
333
333
  if result is not None and result.exception is not None:
334
334
  raise result.exception
335
335
  elif result is None or not result.is_connected:
336
336
  raise FailoverFailedError(Messages.get("FailoverPlugin.UnableToConnectToWriter"))
337
337
 
338
338
  writer_host = self._get_writer(result.topology)
339
+ allowed_hosts = self._plugin_service.hosts
340
+ allowed_hostnames = [host.host for host in allowed_hosts]
341
+ if writer_host.host not in allowed_hostnames:
342
+ raise FailoverFailedError(
343
+ Messages.get_formatted(
344
+ "FailoverPlugin.NewWriterNotAllowed",
345
+ "<null>" if writer_host is None else writer_host.host,
346
+ LogUtils.log_topology(allowed_hosts)))
339
347
 
340
348
  self._plugin_service.set_current_connection(result.new_connection, writer_host)
341
-
342
349
  logger.info("FailoverPlugin.EstablishedConnection", self._plugin_service.current_host_info)
343
350
 
344
351
  self._plugin_service.refresh_host_list()
@@ -438,11 +445,11 @@ class FailoverPlugin(Plugin):
438
445
  def _is_failover_enabled(self) -> bool:
439
446
  return self._enable_failover_setting and \
440
447
  self._rds_url_type != RdsUrlType.RDS_PROXY and \
441
- self._plugin_service.hosts is not None and \
442
- len(self._plugin_service.hosts) > 0
448
+ self._plugin_service.all_hosts is not None and \
449
+ len(self._plugin_service.all_hosts) > 0
443
450
 
444
451
  def _get_current_writer(self) -> Optional[HostInfo]:
445
- topology = self._plugin_service.hosts
452
+ topology = self._plugin_service.all_hosts
446
453
  if topology is None:
447
454
  return None
448
455
 
@@ -50,6 +50,8 @@ MAX_VALUE = 2147483647
50
50
  class FastestResponseStrategyPlugin(Plugin):
51
51
  _FASTEST_RESPONSE_STRATEGY_NAME = "fastest_response"
52
52
  _SUBSCRIBED_METHODS: Set[str] = {"accepts_strategy",
53
+ "connect",
54
+ "force_connect",
53
55
  "get_host_info_by_strategy",
54
56
  "notify_host_list_changed"}
55
57
 
@@ -111,7 +113,7 @@ class FastestResponseStrategyPlugin(Plugin):
111
113
  fastest_response_host: Optional[HostInfo] = self._cached_fastest_response_host_by_role.get(role.name)
112
114
  if fastest_response_host is not None:
113
115
 
114
- # Found a fastest host. Let's find it in the the latest topology.
116
+ # Found a fastest host. Let's find it in the latest topology.
115
117
  for host in self._plugin_service.hosts:
116
118
  if host == fastest_response_host:
117
119
  # found the fastest host in the topology
@@ -22,6 +22,7 @@ from urllib.parse import urlencode
22
22
  from aws_advanced_python_wrapper.credentials_provider_factory import (
23
23
  CredentialsProviderFactory, SamlCredentialsProviderFactory)
24
24
  from aws_advanced_python_wrapper.utils.iam_utils import IamAuthUtils, TokenInfo
25
+ from aws_advanced_python_wrapper.utils.region_utils import RegionUtils
25
26
  from aws_advanced_python_wrapper.utils.saml_utils import SamlUtils
26
27
 
27
28
  if TYPE_CHECKING:
@@ -59,6 +60,7 @@ class FederatedAuthPlugin(Plugin):
59
60
  self._credentials_provider_factory = credentials_provider_factory
60
61
  self._session = session
61
62
 
63
+ self._region_utils = RegionUtils()
62
64
  telemetry_factory = self._plugin_service.get_telemetry_factory()
63
65
  self._fetch_token_counter = telemetry_factory.create_counter("federated.fetch_token.count")
64
66
  self._cache_size_gauge = telemetry_factory.create_gauge("federated.token_cache.size", lambda: len(FederatedAuthPlugin._token_cache))
@@ -82,7 +84,11 @@ class FederatedAuthPlugin(Plugin):
82
84
 
83
85
  host = IamAuthUtils.get_iam_host(props, host_info)
84
86
  port = IamAuthUtils.get_port(props, host_info, self._plugin_service.database_dialect.default_port)
85
- region: str = IamAuthUtils.get_rds_region(self._rds_utils, host, props, self._session)
87
+ region = self._region_utils.get_region(props, WrapperProperties.IAM_REGION.name, host, self._session)
88
+ if not region:
89
+ error_message = "RdsUtils.UnsupportedHostname"
90
+ logger.debug(error_message, host)
91
+ raise AwsWrapperError(Messages.get_formatted(error_message, host))
86
92
 
87
93
  user = WrapperProperties.DB_USER.get(props)
88
94
  cache_key: str = IamAuthUtils.get_cache_key(