agileconfig-python 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgileConfig contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: agileconfig-python
3
+ Version: 0.1.0
4
+ Summary: Python client for AgileConfig with WebSocket updates and environment fallback
5
+ Author: Wei Zheng
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 AgileConfig contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Classifier: Development Status :: 3 - Alpha
28
+ Classifier: Intended Audience :: Developers
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.9
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Software Development :: Libraries
38
+ Requires-Python: >=3.9
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: requests>=2.31
42
+ Requires-Dist: websockets<16,>=15
43
+ Dynamic: license-file
44
+
45
+ # agileconfig-python
46
+
47
+ Python client for [AgileConfig](https://github.com/dotnetcore/AgileConfig). The client loads configuration over HTTP, listens for reload notifications over WebSocket, and falls back to `os.environ` when the server is unavailable.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ python -m pip install agileconfig-python
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ from agileconfig_python import AgileConfigLoader
59
+
60
+ loader = AgileConfigLoader(
61
+ url="wss://config.example.com/ws",
62
+ app_id="my-app",
63
+ secret="your-secret",
64
+ env="DEV",
65
+ )
66
+
67
+ database_host = str(loader.get_var("host", prefix="database"))
68
+ loader.stop()
69
+ ```
70
+
71
+ The initial HTTP request populates the cache. When AgileConfig publishes a reload event, the cache is refreshed. If the server cannot be reached within the configured timeout, `get_var_value` reads the variable name from the process environment and ignores its prefix.
72
+
73
+ ## API
74
+
75
+ - `AgileConfigLoader(url, app_id, secret, env)` creates the singleton loader and starts its listener.
76
+ - `loader.get_var_value(var_name, prefix="")` returns the current value, or an environment fallback.
77
+ - `loader.get_var(var_name, prefix="")` returns a lazy string-like proxy that reads the latest value when converted with `str()`.
78
+ - `loader.stop()` closes the WebSocket and stops the listener thread.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ python -m pip install --upgrade pip build
84
+ python -m pip install -e .
85
+ python -m unittest discover -s unit_tests -v
86
+ python -m build
87
+ ```
88
+
89
+ The live integration tests embedded in the source require an AgileConfig server and are not run by the offline unit-test command.
90
+
91
+ ## Release checklist
92
+
93
+ 1. Update `version` in `pyproject.toml`.
94
+ 2. Run the unit tests and `python -m build`.
95
+ 3. Inspect both artifacts with `python -m twine check dist/*`.
96
+ 4. Upload to TestPyPI and install the uploaded version in a clean environment.
97
+ 5. Create a Git tag matching the version, for example `v0.1.0`.
98
+ 6. Publish to PyPI with a trusted publisher or `twine upload dist/*`.
@@ -0,0 +1,54 @@
1
+ # agileconfig-python
2
+
3
+ Python client for [AgileConfig](https://github.com/dotnetcore/AgileConfig). The client loads configuration over HTTP, listens for reload notifications over WebSocket, and falls back to `os.environ` when the server is unavailable.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ python -m pip install agileconfig-python
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from agileconfig_python import AgileConfigLoader
15
+
16
+ loader = AgileConfigLoader(
17
+ url="wss://config.example.com/ws",
18
+ app_id="my-app",
19
+ secret="your-secret",
20
+ env="DEV",
21
+ )
22
+
23
+ database_host = str(loader.get_var("host", prefix="database"))
24
+ loader.stop()
25
+ ```
26
+
27
+ The initial HTTP request populates the cache. When AgileConfig publishes a reload event, the cache is refreshed. If the server cannot be reached within the configured timeout, `get_var_value` reads the variable name from the process environment and ignores its prefix.
28
+
29
+ ## API
30
+
31
+ - `AgileConfigLoader(url, app_id, secret, env)` creates the singleton loader and starts its listener.
32
+ - `loader.get_var_value(var_name, prefix="")` returns the current value, or an environment fallback.
33
+ - `loader.get_var(var_name, prefix="")` returns a lazy string-like proxy that reads the latest value when converted with `str()`.
34
+ - `loader.stop()` closes the WebSocket and stops the listener thread.
35
+
36
+ ## Development
37
+
38
+ ```bash
39
+ python -m pip install --upgrade pip build
40
+ python -m pip install -e .
41
+ python -m unittest discover -s unit_tests -v
42
+ python -m build
43
+ ```
44
+
45
+ The live integration tests embedded in the source require an AgileConfig server and are not run by the offline unit-test command.
46
+
47
+ ## Release checklist
48
+
49
+ 1. Update `version` in `pyproject.toml`.
50
+ 2. Run the unit tests and `python -m build`.
51
+ 3. Inspect both artifacts with `python -m twine check dist/*`.
52
+ 4. Upload to TestPyPI and install the uploaded version in a clean environment.
53
+ 5. Create a Git tag matching the version, for example `v0.1.0`.
54
+ 6. Publish to PyPI with a trusted publisher or `twine upload dist/*`.
@@ -0,0 +1,5 @@
1
+ """Python client for AgileConfig."""
2
+
3
+ from .config_loader import AgileConfigLoader, ConfigStrWithUpdate
4
+
5
+ __all__ = ["AgileConfigLoader", "ConfigStrWithUpdate"]
@@ -0,0 +1,291 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import threading
5
+ import time
6
+ from base64 import b64encode
7
+ from typing import Optional, Dict, Any
8
+
9
+ import requests
10
+ from websockets import ConnectionClosedError
11
+ from websockets.sync.client import connect as ws_connect
12
+
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
16
+ )
17
+ logger = logging.getLogger(__name__)
18
+
19
+ AGILE_CONFIG_TIMEOUT = 20
20
+
21
+
22
+ class ConfigStrWithUpdate:
23
+ def __init__(self, agile_config_loader, prefix: str, var_name : str):
24
+ self.__prefix = prefix
25
+ self.__var_name = var_name
26
+ self.__agile_config_loader = agile_config_loader
27
+
28
+ def __str__(self):
29
+ return self.__agile_config_loader.get_var_value(self.__var_name, self.__prefix)
30
+
31
+
32
+ class AgileConfigLoader:
33
+ """
34
+ Loads configuration from AgileConfig server via WebSocket.
35
+ Falls back to environment variables if AgileConfig is unavailable.
36
+
37
+ Singleton that starts background WebSocket listening on instantiation.
38
+
39
+ Requires environment variables:
40
+ - AGILE_CONFIG_URL: WebSocket URL (e.g., "ws://localhost:5000/ws" or "wss://config.example.com/ws")
41
+ - AGILE_CONFIG_APP_ID: Application ID for AgileConfig
42
+ - AGILE_CONFIG_SECRET: Secret key for AgileConfig
43
+ - AGILE_CONFIG_ENV: Environment (default: "DEV")
44
+ """
45
+
46
+ _instance = None
47
+
48
+ def __new__(cls, *args, **kwargs):
49
+ """Singleton pattern: ensure only one instance exists."""
50
+ if not cls._instance:
51
+ cls._instance = super().__new__(cls)
52
+ return cls._instance
53
+
54
+ def __init__(self, url: str, app_id: str, secret: str, env: str):
55
+ """Initialize AgileConfigLoader from environment variables and start background listening."""
56
+ # Only initialize once
57
+ if not hasattr(self, '_initialized'):
58
+ self._initialized = True
59
+
60
+ self._url = url
61
+ self._app_id = app_id
62
+ self._secret = secret
63
+ self._env = env
64
+
65
+ self._ws_client: Optional[Any] = None
66
+ self._config_cache: Dict[str, Any] = {}
67
+ self._headers: Optional[Dict[str, Any]] = None
68
+ self._running_thread = None
69
+ self._use_os_env_fallback = False
70
+
71
+ self._start_event = threading.Event()
72
+ self._stop_event = threading.Event()
73
+ self._updated_event = threading.Event()
74
+ self._lock = threading.Lock()
75
+
76
+ # Pre-compute auth headers (doesn't change across retries)
77
+ auth_string = f"{self._app_id}:{self._secret}"
78
+ auth_bytes = b64encode(auth_string.encode()).decode()
79
+ self._headers = {
80
+ "appid": self._app_id,
81
+ "env": self._env,
82
+ "Authorization": f"Basic {auth_bytes}",
83
+ }
84
+ self.start()
85
+
86
+ return
87
+
88
+ def clear_cache(self):
89
+ self._config_cache = {}
90
+
91
+ def _http_url_parser(self) -> str:
92
+ """Derive the HTTP base URL from the configured WebSocket URL."""
93
+ url = self._url
94
+ if url.startswith("wss://"):
95
+ url = "https://" + url[len("wss://"):]
96
+ elif url.startswith("ws://"):
97
+ url = "http://" + url[len("ws://"):]
98
+ if url.endswith("/ws"):
99
+ url = url[: -len("/ws")]
100
+ return url.rstrip("/")
101
+
102
+ def _get_config_from_server(self):
103
+ url = f'{self._http_url_parser()}/api/config/app/{self._app_id}'
104
+ r = requests.get(url, headers=self._headers, params={'env': self._env}, timeout=5.0)
105
+ r.raise_for_status()
106
+ configs = {
107
+ f'{_["group"]}:{_["key"]}' if _["group"] else _["key"]: _['value']
108
+ for _ in r.json()
109
+ }
110
+ self._config_cache = configs
111
+
112
+ def _start_config_listener(self):
113
+ self._start_event.set()
114
+ while not self._stop_event.wait(1):
115
+ if not self._lock.acquire(timeout=1):
116
+ logger.warning('Failed to acquire lock for AgileConfig listener. Retrying...')
117
+ continue
118
+ logger.info('lock acquired before initiating websocket connection.')
119
+
120
+ try:
121
+ with ws_connect(
122
+ self._url,
123
+ additional_headers=self._headers,
124
+ ping_interval=30,
125
+ open_timeout=5,
126
+ close_timeout=5,
127
+ ping_timeout=5
128
+ ) as client:
129
+ if not client.ping().wait(timeout=5):
130
+ raise TimeoutError('Initial ping to AgileConfig server timed out!')
131
+ self._ws_client = client
132
+ self._lock.release()
133
+ logger.info('lock released after a successful websocket connection')
134
+
135
+ self._get_config_from_server()
136
+ self._use_os_env_fallback = False
137
+ self._updated_event.set()
138
+ logger.info(f'Successfully retrieved config from AgileConfig server at {self._http_url_parser()}')
139
+
140
+ logger.info(f"Successfully connected to AgileConfig ws at {self._url}")
141
+ for msg in self._ws_client:
142
+ logger.info(f'Received message {msg} from {self._url}')
143
+ try:
144
+ parsed = json.loads(msg)
145
+ if isinstance(parsed, dict) and parsed.get('Action') == 'reload':
146
+ self._get_config_from_server()
147
+ logger.info('Successfully updated config.')
148
+ except (json.JSONDecodeError, TypeError):
149
+ pass
150
+ except TimeoutError as e:
151
+ logger.warning(f'Encountered timeout error: {e.__repr__()}. Will retry.')
152
+ except requests.exceptions.HTTPError as e:
153
+ logger.warning(
154
+ f'Failed to retrieve config from server with error {e.__repr__()}. Will retry.'
155
+ )
156
+ except ConnectionClosedError:
157
+ if self._stop_event.wait(1):
158
+ logger.info('Connection closed due to termination. Exiting.')
159
+ return
160
+ logger.warning(f'Connection unexpectedly closed. Will retry.')
161
+
162
+ except Exception as e:
163
+ logger.warning(
164
+ f'Encountered unexpected error while subscribing to AgileConfig server: {e.__repr__()}. '
165
+ f'Will retry.'
166
+ )
167
+ finally:
168
+ self._use_os_env_fallback = True
169
+ if self._lock.locked():
170
+ self._lock.release()
171
+ logger.info('lock released after a websocket termination.')
172
+
173
+ logger.info('AgileConfig listener stopped.')
174
+ self._start_event.clear()
175
+
176
+ #
177
+ #
178
+ #
179
+ # while not self._terminated:
180
+ # self._ready = False
181
+ # try:
182
+ # self._get_config_from_server()
183
+ # self._updated_event.set()
184
+ # logger.info(f'Successfully retrieved config from AgileConfig server at {self._url}')
185
+ # self._ready = True
186
+ # except requests.exceptions.HTTPError as e:
187
+ # logger.warning(
188
+ # f'Failed to retrieve config from server with error {e.__repr__()}. Will retry in 5 seconds.'
189
+ # )
190
+ # except Exception as e:
191
+ # logger.warning(
192
+ # f'Failed to retrieve config from server with unexpected error {e.__repr__()}. '
193
+ # f'Will retry in 5 seconds.'
194
+ # )
195
+ # if not self._ready:
196
+ # self._updated_event.clear()
197
+ # if self._stop_event.wait(5):
198
+ # return
199
+ # continue
200
+ #
201
+ # try:
202
+ # with ws_connect(
203
+ # self._url,
204
+ # additional_headers=self._headers,
205
+ # ping_interval=30,
206
+ # ) as client:
207
+ # if not client.ping().wait(timeout=1):
208
+ # raise TimeoutError('Initial ping to AgileConfig server timed out!')
209
+ # self._ws_client = client
210
+ # logger.info(f"Successfully connected to AgileConfig ws at {self._url}")
211
+ # for msg in self._ws_client:
212
+ # logger.info(f'Received message {msg} from {self._url}')
213
+ # if json.loads(msg)['Action'] == 'reload':
214
+ # self._get_config_from_server()
215
+ # logger.info('Successfully updated config.')
216
+ # print(f'asdasdzzzz, break out of loop')
217
+ # except TimeoutError as e:
218
+ # logger.warning(f'Encountered timeout error: {e.__repr__()}. Will retry in 5 seconds.')
219
+ # except requests.exceptions.HTTPError as e:
220
+ # logger.warning(
221
+ # f'Failed to retrieve config from server with error {e.__repr__()}. Will retry in 5 seconds.'
222
+ # )
223
+ # except ConnectionClosedError:
224
+ # if self._stop_event.wait(1):
225
+ # logger.info('Connection closed due to termination. Exiting.')
226
+ # return
227
+ # logger.warning(f'Connection unexpectedly closed. Will retry in 5 seconds.')
228
+ # except Exception as e:
229
+ # logger.warning(
230
+ # f'Encountered unexpected error while subscribing to AgileConfig server: {e.__repr__()}. '
231
+ # f'Will retry in 5 seconds.'
232
+ # )
233
+ # if self._stop_event.wait(5):
234
+ # return
235
+
236
+ def stop(self):
237
+ if not self._start_event.wait(5):
238
+ logger.info('Agile config loader skipped stop. Agile config listener is not running.')
239
+ return False
240
+ logger.info('stop called')
241
+ self._stop_event.set()
242
+ if not self._lock.acquire(timeout=10):
243
+ logger.info('Failed to stop')
244
+ return False
245
+ if self._ws_client:
246
+ self._ws_client.close()
247
+ if self._lock.locked():
248
+ self._lock.release()
249
+ if self._running_thread:
250
+ self._running_thread.join(timeout=5)
251
+ if self._running_thread.is_alive():
252
+ raise RuntimeError('Failed to stop AgileConfig listener thread within timeout.')
253
+ self._running_thread = None
254
+ self._ws_client = None
255
+ self._config_cache = {}
256
+ if self._updated_event.is_set():
257
+ self._updated_event.clear()
258
+ self._use_os_env_fallback = False
259
+ return True
260
+
261
+ def start(self):
262
+ if self._running_thread:
263
+ logger.info(f'The AgileConfig instance is already started.')
264
+ return
265
+ if not self._url or not self._app_id:
266
+ logger.warning(
267
+ "AgileConfig not fully configured. Missing AGILE_CONFIG_URL or AGILE_CONFIG_APP_ID. "
268
+ "Will fall back to environment variables for all config lookups."
269
+ )
270
+ return
271
+
272
+ self._stop_event.clear()
273
+ self._running_thread = threading.Thread(target=self._start_config_listener, daemon=True)
274
+ self._running_thread.start()
275
+
276
+ def get_var_value(self, var_name: str, prefix =''):
277
+ if not self._use_os_env_fallback:
278
+ loaded = self._updated_event.wait(timeout=AGILE_CONFIG_TIMEOUT)
279
+ if not loaded:
280
+ self._use_os_env_fallback = True
281
+
282
+ if self._use_os_env_fallback:
283
+ logger.warning(
284
+ 'AgileConfig connection was not established. Falling back to using os.environ. '
285
+ f'Prefix {prefix} will be ignored.'
286
+ )
287
+ return os.environ.get(var_name)
288
+ return self._config_cache.get(f'{prefix}:{var_name}')
289
+
290
+ def get_var(self, var_name: str, prefix =''):
291
+ return ConfigStrWithUpdate(self, prefix=prefix, var_name=var_name)
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: agileconfig-python
3
+ Version: 0.1.0
4
+ Summary: Python client for AgileConfig with WebSocket updates and environment fallback
5
+ Author: Wei Zheng
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 AgileConfig contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Classifier: Development Status :: 3 - Alpha
28
+ Classifier: Intended Audience :: Developers
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.9
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Software Development :: Libraries
38
+ Requires-Python: >=3.9
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: requests>=2.31
42
+ Requires-Dist: websockets<16,>=15
43
+ Dynamic: license-file
44
+
45
+ # agileconfig-python
46
+
47
+ Python client for [AgileConfig](https://github.com/dotnetcore/AgileConfig). The client loads configuration over HTTP, listens for reload notifications over WebSocket, and falls back to `os.environ` when the server is unavailable.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ python -m pip install agileconfig-python
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ from agileconfig_python import AgileConfigLoader
59
+
60
+ loader = AgileConfigLoader(
61
+ url="wss://config.example.com/ws",
62
+ app_id="my-app",
63
+ secret="your-secret",
64
+ env="DEV",
65
+ )
66
+
67
+ database_host = str(loader.get_var("host", prefix="database"))
68
+ loader.stop()
69
+ ```
70
+
71
+ The initial HTTP request populates the cache. When AgileConfig publishes a reload event, the cache is refreshed. If the server cannot be reached within the configured timeout, `get_var_value` reads the variable name from the process environment and ignores its prefix.
72
+
73
+ ## API
74
+
75
+ - `AgileConfigLoader(url, app_id, secret, env)` creates the singleton loader and starts its listener.
76
+ - `loader.get_var_value(var_name, prefix="")` returns the current value, or an environment fallback.
77
+ - `loader.get_var(var_name, prefix="")` returns a lazy string-like proxy that reads the latest value when converted with `str()`.
78
+ - `loader.stop()` closes the WebSocket and stops the listener thread.
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ python -m pip install --upgrade pip build
84
+ python -m pip install -e .
85
+ python -m unittest discover -s unit_tests -v
86
+ python -m build
87
+ ```
88
+
89
+ The live integration tests embedded in the source require an AgileConfig server and are not run by the offline unit-test command.
90
+
91
+ ## Release checklist
92
+
93
+ 1. Update `version` in `pyproject.toml`.
94
+ 2. Run the unit tests and `python -m build`.
95
+ 3. Inspect both artifacts with `python -m twine check dist/*`.
96
+ 4. Upload to TestPyPI and install the uploaded version in a clean environment.
97
+ 5. Create a Git tag matching the version, for example `v0.1.0`.
98
+ 6. Publish to PyPI with a trusted publisher or `twine upload dist/*`.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ agileconfig_python/__init__.py
5
+ agileconfig_python/config_loader.py
6
+ agileconfig_python.egg-info/PKG-INFO
7
+ agileconfig_python.egg-info/SOURCES.txt
8
+ agileconfig_python.egg-info/dependency_links.txt
9
+ agileconfig_python.egg-info/requires.txt
10
+ agileconfig_python.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests>=2.31
2
+ websockets<16,>=15
@@ -0,0 +1 @@
1
+ agileconfig_python
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "agileconfig-python"
7
+ version = "0.1.0"
8
+ description = "Python client for AgileConfig with WebSocket updates and environment fallback"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "Wei Zheng" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Libraries",
27
+ ]
28
+ dependencies = [
29
+ "requests>=2.31",
30
+ "websockets>=15,<16",
31
+ ]
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["agileconfig_python*"]
35
+
36
+ [tool.setuptools]
37
+ include-package-data = true
38
+
39
+ [tool.pytest.ini_options]
40
+ addopts = "-ra"
41
+
42
+ [tool.coverage.run]
43
+ branch = true
44
+ source = ["agileconfig_python"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+