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