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__)
class AppConfigBackend(config_ninja.backend.Backend):
 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
AppConfigBackend( client: mypy_boto3_appconfigdata.client.AppConfigDataClient, app_id: str, config_profile_id: str, env_id: str)
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.

The boto3 client used to communicate with the AWS AppConfig service.

configuration_profile_id: str
def get(self) -> str:
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.

@classmethod
def get_application_id( cls, name: str, client: mypy_boto3_appconfig.client.AppConfigClient) -> str:
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.

@classmethod
def get_configuration_profile_id( cls, name: str, client: mypy_boto3_appconfig.client.AppConfigClient, application_id: str) -> str:
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.

@classmethod
def get_environment_id( cls, name: str, client: mypy_boto3_appconfig.client.AppConfigClient, application_id: str) -> str:
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.

@classmethod
def new( cls, application_name: str, configuration_profile_name: str, environment_name: str, session: boto3.session.Session | None = None) -> AppConfigBackend:
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)
async def poll(self, interval: int = 60) -> AsyncIterator[str]:
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.