config_ninja.contrib.appconfig
Integrate with the AWS AppConfig service.
Example
The following config-ninja settings file configures the AppConfigBackend to install
/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json from the latest version deployed
through AWS AppConfig:
# the following top-level key is required
CONFIG_NINJA_OBJECTS:
# each second-level key identifies a config-ninja object
example-0:
# set the location that the object is written to
dest:
format: json
path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
# specify where the object is stored / retrieved from
source:
backend: appconfig
format: json
# instantiate the backend class using its 'new()' method
new:
kwargs:
application_name: Sample Application
configuration_profile_name: /dev/amazon-cloudwatch-agent.json
environment_name: dev
1"""Integrate with the AWS AppConfig service. 2 3## Example 4 5The following `config-ninja`_ settings file configures the `AppConfigBackend` to install 6`/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json` from the latest version deployed 7through AWS AppConfig: 8 9```yaml 10.. include:: ../../../examples/appconfig-backend.yaml 11``` 12 13.. _config-ninja: https://bryant-finney.github.io/config-ninja/config_ninja.html 14""" 15 16from __future__ import annotations 17 18import asyncio 19import logging 20import warnings 21from typing import TYPE_CHECKING, Any, AsyncIterator, Literal 22 23import boto3 24 25from config_ninja.backend import Backend 26 27try: # pragma: no cover 28 from typing import TypeAlias # type: ignore[attr-defined,unused-ignore] 29except ImportError: # pragma: no cover 30 from typing_extensions import TypeAlias 31 32 33if TYPE_CHECKING: # pragma: no cover 34 from botocore.paginate import PageIterator 35 from mypy_boto3_appconfig.client import AppConfigClient 36 from mypy_boto3_appconfigdata import AppConfigDataClient 37 38__all__ = ['AppConfigBackend'] 39 40MINIMUM_POLL_INTERVAL_SECONDS = 60 41 42 43OperationName: TypeAlias = Literal['list_applications', 'list_configuration_profiles', 'list_environments'] 44 45logger = logging.getLogger(__name__) 46 47 48class AppConfigBackend(Backend): 49 """Retrieve the deployed configuration from AWS AppConfig. 50 51 ## Usage 52 53 To retrieve the configuration, use the `AppConfigBackend.get()` method: 54 55 >>> backend = AppConfigBackend(appconfigdata_client, 'app-id', 'conf-id', 'env-id') 56 >>> print(backend.get()) 57 key_0: value_0 58 key_1: 1 59 key_2: true 60 key_3: 61 - 1 62 - 2 63 - 3 64 """ 65 66 client: AppConfigDataClient 67 """The `boto3` client used to communicate with the AWS AppConfig service.""" 68 69 application_id: str 70 """See [Creating a namespace for your application in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-namespace.html)""" 71 configuration_profile_id: str 72 """See [Creating a configuration profile in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-profile.html)""" 73 environment_id: str 74 """See [Creating environments for your application in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-environment.html)""" 75 76 def __init__( 77 self, 78 client: AppConfigDataClient, 79 app_id: str, 80 config_profile_id: str, 81 env_id: str, 82 ) -> None: 83 """Initialize the backend.""" 84 logger.debug( 85 "Initialize: %s(client=%s, app_id='%s', conf_id='%s', env_id='%s')", 86 self.__class__.__name__, 87 client, 88 app_id, 89 config_profile_id, 90 env_id, 91 ) 92 self.client = client 93 94 self.application_id = app_id 95 self.configuration_profile_id = config_profile_id 96 self.environment_id = env_id 97 98 def __str__(self) -> str: 99 """Include properties in the string representation. 100 101 >>> print(str( AppConfigBackend(appconfigdata_client, 'app-id', 'conf-id', 'env-id') )) 102 boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='app-id', ConfigurationProfileIdentifier='conf-id', EnvironmentIdentifier='env-id') 103 """ 104 return ( 105 "boto3.client('appconfigdata').start_configuration_session(" 106 f"ApplicationIdentifier='{self.application_id}', " 107 f"ConfigurationProfileIdentifier='{self.configuration_profile_id}', " 108 f"EnvironmentIdentifier='{self.environment_id}')" 109 ) 110 111 @staticmethod 112 def _get_id_from_name(name: str, operation_name: OperationName, client: AppConfigClient, **kwargs: Any) -> str: 113 page_iterator: PageIterator = client.get_paginator(operation_name).paginate(**kwargs) 114 ids: list[str] = list(page_iterator.search(f'Items[?Name == `{name}`].Id')) 115 116 if not ids: 117 raise ValueError(f'no "{operation_name}" results found for Name="{name}"') 118 119 if len(ids) > 1: 120 warnings.warn( 121 f"'{operation_name}' found {len(ids)} results for Name='{name}'; " 122 f"'{ids[0]}' will be used and the others ignored: {ids[1:]}", 123 category=RuntimeWarning, 124 stacklevel=3, 125 ) 126 127 return ids[0] 128 129 def get(self) -> str: 130 """Retrieve the latest configuration deployment as a string.""" 131 logger.debug('Retrieve latest configuration (%s)', self) 132 token = self.client.start_configuration_session( 133 ApplicationIdentifier=self.application_id, 134 EnvironmentIdentifier=self.environment_id, 135 ConfigurationProfileIdentifier=self.configuration_profile_id, 136 RequiredMinimumPollIntervalInSeconds=MINIMUM_POLL_INTERVAL_SECONDS, 137 )['InitialConfigurationToken'] 138 139 resp = self.client.get_latest_configuration(ConfigurationToken=token) 140 return resp['Configuration'].read().decode() 141 142 @classmethod 143 def get_application_id(cls, name: str, client: AppConfigClient) -> str: 144 """Retrieve the application ID for the given application name.""" 145 return cls._get_id_from_name(name, 'list_applications', client) 146 147 @classmethod 148 def get_configuration_profile_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 149 """Retrieve the configuration profile ID for the given configuration profile name.""" 150 return cls._get_id_from_name(name, 'list_configuration_profiles', client, ApplicationId=application_id) 151 152 @classmethod 153 def get_environment_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 154 """Retrieve the environment ID for the given environment name & application ID.""" 155 return cls._get_id_from_name(name, 'list_environments', client, ApplicationId=application_id) 156 157 @classmethod 158 def new( # pylint: disable=arguments-differ # pyright: ignore[reportIncompatibleMethodOverride] 159 cls, 160 application_name: str, 161 configuration_profile_name: str, 162 environment_name: str, 163 session: boto3.Session | None = None, 164 ) -> AppConfigBackend: 165 """Create a new instance of the backend. 166 167 ## Usage: `AppConfigBackend.new()` 168 169 <!-- fixture is used for doctest but excluded from documentation 170 >>> session = getfixture('mock_session_with_1_id') 171 172 --> 173 174 Use `boto3` to fetch IDs for based on name: 175 176 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 177 >>> print(f"{backend}") 178 boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='id-1', ConfigurationProfileIdentifier='id-1', EnvironmentIdentifier='id-1') 179 180 ### Error: No IDs Found 181 182 >>> session = getfixture('mock_session_with_0_ids') # fixture for doctest 183 184 A `ValueError` is raised if no IDs are found for the given name: 185 186 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 187 Traceback (most recent call last): 188 ... 189 ValueError: no "list_applications" results found for Name="app-name" 190 191 ### Warning: Multiple IDs Found 192 193 >>> session = getfixture('mock_session_with_2_ids') 194 195 The first ID is used and the others ignored. 196 197 >>> with pytest.warns(RuntimeWarning): 198 ... backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 199 """ 200 logger.info( 201 'Create new instance: %s(app="%s", conf="%s", env="%s")', 202 cls.__name__, 203 application_name, 204 configuration_profile_name, 205 environment_name, 206 ) 207 208 session = session or boto3.Session() 209 appconfig_client = session.client('appconfig') # pyright: ignore[reportUnknownMemberType] 210 application_id = cls.get_application_id(application_name, appconfig_client) 211 configuration_profile_id = cls.get_configuration_profile_id( 212 configuration_profile_name, appconfig_client, application_id 213 ) 214 environment_id = cls.get_environment_id(environment_name, appconfig_client, application_id) 215 216 client: AppConfigDataClient = session.client('appconfigdata') # pyright: ignore[reportUnknownMemberType] 217 218 return cls(client, application_id, configuration_profile_id, environment_id) 219 220 async def poll(self, interval: int = MINIMUM_POLL_INTERVAL_SECONDS) -> AsyncIterator[str]: 221 """Poll the AppConfig service for configuration changes. 222 223 .. note:: 224 Methods written for `asyncio` need to jump through hoops to run as `doctest` tests. 225 To improve the readability of this documentation, each Python code block corresponds to 226 a `doctest` test defined in a private method. 227 228 ## Usage: `AppConfigBackend.poll()` 229 230 ```py 231 In [1]: async for content in backend.poll(): 232 ...: print(content) # ← executes each time the configuration changes 233 ``` 234 ```yaml 235 key_0: value_0 236 key_1: 1 237 key_2: true 238 key_3: 239 - 1 240 - 2 241 - 3 242 ``` 243 244 .. note:: 245 If polling is done too quickly, the AWS AppConfig client will raise a 246 `BadRequestException`. This is handled automatically by the backend, which will retry 247 the request after waiting for half the given `interval`. 248 """ 249 token = self.client.start_configuration_session( 250 ApplicationIdentifier=self.application_id, 251 EnvironmentIdentifier=self.environment_id, 252 ConfigurationProfileIdentifier=self.configuration_profile_id, 253 RequiredMinimumPollIntervalInSeconds=interval, 254 )['InitialConfigurationToken'] 255 256 while True: 257 logger.debug('Poll for configuration changes') 258 try: 259 resp = self.client.get_latest_configuration(ConfigurationToken=token) 260 except self.client.exceptions.BadRequestException as exc: 261 if exc.response['Error']['Message'] != 'Request too early': # pragma: no cover 262 raise 263 logger.debug('Request too early; retrying in %d seconds', interval / 2) 264 await asyncio.sleep(interval / 2) 265 continue 266 267 token = resp['NextPollConfigurationToken'] 268 if content := resp['Configuration'].read(): 269 yield content.decode() 270 else: 271 logger.debug('No configuration changes') 272 273 await asyncio.sleep(resp['NextPollIntervalInSeconds']) 274 275 def _async_doctests(self) -> None: 276 """Define `async` `doctest` tests in this method to improve documentation. 277 278 Verify that an empty response to the `boto3` client is handled and the polling continues: 279 >>> backend = AppConfigBackend(appconfigdata_client_first_empty, 'app-id', 'conf-id', 'env-id') 280 >>> content = asyncio.run(anext(backend.poll(interval=0.01))) 281 >>> print(content) 282 key_0: value_0 283 key_1: 1 284 key_2: true 285 key_3: 286 - 1 287 - 2 288 - 3 289 290 291 >>> client = getfixture('mock_poll_too_early') # seed a `BadRequestException` 292 293 >>> backend = AppConfigBackend(client, 'app-id', 'conf-id', 'env-id') 294 >>> content = asyncio.run(anext(backend.poll(interval=0.01))) # it is handled successfully 295 >>> print(content) 296 key_0: value_0 297 key_1: 1 298 key_2: true 299 key_3: 300 - 1 301 - 2 302 - 3 303 """ 304 305 306logger.debug('successfully imported %s', __name__)
49class AppConfigBackend(Backend): 50 """Retrieve the deployed configuration from AWS AppConfig. 51 52 ## Usage 53 54 To retrieve the configuration, use the `AppConfigBackend.get()` method: 55 56 >>> backend = AppConfigBackend(appconfigdata_client, 'app-id', 'conf-id', 'env-id') 57 >>> print(backend.get()) 58 key_0: value_0 59 key_1: 1 60 key_2: true 61 key_3: 62 - 1 63 - 2 64 - 3 65 """ 66 67 client: AppConfigDataClient 68 """The `boto3` client used to communicate with the AWS AppConfig service.""" 69 70 application_id: str 71 """See [Creating a namespace for your application in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-namespace.html)""" 72 configuration_profile_id: str 73 """See [Creating a configuration profile in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-profile.html)""" 74 environment_id: str 75 """See [Creating environments for your application in AWS AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-environment.html)""" 76 77 def __init__( 78 self, 79 client: AppConfigDataClient, 80 app_id: str, 81 config_profile_id: str, 82 env_id: str, 83 ) -> None: 84 """Initialize the backend.""" 85 logger.debug( 86 "Initialize: %s(client=%s, app_id='%s', conf_id='%s', env_id='%s')", 87 self.__class__.__name__, 88 client, 89 app_id, 90 config_profile_id, 91 env_id, 92 ) 93 self.client = client 94 95 self.application_id = app_id 96 self.configuration_profile_id = config_profile_id 97 self.environment_id = env_id 98 99 def __str__(self) -> str: 100 """Include properties in the string representation. 101 102 >>> print(str( AppConfigBackend(appconfigdata_client, 'app-id', 'conf-id', 'env-id') )) 103 boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='app-id', ConfigurationProfileIdentifier='conf-id', EnvironmentIdentifier='env-id') 104 """ 105 return ( 106 "boto3.client('appconfigdata').start_configuration_session(" 107 f"ApplicationIdentifier='{self.application_id}', " 108 f"ConfigurationProfileIdentifier='{self.configuration_profile_id}', " 109 f"EnvironmentIdentifier='{self.environment_id}')" 110 ) 111 112 @staticmethod 113 def _get_id_from_name(name: str, operation_name: OperationName, client: AppConfigClient, **kwargs: Any) -> str: 114 page_iterator: PageIterator = client.get_paginator(operation_name).paginate(**kwargs) 115 ids: list[str] = list(page_iterator.search(f'Items[?Name == `{name}`].Id')) 116 117 if not ids: 118 raise ValueError(f'no "{operation_name}" results found for Name="{name}"') 119 120 if len(ids) > 1: 121 warnings.warn( 122 f"'{operation_name}' found {len(ids)} results for Name='{name}'; " 123 f"'{ids[0]}' will be used and the others ignored: {ids[1:]}", 124 category=RuntimeWarning, 125 stacklevel=3, 126 ) 127 128 return ids[0] 129 130 def get(self) -> str: 131 """Retrieve the latest configuration deployment as a string.""" 132 logger.debug('Retrieve latest configuration (%s)', self) 133 token = self.client.start_configuration_session( 134 ApplicationIdentifier=self.application_id, 135 EnvironmentIdentifier=self.environment_id, 136 ConfigurationProfileIdentifier=self.configuration_profile_id, 137 RequiredMinimumPollIntervalInSeconds=MINIMUM_POLL_INTERVAL_SECONDS, 138 )['InitialConfigurationToken'] 139 140 resp = self.client.get_latest_configuration(ConfigurationToken=token) 141 return resp['Configuration'].read().decode() 142 143 @classmethod 144 def get_application_id(cls, name: str, client: AppConfigClient) -> str: 145 """Retrieve the application ID for the given application name.""" 146 return cls._get_id_from_name(name, 'list_applications', client) 147 148 @classmethod 149 def get_configuration_profile_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 150 """Retrieve the configuration profile ID for the given configuration profile name.""" 151 return cls._get_id_from_name(name, 'list_configuration_profiles', client, ApplicationId=application_id) 152 153 @classmethod 154 def get_environment_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 155 """Retrieve the environment ID for the given environment name & application ID.""" 156 return cls._get_id_from_name(name, 'list_environments', client, ApplicationId=application_id) 157 158 @classmethod 159 def new( # pylint: disable=arguments-differ # pyright: ignore[reportIncompatibleMethodOverride] 160 cls, 161 application_name: str, 162 configuration_profile_name: str, 163 environment_name: str, 164 session: boto3.Session | None = None, 165 ) -> AppConfigBackend: 166 """Create a new instance of the backend. 167 168 ## Usage: `AppConfigBackend.new()` 169 170 <!-- fixture is used for doctest but excluded from documentation 171 >>> session = getfixture('mock_session_with_1_id') 172 173 --> 174 175 Use `boto3` to fetch IDs for based on name: 176 177 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 178 >>> print(f"{backend}") 179 boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='id-1', ConfigurationProfileIdentifier='id-1', EnvironmentIdentifier='id-1') 180 181 ### Error: No IDs Found 182 183 >>> session = getfixture('mock_session_with_0_ids') # fixture for doctest 184 185 A `ValueError` is raised if no IDs are found for the given name: 186 187 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 188 Traceback (most recent call last): 189 ... 190 ValueError: no "list_applications" results found for Name="app-name" 191 192 ### Warning: Multiple IDs Found 193 194 >>> session = getfixture('mock_session_with_2_ids') 195 196 The first ID is used and the others ignored. 197 198 >>> with pytest.warns(RuntimeWarning): 199 ... backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 200 """ 201 logger.info( 202 'Create new instance: %s(app="%s", conf="%s", env="%s")', 203 cls.__name__, 204 application_name, 205 configuration_profile_name, 206 environment_name, 207 ) 208 209 session = session or boto3.Session() 210 appconfig_client = session.client('appconfig') # pyright: ignore[reportUnknownMemberType] 211 application_id = cls.get_application_id(application_name, appconfig_client) 212 configuration_profile_id = cls.get_configuration_profile_id( 213 configuration_profile_name, appconfig_client, application_id 214 ) 215 environment_id = cls.get_environment_id(environment_name, appconfig_client, application_id) 216 217 client: AppConfigDataClient = session.client('appconfigdata') # pyright: ignore[reportUnknownMemberType] 218 219 return cls(client, application_id, configuration_profile_id, environment_id) 220 221 async def poll(self, interval: int = MINIMUM_POLL_INTERVAL_SECONDS) -> AsyncIterator[str]: 222 """Poll the AppConfig service for configuration changes. 223 224 .. note:: 225 Methods written for `asyncio` need to jump through hoops to run as `doctest` tests. 226 To improve the readability of this documentation, each Python code block corresponds to 227 a `doctest` test defined in a private method. 228 229 ## Usage: `AppConfigBackend.poll()` 230 231 ```py 232 In [1]: async for content in backend.poll(): 233 ...: print(content) # ← executes each time the configuration changes 234 ``` 235 ```yaml 236 key_0: value_0 237 key_1: 1 238 key_2: true 239 key_3: 240 - 1 241 - 2 242 - 3 243 ``` 244 245 .. note:: 246 If polling is done too quickly, the AWS AppConfig client will raise a 247 `BadRequestException`. This is handled automatically by the backend, which will retry 248 the request after waiting for half the given `interval`. 249 """ 250 token = self.client.start_configuration_session( 251 ApplicationIdentifier=self.application_id, 252 EnvironmentIdentifier=self.environment_id, 253 ConfigurationProfileIdentifier=self.configuration_profile_id, 254 RequiredMinimumPollIntervalInSeconds=interval, 255 )['InitialConfigurationToken'] 256 257 while True: 258 logger.debug('Poll for configuration changes') 259 try: 260 resp = self.client.get_latest_configuration(ConfigurationToken=token) 261 except self.client.exceptions.BadRequestException as exc: 262 if exc.response['Error']['Message'] != 'Request too early': # pragma: no cover 263 raise 264 logger.debug('Request too early; retrying in %d seconds', interval / 2) 265 await asyncio.sleep(interval / 2) 266 continue 267 268 token = resp['NextPollConfigurationToken'] 269 if content := resp['Configuration'].read(): 270 yield content.decode() 271 else: 272 logger.debug('No configuration changes') 273 274 await asyncio.sleep(resp['NextPollIntervalInSeconds']) 275 276 def _async_doctests(self) -> None: 277 """Define `async` `doctest` tests in this method to improve documentation. 278 279 Verify that an empty response to the `boto3` client is handled and the polling continues: 280 >>> backend = AppConfigBackend(appconfigdata_client_first_empty, 'app-id', 'conf-id', 'env-id') 281 >>> content = asyncio.run(anext(backend.poll(interval=0.01))) 282 >>> print(content) 283 key_0: value_0 284 key_1: 1 285 key_2: true 286 key_3: 287 - 1 288 - 2 289 - 3 290 291 292 >>> client = getfixture('mock_poll_too_early') # seed a `BadRequestException` 293 294 >>> backend = AppConfigBackend(client, 'app-id', 'conf-id', 'env-id') 295 >>> content = asyncio.run(anext(backend.poll(interval=0.01))) # it is handled successfully 296 >>> print(content) 297 key_0: value_0 298 key_1: 1 299 key_2: true 300 key_3: 301 - 1 302 - 2 303 - 3 304 """
Retrieve the deployed configuration from AWS AppConfig.
Usage
To retrieve the configuration, use the AppConfigBackend.get() method:
>>> backend = AppConfigBackend(appconfigdata_client, 'app-id', 'conf-id', 'env-id')
>>> print(backend.get())
key_0: value_0
key_1: 1
key_2: true
key_3:
- 1
- 2
- 3
77 def __init__( 78 self, 79 client: AppConfigDataClient, 80 app_id: str, 81 config_profile_id: str, 82 env_id: str, 83 ) -> None: 84 """Initialize the backend.""" 85 logger.debug( 86 "Initialize: %s(client=%s, app_id='%s', conf_id='%s', env_id='%s')", 87 self.__class__.__name__, 88 client, 89 app_id, 90 config_profile_id, 91 env_id, 92 ) 93 self.client = client 94 95 self.application_id = app_id 96 self.configuration_profile_id = config_profile_id 97 self.environment_id = env_id
Initialize the backend.
130 def get(self) -> str: 131 """Retrieve the latest configuration deployment as a string.""" 132 logger.debug('Retrieve latest configuration (%s)', self) 133 token = self.client.start_configuration_session( 134 ApplicationIdentifier=self.application_id, 135 EnvironmentIdentifier=self.environment_id, 136 ConfigurationProfileIdentifier=self.configuration_profile_id, 137 RequiredMinimumPollIntervalInSeconds=MINIMUM_POLL_INTERVAL_SECONDS, 138 )['InitialConfigurationToken'] 139 140 resp = self.client.get_latest_configuration(ConfigurationToken=token) 141 return resp['Configuration'].read().decode()
Retrieve the latest configuration deployment as a string.
143 @classmethod 144 def get_application_id(cls, name: str, client: AppConfigClient) -> str: 145 """Retrieve the application ID for the given application name.""" 146 return cls._get_id_from_name(name, 'list_applications', client)
Retrieve the application ID for the given application name.
148 @classmethod 149 def get_configuration_profile_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 150 """Retrieve the configuration profile ID for the given configuration profile name.""" 151 return cls._get_id_from_name(name, 'list_configuration_profiles', client, ApplicationId=application_id)
Retrieve the configuration profile ID for the given configuration profile name.
153 @classmethod 154 def get_environment_id(cls, name: str, client: AppConfigClient, application_id: str) -> str: 155 """Retrieve the environment ID for the given environment name & application ID.""" 156 return cls._get_id_from_name(name, 'list_environments', client, ApplicationId=application_id)
Retrieve the environment ID for the given environment name & application ID.
158 @classmethod 159 def new( # pylint: disable=arguments-differ # pyright: ignore[reportIncompatibleMethodOverride] 160 cls, 161 application_name: str, 162 configuration_profile_name: str, 163 environment_name: str, 164 session: boto3.Session | None = None, 165 ) -> AppConfigBackend: 166 """Create a new instance of the backend. 167 168 ## Usage: `AppConfigBackend.new()` 169 170 <!-- fixture is used for doctest but excluded from documentation 171 >>> session = getfixture('mock_session_with_1_id') 172 173 --> 174 175 Use `boto3` to fetch IDs for based on name: 176 177 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 178 >>> print(f"{backend}") 179 boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='id-1', ConfigurationProfileIdentifier='id-1', EnvironmentIdentifier='id-1') 180 181 ### Error: No IDs Found 182 183 >>> session = getfixture('mock_session_with_0_ids') # fixture for doctest 184 185 A `ValueError` is raised if no IDs are found for the given name: 186 187 >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 188 Traceback (most recent call last): 189 ... 190 ValueError: no "list_applications" results found for Name="app-name" 191 192 ### Warning: Multiple IDs Found 193 194 >>> session = getfixture('mock_session_with_2_ids') 195 196 The first ID is used and the others ignored. 197 198 >>> with pytest.warns(RuntimeWarning): 199 ... backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session) 200 """ 201 logger.info( 202 'Create new instance: %s(app="%s", conf="%s", env="%s")', 203 cls.__name__, 204 application_name, 205 configuration_profile_name, 206 environment_name, 207 ) 208 209 session = session or boto3.Session() 210 appconfig_client = session.client('appconfig') # pyright: ignore[reportUnknownMemberType] 211 application_id = cls.get_application_id(application_name, appconfig_client) 212 configuration_profile_id = cls.get_configuration_profile_id( 213 configuration_profile_name, appconfig_client, application_id 214 ) 215 environment_id = cls.get_environment_id(environment_name, appconfig_client, application_id) 216 217 client: AppConfigDataClient = session.client('appconfigdata') # pyright: ignore[reportUnknownMemberType] 218 219 return cls(client, application_id, configuration_profile_id, environment_id)
Create a new instance of the backend.
Usage: AppConfigBackend.new()
Use boto3 to fetch IDs for based on name:
>>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
>>> print(f"{backend}")
boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='id-1', ConfigurationProfileIdentifier='id-1', EnvironmentIdentifier='id-1')
Error: No IDs Found
>>> session = getfixture('mock_session_with_0_ids') # fixture for doctest
A ValueError is raised if no IDs are found for the given name:
>>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
Traceback (most recent call last):
...
ValueError: no "list_applications" results found for Name="app-name"
Warning: Multiple IDs Found
>>> session = getfixture('mock_session_with_2_ids')
The first ID is used and the others ignored.
>>> with pytest.warns(RuntimeWarning):
... backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
221 async def poll(self, interval: int = MINIMUM_POLL_INTERVAL_SECONDS) -> AsyncIterator[str]: 222 """Poll the AppConfig service for configuration changes. 223 224 .. note:: 225 Methods written for `asyncio` need to jump through hoops to run as `doctest` tests. 226 To improve the readability of this documentation, each Python code block corresponds to 227 a `doctest` test defined in a private method. 228 229 ## Usage: `AppConfigBackend.poll()` 230 231 ```py 232 In [1]: async for content in backend.poll(): 233 ...: print(content) # ← executes each time the configuration changes 234 ``` 235 ```yaml 236 key_0: value_0 237 key_1: 1 238 key_2: true 239 key_3: 240 - 1 241 - 2 242 - 3 243 ``` 244 245 .. note:: 246 If polling is done too quickly, the AWS AppConfig client will raise a 247 `BadRequestException`. This is handled automatically by the backend, which will retry 248 the request after waiting for half the given `interval`. 249 """ 250 token = self.client.start_configuration_session( 251 ApplicationIdentifier=self.application_id, 252 EnvironmentIdentifier=self.environment_id, 253 ConfigurationProfileIdentifier=self.configuration_profile_id, 254 RequiredMinimumPollIntervalInSeconds=interval, 255 )['InitialConfigurationToken'] 256 257 while True: 258 logger.debug('Poll for configuration changes') 259 try: 260 resp = self.client.get_latest_configuration(ConfigurationToken=token) 261 except self.client.exceptions.BadRequestException as exc: 262 if exc.response['Error']['Message'] != 'Request too early': # pragma: no cover 263 raise 264 logger.debug('Request too early; retrying in %d seconds', interval / 2) 265 await asyncio.sleep(interval / 2) 266 continue 267 268 token = resp['NextPollConfigurationToken'] 269 if content := resp['Configuration'].read(): 270 yield content.decode() 271 else: 272 logger.debug('No configuration changes') 273 274 await asyncio.sleep(resp['NextPollIntervalInSeconds'])
Poll the AppConfig service for configuration changes.
Methods written for asyncio need to jump through hoops to run as doctest tests.
To improve the readability of this documentation, each Python code block corresponds to
a doctest test defined in a private method.
Usage: AppConfigBackend.poll()
In [1]: async for content in backend.poll():
...: print(content) # ← executes each time the configuration changes
key_0: value_0
key_1: 1
key_2: true
key_3:
- 1
- 2
- 3
If polling is done too quickly, the AWS AppConfig client will raise a
BadRequestException. This is handled automatically by the backend, which will retry
the request after waiting for half the given interval.