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