configvault-sdk 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,487 @@
1
+ ## Ignore Visual Studio temporary files, build results, and
2
+ ## files generated by popular Visual Studio add-ons.
3
+ ##
4
+ ## Get latest from `dotnet new gitignore`
5
+
6
+ # dotenv files
7
+ .env
8
+
9
+ # User-specific files
10
+ *.rsuser
11
+ *.suo
12
+ *.user
13
+ *.userosscache
14
+ *.sln.docstates
15
+
16
+ # User-specific files (MonoDevelop/Xamarin Studio)
17
+ *.userprefs
18
+
19
+ # Mono auto generated files
20
+ mono_crash.*
21
+
22
+ # Build results
23
+ [Dd]ebug/
24
+ [Dd]ebugPublic/
25
+ [Rr]elease/
26
+ [Rr]eleases/
27
+ x64/
28
+ x86/
29
+ [Ww][Ii][Nn]32/
30
+ [Aa][Rr][Mm]/
31
+ [Aa][Rr][Mm]64/
32
+ bld/
33
+ [Bb]in/
34
+ [Oo]bj/
35
+ [Ll]og/
36
+ [Ll]ogs/
37
+
38
+ # Visual Studio 2015/2017 cache/options directory
39
+ .vs/
40
+ # Uncomment if you have tasks that create the project's static files in wwwroot
41
+ #wwwroot/
42
+
43
+ # Visual Studio 2017 auto generated files
44
+ Generated\ Files/
45
+
46
+ # MSTest test Results
47
+ [Tt]est[Rr]esult*/
48
+ [Bb]uild[Ll]og.*
49
+
50
+ # NUnit
51
+ *.VisualState.xml
52
+ TestResult.xml
53
+ nunit-*.xml
54
+
55
+ # Build Results of an ATL Project
56
+ [Dd]ebugPS/
57
+ [Rr]eleasePS/
58
+ dlldata.c
59
+
60
+ # Benchmark Results
61
+ BenchmarkDotNet.Artifacts/
62
+
63
+ # .NET
64
+ project.lock.json
65
+ project.fragment.lock.json
66
+ artifacts/
67
+
68
+ # Tye
69
+ .tye/
70
+
71
+ # ASP.NET Scaffolding
72
+ ScaffoldingReadMe.txt
73
+
74
+ # StyleCop
75
+ StyleCopReport.xml
76
+
77
+ # Files built by Visual Studio
78
+ *_i.c
79
+ *_p.c
80
+ *_h.h
81
+ *.ilk
82
+ *.meta
83
+ *.obj
84
+ *.iobj
85
+ *.pch
86
+ *.pdb
87
+ *.ipdb
88
+ *.pgc
89
+ *.pgd
90
+ *.rsp
91
+ *.sbr
92
+ *.tlb
93
+ *.tli
94
+ *.tlh
95
+ *.tmp
96
+ *.tmp_proj
97
+ *_wpftmp.csproj
98
+ *.log
99
+ *.tlog
100
+ *.vspscc
101
+ *.vssscc
102
+ .builds
103
+ *.pidb
104
+ *.svclog
105
+ *.scc
106
+
107
+ # Chutzpah Test files
108
+ _Chutzpah*
109
+
110
+ # Visual C++ cache files
111
+ ipch/
112
+ *.aps
113
+ *.ncb
114
+ *.opendb
115
+ *.opensdf
116
+ *.sdf
117
+ *.cachefile
118
+ *.VC.db
119
+ *.VC.VC.opendb
120
+
121
+ # Visual Studio profiler
122
+ *.psess
123
+ *.vsp
124
+ *.vspx
125
+ *.sap
126
+
127
+ # Visual Studio Trace Files
128
+ *.e2e
129
+
130
+ # TFS 2012 Local Workspace
131
+ $tf/
132
+
133
+ # Guidance Automation Toolkit
134
+ *.gpState
135
+
136
+ # ReSharper is a .NET coding add-in
137
+ _ReSharper*/
138
+ *.[Rr]e[Ss]harper
139
+ *.DotSettings.user
140
+
141
+ # TeamCity is a build add-in
142
+ _TeamCity*
143
+
144
+ # DotCover is a Code Coverage Tool
145
+ *.dotCover
146
+
147
+ # AxoCover is a Code Coverage Tool
148
+ .axoCover/*
149
+ !.axoCover/settings.json
150
+
151
+ # Coverlet is a free, cross platform Code Coverage Tool
152
+ coverage*.json
153
+ coverage*.xml
154
+ coverage*.info
155
+
156
+ # Visual Studio code coverage results
157
+ *.coverage
158
+ *.coveragexml
159
+
160
+ # NCrunch
161
+ _NCrunch_*
162
+ .*crunch*.local.xml
163
+ nCrunchTemp_*
164
+
165
+ # MightyMoose
166
+ *.mm.*
167
+ AutoTest.Net/
168
+
169
+ # Web workbench (sass)
170
+ .sass-cache/
171
+
172
+ # Installshield output folder
173
+ [Ee]xpress/
174
+
175
+ # DocProject is a documentation generator add-in
176
+ DocProject/buildhelp/
177
+ DocProject/Help/*.HxT
178
+ DocProject/Help/*.HxC
179
+ DocProject/Help/*.hhc
180
+ DocProject/Help/*.hhk
181
+ DocProject/Help/*.hhp
182
+ DocProject/Help/Html2
183
+ DocProject/Help/html
184
+
185
+ # Click-Once directory
186
+ publish/
187
+
188
+ # Publish Web Output
189
+ *.[Pp]ublish.xml
190
+ *.azurePubxml
191
+ # Note: Comment the next line if you want to checkin your web deploy settings,
192
+ # but database connection strings (with potential passwords) will be unencrypted
193
+ *.pubxml
194
+ *.publishproj
195
+
196
+ # Microsoft Azure Web App publish settings. Comment the next line if you want to
197
+ # checkin your Azure Web App publish settings, but sensitive information contained
198
+ # in these scripts will be unencrypted
199
+ PublishScripts/
200
+
201
+ # NuGet Packages
202
+ *.nupkg
203
+ # NuGet Symbol Packages
204
+ *.snupkg
205
+ # The packages folder can be ignored because of Package Restore
206
+ **/[Pp]ackages/*
207
+ # except build/, which is used as an MSBuild target.
208
+ !**/[Pp]ackages/build/
209
+ # Uncomment if necessary however generally it will be regenerated when needed
210
+ #!**/[Pp]ackages/repositories.config
211
+ # NuGet v3's project.json files produces more ignorable files
212
+ *.nuget.props
213
+ *.nuget.targets
214
+
215
+ # Microsoft Azure Build Output
216
+ csx/
217
+ *.build.csdef
218
+
219
+ # Microsoft Azure Emulator
220
+ ecf/
221
+ rcf/
222
+
223
+ # Windows Store app package directories and files
224
+ AppPackages/
225
+ BundleArtifacts/
226
+ Package.StoreAssociation.xml
227
+ _pkginfo.txt
228
+ *.appx
229
+ *.appxbundle
230
+ *.appxupload
231
+
232
+ # Visual Studio cache files
233
+ # files ending in .cache can be ignored
234
+ *.[Cc]ache
235
+ # but keep track of directories ending in .cache
236
+ !?*.[Cc]ache/
237
+
238
+ # Others
239
+ ClientBin/
240
+ ~$*
241
+ *~
242
+ *.dbmdl
243
+ *.dbproj.schemaview
244
+ *.jfm
245
+ *.pfx
246
+ *.publishsettings
247
+ orleans.codegen.cs
248
+
249
+ # Including strong name files can present a security risk
250
+ # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251
+ #*.snk
252
+
253
+ # Since there are multiple workflows, uncomment next line to ignore bower_components
254
+ # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255
+ #bower_components/
256
+
257
+ # RIA/Silverlight projects
258
+ Generated_Code/
259
+
260
+ # Backup & report files from converting an old project file
261
+ # to a newer Visual Studio version. Backup files are not needed,
262
+ # because we have git ;-)
263
+ _UpgradeReport_Files/
264
+ Backup*/
265
+ UpgradeLog*.XML
266
+ UpgradeLog*.htm
267
+ ServiceFabricBackup/
268
+ *.rptproj.bak
269
+
270
+ # SQL Server files
271
+ *.mdf
272
+ *.ldf
273
+ *.ndf
274
+
275
+ # Business Intelligence projects
276
+ *.rdl.data
277
+ *.bim.layout
278
+ *.bim_*.settings
279
+ *.rptproj.rsuser
280
+ *- [Bb]ackup.rdl
281
+ *- [Bb]ackup ([0-9]).rdl
282
+ *- [Bb]ackup ([0-9][0-9]).rdl
283
+
284
+ # Microsoft Fakes
285
+ FakesAssemblies/
286
+
287
+ # GhostDoc plugin setting file
288
+ *.GhostDoc.xml
289
+
290
+ # Node.js Tools for Visual Studio
291
+ .ntvs_analysis.dat
292
+ node_modules/
293
+ dist/
294
+
295
+ # Visual Studio 6 build log
296
+ *.plg
297
+
298
+ # Visual Studio 6 workspace options file
299
+ *.opt
300
+
301
+ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
302
+ *.vbw
303
+
304
+ # Visual Studio 6 auto-generated project file (contains which files were open etc.)
305
+ *.vbp
306
+
307
+ # Visual Studio 6 workspace and project file (working project files containing files to include in project)
308
+ *.dsw
309
+ *.dsp
310
+
311
+ # Visual Studio 6 technical files
312
+ *.ncb
313
+ *.aps
314
+
315
+ # Visual Studio LightSwitch build output
316
+ **/*.HTMLClient/GeneratedArtifacts
317
+ **/*.DesktopClient/GeneratedArtifacts
318
+ **/*.DesktopClient/ModelManifest.xml
319
+ **/*.Server/GeneratedArtifacts
320
+ **/*.Server/ModelManifest.xml
321
+ _Pvt_Extensions
322
+
323
+ # Paket dependency manager
324
+ .paket/paket.exe
325
+ paket-files/
326
+
327
+ # FAKE - F# Make
328
+ .fake/
329
+
330
+ # CodeRush personal settings
331
+ .cr/personal
332
+
333
+ # Python Tools for Visual Studio (PTVS)
334
+ __pycache__/
335
+ *.pyc
336
+
337
+ # Cake - Uncomment if you are using it
338
+ # tools/**
339
+ # !tools/packages.config
340
+
341
+ # Tabs Studio
342
+ *.tss
343
+
344
+ # Telerik's JustMock configuration file
345
+ *.jmconfig
346
+
347
+ # BizTalk build output
348
+ *.btp.cs
349
+ *.btm.cs
350
+ *.odx.cs
351
+ *.xsd.cs
352
+
353
+ # OpenCover UI analysis results
354
+ OpenCover/
355
+
356
+ # Azure Stream Analytics local run output
357
+ ASALocalRun/
358
+
359
+ # MSBuild Binary and Structured Log
360
+ *.binlog
361
+
362
+ # NVidia Nsight GPU debugger configuration file
363
+ *.nvuser
364
+
365
+ # MFractors (Xamarin productivity tool) working folder
366
+ .mfractor/
367
+
368
+ # Local History for Visual Studio
369
+ .localhistory/
370
+
371
+ # Visual Studio History (VSHistory) files
372
+ .vshistory/
373
+
374
+ # BeatPulse healthcheck temp database
375
+ healthchecksdb
376
+
377
+ # Backup folder for Package Reference Convert tool in Visual Studio 2017
378
+ MigrationBackup/
379
+
380
+ # Ionide (cross platform F# VS Code tools) working folder
381
+ .ionide/
382
+
383
+ # Fody - auto-generated XML schema
384
+ FodyWeavers.xsd
385
+
386
+ # VS Code files for those working on multiple tools
387
+ .vscode/*
388
+ !.vscode/settings.json
389
+ !.vscode/tasks.json
390
+ !.vscode/launch.json
391
+ !.vscode/extensions.json
392
+ *.code-workspace
393
+
394
+ # Local History for Visual Studio Code
395
+ .history/
396
+
397
+ # Windows Installer files from build outputs
398
+ *.cab
399
+ *.msi
400
+ *.msix
401
+ *.msm
402
+ *.msp
403
+
404
+ # JetBrains Rider
405
+ *.sln.iml
406
+ .idea/
407
+
408
+ ##
409
+ ## Visual studio for Mac
410
+ ##
411
+
412
+
413
+ # globs
414
+ Makefile.in
415
+ *.userprefs
416
+ *.usertasks
417
+ config.make
418
+ config.status
419
+ aclocal.m4
420
+ install-sh
421
+ autom4te.cache/
422
+ *.tar.gz
423
+ tarballs/
424
+ test-results/
425
+
426
+ # Mac bundle stuff
427
+ *.dmg
428
+ *.app
429
+
430
+ # content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
431
+ # General
432
+ .DS_Store
433
+ .AppleDouble
434
+ .LSOverride
435
+
436
+ # Icon must end with two \r
437
+ Icon
438
+
439
+
440
+ # Thumbnails
441
+ ._*
442
+
443
+ # Files that might appear in the root of a volume
444
+ .DocumentRevisions-V100
445
+ .fseventsd
446
+ .Spotlight-V100
447
+ .TemporaryItems
448
+ .Trashes
449
+ .VolumeIcon.icns
450
+ .com.apple.timemachine.donotpresent
451
+
452
+ # Directories potentially created on remote AFP share
453
+ .AppleDB
454
+ .AppleDesktop
455
+ Network Trash Folder
456
+ Temporary Items
457
+ .apdisk
458
+
459
+ # content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
460
+ # Windows thumbnail cache files
461
+ Thumbs.db
462
+ ehthumbs.db
463
+ ehthumbs_vista.db
464
+
465
+ # Dump file
466
+ *.stackdump
467
+
468
+ # Folder config file
469
+ [Dd]esktop.ini
470
+
471
+ # Recycle Bin used on file shares
472
+ $RECYCLE.BIN/
473
+
474
+ # Windows Installer files
475
+ *.cab
476
+ *.msi
477
+ *.msix
478
+ *.msm
479
+ *.msp
480
+
481
+ # Windows shortcuts
482
+ *.lnk
483
+
484
+ # Vim temporary swap files
485
+ *.swp
486
+
487
+ .worktrees
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: configvault-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for ConfigVault API
5
+ Project-URL: Homepage, https://github.com/sitien173/config-vault
6
+ Project-URL: Repository, https://github.com/sitien173/config-vault
7
+ Project-URL: Issues, https://github.com/sitien173/config-vault/issues
8
+ Author: nst173
9
+ License-Expression: MIT
10
+ Keywords: config,configvault,sdk,vault
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx-sse>=0.4.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: pydantic>=2.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
27
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ConfigVault Python SDK
31
+
32
+ Python client for the ConfigVault configuration management API.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install configvault-sdk
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from configvault import ConfigVaultClient
44
+
45
+ client = ConfigVaultClient(
46
+ base_url="http://localhost:5000",
47
+ api_key="your-api-key"
48
+ )
49
+
50
+ # Get a configuration value
51
+ value = await client.get("production/database/connection")
52
+
53
+ # Check if key exists
54
+ exists = await client.exists("production/database/connection")
55
+
56
+ # List all configs in namespace
57
+ configs = await client.list("production")
58
+
59
+ # Check service health
60
+ health = await client.health()
61
+ ```
62
+
63
+ ## Watching for Changes
64
+
65
+ ```python
66
+ from configvault import ConfigVaultClient
67
+
68
+ client = ConfigVaultClient(
69
+ base_url="http://localhost:5000",
70
+ api_key="your-api-key"
71
+ )
72
+
73
+ # Watch all changes
74
+ watcher = client.watch()
75
+
76
+ # Or filter by pattern
77
+ watcher = client.watch("production/*")
78
+
79
+ async for event in watcher.watch():
80
+ print(f"Changed keys: {event.keys}")
81
+ print(f"Timestamp: {event.timestamp}")
82
+
83
+ # Stop watching
84
+ watcher.stop()
85
+ ```
@@ -0,0 +1,56 @@
1
+ # ConfigVault Python SDK
2
+
3
+ Python client for the ConfigVault configuration management API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install configvault-sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from configvault import ConfigVaultClient
15
+
16
+ client = ConfigVaultClient(
17
+ base_url="http://localhost:5000",
18
+ api_key="your-api-key"
19
+ )
20
+
21
+ # Get a configuration value
22
+ value = await client.get("production/database/connection")
23
+
24
+ # Check if key exists
25
+ exists = await client.exists("production/database/connection")
26
+
27
+ # List all configs in namespace
28
+ configs = await client.list("production")
29
+
30
+ # Check service health
31
+ health = await client.health()
32
+ ```
33
+
34
+ ## Watching for Changes
35
+
36
+ ```python
37
+ from configvault import ConfigVaultClient
38
+
39
+ client = ConfigVaultClient(
40
+ base_url="http://localhost:5000",
41
+ api_key="your-api-key"
42
+ )
43
+
44
+ # Watch all changes
45
+ watcher = client.watch()
46
+
47
+ # Or filter by pattern
48
+ watcher = client.watch("production/*")
49
+
50
+ async for event in watcher.watch():
51
+ print(f"Changed keys: {event.keys}")
52
+ print(f"Timestamp: {event.timestamp}")
53
+
54
+ # Stop watching
55
+ watcher.stop()
56
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "configvault-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for ConfigVault API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "nst173" }]
13
+ keywords = ["configvault", "config", "vault", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Typing :: Typed",
24
+ ]
25
+ urls = { Homepage = "https://github.com/sitien173/config-vault", Repository = "https://github.com/sitien173/config-vault", Issues = "https://github.com/sitien173/config-vault/issues" }
26
+ dependencies = [
27
+ "httpx>=0.27.0",
28
+ "httpx-sse>=0.4.0",
29
+ "pydantic>=2.0.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=8.0.0",
35
+ "pytest-asyncio>=0.23.0",
36
+ "respx>=0.21.0",
37
+ ]
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/configvault"]
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
44
+ testpaths = ["tests"]
@@ -0,0 +1,25 @@
1
+ """ConfigVault Python SDK."""
2
+
3
+ from configvault.client import ConfigVaultClient
4
+ from configvault.models import ConfigResponse, ConfigListResponse, HealthResponse
5
+ from configvault.exceptions import (
6
+ ConfigVaultError,
7
+ ConfigNotFoundError,
8
+ AuthenticationError,
9
+ ServiceUnavailableError,
10
+ )
11
+ from configvault.watcher import ConfigWatcher, ConfigChangedEvent
12
+
13
+ __version__ = "0.1.0"
14
+ __all__ = [
15
+ "ConfigVaultClient",
16
+ "ConfigResponse",
17
+ "ConfigListResponse",
18
+ "HealthResponse",
19
+ "ConfigVaultError",
20
+ "ConfigNotFoundError",
21
+ "AuthenticationError",
22
+ "ServiceUnavailableError",
23
+ "ConfigWatcher",
24
+ "ConfigChangedEvent",
25
+ ]
@@ -0,0 +1,165 @@
1
+ """ConfigVault API client."""
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+
7
+ from configvault.exceptions import (
8
+ AuthenticationError,
9
+ ConfigNotFoundError,
10
+ ConfigVaultError,
11
+ ServiceUnavailableError,
12
+ )
13
+ from configvault.models import ConfigListResponse, ConfigResponse, HealthResponse
14
+
15
+
16
+ class ConfigVaultClient:
17
+ """Async client for ConfigVault API."""
18
+
19
+ def __init__(
20
+ self,
21
+ base_url: str,
22
+ api_key: str,
23
+ timeout: float = 30.0,
24
+ ) -> None:
25
+ """
26
+ Initialize the ConfigVault client.
27
+
28
+ Args:
29
+ base_url: Base URL of the ConfigVault API (e.g., "http://localhost:5000")
30
+ api_key: API key for authentication
31
+ timeout: Request timeout in seconds
32
+ """
33
+ self._base_url = base_url.rstrip("/")
34
+ self._api_key = api_key
35
+ self._timeout = timeout
36
+ self._client: Optional[httpx.AsyncClient] = None
37
+
38
+ async def _get_client(self) -> httpx.AsyncClient:
39
+ """Get or create the HTTP client."""
40
+ if self._client is None or self._client.is_closed:
41
+ self._client = httpx.AsyncClient(
42
+ base_url=self._base_url,
43
+ headers={"X-Api-Key": self._api_key},
44
+ timeout=self._timeout,
45
+ )
46
+ return self._client
47
+
48
+ async def close(self) -> None:
49
+ """Close the HTTP client."""
50
+ if self._client is not None:
51
+ await self._client.aclose()
52
+ self._client = None
53
+
54
+ async def __aenter__(self) -> "ConfigVaultClient":
55
+ """Async context manager entry."""
56
+ return self
57
+
58
+ async def __aexit__(self, *args) -> None:
59
+ """Async context manager exit."""
60
+ await self.close()
61
+
62
+ def watch(self, filter_pattern: str | None = None) -> "ConfigWatcher":
63
+ """
64
+ Create a watcher for configuration changes.
65
+
66
+ Args:
67
+ filter_pattern: Optional glob pattern to filter keys (e.g., "production/*")
68
+
69
+ Returns:
70
+ ConfigWatcher instance for async iteration
71
+ """
72
+ from configvault.watcher import ConfigWatcher
73
+
74
+ return ConfigWatcher(
75
+ base_url=self._base_url,
76
+ api_key=self._api_key,
77
+ filter_pattern=filter_pattern,
78
+ )
79
+
80
+ def _handle_error_response(self, response: httpx.Response, key: Optional[str] = None) -> None:
81
+ """Handle error responses from the API."""
82
+ if response.status_code == 401:
83
+ raise AuthenticationError()
84
+ if response.status_code == 404 and key:
85
+ raise ConfigNotFoundError(key)
86
+ if response.status_code == 503:
87
+ raise ServiceUnavailableError()
88
+ if response.status_code >= 400:
89
+ raise ConfigVaultError(f"API error: {response.status_code}")
90
+
91
+ async def get(self, key: str) -> str:
92
+ """
93
+ Get a configuration value by key.
94
+
95
+ Args:
96
+ key: Hierarchical key (e.g., "production/database/connection")
97
+
98
+ Returns:
99
+ The configuration value
100
+
101
+ Raises:
102
+ ConfigNotFoundError: If the key does not exist
103
+ AuthenticationError: If the API key is invalid
104
+ ServiceUnavailableError: If the service is unavailable
105
+ """
106
+ client = await self._get_client()
107
+ response = await client.get(f"/config/{key}")
108
+
109
+ self._handle_error_response(response, key)
110
+
111
+ data = ConfigResponse.model_validate(response.json())
112
+ return data.value
113
+
114
+ async def exists(self, key: str) -> bool:
115
+ """
116
+ Check if a configuration key exists.
117
+
118
+ Args:
119
+ key: Hierarchical key to check
120
+
121
+ Returns:
122
+ True if the key exists, False otherwise
123
+ """
124
+ client = await self._get_client()
125
+ response = await client.head(f"/config/{key}")
126
+
127
+ if response.status_code == 404:
128
+ return False
129
+ if response.status_code == 401:
130
+ raise AuthenticationError()
131
+ if response.status_code == 503:
132
+ raise ServiceUnavailableError()
133
+
134
+ return response.status_code == 200
135
+
136
+ async def list(self, namespace: str) -> dict[str, str]:
137
+ """
138
+ List all configurations in a namespace.
139
+
140
+ Args:
141
+ namespace: The namespace (folder) to list
142
+
143
+ Returns:
144
+ Dictionary mapping keys to values
145
+ """
146
+ client = await self._get_client()
147
+ response = await client.get("/config", params={"prefix": namespace})
148
+
149
+ self._handle_error_response(response)
150
+
151
+ data = ConfigListResponse.model_validate(response.json())
152
+ return data.configs
153
+
154
+ async def health(self) -> HealthResponse:
155
+ """
156
+ Check the health of the ConfigVault service.
157
+
158
+ Returns:
159
+ Health status information
160
+ """
161
+ client = await self._get_client()
162
+ # Health endpoint doesn't require API key, but we send it anyway
163
+ response = await client.get("/health")
164
+
165
+ return HealthResponse.model_validate(response.json())
@@ -0,0 +1,29 @@
1
+ """ConfigVault SDK exceptions."""
2
+
3
+
4
+ class ConfigVaultError(Exception):
5
+ """Base exception for ConfigVault SDK."""
6
+
7
+ pass
8
+
9
+
10
+ class ConfigNotFoundError(ConfigVaultError):
11
+ """Raised when a configuration key is not found."""
12
+
13
+ def __init__(self, key: str) -> None:
14
+ self.key = key
15
+ super().__init__(f"Configuration key '{key}' not found")
16
+
17
+
18
+ class AuthenticationError(ConfigVaultError):
19
+ """Raised when API key authentication fails."""
20
+
21
+ def __init__(self, message: str = "Invalid or missing API key") -> None:
22
+ super().__init__(message)
23
+
24
+
25
+ class ServiceUnavailableError(ConfigVaultError):
26
+ """Raised when the ConfigVault service is unavailable."""
27
+
28
+ def __init__(self, message: str = "ConfigVault service unavailable") -> None:
29
+ super().__init__(message)
@@ -0,0 +1,27 @@
1
+ """ConfigVault SDK models."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ConfigResponse(BaseModel):
9
+ """Response model for a single configuration value."""
10
+
11
+ key: str
12
+ value: str
13
+
14
+
15
+ class ConfigListResponse(BaseModel):
16
+ """Response model for listing configurations."""
17
+
18
+ namespace: str = Field(alias="namespace")
19
+ configs: dict[str, str]
20
+
21
+
22
+ class HealthResponse(BaseModel):
23
+ """Response model for health check."""
24
+
25
+ status: str
26
+ vault: str
27
+ timestamp: datetime
File without changes
@@ -0,0 +1,94 @@
1
+ """Configuration change watcher using SSE."""
2
+
3
+ from collections.abc import AsyncIterator
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ import asyncio
7
+ import json
8
+
9
+ import httpx
10
+ from httpx_sse import aconnect_sse
11
+
12
+ from configvault.exceptions import AuthenticationError, ServiceUnavailableError
13
+
14
+
15
+ @dataclass
16
+ class ConfigChangedEvent:
17
+ """Event emitted when configuration changes are detected."""
18
+
19
+ keys: list[str]
20
+ timestamp: datetime
21
+
22
+
23
+ class ConfigWatcher:
24
+ """Watches for configuration changes via SSE."""
25
+
26
+ def __init__(
27
+ self,
28
+ base_url: str,
29
+ api_key: str,
30
+ filter_pattern: str | None = None,
31
+ reconnect_delay: float = 5.0,
32
+ ) -> None:
33
+ """
34
+ Initialize the watcher.
35
+
36
+ Args:
37
+ base_url: Base URL of the ConfigVault API.
38
+ api_key: API key for authentication.
39
+ filter_pattern: Optional glob pattern to filter keys.
40
+ reconnect_delay: Delay before reconnecting after a failure.
41
+ """
42
+ self._base_url = base_url.rstrip("/")
43
+ self._api_key = api_key
44
+ self._filter_pattern = filter_pattern
45
+ self._reconnect_delay = reconnect_delay
46
+ self._running = False
47
+
48
+ async def watch(self) -> AsyncIterator[ConfigChangedEvent]:
49
+ """
50
+ Watch for configuration changes.
51
+
52
+ Yields:
53
+ ConfigChangedEvent instances when changes are detected.
54
+ """
55
+ self._running = True
56
+
57
+ while self._running:
58
+ try:
59
+ async for event in self._connect():
60
+ yield event
61
+ except httpx.HTTPStatusError as exc:
62
+ if exc.response.status_code == 401:
63
+ raise AuthenticationError() from exc
64
+ if exc.response.status_code == 503:
65
+ raise ServiceUnavailableError() from exc
66
+ raise
67
+ except (httpx.ConnectError, httpx.ReadError):
68
+ if self._running:
69
+ await asyncio.sleep(self._reconnect_delay)
70
+
71
+ async def _connect(self) -> AsyncIterator[ConfigChangedEvent]:
72
+ """Connect to the SSE endpoint and yield events."""
73
+ url = f"{self._base_url}/events"
74
+ if self._filter_pattern:
75
+ url += f"?filter={self._filter_pattern}"
76
+
77
+ async with httpx.AsyncClient() as client:
78
+ async with aconnect_sse(
79
+ client,
80
+ "GET",
81
+ url,
82
+ headers={"X-Api-Key": self._api_key},
83
+ ) as event_source:
84
+ async for sse in event_source.aiter_sse():
85
+ if sse.event == "config-changed":
86
+ data = json.loads(sse.data)
87
+ timestamp = datetime.fromisoformat(
88
+ data["timestamp"].replace("Z", "+00:00")
89
+ )
90
+ yield ConfigChangedEvent(keys=data["keys"], timestamp=timestamp)
91
+
92
+ def stop(self) -> None:
93
+ """Stop watching for changes."""
94
+ self._running = False
File without changes
@@ -0,0 +1,129 @@
1
+ """Tests for ConfigVault client."""
2
+
3
+ import httpx
4
+ import pytest
5
+ import respx
6
+
7
+ from configvault import (
8
+ AuthenticationError,
9
+ ConfigNotFoundError,
10
+ ConfigVaultClient,
11
+ ServiceUnavailableError,
12
+ )
13
+
14
+
15
+ @pytest.fixture
16
+ def base_url() -> str:
17
+ return "http://localhost:5000"
18
+
19
+
20
+ @pytest.fixture
21
+ def api_key() -> str:
22
+ return "test-api-key"
23
+
24
+
25
+ class TestConfigVaultClient:
26
+ @respx.mock
27
+ async def test_get_returns_value(self, base_url: str, api_key: str) -> None:
28
+ respx.get(f"{base_url}/config/prod/db/host").mock(
29
+ return_value=httpx.Response(
30
+ 200,
31
+ json={"key": "prod/db/host", "value": "localhost"},
32
+ )
33
+ )
34
+
35
+ async with ConfigVaultClient(base_url, api_key) as client:
36
+ value = await client.get("prod/db/host")
37
+
38
+ assert value == "localhost"
39
+
40
+ @respx.mock
41
+ async def test_get_raises_not_found(self, base_url: str, api_key: str) -> None:
42
+ respx.get(f"{base_url}/config/unknown/key").mock(
43
+ return_value=httpx.Response(404, json={"error": "Not found"})
44
+ )
45
+
46
+ async with ConfigVaultClient(base_url, api_key) as client:
47
+ with pytest.raises(ConfigNotFoundError) as exc:
48
+ await client.get("unknown/key")
49
+
50
+ assert exc.value.key == "unknown/key"
51
+
52
+ @respx.mock
53
+ async def test_get_raises_auth_error(self, base_url: str, api_key: str) -> None:
54
+ respx.get(f"{base_url}/config/prod/key").mock(
55
+ return_value=httpx.Response(401, json={"error": "Unauthorized"})
56
+ )
57
+
58
+ async with ConfigVaultClient(base_url, api_key) as client:
59
+ with pytest.raises(AuthenticationError):
60
+ await client.get("prod/key")
61
+
62
+ @respx.mock
63
+ async def test_exists_returns_true(self, base_url: str, api_key: str) -> None:
64
+ respx.head(f"{base_url}/config/prod/db/host").mock(
65
+ return_value=httpx.Response(200)
66
+ )
67
+
68
+ async with ConfigVaultClient(base_url, api_key) as client:
69
+ exists = await client.exists("prod/db/host")
70
+
71
+ assert exists is True
72
+
73
+ @respx.mock
74
+ async def test_exists_returns_false(self, base_url: str, api_key: str) -> None:
75
+ respx.head(f"{base_url}/config/unknown/key").mock(
76
+ return_value=httpx.Response(404)
77
+ )
78
+
79
+ async with ConfigVaultClient(base_url, api_key) as client:
80
+ exists = await client.exists("unknown/key")
81
+
82
+ assert exists is False
83
+
84
+ @respx.mock
85
+ async def test_list_returns_configs(self, base_url: str, api_key: str) -> None:
86
+ respx.get(f"{base_url}/config", params={"prefix": "production"}).mock(
87
+ return_value=httpx.Response(
88
+ 200,
89
+ json={
90
+ "namespace": "production",
91
+ "configs": {"db/host": "localhost", "db/port": "5432"},
92
+ },
93
+ )
94
+ )
95
+
96
+ async with ConfigVaultClient(base_url, api_key) as client:
97
+ configs = await client.list("production")
98
+
99
+ assert configs["db/host"] == "localhost"
100
+ assert configs["db/port"] == "5432"
101
+
102
+ @respx.mock
103
+ async def test_health_returns_status(self, base_url: str, api_key: str) -> None:
104
+ respx.get(f"{base_url}/health").mock(
105
+ return_value=httpx.Response(
106
+ 200,
107
+ json={
108
+ "status": "healthy",
109
+ "vault": "unlocked",
110
+ "timestamp": "2026-02-02T12:00:00Z",
111
+ },
112
+ )
113
+ )
114
+
115
+ async with ConfigVaultClient(base_url, api_key) as client:
116
+ health = await client.health()
117
+
118
+ assert health.status == "healthy"
119
+ assert health.vault == "unlocked"
120
+
121
+ @respx.mock
122
+ async def test_service_unavailable(self, base_url: str, api_key: str) -> None:
123
+ respx.get(f"{base_url}/config/prod/key").mock(
124
+ return_value=httpx.Response(503, json={"error": "Service unavailable"})
125
+ )
126
+
127
+ async with ConfigVaultClient(base_url, api_key) as client:
128
+ with pytest.raises(ServiceUnavailableError):
129
+ await client.get("prod/key")
@@ -0,0 +1,76 @@
1
+ """Tests for ConfigVault models and exceptions."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ import pytest
6
+
7
+ from configvault.exceptions import (
8
+ AuthenticationError,
9
+ ConfigNotFoundError,
10
+ ServiceUnavailableError,
11
+ )
12
+ from configvault.models import ConfigResponse, ConfigListResponse, HealthResponse
13
+
14
+
15
+ class TestConfigResponse:
16
+ def test_create_from_dict(self) -> None:
17
+ data = {"key": "prod/db/host", "value": "localhost"}
18
+ response = ConfigResponse.model_validate(data)
19
+
20
+ assert response.key == "prod/db/host"
21
+ assert response.value == "localhost"
22
+
23
+
24
+ class TestConfigListResponse:
25
+ def test_create_from_dict(self) -> None:
26
+ data = {
27
+ "namespace": "production",
28
+ "configs": {"db/host": "localhost", "db/port": "5432"},
29
+ }
30
+ response = ConfigListResponse.model_validate(data)
31
+
32
+ assert response.namespace == "production"
33
+ assert response.configs["db/host"] == "localhost"
34
+ assert len(response.configs) == 2
35
+
36
+
37
+ class TestHealthResponse:
38
+ def test_create_from_dict(self) -> None:
39
+ data = {
40
+ "status": "healthy",
41
+ "vault": "unlocked",
42
+ "timestamp": "2026-02-02T12:00:00Z",
43
+ }
44
+ response = HealthResponse.model_validate(data)
45
+
46
+ assert response.status == "healthy"
47
+ assert response.vault == "unlocked"
48
+
49
+ def test_timestamp_parses_to_datetime(self) -> None:
50
+ data = {
51
+ "status": "healthy",
52
+ "vault": "unlocked",
53
+ "timestamp": "2026-02-02T12:00:00Z",
54
+ }
55
+ response = HealthResponse.model_validate(data)
56
+
57
+ assert isinstance(response.timestamp, datetime)
58
+ assert response.timestamp.tzinfo is not None
59
+ assert response.timestamp == datetime(2026, 2, 2, 12, 0, 0, tzinfo=timezone.utc)
60
+
61
+
62
+ class TestExceptions:
63
+ def test_config_not_found_error_message(self) -> None:
64
+ error = ConfigNotFoundError("prod/missing")
65
+
66
+ assert str(error) == "Configuration key 'prod/missing' not found"
67
+
68
+ def test_authentication_error_default_message(self) -> None:
69
+ error = AuthenticationError()
70
+
71
+ assert str(error) == "Invalid or missing API key"
72
+
73
+ def test_service_unavailable_error_default_message(self) -> None:
74
+ error = ServiceUnavailableError()
75
+
76
+ assert str(error) == "ConfigVault service unavailable"
@@ -0,0 +1,48 @@
1
+ """Tests for ConfigWatcher."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from configvault import ConfigVaultClient
6
+ from configvault.watcher import ConfigChangedEvent, ConfigWatcher
7
+
8
+
9
+ class TestConfigWatcher:
10
+ def test_creates_with_filter(self) -> None:
11
+ watcher = ConfigWatcher(
12
+ base_url="http://localhost:5000",
13
+ api_key="test-key",
14
+ filter_pattern="production/*",
15
+ )
16
+
17
+ assert watcher._filter_pattern == "production/*"
18
+ assert watcher._running is False
19
+
20
+ def test_stop_sets_running_false(self) -> None:
21
+ watcher = ConfigWatcher(
22
+ base_url="http://localhost:5000",
23
+ api_key="test-key",
24
+ )
25
+ watcher._running = True
26
+
27
+ watcher.stop()
28
+
29
+ assert watcher._running is False
30
+
31
+ def test_client_watch_returns_watcher(self) -> None:
32
+ client = ConfigVaultClient("http://localhost:5000", "test-key")
33
+
34
+ watcher = client.watch("production/*")
35
+
36
+ assert isinstance(watcher, ConfigWatcher)
37
+ assert watcher._filter_pattern == "production/*"
38
+
39
+
40
+ class TestConfigChangedEvent:
41
+ def test_creates_event(self) -> None:
42
+ event = ConfigChangedEvent(
43
+ keys=["prod/db/host", "prod/db/port"],
44
+ timestamp=datetime(2026, 2, 2, 12, 0, 0, tzinfo=timezone.utc),
45
+ )
46
+
47
+ assert len(event.keys) == 2
48
+ assert "prod/db/host" in event.keys