pyw-config 0.0.0__tar.gz → 0.0.0.post2__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.
- pyw_config-0.0.0.post2/PKG-INFO +411 -0
- pyw_config-0.0.0.post2/README.md +391 -0
- pyw_config-0.0.0.post2/pyproject.toml +30 -0
- pyw_config-0.0.0/PKG-INFO +0 -15
- pyw_config-0.0.0/README.md +0 -3
- pyw_config-0.0.0/pyproject.toml +0 -19
- {pyw_config-0.0.0 → pyw_config-0.0.0.post2}/LICENSE +0 -0
- {pyw_config-0.0.0 → pyw_config-0.0.0.post2}/src/pyw/__init__.py +0 -0
- {pyw_config-0.0.0 → pyw_config-0.0.0.post2}/src/pyw/config/__init__.py +0 -0
@@ -0,0 +1,411 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: pyw-config
|
3
|
+
Version: 0.0.0.post2
|
4
|
+
Summary: Reserved placeholder for pyw-config (configuration utilities for pythonWoods suite)
|
5
|
+
Project-URL: Homepage, https://github.com/pythonWoods/pyw-config
|
6
|
+
Project-URL: Documentation, https://pythonwoods.dev/docs/pyw-config/latest/
|
7
|
+
Project-URL: Issues, https://github.com/pythonWoods/pyw-config/issues
|
8
|
+
Project-URL: Changelog, https://github.com/pythonWoods/pyw-config/releases
|
9
|
+
Author-email: pythonWoods <you@example.com>
|
10
|
+
License: MIT
|
11
|
+
License-File: LICENSE
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Typing :: Typed
|
17
|
+
Requires-Python: >=3.9
|
18
|
+
Requires-Dist: pyw-core>=0.0.0
|
19
|
+
Description-Content-Type: text/markdown
|
20
|
+
|
21
|
+
# pyw-config ⚙️
|
22
|
+
[](https://pypi.org/project/pyw-config/)
|
23
|
+
[](https://github.com/pythonWoods/pyw-config/actions/workflows/ci.yml)
|
24
|
+
[](LICENSE)
|
25
|
+
|
26
|
+
> Configuration management utilities for the **pythonWoods** ecosystem.
|
27
|
+
|
28
|
+
## Overview
|
29
|
+
|
30
|
+
**pyw-config** fornisce un sistema di gestione configurazioni type-safe e flessibile, con supporto per multipli backend (file, environment variables, remote sources) e validazione automatica tramite Pydantic.
|
31
|
+
|
32
|
+
## Philosophy
|
33
|
+
|
34
|
+
* **Type-safe configs** – Pydantic models per zero errori di configurazione
|
35
|
+
* **Multiple sources** – YAML, JSON, TOML, .env, environment variables
|
36
|
+
* **Hierarchical merging** – override intelligente di configurazioni
|
37
|
+
* **Environment-aware** – profili per dev/staging/prod
|
38
|
+
* **Validation-first** – errori chiari e actionable per configurazioni invalide
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
```bash
|
43
|
+
pip install pyw-config
|
44
|
+
```
|
45
|
+
|
46
|
+
Per backend aggiuntivi:
|
47
|
+
|
48
|
+
```bash
|
49
|
+
pip install pyw-config[yaml] # + PyYAML per file YAML
|
50
|
+
pip install pyw-config[toml] # + tomli/tomllib per TOML
|
51
|
+
pip install pyw-config[vault] # + hvac per HashiCorp Vault
|
52
|
+
pip install pyw-config[remote] # + requests per config remote
|
53
|
+
pip install pyw-config[full] # tutti i backend
|
54
|
+
```
|
55
|
+
|
56
|
+
## Quick Start
|
57
|
+
|
58
|
+
### Basic Configuration
|
59
|
+
|
60
|
+
```python
|
61
|
+
from pyw.config import BaseConfig, Field
|
62
|
+
from pyw.config.sources import from_file, from_env
|
63
|
+
|
64
|
+
class DatabaseConfig(BaseConfig):
|
65
|
+
host: str = Field(default="localhost")
|
66
|
+
port: int = Field(default=5432, ge=1, le=65535)
|
67
|
+
username: str
|
68
|
+
password: str = Field(..., min_length=8)
|
69
|
+
database: str
|
70
|
+
ssl_enabled: bool = Field(default=True)
|
71
|
+
|
72
|
+
class AppConfig(BaseConfig):
|
73
|
+
debug: bool = Field(default=False)
|
74
|
+
secret_key: str = Field(..., min_length=32)
|
75
|
+
db: DatabaseConfig
|
76
|
+
api_timeout: float = Field(default=30.0, gt=0)
|
77
|
+
|
78
|
+
# Carica da file + environment
|
79
|
+
config = AppConfig.from_sources(
|
80
|
+
from_file("config.yaml"),
|
81
|
+
from_env(prefix="MYAPP_")
|
82
|
+
)
|
83
|
+
|
84
|
+
print(f"Connecting to {config.db.host}:{config.db.port}")
|
85
|
+
```
|
86
|
+
|
87
|
+
### Configuration Files
|
88
|
+
|
89
|
+
**config.yaml:**
|
90
|
+
```yaml
|
91
|
+
debug: false
|
92
|
+
secret_key: "your-super-secret-key-here-min-32-chars"
|
93
|
+
db:
|
94
|
+
host: "prod-db.example.com"
|
95
|
+
username: "myapp"
|
96
|
+
database: "production"
|
97
|
+
ssl_enabled: true
|
98
|
+
api_timeout: 60.0
|
99
|
+
```
|
100
|
+
|
101
|
+
**Environment Variables:**
|
102
|
+
```bash
|
103
|
+
export MYAPP_DEBUG=true
|
104
|
+
export MYAPP_DB_PASSWORD=secure_password_123
|
105
|
+
export MYAPP_DB_PORT=5433
|
106
|
+
```
|
107
|
+
|
108
|
+
## Features
|
109
|
+
|
110
|
+
### 🔄 Multiple Configuration Sources
|
111
|
+
|
112
|
+
```python
|
113
|
+
from pyw.config.sources import (
|
114
|
+
from_file, from_env, from_dict,
|
115
|
+
from_vault, from_url
|
116
|
+
)
|
117
|
+
|
118
|
+
config = AppConfig.from_sources(
|
119
|
+
# 1. File di base
|
120
|
+
from_file("config.yaml"),
|
121
|
+
|
122
|
+
# 2. Override per environment
|
123
|
+
from_file(f"config.{env}.yaml", optional=True),
|
124
|
+
|
125
|
+
# 3. Secrets da Vault
|
126
|
+
from_vault("secret/myapp", optional=True),
|
127
|
+
|
128
|
+
# 4. Environment variables (priorità massima)
|
129
|
+
from_env(prefix="MYAPP_"),
|
130
|
+
|
131
|
+
# 5. Config remota
|
132
|
+
from_url("https://config.myapp.com/api/config", optional=True)
|
133
|
+
)
|
134
|
+
```
|
135
|
+
|
136
|
+
### 🌍 Environment Profiles
|
137
|
+
|
138
|
+
```python
|
139
|
+
from pyw.config import ConfigProfile
|
140
|
+
|
141
|
+
class AppConfig(BaseConfig):
|
142
|
+
class Meta:
|
143
|
+
profiles = {
|
144
|
+
"development": {
|
145
|
+
"debug": True,
|
146
|
+
"db.host": "localhost",
|
147
|
+
"api_timeout": 5.0
|
148
|
+
},
|
149
|
+
"production": {
|
150
|
+
"debug": False,
|
151
|
+
"db.ssl_enabled": True,
|
152
|
+
"api_timeout": 30.0
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
# Carica profilo automaticamente da ENV
|
157
|
+
config = AppConfig.load_profile() # Legge ENVIRONMENT=production
|
158
|
+
|
159
|
+
# Oppure esplicitamente
|
160
|
+
config = AppConfig.load_profile("development")
|
161
|
+
```
|
162
|
+
|
163
|
+
### 🔒 Secrets Management
|
164
|
+
|
165
|
+
```python
|
166
|
+
from pyw.config import SecretStr, SecretBytes
|
167
|
+
from pyw.config.secrets import from_keyring, from_1password
|
168
|
+
|
169
|
+
class Config(BaseConfig):
|
170
|
+
# Secrets non loggati/serializzati
|
171
|
+
api_key: SecretStr
|
172
|
+
private_key: SecretBytes
|
173
|
+
|
174
|
+
# Caricamento da secret manager
|
175
|
+
class Meta:
|
176
|
+
secret_sources = [
|
177
|
+
from_keyring("myapp"),
|
178
|
+
from_1password("myapp-vault")
|
179
|
+
]
|
180
|
+
|
181
|
+
# I secrets sono automaticamente mascherati
|
182
|
+
print(config.api_key) # → SecretStr('**********')
|
183
|
+
print(config.api_key.get_secret_value()) # → valore reale
|
184
|
+
```
|
185
|
+
|
186
|
+
### 🔄 Dynamic Configuration
|
187
|
+
|
188
|
+
```python
|
189
|
+
from pyw.config import WatchableConfig
|
190
|
+
import asyncio
|
191
|
+
|
192
|
+
class AppConfig(WatchableConfig):
|
193
|
+
feature_flags: dict[str, bool] = Field(default_factory=dict)
|
194
|
+
rate_limit: int = 100
|
195
|
+
|
196
|
+
# Reload automatico su cambio file
|
197
|
+
config = AppConfig.from_file("config.yaml", watch=True)
|
198
|
+
|
199
|
+
@config.on_change
|
200
|
+
async def config_changed(old_config, new_config):
|
201
|
+
if old_config.rate_limit != new_config.rate_limit:
|
202
|
+
await update_rate_limiter(new_config.rate_limit)
|
203
|
+
|
204
|
+
# Avvia watching
|
205
|
+
await config.start_watching()
|
206
|
+
```
|
207
|
+
|
208
|
+
### 📊 Configuration Validation
|
209
|
+
|
210
|
+
```python
|
211
|
+
from pyw.config import validator, root_validator
|
212
|
+
from typing import Optional
|
213
|
+
|
214
|
+
class DatabaseConfig(BaseConfig):
|
215
|
+
host: str
|
216
|
+
port: int = Field(ge=1, le=65535)
|
217
|
+
replica_hosts: Optional[list[str]] = None
|
218
|
+
|
219
|
+
@validator('host')
|
220
|
+
def validate_host(cls, v):
|
221
|
+
if not v or v == 'localhost':
|
222
|
+
return v
|
223
|
+
# Valida formato hostname/IP
|
224
|
+
import socket
|
225
|
+
try:
|
226
|
+
socket.gethostbyname(v)
|
227
|
+
return v
|
228
|
+
except socket.gaierror:
|
229
|
+
raise ValueError(f"Invalid hostname: {v}")
|
230
|
+
|
231
|
+
@root_validator
|
232
|
+
def validate_replicas(cls, values):
|
233
|
+
if values.get('replica_hosts'):
|
234
|
+
main_host = values.get('host')
|
235
|
+
if main_host in values['replica_hosts']:
|
236
|
+
raise ValueError("Main host cannot be in replica list")
|
237
|
+
return values
|
238
|
+
```
|
239
|
+
|
240
|
+
### 🧪 Testing Support
|
241
|
+
|
242
|
+
```python
|
243
|
+
from pyw.config.testing import temporary_config, mock_env
|
244
|
+
|
245
|
+
class TestApp:
|
246
|
+
def test_with_temp_config(self):
|
247
|
+
with temporary_config(AppConfig, {"debug": True}):
|
248
|
+
config = AppConfig.load()
|
249
|
+
assert config.debug is True
|
250
|
+
|
251
|
+
def test_with_mock_env(self):
|
252
|
+
with mock_env(MYAPP_DEBUG="false"):
|
253
|
+
config = AppConfig.from_env(prefix="MYAPP_")
|
254
|
+
assert config.debug is False
|
255
|
+
```
|
256
|
+
|
257
|
+
## Advanced Usage
|
258
|
+
|
259
|
+
### Custom Configuration Sources
|
260
|
+
|
261
|
+
```python
|
262
|
+
from pyw.config.sources import ConfigSource
|
263
|
+
import redis
|
264
|
+
|
265
|
+
class RedisConfigSource(ConfigSource):
|
266
|
+
def __init__(self, redis_client, key_prefix="config:"):
|
267
|
+
self.redis = redis_client
|
268
|
+
self.prefix = key_prefix
|
269
|
+
|
270
|
+
def load(self) -> dict:
|
271
|
+
keys = self.redis.keys(f"{self.prefix}*")
|
272
|
+
config = {}
|
273
|
+
for key in keys:
|
274
|
+
config_key = key.decode().replace(self.prefix, "")
|
275
|
+
config[config_key] = self.redis.get(key).decode()
|
276
|
+
return config
|
277
|
+
|
278
|
+
# Utilizzo
|
279
|
+
redis_client = redis.Redis()
|
280
|
+
config = AppConfig.from_sources(
|
281
|
+
RedisConfigSource(redis_client),
|
282
|
+
from_env(prefix="MYAPP_")
|
283
|
+
)
|
284
|
+
```
|
285
|
+
|
286
|
+
### Configuration Schemas
|
287
|
+
|
288
|
+
```python
|
289
|
+
from pyw.config import ConfigSchema, generate_schema
|
290
|
+
|
291
|
+
# Genera JSON Schema
|
292
|
+
schema = generate_schema(AppConfig)
|
293
|
+
print(schema)
|
294
|
+
|
295
|
+
# Genera esempio di configurazione
|
296
|
+
example = AppConfig.generate_example()
|
297
|
+
with open("config.example.yaml", "w") as f:
|
298
|
+
yaml.dump(example, f)
|
299
|
+
|
300
|
+
# Validazione esterna
|
301
|
+
from pyw.config.validation import validate_file
|
302
|
+
|
303
|
+
errors = validate_file("config.yaml", AppConfig)
|
304
|
+
if errors:
|
305
|
+
for error in errors:
|
306
|
+
print(f"❌ {error.location}: {error.message}")
|
307
|
+
```
|
308
|
+
|
309
|
+
### Configuration Migrations
|
310
|
+
|
311
|
+
```python
|
312
|
+
from pyw.config.migrations import ConfigMigration
|
313
|
+
|
314
|
+
class Migration001(ConfigMigration):
|
315
|
+
"""Rename db_host to database.host"""
|
316
|
+
version = "0.0.1"
|
317
|
+
|
318
|
+
def migrate(self, config: dict) -> dict:
|
319
|
+
if "db_host" in config:
|
320
|
+
config.setdefault("database", {})
|
321
|
+
config["database"]["host"] = config.pop("db_host")
|
322
|
+
return config
|
323
|
+
|
324
|
+
# Auto-apply migrations
|
325
|
+
config = AppConfig.from_file("old-config.yaml",
|
326
|
+
migrations=[Migration001()])
|
327
|
+
```
|
328
|
+
|
329
|
+
## Integration Examples
|
330
|
+
|
331
|
+
### FastAPI Integration
|
332
|
+
|
333
|
+
```python
|
334
|
+
from fastapi import FastAPI, Depends
|
335
|
+
from pyw.config import inject_config
|
336
|
+
|
337
|
+
app = FastAPI()
|
338
|
+
|
339
|
+
@app.get("/status")
|
340
|
+
def get_status(config: AppConfig = Depends(inject_config(AppConfig))):
|
341
|
+
return {
|
342
|
+
"debug": config.debug,
|
343
|
+
"database_host": config.db.host
|
344
|
+
}
|
345
|
+
```
|
346
|
+
|
347
|
+
### Django Integration
|
348
|
+
|
349
|
+
```python
|
350
|
+
# settings.py
|
351
|
+
from pyw.config import django_settings
|
352
|
+
|
353
|
+
class DjangoConfig(BaseConfig):
|
354
|
+
SECRET_KEY: str
|
355
|
+
DEBUG: bool = False
|
356
|
+
DATABASES: dict
|
357
|
+
ALLOWED_HOSTS: list[str] = Field(default_factory=list)
|
358
|
+
|
359
|
+
# Auto-populate Django settings
|
360
|
+
config = DjangoConfig.from_sources(
|
361
|
+
from_file("django.yaml"),
|
362
|
+
from_env(prefix="DJANGO_")
|
363
|
+
)
|
364
|
+
|
365
|
+
globals().update(django_settings(config))
|
366
|
+
```
|
367
|
+
|
368
|
+
## CLI Integration
|
369
|
+
|
370
|
+
```bash
|
371
|
+
# Valida configurazione
|
372
|
+
pyw-config validate config.yaml --schema=myapp.config:AppConfig
|
373
|
+
|
374
|
+
# Genera esempio
|
375
|
+
pyw-config generate-example myapp.config:AppConfig > config.example.yaml
|
376
|
+
|
377
|
+
# Merge configurazioni
|
378
|
+
pyw-config merge base.yaml override.yaml > final.yaml
|
379
|
+
|
380
|
+
# Mostra configurazione risolta (con secrets mascherati)
|
381
|
+
pyw-config show --env=production
|
382
|
+
```
|
383
|
+
|
384
|
+
## Roadmap
|
385
|
+
|
386
|
+
- 🔐 **Enhanced secrets**: Integrazione con AWS Secrets Manager, Azure Key Vault
|
387
|
+
- 🌐 **Remote configs**: Etcd, Consul, Kubernetes ConfigMaps
|
388
|
+
- 📝 **Configuration UI**: Web interface per editing configurazioni
|
389
|
+
- 🔄 **Hot reload**: Reload automatico in runtime senza restart
|
390
|
+
- 📊 **Config analytics**: Metriche di utilizzo e drift detection
|
391
|
+
- 🧩 **Plugin system**: Custom validators e sources
|
392
|
+
|
393
|
+
## Contributing
|
394
|
+
|
395
|
+
1. Fork il repo: `git clone https://github.com/pythonWoods/pyw-config.git`
|
396
|
+
2. Crea virtual-env: `poetry install && poetry shell`
|
397
|
+
3. Lancia tests: `pytest`
|
398
|
+
4. Lancia linter: `ruff check . && mypy`
|
399
|
+
5. Apri la PR: CI esegue tutti i check
|
400
|
+
|
401
|
+
Felice configurazione nella foresta di **pythonWoods**! 🌲⚙️
|
402
|
+
|
403
|
+
## Links utili
|
404
|
+
|
405
|
+
Documentazione dev (work-in-progress) → https://pythonwoods.dev/docs/pyw-config/latest/
|
406
|
+
|
407
|
+
Issue tracker → https://github.com/pythonWoods/pyw-config/issues
|
408
|
+
|
409
|
+
Changelog → https://github.com/pythonWoods/pyw-config/releases
|
410
|
+
|
411
|
+
© pythonWoods — MIT License
|
@@ -0,0 +1,391 @@
|
|
1
|
+
# pyw-config ⚙️
|
2
|
+
[](https://pypi.org/project/pyw-config/)
|
3
|
+
[](https://github.com/pythonWoods/pyw-config/actions/workflows/ci.yml)
|
4
|
+
[](LICENSE)
|
5
|
+
|
6
|
+
> Configuration management utilities for the **pythonWoods** ecosystem.
|
7
|
+
|
8
|
+
## Overview
|
9
|
+
|
10
|
+
**pyw-config** fornisce un sistema di gestione configurazioni type-safe e flessibile, con supporto per multipli backend (file, environment variables, remote sources) e validazione automatica tramite Pydantic.
|
11
|
+
|
12
|
+
## Philosophy
|
13
|
+
|
14
|
+
* **Type-safe configs** – Pydantic models per zero errori di configurazione
|
15
|
+
* **Multiple sources** – YAML, JSON, TOML, .env, environment variables
|
16
|
+
* **Hierarchical merging** – override intelligente di configurazioni
|
17
|
+
* **Environment-aware** – profili per dev/staging/prod
|
18
|
+
* **Validation-first** – errori chiari e actionable per configurazioni invalide
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
```bash
|
23
|
+
pip install pyw-config
|
24
|
+
```
|
25
|
+
|
26
|
+
Per backend aggiuntivi:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
pip install pyw-config[yaml] # + PyYAML per file YAML
|
30
|
+
pip install pyw-config[toml] # + tomli/tomllib per TOML
|
31
|
+
pip install pyw-config[vault] # + hvac per HashiCorp Vault
|
32
|
+
pip install pyw-config[remote] # + requests per config remote
|
33
|
+
pip install pyw-config[full] # tutti i backend
|
34
|
+
```
|
35
|
+
|
36
|
+
## Quick Start
|
37
|
+
|
38
|
+
### Basic Configuration
|
39
|
+
|
40
|
+
```python
|
41
|
+
from pyw.config import BaseConfig, Field
|
42
|
+
from pyw.config.sources import from_file, from_env
|
43
|
+
|
44
|
+
class DatabaseConfig(BaseConfig):
|
45
|
+
host: str = Field(default="localhost")
|
46
|
+
port: int = Field(default=5432, ge=1, le=65535)
|
47
|
+
username: str
|
48
|
+
password: str = Field(..., min_length=8)
|
49
|
+
database: str
|
50
|
+
ssl_enabled: bool = Field(default=True)
|
51
|
+
|
52
|
+
class AppConfig(BaseConfig):
|
53
|
+
debug: bool = Field(default=False)
|
54
|
+
secret_key: str = Field(..., min_length=32)
|
55
|
+
db: DatabaseConfig
|
56
|
+
api_timeout: float = Field(default=30.0, gt=0)
|
57
|
+
|
58
|
+
# Carica da file + environment
|
59
|
+
config = AppConfig.from_sources(
|
60
|
+
from_file("config.yaml"),
|
61
|
+
from_env(prefix="MYAPP_")
|
62
|
+
)
|
63
|
+
|
64
|
+
print(f"Connecting to {config.db.host}:{config.db.port}")
|
65
|
+
```
|
66
|
+
|
67
|
+
### Configuration Files
|
68
|
+
|
69
|
+
**config.yaml:**
|
70
|
+
```yaml
|
71
|
+
debug: false
|
72
|
+
secret_key: "your-super-secret-key-here-min-32-chars"
|
73
|
+
db:
|
74
|
+
host: "prod-db.example.com"
|
75
|
+
username: "myapp"
|
76
|
+
database: "production"
|
77
|
+
ssl_enabled: true
|
78
|
+
api_timeout: 60.0
|
79
|
+
```
|
80
|
+
|
81
|
+
**Environment Variables:**
|
82
|
+
```bash
|
83
|
+
export MYAPP_DEBUG=true
|
84
|
+
export MYAPP_DB_PASSWORD=secure_password_123
|
85
|
+
export MYAPP_DB_PORT=5433
|
86
|
+
```
|
87
|
+
|
88
|
+
## Features
|
89
|
+
|
90
|
+
### 🔄 Multiple Configuration Sources
|
91
|
+
|
92
|
+
```python
|
93
|
+
from pyw.config.sources import (
|
94
|
+
from_file, from_env, from_dict,
|
95
|
+
from_vault, from_url
|
96
|
+
)
|
97
|
+
|
98
|
+
config = AppConfig.from_sources(
|
99
|
+
# 1. File di base
|
100
|
+
from_file("config.yaml"),
|
101
|
+
|
102
|
+
# 2. Override per environment
|
103
|
+
from_file(f"config.{env}.yaml", optional=True),
|
104
|
+
|
105
|
+
# 3. Secrets da Vault
|
106
|
+
from_vault("secret/myapp", optional=True),
|
107
|
+
|
108
|
+
# 4. Environment variables (priorità massima)
|
109
|
+
from_env(prefix="MYAPP_"),
|
110
|
+
|
111
|
+
# 5. Config remota
|
112
|
+
from_url("https://config.myapp.com/api/config", optional=True)
|
113
|
+
)
|
114
|
+
```
|
115
|
+
|
116
|
+
### 🌍 Environment Profiles
|
117
|
+
|
118
|
+
```python
|
119
|
+
from pyw.config import ConfigProfile
|
120
|
+
|
121
|
+
class AppConfig(BaseConfig):
|
122
|
+
class Meta:
|
123
|
+
profiles = {
|
124
|
+
"development": {
|
125
|
+
"debug": True,
|
126
|
+
"db.host": "localhost",
|
127
|
+
"api_timeout": 5.0
|
128
|
+
},
|
129
|
+
"production": {
|
130
|
+
"debug": False,
|
131
|
+
"db.ssl_enabled": True,
|
132
|
+
"api_timeout": 30.0
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
# Carica profilo automaticamente da ENV
|
137
|
+
config = AppConfig.load_profile() # Legge ENVIRONMENT=production
|
138
|
+
|
139
|
+
# Oppure esplicitamente
|
140
|
+
config = AppConfig.load_profile("development")
|
141
|
+
```
|
142
|
+
|
143
|
+
### 🔒 Secrets Management
|
144
|
+
|
145
|
+
```python
|
146
|
+
from pyw.config import SecretStr, SecretBytes
|
147
|
+
from pyw.config.secrets import from_keyring, from_1password
|
148
|
+
|
149
|
+
class Config(BaseConfig):
|
150
|
+
# Secrets non loggati/serializzati
|
151
|
+
api_key: SecretStr
|
152
|
+
private_key: SecretBytes
|
153
|
+
|
154
|
+
# Caricamento da secret manager
|
155
|
+
class Meta:
|
156
|
+
secret_sources = [
|
157
|
+
from_keyring("myapp"),
|
158
|
+
from_1password("myapp-vault")
|
159
|
+
]
|
160
|
+
|
161
|
+
# I secrets sono automaticamente mascherati
|
162
|
+
print(config.api_key) # → SecretStr('**********')
|
163
|
+
print(config.api_key.get_secret_value()) # → valore reale
|
164
|
+
```
|
165
|
+
|
166
|
+
### 🔄 Dynamic Configuration
|
167
|
+
|
168
|
+
```python
|
169
|
+
from pyw.config import WatchableConfig
|
170
|
+
import asyncio
|
171
|
+
|
172
|
+
class AppConfig(WatchableConfig):
|
173
|
+
feature_flags: dict[str, bool] = Field(default_factory=dict)
|
174
|
+
rate_limit: int = 100
|
175
|
+
|
176
|
+
# Reload automatico su cambio file
|
177
|
+
config = AppConfig.from_file("config.yaml", watch=True)
|
178
|
+
|
179
|
+
@config.on_change
|
180
|
+
async def config_changed(old_config, new_config):
|
181
|
+
if old_config.rate_limit != new_config.rate_limit:
|
182
|
+
await update_rate_limiter(new_config.rate_limit)
|
183
|
+
|
184
|
+
# Avvia watching
|
185
|
+
await config.start_watching()
|
186
|
+
```
|
187
|
+
|
188
|
+
### 📊 Configuration Validation
|
189
|
+
|
190
|
+
```python
|
191
|
+
from pyw.config import validator, root_validator
|
192
|
+
from typing import Optional
|
193
|
+
|
194
|
+
class DatabaseConfig(BaseConfig):
|
195
|
+
host: str
|
196
|
+
port: int = Field(ge=1, le=65535)
|
197
|
+
replica_hosts: Optional[list[str]] = None
|
198
|
+
|
199
|
+
@validator('host')
|
200
|
+
def validate_host(cls, v):
|
201
|
+
if not v or v == 'localhost':
|
202
|
+
return v
|
203
|
+
# Valida formato hostname/IP
|
204
|
+
import socket
|
205
|
+
try:
|
206
|
+
socket.gethostbyname(v)
|
207
|
+
return v
|
208
|
+
except socket.gaierror:
|
209
|
+
raise ValueError(f"Invalid hostname: {v}")
|
210
|
+
|
211
|
+
@root_validator
|
212
|
+
def validate_replicas(cls, values):
|
213
|
+
if values.get('replica_hosts'):
|
214
|
+
main_host = values.get('host')
|
215
|
+
if main_host in values['replica_hosts']:
|
216
|
+
raise ValueError("Main host cannot be in replica list")
|
217
|
+
return values
|
218
|
+
```
|
219
|
+
|
220
|
+
### 🧪 Testing Support
|
221
|
+
|
222
|
+
```python
|
223
|
+
from pyw.config.testing import temporary_config, mock_env
|
224
|
+
|
225
|
+
class TestApp:
|
226
|
+
def test_with_temp_config(self):
|
227
|
+
with temporary_config(AppConfig, {"debug": True}):
|
228
|
+
config = AppConfig.load()
|
229
|
+
assert config.debug is True
|
230
|
+
|
231
|
+
def test_with_mock_env(self):
|
232
|
+
with mock_env(MYAPP_DEBUG="false"):
|
233
|
+
config = AppConfig.from_env(prefix="MYAPP_")
|
234
|
+
assert config.debug is False
|
235
|
+
```
|
236
|
+
|
237
|
+
## Advanced Usage
|
238
|
+
|
239
|
+
### Custom Configuration Sources
|
240
|
+
|
241
|
+
```python
|
242
|
+
from pyw.config.sources import ConfigSource
|
243
|
+
import redis
|
244
|
+
|
245
|
+
class RedisConfigSource(ConfigSource):
|
246
|
+
def __init__(self, redis_client, key_prefix="config:"):
|
247
|
+
self.redis = redis_client
|
248
|
+
self.prefix = key_prefix
|
249
|
+
|
250
|
+
def load(self) -> dict:
|
251
|
+
keys = self.redis.keys(f"{self.prefix}*")
|
252
|
+
config = {}
|
253
|
+
for key in keys:
|
254
|
+
config_key = key.decode().replace(self.prefix, "")
|
255
|
+
config[config_key] = self.redis.get(key).decode()
|
256
|
+
return config
|
257
|
+
|
258
|
+
# Utilizzo
|
259
|
+
redis_client = redis.Redis()
|
260
|
+
config = AppConfig.from_sources(
|
261
|
+
RedisConfigSource(redis_client),
|
262
|
+
from_env(prefix="MYAPP_")
|
263
|
+
)
|
264
|
+
```
|
265
|
+
|
266
|
+
### Configuration Schemas
|
267
|
+
|
268
|
+
```python
|
269
|
+
from pyw.config import ConfigSchema, generate_schema
|
270
|
+
|
271
|
+
# Genera JSON Schema
|
272
|
+
schema = generate_schema(AppConfig)
|
273
|
+
print(schema)
|
274
|
+
|
275
|
+
# Genera esempio di configurazione
|
276
|
+
example = AppConfig.generate_example()
|
277
|
+
with open("config.example.yaml", "w") as f:
|
278
|
+
yaml.dump(example, f)
|
279
|
+
|
280
|
+
# Validazione esterna
|
281
|
+
from pyw.config.validation import validate_file
|
282
|
+
|
283
|
+
errors = validate_file("config.yaml", AppConfig)
|
284
|
+
if errors:
|
285
|
+
for error in errors:
|
286
|
+
print(f"❌ {error.location}: {error.message}")
|
287
|
+
```
|
288
|
+
|
289
|
+
### Configuration Migrations
|
290
|
+
|
291
|
+
```python
|
292
|
+
from pyw.config.migrations import ConfigMigration
|
293
|
+
|
294
|
+
class Migration001(ConfigMigration):
|
295
|
+
"""Rename db_host to database.host"""
|
296
|
+
version = "0.0.1"
|
297
|
+
|
298
|
+
def migrate(self, config: dict) -> dict:
|
299
|
+
if "db_host" in config:
|
300
|
+
config.setdefault("database", {})
|
301
|
+
config["database"]["host"] = config.pop("db_host")
|
302
|
+
return config
|
303
|
+
|
304
|
+
# Auto-apply migrations
|
305
|
+
config = AppConfig.from_file("old-config.yaml",
|
306
|
+
migrations=[Migration001()])
|
307
|
+
```
|
308
|
+
|
309
|
+
## Integration Examples
|
310
|
+
|
311
|
+
### FastAPI Integration
|
312
|
+
|
313
|
+
```python
|
314
|
+
from fastapi import FastAPI, Depends
|
315
|
+
from pyw.config import inject_config
|
316
|
+
|
317
|
+
app = FastAPI()
|
318
|
+
|
319
|
+
@app.get("/status")
|
320
|
+
def get_status(config: AppConfig = Depends(inject_config(AppConfig))):
|
321
|
+
return {
|
322
|
+
"debug": config.debug,
|
323
|
+
"database_host": config.db.host
|
324
|
+
}
|
325
|
+
```
|
326
|
+
|
327
|
+
### Django Integration
|
328
|
+
|
329
|
+
```python
|
330
|
+
# settings.py
|
331
|
+
from pyw.config import django_settings
|
332
|
+
|
333
|
+
class DjangoConfig(BaseConfig):
|
334
|
+
SECRET_KEY: str
|
335
|
+
DEBUG: bool = False
|
336
|
+
DATABASES: dict
|
337
|
+
ALLOWED_HOSTS: list[str] = Field(default_factory=list)
|
338
|
+
|
339
|
+
# Auto-populate Django settings
|
340
|
+
config = DjangoConfig.from_sources(
|
341
|
+
from_file("django.yaml"),
|
342
|
+
from_env(prefix="DJANGO_")
|
343
|
+
)
|
344
|
+
|
345
|
+
globals().update(django_settings(config))
|
346
|
+
```
|
347
|
+
|
348
|
+
## CLI Integration
|
349
|
+
|
350
|
+
```bash
|
351
|
+
# Valida configurazione
|
352
|
+
pyw-config validate config.yaml --schema=myapp.config:AppConfig
|
353
|
+
|
354
|
+
# Genera esempio
|
355
|
+
pyw-config generate-example myapp.config:AppConfig > config.example.yaml
|
356
|
+
|
357
|
+
# Merge configurazioni
|
358
|
+
pyw-config merge base.yaml override.yaml > final.yaml
|
359
|
+
|
360
|
+
# Mostra configurazione risolta (con secrets mascherati)
|
361
|
+
pyw-config show --env=production
|
362
|
+
```
|
363
|
+
|
364
|
+
## Roadmap
|
365
|
+
|
366
|
+
- 🔐 **Enhanced secrets**: Integrazione con AWS Secrets Manager, Azure Key Vault
|
367
|
+
- 🌐 **Remote configs**: Etcd, Consul, Kubernetes ConfigMaps
|
368
|
+
- 📝 **Configuration UI**: Web interface per editing configurazioni
|
369
|
+
- 🔄 **Hot reload**: Reload automatico in runtime senza restart
|
370
|
+
- 📊 **Config analytics**: Metriche di utilizzo e drift detection
|
371
|
+
- 🧩 **Plugin system**: Custom validators e sources
|
372
|
+
|
373
|
+
## Contributing
|
374
|
+
|
375
|
+
1. Fork il repo: `git clone https://github.com/pythonWoods/pyw-config.git`
|
376
|
+
2. Crea virtual-env: `poetry install && poetry shell`
|
377
|
+
3. Lancia tests: `pytest`
|
378
|
+
4. Lancia linter: `ruff check . && mypy`
|
379
|
+
5. Apri la PR: CI esegue tutti i check
|
380
|
+
|
381
|
+
Felice configurazione nella foresta di **pythonWoods**! 🌲⚙️
|
382
|
+
|
383
|
+
## Links utili
|
384
|
+
|
385
|
+
Documentazione dev (work-in-progress) → https://pythonwoods.dev/docs/pyw-config/latest/
|
386
|
+
|
387
|
+
Issue tracker → https://github.com/pythonWoods/pyw-config/issues
|
388
|
+
|
389
|
+
Changelog → https://github.com/pythonWoods/pyw-config/releases
|
390
|
+
|
391
|
+
© pythonWoods — MIT License
|
@@ -0,0 +1,30 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["hatchling>=1.18"]
|
3
|
+
build-backend = "hatchling.build"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "pyw-config"
|
7
|
+
version = "0.0.0.post2"
|
8
|
+
description = "Reserved placeholder for pyw-config (configuration utilities for pythonWoods suite)"
|
9
|
+
authors = [{name = "pythonWoods", email = "you@example.com"}]
|
10
|
+
license = {text = "MIT"}
|
11
|
+
requires-python = ">=3.9"
|
12
|
+
readme = "README.md"
|
13
|
+
dependencies = ["pyw-core>=0.0.0"]
|
14
|
+
|
15
|
+
classifiers = [
|
16
|
+
"Development Status :: 2 - Pre-Alpha",
|
17
|
+
"License :: OSI Approved :: MIT License",
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
19
|
+
"Programming Language :: Python :: 3.11",
|
20
|
+
"Typing :: Typed",
|
21
|
+
]
|
22
|
+
|
23
|
+
[project.urls]
|
24
|
+
Homepage = "https://github.com/pythonWoods/pyw-config"
|
25
|
+
Documentation = "https://pythonwoods.dev/docs/pyw-config/latest/"
|
26
|
+
Issues = "https://github.com/pythonWoods/pyw-config/issues"
|
27
|
+
Changelog = "https://github.com/pythonWoods/pyw-config/releases"
|
28
|
+
|
29
|
+
[tool.hatch.build.targets.wheel]
|
30
|
+
packages = ["src/pyw"]
|
pyw_config-0.0.0/PKG-INFO
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: pyw-config
|
3
|
-
Version: 0.0.0
|
4
|
-
Summary: Reserved placeholder for pyw-config (configuration utilities for pythonWoods suite)
|
5
|
-
Project-URL: Homepage, https://github.com/pythonWoods/pyw
|
6
|
-
Author-email: pythonWoods <you@example.com>
|
7
|
-
License: MIT
|
8
|
-
License-File: LICENSE
|
9
|
-
Requires-Python: >=3.9
|
10
|
-
Requires-Dist: pyw-core>=0.0.0
|
11
|
-
Description-Content-Type: text/markdown
|
12
|
-
|
13
|
-
# pyw-config
|
14
|
-
|
15
|
-
Placeholder package to reserve the name on PyPI. Configuration utilities coming soon.
|
pyw_config-0.0.0/README.md
DELETED
pyw_config-0.0.0/pyproject.toml
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
[build-system]
|
2
|
-
requires = ["hatchling>=1.18"]
|
3
|
-
build-backend = "hatchling.build"
|
4
|
-
|
5
|
-
[project]
|
6
|
-
name = "pyw-config"
|
7
|
-
version = "0.0.0"
|
8
|
-
description = "Reserved placeholder for pyw-config (configuration utilities for pythonWoods suite)"
|
9
|
-
authors = [{name = "pythonWoods", email = "you@example.com"}]
|
10
|
-
license = {text = "MIT"}
|
11
|
-
requires-python = ">=3.9"
|
12
|
-
readme = "README.md"
|
13
|
-
dependencies = ["pyw-core>=0.0.0"]
|
14
|
-
|
15
|
-
[project.urls]
|
16
|
-
Homepage = "https://github.com/pythonWoods/pyw"
|
17
|
-
|
18
|
-
[tool.hatch.build.targets.wheel]
|
19
|
-
packages = ["src/pyw"]
|
File without changes
|
File without changes
|
File without changes
|