glean-config 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.
- glean_config-0.1.0/PKG-INFO +312 -0
- glean_config-0.1.0/README.md +302 -0
- glean_config-0.1.0/pyproject.toml +22 -0
- glean_config-0.1.0/src/glean_config/__init__.py +1 -0
- glean_config-0.1.0/src/glean_config/__main__.py +70 -0
- glean_config-0.1.0/src/glean_config/config.py +161 -0
- glean_config-0.1.0/src/glean_config/py.typed +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: glean-config
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: grimmdgg
|
|
6
|
+
Author-email: grimmdgg <grimmdgg@gmail.com>
|
|
7
|
+
Requires-Dist: tomli-w>=1.2.0
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# glean-config
|
|
12
|
+
|
|
13
|
+
A flexible, thread-safe Python configuration manager using TOML files with support for environment variables, base64 encoding, and fileless operation modes.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **TOML-based configuration**: Easy-to-read and write configuration files
|
|
18
|
+
- **Singleton pattern**: Ensures consistent configuration across your application
|
|
19
|
+
- **Thread-safe**: Built-in locking for concurrent access
|
|
20
|
+
- **Environment variable integration**: Auto-load environment variables by name or regex pattern
|
|
21
|
+
- **Automatic base64 encoding**: Prefix keys with `encoded_` for automatic encoding/decoding
|
|
22
|
+
- **Fileless mode**: Run without a config file for testing or ephemeral configurations
|
|
23
|
+
- **Context manager support**: Use with `with` statement for automatic saving
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install glean-config
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or for development:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone <repository-url>
|
|
35
|
+
cd glean-config
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Basic Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from glean_config.config import Glean_config
|
|
45
|
+
|
|
46
|
+
# Load or create a config file
|
|
47
|
+
config = Glean_config.get_instance('config.toml')
|
|
48
|
+
|
|
49
|
+
# Set values
|
|
50
|
+
config['api_key'] = 'your-api-key'
|
|
51
|
+
config['debug'] = 'true'
|
|
52
|
+
|
|
53
|
+
# Get values
|
|
54
|
+
api_key = config['api_key']
|
|
55
|
+
|
|
56
|
+
# Save changes
|
|
57
|
+
config.save()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Using the CLI
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Create/view config with a specific file
|
|
64
|
+
python -m glean_config -f config.toml
|
|
65
|
+
|
|
66
|
+
# Add a key-value pair
|
|
67
|
+
python -m glean_config -f config.toml -a "database_url::postgresql://localhost/mydb"
|
|
68
|
+
|
|
69
|
+
# Show a specific key
|
|
70
|
+
python -m glean_config -f config.toml -k database_url
|
|
71
|
+
|
|
72
|
+
# Use environment variable for config file location
|
|
73
|
+
export GLEAN_CONFIG_FILE=/path/to/config.toml
|
|
74
|
+
python -m glean_config -k database_url
|
|
75
|
+
|
|
76
|
+
# Run in fileless mode (no file or env var needed)
|
|
77
|
+
python -m glean_config -a "temp_key::temp_value"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration Modes
|
|
81
|
+
|
|
82
|
+
### 1. File-based Mode (Default)
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
config = Glean_config.get_instance('myconfig.toml')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Loads configuration from the specified TOML file. Creates the file if it doesn't exist.
|
|
89
|
+
|
|
90
|
+
### 2. Environment Variable Mode
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Set the environment variable
|
|
94
|
+
import os
|
|
95
|
+
os.environ['GLEAN_CONFIG_FILE'] = '/path/to/config.toml'
|
|
96
|
+
|
|
97
|
+
# No file argument needed
|
|
98
|
+
config = Glean_config.get_instance()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Fileless Mode
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# No file, no environment variable
|
|
105
|
+
config = Glean_config.get_instance()
|
|
106
|
+
# Config exists only in memory
|
|
107
|
+
config['key'] = 'value'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Advanced Features
|
|
111
|
+
|
|
112
|
+
### Automatic Base64 Encoding
|
|
113
|
+
|
|
114
|
+
Keys starting with `encoded_` are automatically base64 encoded when set and decoded when retrieved:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
config['encoded_password'] = 'my-secret-password'
|
|
118
|
+
# Stored as base64 in the file
|
|
119
|
+
|
|
120
|
+
password = config['encoded_password']
|
|
121
|
+
# Returns: 'my-secret-password' (decoded)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Environment Variable Integration
|
|
125
|
+
|
|
126
|
+
Configure your TOML file to automatically load environment variables:
|
|
127
|
+
|
|
128
|
+
```toml
|
|
129
|
+
config_name = 'myapp'
|
|
130
|
+
|
|
131
|
+
[environment]
|
|
132
|
+
# Regex pattern to match environment variables
|
|
133
|
+
env_rex = '^MYAPP_'
|
|
134
|
+
# Explicit list of environment variables to load
|
|
135
|
+
env = ['DATABASE_URL', 'SECRET_KEY']
|
|
136
|
+
|
|
137
|
+
[config_items]
|
|
138
|
+
# Your config items here
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Now environment variables matching the pattern or in the list are automatically loaded:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import os
|
|
145
|
+
os.environ['MYAPP_DEBUG'] = 'true'
|
|
146
|
+
os.environ['DATABASE_URL'] = 'postgresql://localhost/db'
|
|
147
|
+
|
|
148
|
+
config = Glean_config.get_instance('config.toml')
|
|
149
|
+
print(config['MYAPP_DEBUG']) # 'true'
|
|
150
|
+
print(config['DATABASE_URL']) # 'postgresql://localhost/db'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Context Manager
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
with Glean_config.get_instance('config.toml') as config:
|
|
157
|
+
config['key'] = 'value'
|
|
158
|
+
config['another_key'] = 'another_value'
|
|
159
|
+
# Automatically saved on exit
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Bulk Configuration
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Replace entire config
|
|
166
|
+
new_config = {
|
|
167
|
+
'api_key': 'new-key',
|
|
168
|
+
'timeout': '30',
|
|
169
|
+
'retries': '3'
|
|
170
|
+
}
|
|
171
|
+
config.set_config(new_config)
|
|
172
|
+
|
|
173
|
+
# Merge with existing config
|
|
174
|
+
config.set_config(new_config, merge=True)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Export Formats
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# Export as TOML string
|
|
181
|
+
toml_str = config.get_toml()
|
|
182
|
+
|
|
183
|
+
# Export as JSON string
|
|
184
|
+
json_str = config.get_json()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## TOML File Structure
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
config_name = 'my_application'
|
|
191
|
+
|
|
192
|
+
[environment]
|
|
193
|
+
# Regex pattern for environment variables to auto-load
|
|
194
|
+
env_rex = '^AP_'
|
|
195
|
+
# Explicit environment variables to load
|
|
196
|
+
env = ['DATABASE_URL', 'SECRET_KEY']
|
|
197
|
+
|
|
198
|
+
[config_items]
|
|
199
|
+
# Your application configuration
|
|
200
|
+
api_key = "your-api-key"
|
|
201
|
+
debug = "false"
|
|
202
|
+
timeout = "30"
|
|
203
|
+
encoded_password = "bXktc2VjcmV0LXBhc3N3b3Jk" # base64 encoded
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## API Reference
|
|
207
|
+
|
|
208
|
+
### `Glean_config.get_instance(toml_file=None)`
|
|
209
|
+
|
|
210
|
+
Get or create the singleton config instance.
|
|
211
|
+
|
|
212
|
+
- **toml_file** (str, optional): Path to TOML file. If `None`, uses `GLEAN_CONFIG_FILE` env var or fileless mode.
|
|
213
|
+
- **Returns**: `Glean_config` instance
|
|
214
|
+
|
|
215
|
+
### Instance Methods
|
|
216
|
+
|
|
217
|
+
#### `config[key]` / `config[key] = value`
|
|
218
|
+
|
|
219
|
+
Get or set configuration values.
|
|
220
|
+
|
|
221
|
+
#### `save(force=False)`
|
|
222
|
+
|
|
223
|
+
Save configuration to file (no-op in fileless mode).
|
|
224
|
+
|
|
225
|
+
- **force** (bool): Save even if not modified
|
|
226
|
+
|
|
227
|
+
#### `get_config()`
|
|
228
|
+
|
|
229
|
+
Get the entire config_items dictionary.
|
|
230
|
+
|
|
231
|
+
- **Returns**: dict
|
|
232
|
+
|
|
233
|
+
#### `set_config(config, merge=False)`
|
|
234
|
+
|
|
235
|
+
Set the entire config_items dictionary.
|
|
236
|
+
|
|
237
|
+
- **config** (dict): New configuration
|
|
238
|
+
- **merge** (bool): Merge with existing config instead of replacing
|
|
239
|
+
|
|
240
|
+
#### `get_toml()`
|
|
241
|
+
|
|
242
|
+
Export configuration as TOML string.
|
|
243
|
+
|
|
244
|
+
- **Returns**: str
|
|
245
|
+
|
|
246
|
+
#### `get_json()`
|
|
247
|
+
|
|
248
|
+
Export configuration as JSON string.
|
|
249
|
+
|
|
250
|
+
- **Returns**: str
|
|
251
|
+
|
|
252
|
+
#### `parameters()`
|
|
253
|
+
|
|
254
|
+
Get list of top-level configuration keys.
|
|
255
|
+
|
|
256
|
+
- **Returns**: list[str]
|
|
257
|
+
|
|
258
|
+
#### `close()`
|
|
259
|
+
|
|
260
|
+
Explicitly save and close the configuration.
|
|
261
|
+
|
|
262
|
+
## Testing
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Run all tests
|
|
266
|
+
pytest tests/
|
|
267
|
+
|
|
268
|
+
# Run with coverage
|
|
269
|
+
pytest tests/ --cov=glean_config
|
|
270
|
+
|
|
271
|
+
# Run specific test
|
|
272
|
+
pytest tests/test_config.py::TestGleanConfig::test_fileless_mode_no_args
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Thread Safety
|
|
276
|
+
|
|
277
|
+
All operations are thread-safe using internal locks:
|
|
278
|
+
|
|
279
|
+
- Object-level lock for singleton instance creation and config updates
|
|
280
|
+
- File-level lock for concurrent file I/O operations
|
|
281
|
+
|
|
282
|
+
## Error Handling
|
|
283
|
+
|
|
284
|
+
### `ConfigObjectError`
|
|
285
|
+
|
|
286
|
+
Raised when trying to instantiate `Glean_config` directly instead of using `get_instance()`.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from glean_config.config import Glean_config, ConfigObjectError
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
config = Glean_config('config.toml') # Wrong!
|
|
293
|
+
except ConfigObjectError:
|
|
294
|
+
config = Glean_config.get_instance('config.toml') # Correct
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### `KeyError`
|
|
298
|
+
|
|
299
|
+
Raised when configuration structure is invalid (missing `config_items` key).
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
MIT
|
|
304
|
+
|
|
305
|
+
## Contributing
|
|
306
|
+
|
|
307
|
+
Contributions are welcome! Please ensure:
|
|
308
|
+
|
|
309
|
+
1. All tests pass: `pytest tests/`
|
|
310
|
+
2. Code follows PEP 8 style guidelines
|
|
311
|
+
3. New features include tests
|
|
312
|
+
4. Documentation is updated
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# glean-config
|
|
2
|
+
|
|
3
|
+
A flexible, thread-safe Python configuration manager using TOML files with support for environment variables, base64 encoding, and fileless operation modes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **TOML-based configuration**: Easy-to-read and write configuration files
|
|
8
|
+
- **Singleton pattern**: Ensures consistent configuration across your application
|
|
9
|
+
- **Thread-safe**: Built-in locking for concurrent access
|
|
10
|
+
- **Environment variable integration**: Auto-load environment variables by name or regex pattern
|
|
11
|
+
- **Automatic base64 encoding**: Prefix keys with `encoded_` for automatic encoding/decoding
|
|
12
|
+
- **Fileless mode**: Run without a config file for testing or ephemeral configurations
|
|
13
|
+
- **Context manager support**: Use with `with` statement for automatic saving
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install glean-config
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or for development:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone <repository-url>
|
|
25
|
+
cd glean-config
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Basic Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from glean_config.config import Glean_config
|
|
35
|
+
|
|
36
|
+
# Load or create a config file
|
|
37
|
+
config = Glean_config.get_instance('config.toml')
|
|
38
|
+
|
|
39
|
+
# Set values
|
|
40
|
+
config['api_key'] = 'your-api-key'
|
|
41
|
+
config['debug'] = 'true'
|
|
42
|
+
|
|
43
|
+
# Get values
|
|
44
|
+
api_key = config['api_key']
|
|
45
|
+
|
|
46
|
+
# Save changes
|
|
47
|
+
config.save()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Using the CLI
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Create/view config with a specific file
|
|
54
|
+
python -m glean_config -f config.toml
|
|
55
|
+
|
|
56
|
+
# Add a key-value pair
|
|
57
|
+
python -m glean_config -f config.toml -a "database_url::postgresql://localhost/mydb"
|
|
58
|
+
|
|
59
|
+
# Show a specific key
|
|
60
|
+
python -m glean_config -f config.toml -k database_url
|
|
61
|
+
|
|
62
|
+
# Use environment variable for config file location
|
|
63
|
+
export GLEAN_CONFIG_FILE=/path/to/config.toml
|
|
64
|
+
python -m glean_config -k database_url
|
|
65
|
+
|
|
66
|
+
# Run in fileless mode (no file or env var needed)
|
|
67
|
+
python -m glean_config -a "temp_key::temp_value"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration Modes
|
|
71
|
+
|
|
72
|
+
### 1. File-based Mode (Default)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
config = Glean_config.get_instance('myconfig.toml')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Loads configuration from the specified TOML file. Creates the file if it doesn't exist.
|
|
79
|
+
|
|
80
|
+
### 2. Environment Variable Mode
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Set the environment variable
|
|
84
|
+
import os
|
|
85
|
+
os.environ['GLEAN_CONFIG_FILE'] = '/path/to/config.toml'
|
|
86
|
+
|
|
87
|
+
# No file argument needed
|
|
88
|
+
config = Glean_config.get_instance()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Fileless Mode
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# No file, no environment variable
|
|
95
|
+
config = Glean_config.get_instance()
|
|
96
|
+
# Config exists only in memory
|
|
97
|
+
config['key'] = 'value'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Advanced Features
|
|
101
|
+
|
|
102
|
+
### Automatic Base64 Encoding
|
|
103
|
+
|
|
104
|
+
Keys starting with `encoded_` are automatically base64 encoded when set and decoded when retrieved:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
config['encoded_password'] = 'my-secret-password'
|
|
108
|
+
# Stored as base64 in the file
|
|
109
|
+
|
|
110
|
+
password = config['encoded_password']
|
|
111
|
+
# Returns: 'my-secret-password' (decoded)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Environment Variable Integration
|
|
115
|
+
|
|
116
|
+
Configure your TOML file to automatically load environment variables:
|
|
117
|
+
|
|
118
|
+
```toml
|
|
119
|
+
config_name = 'myapp'
|
|
120
|
+
|
|
121
|
+
[environment]
|
|
122
|
+
# Regex pattern to match environment variables
|
|
123
|
+
env_rex = '^MYAPP_'
|
|
124
|
+
# Explicit list of environment variables to load
|
|
125
|
+
env = ['DATABASE_URL', 'SECRET_KEY']
|
|
126
|
+
|
|
127
|
+
[config_items]
|
|
128
|
+
# Your config items here
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Now environment variables matching the pattern or in the list are automatically loaded:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import os
|
|
135
|
+
os.environ['MYAPP_DEBUG'] = 'true'
|
|
136
|
+
os.environ['DATABASE_URL'] = 'postgresql://localhost/db'
|
|
137
|
+
|
|
138
|
+
config = Glean_config.get_instance('config.toml')
|
|
139
|
+
print(config['MYAPP_DEBUG']) # 'true'
|
|
140
|
+
print(config['DATABASE_URL']) # 'postgresql://localhost/db'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Context Manager
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
with Glean_config.get_instance('config.toml') as config:
|
|
147
|
+
config['key'] = 'value'
|
|
148
|
+
config['another_key'] = 'another_value'
|
|
149
|
+
# Automatically saved on exit
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Bulk Configuration
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Replace entire config
|
|
156
|
+
new_config = {
|
|
157
|
+
'api_key': 'new-key',
|
|
158
|
+
'timeout': '30',
|
|
159
|
+
'retries': '3'
|
|
160
|
+
}
|
|
161
|
+
config.set_config(new_config)
|
|
162
|
+
|
|
163
|
+
# Merge with existing config
|
|
164
|
+
config.set_config(new_config, merge=True)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Export Formats
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# Export as TOML string
|
|
171
|
+
toml_str = config.get_toml()
|
|
172
|
+
|
|
173
|
+
# Export as JSON string
|
|
174
|
+
json_str = config.get_json()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## TOML File Structure
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
config_name = 'my_application'
|
|
181
|
+
|
|
182
|
+
[environment]
|
|
183
|
+
# Regex pattern for environment variables to auto-load
|
|
184
|
+
env_rex = '^AP_'
|
|
185
|
+
# Explicit environment variables to load
|
|
186
|
+
env = ['DATABASE_URL', 'SECRET_KEY']
|
|
187
|
+
|
|
188
|
+
[config_items]
|
|
189
|
+
# Your application configuration
|
|
190
|
+
api_key = "your-api-key"
|
|
191
|
+
debug = "false"
|
|
192
|
+
timeout = "30"
|
|
193
|
+
encoded_password = "bXktc2VjcmV0LXBhc3N3b3Jk" # base64 encoded
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## API Reference
|
|
197
|
+
|
|
198
|
+
### `Glean_config.get_instance(toml_file=None)`
|
|
199
|
+
|
|
200
|
+
Get or create the singleton config instance.
|
|
201
|
+
|
|
202
|
+
- **toml_file** (str, optional): Path to TOML file. If `None`, uses `GLEAN_CONFIG_FILE` env var or fileless mode.
|
|
203
|
+
- **Returns**: `Glean_config` instance
|
|
204
|
+
|
|
205
|
+
### Instance Methods
|
|
206
|
+
|
|
207
|
+
#### `config[key]` / `config[key] = value`
|
|
208
|
+
|
|
209
|
+
Get or set configuration values.
|
|
210
|
+
|
|
211
|
+
#### `save(force=False)`
|
|
212
|
+
|
|
213
|
+
Save configuration to file (no-op in fileless mode).
|
|
214
|
+
|
|
215
|
+
- **force** (bool): Save even if not modified
|
|
216
|
+
|
|
217
|
+
#### `get_config()`
|
|
218
|
+
|
|
219
|
+
Get the entire config_items dictionary.
|
|
220
|
+
|
|
221
|
+
- **Returns**: dict
|
|
222
|
+
|
|
223
|
+
#### `set_config(config, merge=False)`
|
|
224
|
+
|
|
225
|
+
Set the entire config_items dictionary.
|
|
226
|
+
|
|
227
|
+
- **config** (dict): New configuration
|
|
228
|
+
- **merge** (bool): Merge with existing config instead of replacing
|
|
229
|
+
|
|
230
|
+
#### `get_toml()`
|
|
231
|
+
|
|
232
|
+
Export configuration as TOML string.
|
|
233
|
+
|
|
234
|
+
- **Returns**: str
|
|
235
|
+
|
|
236
|
+
#### `get_json()`
|
|
237
|
+
|
|
238
|
+
Export configuration as JSON string.
|
|
239
|
+
|
|
240
|
+
- **Returns**: str
|
|
241
|
+
|
|
242
|
+
#### `parameters()`
|
|
243
|
+
|
|
244
|
+
Get list of top-level configuration keys.
|
|
245
|
+
|
|
246
|
+
- **Returns**: list[str]
|
|
247
|
+
|
|
248
|
+
#### `close()`
|
|
249
|
+
|
|
250
|
+
Explicitly save and close the configuration.
|
|
251
|
+
|
|
252
|
+
## Testing
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Run all tests
|
|
256
|
+
pytest tests/
|
|
257
|
+
|
|
258
|
+
# Run with coverage
|
|
259
|
+
pytest tests/ --cov=glean_config
|
|
260
|
+
|
|
261
|
+
# Run specific test
|
|
262
|
+
pytest tests/test_config.py::TestGleanConfig::test_fileless_mode_no_args
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Thread Safety
|
|
266
|
+
|
|
267
|
+
All operations are thread-safe using internal locks:
|
|
268
|
+
|
|
269
|
+
- Object-level lock for singleton instance creation and config updates
|
|
270
|
+
- File-level lock for concurrent file I/O operations
|
|
271
|
+
|
|
272
|
+
## Error Handling
|
|
273
|
+
|
|
274
|
+
### `ConfigObjectError`
|
|
275
|
+
|
|
276
|
+
Raised when trying to instantiate `Glean_config` directly instead of using `get_instance()`.
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from glean_config.config import Glean_config, ConfigObjectError
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
config = Glean_config('config.toml') # Wrong!
|
|
283
|
+
except ConfigObjectError:
|
|
284
|
+
config = Glean_config.get_instance('config.toml') # Correct
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `KeyError`
|
|
288
|
+
|
|
289
|
+
Raised when configuration structure is invalid (missing `config_items` key).
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
|
294
|
+
|
|
295
|
+
## Contributing
|
|
296
|
+
|
|
297
|
+
Contributions are welcome! Please ensure:
|
|
298
|
+
|
|
299
|
+
1. All tests pass: `pytest tests/`
|
|
300
|
+
2. Code follows PEP 8 style guidelines
|
|
301
|
+
3. New features include tests
|
|
302
|
+
4. Documentation is updated
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "glean-config"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "grimmdgg", email = "grimmdgg@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"tomli-w>=1.2.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.4.2",
|
|
21
|
+
"pytest-cov>=7.0.0",
|
|
22
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# __init__.py
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from sys import exit
|
|
3
|
+
|
|
4
|
+
from glean_config.config import Glean_config
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser("Glean_config")
|
|
8
|
+
parser.add_argument(
|
|
9
|
+
'-f',
|
|
10
|
+
'--file',
|
|
11
|
+
dest='toml_file',
|
|
12
|
+
help='path to toml config file (optional: uses GLEAN_CONFIG_FILE env var or fileless mode if not provided)',
|
|
13
|
+
default=None
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
'-a',
|
|
17
|
+
'--add',
|
|
18
|
+
dest='add_kvp',
|
|
19
|
+
help='add a kvp to file in the form "key::value"'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
'-k',
|
|
24
|
+
'--show_key',
|
|
25
|
+
dest='key',
|
|
26
|
+
help='display key value'
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
# Get config instance (may use file, env var, or fileless mode)
|
|
32
|
+
config = Glean_config.get_instance(args.toml_file)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Validate and process the add_kvp argument
|
|
37
|
+
try:
|
|
38
|
+
validate_parameters(args, config)
|
|
39
|
+
except ValueError as ve:
|
|
40
|
+
print(ve)
|
|
41
|
+
exit(1)
|
|
42
|
+
|
|
43
|
+
# Removed redundant call to config.get_toml()
|
|
44
|
+
|
|
45
|
+
def validate_parameters(args, config):
|
|
46
|
+
if args.add_kvp:
|
|
47
|
+
|
|
48
|
+
if "::" not in args.add_kvp:
|
|
49
|
+
|
|
50
|
+
raise ValueError("Error: The key-value pair must be in the form 'key::value'.")
|
|
51
|
+
key, value = args.add_kvp.split('::', 1)
|
|
52
|
+
config[key] = value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Validate and process the key argument
|
|
56
|
+
if args.key:
|
|
57
|
+
if args.key not in config.get_config():
|
|
58
|
+
raise ValueError(f"Error: The key '{args.key}' does not exist in the configuration.")
|
|
59
|
+
|
|
60
|
+
print(f"{args.key}: {config[args.key]}")
|
|
61
|
+
exit(0)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
config.save(force=True)
|
|
65
|
+
print(config.get_toml())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__=="__main__":
|
|
69
|
+
main()
|
|
70
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from base64 import b64decode, b64encode
|
|
5
|
+
from os import environ
|
|
6
|
+
from pathlib import Path, PurePath
|
|
7
|
+
from threading import Lock
|
|
8
|
+
import tomli_w
|
|
9
|
+
from typing import Any, TypeVar, TypeAlias
|
|
10
|
+
|
|
11
|
+
# Method-level generic for config value types.
|
|
12
|
+
# Use `Config[T]` in method signatures when you want a specific value type.
|
|
13
|
+
T = TypeVar('T')
|
|
14
|
+
# modern TypeAlias (Python 3.10+)
|
|
15
|
+
Config: TypeAlias = dict[str, T]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigObjectError(Exception):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super(ConfigObjectError,self).__init__("Please instantiate this class with the 'get_instance(toml_file:str)' class method")
|
|
21
|
+
|
|
22
|
+
class Glean_config():
|
|
23
|
+
|
|
24
|
+
_config_object: 'Glean_config | None' = None
|
|
25
|
+
_object_lock = Lock()
|
|
26
|
+
_file_lock = Lock()
|
|
27
|
+
# internal storage can hold heterogeneous values loaded from TOML
|
|
28
|
+
config: Config[Any] = {}
|
|
29
|
+
|
|
30
|
+
def __init__(self, toml_file: str | None = None, override: bool = False):
|
|
31
|
+
self.modified = True
|
|
32
|
+
self.fileless_mode = False
|
|
33
|
+
|
|
34
|
+
# Determine the file path
|
|
35
|
+
if toml_file is None:
|
|
36
|
+
# Check environment variable
|
|
37
|
+
toml_file = environ.get('glean_config_file')
|
|
38
|
+
if toml_file is None:
|
|
39
|
+
# Fileless mode
|
|
40
|
+
self.fileless_mode = True
|
|
41
|
+
self.toml_file = None
|
|
42
|
+
else:
|
|
43
|
+
self.toml_file = toml_file
|
|
44
|
+
else:
|
|
45
|
+
self.toml_file = toml_file
|
|
46
|
+
|
|
47
|
+
if not override:
|
|
48
|
+
raise ConfigObjectError()
|
|
49
|
+
else:
|
|
50
|
+
# Prepare config text
|
|
51
|
+
if self.fileless_mode:
|
|
52
|
+
config_text = Glean_config.empty_config_toml % 'fileless_config'
|
|
53
|
+
else:
|
|
54
|
+
config_text = Glean_config.empty_config_toml % PurePath(self.toml_file).name
|
|
55
|
+
if Path(self.toml_file).exists():
|
|
56
|
+
config_text = Path(self.toml_file).read_text(encoding='utf-8')
|
|
57
|
+
self.modified = False
|
|
58
|
+
|
|
59
|
+
self.config = tomllib.loads(config_text)
|
|
60
|
+
self._read_env()
|
|
61
|
+
|
|
62
|
+
def _read_env(self):
|
|
63
|
+
environment = self.config.get('environment', {})
|
|
64
|
+
|
|
65
|
+
env_vars = environment.get('env', [])
|
|
66
|
+
for var in env_vars:
|
|
67
|
+
value = environ.get(var)
|
|
68
|
+
if value is not None:
|
|
69
|
+
self.__setitem__(var, value)
|
|
70
|
+
|
|
71
|
+
env_rex = environment.get('env_rex')
|
|
72
|
+
if env_rex:
|
|
73
|
+
config_items = self.config.get('config_items', {})
|
|
74
|
+
for var in environ:
|
|
75
|
+
if var not in config_items and re.match(env_rex, var):
|
|
76
|
+
# environ is being iterated over its keys, so indexing is safe
|
|
77
|
+
self.__setitem__(var, environ[var])
|
|
78
|
+
|
|
79
|
+
def parameters(self) -> list[str]:
|
|
80
|
+
return list(self.config.keys())
|
|
81
|
+
|
|
82
|
+
def save(self, force: bool = False) -> None:
|
|
83
|
+
if self.fileless_mode:
|
|
84
|
+
# In fileless mode, just mark as not modified but don't save
|
|
85
|
+
self.modified = False
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if (self.modified or force) and self.config and self.toml_file:
|
|
89
|
+
with Glean_config._file_lock:
|
|
90
|
+
with open(self.toml_file, mode="wb") as fp:
|
|
91
|
+
tomli_w.dump(self.config, fp)
|
|
92
|
+
self.modified = False
|
|
93
|
+
|
|
94
|
+
def get_toml(self)->str:
|
|
95
|
+
return tomli_w.dumps(self.config)
|
|
96
|
+
|
|
97
|
+
def get_json(self)->str:
|
|
98
|
+
return json.dumps(self.config)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_instance(cls, toml_file: str | None = None) -> 'Glean_config':
|
|
102
|
+
with cls._object_lock:
|
|
103
|
+
if not Glean_config._config_object:
|
|
104
|
+
Glean_config._config_object = Glean_config(toml_file=toml_file, override=True)
|
|
105
|
+
return Glean_config._config_object
|
|
106
|
+
|
|
107
|
+
def get_config(self) -> Config[Any]:
|
|
108
|
+
if 'config_items' not in self.config:
|
|
109
|
+
raise KeyError("Configuration structure invalid: 'config_items' key not found")
|
|
110
|
+
return self.config['config_items']
|
|
111
|
+
|
|
112
|
+
def set_config(self, config: Config[T], merge:bool = False) -> None:
|
|
113
|
+
with Glean_config._object_lock:
|
|
114
|
+
if 'config_items' not in self.config:
|
|
115
|
+
raise KeyError("Configuration structure invalid: 'config_items' key not found")
|
|
116
|
+
if merge:
|
|
117
|
+
self.config['config_items'].update(config)
|
|
118
|
+
else:
|
|
119
|
+
self.config['config_items'] = config
|
|
120
|
+
self.modified = True
|
|
121
|
+
self.save()
|
|
122
|
+
|
|
123
|
+
def decode(self,encoded:str)-> str:
|
|
124
|
+
return str(b64decode(encoded),'utf-8')
|
|
125
|
+
|
|
126
|
+
def __getitem__(self, key: str):
|
|
127
|
+
if 'config_items' not in self.config:
|
|
128
|
+
raise KeyError("Configuration structure invalid: 'config_items' key not found")
|
|
129
|
+
returnValue = self.config['config_items'].get(key, None)
|
|
130
|
+
|
|
131
|
+
if returnValue and key.startswith('encoded_'):
|
|
132
|
+
returnValue = self.decode(returnValue)
|
|
133
|
+
return returnValue
|
|
134
|
+
|
|
135
|
+
def __setitem__(self, key: str, value: str) -> None:
|
|
136
|
+
if 'config_items' not in self.config:
|
|
137
|
+
raise KeyError("Configuration structure invalid: 'config_items' key not found")
|
|
138
|
+
if key.startswith('encoded_'):
|
|
139
|
+
value = str(b64encode(bytes(value, 'utf-8')), 'utf-8')
|
|
140
|
+
self.config['config_items'][key] = value
|
|
141
|
+
self.modified = True
|
|
142
|
+
|
|
143
|
+
def __enter__(self) -> 'Glean_config':
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __exit__(self, exc_type: type | None, exc_value: BaseException | None, traceback: object | None) -> None:
|
|
147
|
+
self.save()
|
|
148
|
+
|
|
149
|
+
def close(self) -> None:
|
|
150
|
+
"""Explicitly save and close the config. Preferred over relying on __del__."""
|
|
151
|
+
self.save()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
empty_config_toml=\
|
|
155
|
+
"""
|
|
156
|
+
config_name = '%s'
|
|
157
|
+
[environment]
|
|
158
|
+
env_rex='^glean_'
|
|
159
|
+
env=[]
|
|
160
|
+
[config_items]
|
|
161
|
+
"""
|
|
File without changes
|