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

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.AppConfigDataClient, app_id: str, config_profile_id: str, env_id: str)
76    def __init__(
77        self,
78        client: AppConfigDataClient,
79        app_id: str,
80        config_profile_id: str,
81        env_id: str,
82    ) -> None:
83        """Initialize the backend."""
84        logger.debug(
85            "Initialize: %s(client=%s, app_id='%s', conf_id='%s', env_id='%s')",
86            self.__class__.__name__,
87            client,
88            app_id,
89            config_profile_id,
90            env_id,
91        )
92        self.client = client
93
94        self.application_id = app_id
95        self.configuration_profile_id = config_profile_id
96        self.environment_id = env_id

Initialize the backend.

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

configuration_profile_id: str
def get(self) -> str:
131    def get(self) -> str:
132        """Retrieve the latest configuration deployment as a string."""
133        logger.debug('Retrieve latest configuration (%s)', self)
134        token = self.client.start_configuration_session(
135            ApplicationIdentifier=self.application_id,
136            EnvironmentIdentifier=self.environment_id,
137            ConfigurationProfileIdentifier=self.configuration_profile_id,
138            RequiredMinimumPollIntervalInSeconds=MINIMUM_POLL_INTERVAL_SECONDS,
139        )['InitialConfigurationToken']
140
141        resp = self.client.get_latest_configuration(ConfigurationToken=token)
142        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.AppConfigClient) -> str:
144    @classmethod
145    def get_application_id(cls, name: str, client: AppConfigClient) -> str:
146        """Retrieve the application ID for the given application name."""
147        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.AppConfigClient, application_id: str) -> str:
149    @classmethod
150    def get_configuration_profile_id(cls, name: str, client: AppConfigClient, application_id: str) -> str:
151        """Retrieve the configuration profile ID for the given configuration profile name."""
152        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.

@classmethod
def get_environment_id( cls, name: str, client: mypy_boto3_appconfig.AppConfigClient, application_id: str) -> str:
154    @classmethod
155    def get_environment_id(cls, name: str, client: AppConfigClient, application_id: str) -> str:
156        """Retrieve the environment ID for the given environment name & application ID."""
157        return cls._get_id_from_name(name, 'list_environments', client, ApplicationId=application_id)

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:
159    @classmethod
160    def new(  # pylint: disable=arguments-differ  # pyright: ignore[reportIncompatibleMethodOverride]
161        cls,
162        application_name: str,
163        configuration_profile_name: str,
164        environment_name: str,
165        session: boto3.Session | None = None,
166    ) -> AppConfigBackend:
167        """Create a new instance of the backend.
168
169        ## Usage: `AppConfigBackend.new()`
170
171        <!-- fixture is used for doctest but excluded from documentation
172        >>> session = getfixture('mock_session_with_1_id')
173
174        -->
175
176        Use `boto3` to fetch IDs for based on name:
177
178        >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
179        >>> print(f"{backend}")
180        boto3.client('appconfigdata').start_configuration_session(ApplicationIdentifier='id-1', ConfigurationProfileIdentifier='id-1', EnvironmentIdentifier='id-1')
181
182        ### Error: No IDs Found
183
184        >>> session = getfixture('mock_session_with_0_ids')  # fixture for doctest
185
186        A `ValueError` is raised if no IDs are found for the given name:
187
188        >>> backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
189        Traceback (most recent call last):
190        ...
191        ValueError: no "list_applications" results found for Name="app-name"
192
193        ### Warning: Multiple IDs Found
194
195        >>> session = getfixture('mock_session_with_2_ids')
196
197        The first ID is used and the others ignored.
198
199        >>> with pytest.warns(RuntimeWarning):
200        ...     backend = AppConfigBackend.new('app-name', 'conf-name', 'env-name', session)
201        """
202        logger.info(
203            'Create new instance: %s(app="%s", conf="%s", env="%s")',
204            cls.__name__,
205            application_name,
206            configuration_profile_name,
207            environment_name,
208        )
209
210        session = session or boto3.Session()
211        appconfig_client = session.client('appconfig')  # pyright: ignore[reportUnknownMemberType]
212        application_id = cls.get_application_id(application_name, appconfig_client)
213        configuration_profile_id = cls.get_configuration_profile_id(
214            configuration_profile_name, appconfig_client, application_id
215        )
216        environment_id = cls.get_environment_id(environment_name, appconfig_client, application_id)
217
218        client: AppConfigDataClient = session.client('appconfigdata')  # pyright: ignore[reportUnknownMemberType]
219
220        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)
async def poll(self, interval: int = 60) -> AsyncIterator[str]:
222    async def poll(self, interval: int = MINIMUM_POLL_INTERVAL_SECONDS) -> AsyncIterator[str]:
223        """Poll the AppConfig service for configuration changes.
224
225        .. note::
226            Methods written for `asyncio` need to jump through hoops to run as `doctest` tests.
227            To improve the readability of this documentation, each Python code block corresponds to
228            a `doctest` test defined in a private method.
229
230        ## Usage: `AppConfigBackend.poll()`
231
232        ```py
233        In [1]: async for content in backend.poll():
234           ...:     print(content)  # ← executes each time the configuration changes
235        ```
236        ```yaml
237        key_0: value_0
238        key_1: 1
239        key_2: true
240        key_3:
241            - 1
242            - 2
243            - 3
244        ```
245
246        .. note::
247            If polling is done too quickly, the AWS AppConfig client will raise a
248            `BadRequestException`. This is handled automatically by the backend, which will retry
249            the request after waiting for half the given `interval`.
250        """
251        token = self.client.start_configuration_session(
252            ApplicationIdentifier=self.application_id,
253            EnvironmentIdentifier=self.environment_id,
254            ConfigurationProfileIdentifier=self.configuration_profile_id,
255            RequiredMinimumPollIntervalInSeconds=interval,
256        )['InitialConfigurationToken']
257
258        while True:
259            logger.debug('Poll for configuration changes')
260            try:
261                resp = self.client.get_latest_configuration(ConfigurationToken=token)
262            except self.client.exceptions.BadRequestException as exc:
263                if exc.response['Error']['Message'] != 'Request too early':  # pragma: no cover
264                    raise
265                logger.debug('Request too early; retrying in %d seconds', interval / 2)
266                await asyncio.sleep(interval / 2)
267                continue
268
269            token = resp['NextPollConfigurationToken']
270            if content := resp['Configuration'].read():
271                yield content.decode()
272            else:
273                logger.debug('No configuration changes')
274
275            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.