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