turboenv 0.2.0a1__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.
- turboenv-0.2.0a1/PKG-INFO +232 -0
- turboenv-0.2.0a1/README.md +203 -0
- turboenv-0.2.0a1/pyproject.toml +118 -0
- turboenv-0.2.0a1/src/turboenv/__init__.py +0 -0
- turboenv-0.2.0a1/src/turboenv/__main__.py +21 -0
- turboenv-0.2.0a1/src/turboenv/exceptions.py +22 -0
- turboenv-0.2.0a1/src/turboenv/main.py +668 -0
- turboenv-0.2.0a1/src/turboenv/namespaces.py +25 -0
- turboenv-0.2.0a1/src/turboenv/plugins.py +17 -0
- turboenv-0.2.0a1/src/turboenv/typings.py +10 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: turboenv
|
|
3
|
+
Version: 0.2.0a1
|
|
4
|
+
Summary: TurboEnv is a Python library that provides a simple and efficient way to manage environment variables in your applications. It allows you to easily load environment variables from .env files, access them in your code, and handle different environments (development, testing, production) with ease.
|
|
5
|
+
Author: John Pendenque
|
|
6
|
+
Author-email: John Pendenque <pendenquejohn@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 1 - Planning
|
|
9
|
+
Classifier: Operating System :: POSIX
|
|
10
|
+
Classifier: Operating System :: Unix
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Internet
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Natural Language :: English
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
19
|
+
Classifier: Operating System :: MacOS
|
|
20
|
+
Requires-Dist: pydantic>=2.13.4
|
|
21
|
+
Maintainer: John Pendenque
|
|
22
|
+
Maintainer-email: John Pendenque <pendenquejohn@gmail.com>
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Project-URL: Changelog, https://github.com/Zadigo/turboenv/blob/main/CHANGELOG.md
|
|
25
|
+
Project-URL: Documentation, https://github.com/Zadigo/turboenv/wiki
|
|
26
|
+
Project-URL: Homepage, https://github.com/Zadigo/turboenv
|
|
27
|
+
Project-URL: Repository, https://github.com/Zadigo/turboenv.git
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Turbo Env
|
|
31
|
+
|
|
32
|
+
TurboEnv is a Python library that provides a simple and efficient way to manage environment variables in your applications. It allows you to easily load environment variables from .env files, access them in your code, and handle different environments (development, testing, production) with ease.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```Shell
|
|
37
|
+
pip install turboenv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Loading Environment variables
|
|
41
|
+
|
|
42
|
+
## Automatic detection
|
|
43
|
+
|
|
44
|
+
When `load_envs` is first called, it looks for any `.env` located in the absolute path of the file that is calling it. If it finds one, it loads the environment variables from that file in the `default` namespace.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from turboenv import TurboEnv
|
|
48
|
+
|
|
49
|
+
env = TurboEnv()
|
|
50
|
+
env.load_envs()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You can also specify the path to the `.env` file and the namespace you want to load it into.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from turboenv import TurboEnv
|
|
57
|
+
|
|
58
|
+
env = TurboEnv()
|
|
59
|
+
env.load_envs('path/to/.env')
|
|
60
|
+
|
|
61
|
+
env.string('DB_USER') # Accesses the DB_USER variable from the default namespace
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Accessing Environment Variables
|
|
65
|
+
|
|
66
|
+
Once the environment variables are loaded, you can access them in multiple different manners including typecasting their values to a specific one:
|
|
67
|
+
|
|
68
|
+
### Get method
|
|
69
|
+
|
|
70
|
+
Tries to get the value of the environment variable with the given name. If the variable is not found, an `exceptions.MissingEnvVariableError` is raised.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
db_user = env.get("DB_USER")
|
|
74
|
+
db_password = env.get("DB_PASSWORD")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Boolean - Type Casting
|
|
78
|
+
|
|
79
|
+
Returns the boolean value of the environment variable with the given name:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
db_password = env.boolean("USE_DB")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### String - Type Casting
|
|
86
|
+
|
|
87
|
+
Returns the string value of the environment variable with the given name:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
db_password = env.string("DB_PASSWORD")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Array - Type Casting
|
|
94
|
+
|
|
95
|
+
Returns a list of strings by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
allowed_hosts = env.array("ALLOWED_HOSTS", cast=str)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### String List - Type Casting
|
|
102
|
+
|
|
103
|
+
Returns a list of strings by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
allowed_hosts = env.str_list("ALLOWED_HOSTS")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Integer List - Type Casting
|
|
110
|
+
|
|
111
|
+
Returns a list of integers by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
allowed_ports = env.int_list("ALLOWED_PORTS")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Domain List - Type Casting
|
|
118
|
+
|
|
119
|
+
Returns a list of domain names by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
allowed_domains = env.domain_list("ALLOWED_DOMAINS")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### URL List - Type Casting
|
|
126
|
+
|
|
127
|
+
Returns a list of URLs by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
allowed_urls = env.url_list("ALLOWED_URLS")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Secret - Type Casting
|
|
134
|
+
|
|
135
|
+
Returns the value of the environment variable with the given name, decoded from base64:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
db_password = env.secret("DB_PASSWORD")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Path - Type Casting
|
|
142
|
+
|
|
143
|
+
Returns a `Path` object representing the path specified by the environment variable with the given name:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
config_path = env.path("CONFIG_PATH")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> [!NOTE]
|
|
150
|
+
> The secret value should already be encoded in base64 or this will raise an error.
|
|
151
|
+
|
|
152
|
+
## Conditionals
|
|
153
|
+
|
|
154
|
+
Conditionals are used to guarantee the integrity of your environment variables by checking if they meet certain conditions.
|
|
155
|
+
When the conditions are not met, an exception is raised.
|
|
156
|
+
|
|
157
|
+
### Depends On
|
|
158
|
+
|
|
159
|
+
Requires the presence of the specified environment variable in the environment.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
env.conditional("DB_USER").depends_on(values=["DB_PASSWORD"])
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
> [!CAUTION]
|
|
166
|
+
> It does not check the actual values of the environment variables, only their presence.
|
|
167
|
+
|
|
168
|
+
### To Be
|
|
169
|
+
|
|
170
|
+
Requires the value of the environment variable to be equal to a specified value.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
env.conditional("DB_USER").to_be("admin")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Not To Be
|
|
177
|
+
|
|
178
|
+
Requires the value of the environment variable to not be equal to a specified value.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
env.conditional("DB_PASSWORD").not_to_be("password")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### To Exist
|
|
185
|
+
|
|
186
|
+
Requires the environment variable to be present.
|
|
187
|
+
|
|
188
|
+
> [!CAUTION]
|
|
189
|
+
> This does not check if the value of the environment variable is empty or not, it only checks if it is present in the environment.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
env.conditional("DB_USER").to_exist()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### To Not Be Empty
|
|
196
|
+
|
|
197
|
+
Requires the value of the environment variable to not be empty.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
env.conditional("DB_PASSWORD").to_not_be_empty()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### To Contain
|
|
204
|
+
|
|
205
|
+
Requires the value of the environment variable to contain a specified substring.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
env.conditional("ALLOWED_HOSTS").to_contain("example.com")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Path To Exist
|
|
212
|
+
|
|
213
|
+
Requires the value of the environment variable to be a valid path that exists in the file system.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
env.conditional("CONFIG_PATH").path_to_exist()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
# Contributing
|
|
220
|
+
|
|
221
|
+
Contributions are welcome! Please feel free to submit a pull request or open an issue if you have any suggestions or find any bugs.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
To run the tests, use the following command:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
# License
|
|
231
|
+
|
|
232
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Turbo Env
|
|
2
|
+
|
|
3
|
+
TurboEnv is a Python library that provides a simple and efficient way to manage environment variables in your applications. It allows you to easily load environment variables from .env files, access them in your code, and handle different environments (development, testing, production) with ease.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```Shell
|
|
8
|
+
pip install turboenv
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Loading Environment variables
|
|
12
|
+
|
|
13
|
+
## Automatic detection
|
|
14
|
+
|
|
15
|
+
When `load_envs` is first called, it looks for any `.env` located in the absolute path of the file that is calling it. If it finds one, it loads the environment variables from that file in the `default` namespace.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from turboenv import TurboEnv
|
|
19
|
+
|
|
20
|
+
env = TurboEnv()
|
|
21
|
+
env.load_envs()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
You can also specify the path to the `.env` file and the namespace you want to load it into.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from turboenv import TurboEnv
|
|
28
|
+
|
|
29
|
+
env = TurboEnv()
|
|
30
|
+
env.load_envs('path/to/.env')
|
|
31
|
+
|
|
32
|
+
env.string('DB_USER') # Accesses the DB_USER variable from the default namespace
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Accessing Environment Variables
|
|
36
|
+
|
|
37
|
+
Once the environment variables are loaded, you can access them in multiple different manners including typecasting their values to a specific one:
|
|
38
|
+
|
|
39
|
+
### Get method
|
|
40
|
+
|
|
41
|
+
Tries to get the value of the environment variable with the given name. If the variable is not found, an `exceptions.MissingEnvVariableError` is raised.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
db_user = env.get("DB_USER")
|
|
45
|
+
db_password = env.get("DB_PASSWORD")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Boolean - Type Casting
|
|
49
|
+
|
|
50
|
+
Returns the boolean value of the environment variable with the given name:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
db_password = env.boolean("USE_DB")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### String - Type Casting
|
|
57
|
+
|
|
58
|
+
Returns the string value of the environment variable with the given name:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
db_password = env.string("DB_PASSWORD")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Array - Type Casting
|
|
65
|
+
|
|
66
|
+
Returns a list of strings by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
allowed_hosts = env.array("ALLOWED_HOSTS", cast=str)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### String List - Type Casting
|
|
73
|
+
|
|
74
|
+
Returns a list of strings by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
allowed_hosts = env.str_list("ALLOWED_HOSTS")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Integer List - Type Casting
|
|
81
|
+
|
|
82
|
+
Returns a list of integers by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
allowed_ports = env.int_list("ALLOWED_PORTS")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Domain List - Type Casting
|
|
89
|
+
|
|
90
|
+
Returns a list of domain names by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
allowed_domains = env.domain_list("ALLOWED_DOMAINS")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### URL List - Type Casting
|
|
97
|
+
|
|
98
|
+
Returns a list of URLs by splitting the value of the environment variable with the given name using a specified separator (default is comma):
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
allowed_urls = env.url_list("ALLOWED_URLS")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Secret - Type Casting
|
|
105
|
+
|
|
106
|
+
Returns the value of the environment variable with the given name, decoded from base64:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
db_password = env.secret("DB_PASSWORD")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Path - Type Casting
|
|
113
|
+
|
|
114
|
+
Returns a `Path` object representing the path specified by the environment variable with the given name:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
config_path = env.path("CONFIG_PATH")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> [!NOTE]
|
|
121
|
+
> The secret value should already be encoded in base64 or this will raise an error.
|
|
122
|
+
|
|
123
|
+
## Conditionals
|
|
124
|
+
|
|
125
|
+
Conditionals are used to guarantee the integrity of your environment variables by checking if they meet certain conditions.
|
|
126
|
+
When the conditions are not met, an exception is raised.
|
|
127
|
+
|
|
128
|
+
### Depends On
|
|
129
|
+
|
|
130
|
+
Requires the presence of the specified environment variable in the environment.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
env.conditional("DB_USER").depends_on(values=["DB_PASSWORD"])
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
> [!CAUTION]
|
|
137
|
+
> It does not check the actual values of the environment variables, only their presence.
|
|
138
|
+
|
|
139
|
+
### To Be
|
|
140
|
+
|
|
141
|
+
Requires the value of the environment variable to be equal to a specified value.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
env.conditional("DB_USER").to_be("admin")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Not To Be
|
|
148
|
+
|
|
149
|
+
Requires the value of the environment variable to not be equal to a specified value.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
env.conditional("DB_PASSWORD").not_to_be("password")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### To Exist
|
|
156
|
+
|
|
157
|
+
Requires the environment variable to be present.
|
|
158
|
+
|
|
159
|
+
> [!CAUTION]
|
|
160
|
+
> This does not check if the value of the environment variable is empty or not, it only checks if it is present in the environment.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
env.conditional("DB_USER").to_exist()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### To Not Be Empty
|
|
167
|
+
|
|
168
|
+
Requires the value of the environment variable to not be empty.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
env.conditional("DB_PASSWORD").to_not_be_empty()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### To Contain
|
|
175
|
+
|
|
176
|
+
Requires the value of the environment variable to contain a specified substring.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
env.conditional("ALLOWED_HOSTS").to_contain("example.com")
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Path To Exist
|
|
183
|
+
|
|
184
|
+
Requires the value of the environment variable to be a valid path that exists in the file system.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
env.conditional("CONFIG_PATH").path_to_exist()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
# Contributing
|
|
191
|
+
|
|
192
|
+
Contributions are welcome! Please feel free to submit a pull request or open an issue if you have any suggestions or find any bugs.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
To run the tests, use the following command:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pytest
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
# License
|
|
202
|
+
|
|
203
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "turboenv"
|
|
3
|
+
version = "0.2.0a1"
|
|
4
|
+
description = "TurboEnv is a Python library that provides a simple and efficient way to manage environment variables in your applications. It allows you to easily load environment variables from .env files, access them in your code, and handle different environments (development, testing, production) with ease."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "John Pendenque", email = "pendenquejohn@gmail.com"},
|
|
10
|
+
]
|
|
11
|
+
maintainers = [
|
|
12
|
+
{name = "John Pendenque", email = "pendenquejohn@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 1 - Planning",
|
|
16
|
+
"Operating System :: POSIX",
|
|
17
|
+
"Operating System :: Unix",
|
|
18
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Internet",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Natural Language :: English",
|
|
25
|
+
"Operating System :: Microsoft :: Windows :: Windows 11",
|
|
26
|
+
"Operating System :: MacOS"
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pydantic>=2.13.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"autopep8>=2.3.2",
|
|
35
|
+
"coverage>=7.14.1",
|
|
36
|
+
"django>=6.0.6",
|
|
37
|
+
"django-environ>=0.13.0",
|
|
38
|
+
"dotenv>=0.9.9",
|
|
39
|
+
"factory-boy>=3.3.3",
|
|
40
|
+
"fastapi[standard]>=0.136.3",
|
|
41
|
+
"pytest>=9.0.3",
|
|
42
|
+
"pytest-aio>=2.1.7",
|
|
43
|
+
"pytest-cov>=7.1.0",
|
|
44
|
+
"pytest-datafiles>=3.0.1",
|
|
45
|
+
"pytest-django>=4.12.0",
|
|
46
|
+
"pytest-env>=1.6.0",
|
|
47
|
+
"pytest-reportlog>=1.0.0",
|
|
48
|
+
"ruff>=0.15.16",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/Zadigo/turboenv"
|
|
53
|
+
Documentation = "https://github.com/Zadigo/turboenv/wiki"
|
|
54
|
+
Repository = "https://github.com/Zadigo/turboenv.git"
|
|
55
|
+
Changelog = "https://github.com/Zadigo/turboenv/blob/main/CHANGELOG.md"
|
|
56
|
+
|
|
57
|
+
[tool.pyright]
|
|
58
|
+
typeCheckingMode = "basic"
|
|
59
|
+
reportMissingImports = true
|
|
60
|
+
reportUntypedBaseClass = true
|
|
61
|
+
|
|
62
|
+
[tool.pytest]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
timeout="5000"
|
|
65
|
+
markers = [
|
|
66
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
67
|
+
"api: marks tests as api tests",
|
|
68
|
+
"integration: marks tests as integration tests",
|
|
69
|
+
"unit: marks tests as unit tests",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.pytest_env]
|
|
73
|
+
env_files = [".env", ".env.test"]
|
|
74
|
+
|
|
75
|
+
[tool.autopep8]
|
|
76
|
+
max_line_length = 120
|
|
77
|
+
in-place = true
|
|
78
|
+
recursive = true
|
|
79
|
+
aggressive = 3
|
|
80
|
+
|
|
81
|
+
[[tool.uv.index]]
|
|
82
|
+
name = "testpypi"
|
|
83
|
+
url = "https://test.pypi.org/simple/"
|
|
84
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
85
|
+
explicit = true
|
|
86
|
+
|
|
87
|
+
[build-system]
|
|
88
|
+
requires = ["uv_build>=0.9,<0.10"]
|
|
89
|
+
build-backend = "uv_build"
|
|
90
|
+
|
|
91
|
+
[tool.coverage.run]
|
|
92
|
+
omit = [
|
|
93
|
+
"manage.py",
|
|
94
|
+
"*/migrations/*",
|
|
95
|
+
"*/__init__.py",
|
|
96
|
+
"*/typings.py",
|
|
97
|
+
"conftest.py",
|
|
98
|
+
"tests/*",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.coverage.report]
|
|
102
|
+
exclude_lines = [
|
|
103
|
+
"pragma: no cover",
|
|
104
|
+
"def __repr__",
|
|
105
|
+
"raise NotImplementedError",
|
|
106
|
+
"if TYPE_CHECKING:",
|
|
107
|
+
"if __name__ == .__main__.:",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[tool.uv.build-backend]
|
|
111
|
+
source-exclude = [
|
|
112
|
+
"**/tests/**",
|
|
113
|
+
"docs/**",
|
|
114
|
+
"*.log",
|
|
115
|
+
"**/.DS_Store",
|
|
116
|
+
"conftest.py",
|
|
117
|
+
"manage.py"
|
|
118
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
if __name__ == '__main__':
|
|
6
|
+
argparser = argparse.ArgumentParser(description='TurboEnv: A tool for managing environment variables.')
|
|
7
|
+
|
|
8
|
+
argparser.add_argument(
|
|
9
|
+
'-l',
|
|
10
|
+
'--list-files',
|
|
11
|
+
action='store_true',
|
|
12
|
+
help='List all the files in the current directory that end with .env'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
namespace = argparser.parse_args()
|
|
16
|
+
|
|
17
|
+
if namespace.list_files:
|
|
18
|
+
current_dir = pathlib.Path(os.getcwd())
|
|
19
|
+
files = current_dir.glob('*.env')
|
|
20
|
+
for file in files:
|
|
21
|
+
print(f' * {file}')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from turboenv.typings import TypeAny
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TurboEnvError(Exception):
|
|
5
|
+
message = "An error occurred in TurboEnv."
|
|
6
|
+
|
|
7
|
+
class MissingEnvVariableError(TurboEnvError):
|
|
8
|
+
message = "The specified environment variable is missing: {values}"
|
|
9
|
+
|
|
10
|
+
def __init__(self, *values: str):
|
|
11
|
+
self.values = ', '.join(values)
|
|
12
|
+
message = self.message.format(values=self.values)
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConditionalError(TurboEnvError):
|
|
17
|
+
message = "A conditional check failed: {details}"
|
|
18
|
+
|
|
19
|
+
def __init__(self, value: TypeAny | None, expected: TypeAny, condition: str):
|
|
20
|
+
self.details = f"Expected {value} {condition} {expected}"
|
|
21
|
+
message = self.message.format(details=self.details)
|
|
22
|
+
super().__init__(message)
|
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json as json_module
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import re
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Callable, Generator, Self, Sequence
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
from turboenv import exceptions
|
|
13
|
+
from turboenv.typings import TypeAny, TypeCast
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def expand(instance: 'TurboEnv', key: str) -> str | None:
|
|
19
|
+
"""
|
|
20
|
+
Expands environment variables in the given string.
|
|
21
|
+
|
|
22
|
+
Example usage::
|
|
23
|
+
|
|
24
|
+
from turboenv import expand
|
|
25
|
+
|
|
26
|
+
expanded_value = expand(instance, "USER")
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
instance (TurboEnv): The TurboEnv instance.
|
|
30
|
+
key (str): The key of the environment variable to expand.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str | None: The string with environment variables expanded, or None if the value is not found.
|
|
34
|
+
"""
|
|
35
|
+
value = instance._cache.get(key, None)
|
|
36
|
+
|
|
37
|
+
if value is not None:
|
|
38
|
+
other_env = re.search(r'%(\w+)%', value)
|
|
39
|
+
if other_env:
|
|
40
|
+
env_var = other_env.group(1)
|
|
41
|
+
new_value = os.getenv(env_var, "")
|
|
42
|
+
new_env_value = value.replace(f"%{env_var}%", new_value)
|
|
43
|
+
instance._cache[key] = new_env_value
|
|
44
|
+
return new_env_value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
48
|
+
def _load_file(path: pathlib.Path, encoding: str = 'utf-8') -> Generator[list[str], None, None]:
|
|
49
|
+
with path.open(encoding=encoding) as f:
|
|
50
|
+
lines = f.readlines()
|
|
51
|
+
yield lines
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Conditionals:
|
|
55
|
+
def __init__(self, instance: 'TurboEnv', name: str):
|
|
56
|
+
self.instance = instance
|
|
57
|
+
self._name = name
|
|
58
|
+
|
|
59
|
+
def value(self):
|
|
60
|
+
return self.instance._cache.get(self._name, None)
|
|
61
|
+
|
|
62
|
+
def depends_on(self, values: list[str] = []) -> Self:
|
|
63
|
+
"""Blocks the execution of the code until the specified environment variables exist.
|
|
64
|
+
If any of the specified environment variables do not exist, it raises an ExceptionGroup
|
|
65
|
+
containing all the MissingEnvVariableError instances for the missing variables.
|
|
66
|
+
|
|
67
|
+
A typical use case for this method is when you have environment variables that depend on each other,
|
|
68
|
+
like in the case of a database URL that depends on the existence of a database host, port, username, and password.
|
|
69
|
+
|
|
70
|
+
Example usage::
|
|
71
|
+
|
|
72
|
+
from turboenv import TurboEnv
|
|
73
|
+
|
|
74
|
+
env = TurboEnv()
|
|
75
|
+
env.load_envs('.env')
|
|
76
|
+
|
|
77
|
+
# This will block until the environment variables DB_HOST, DB_PORT, DB_USER, and DB_PASSWORD exist
|
|
78
|
+
env.conditional('DATABASE_URL').depends_on(['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD'])
|
|
79
|
+
|
|
80
|
+
env.string('DATABASE_URL') # This will now work because the required environment variables exist
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
values (list[str]): A list of strings representing the names of the environment variables to check for existence.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ExceptionGroup: If any of the specified environment variables do not exist, an ExceptionGroup is raised containing all the MissingEnvVariableError instances for the missing variables.
|
|
87
|
+
"""
|
|
88
|
+
errors: list[exceptions.MissingEnvVariableError] = []
|
|
89
|
+
for name in values:
|
|
90
|
+
if not self.instance._exists(name):
|
|
91
|
+
errors.append(exceptions.MissingEnvVariableError(name))
|
|
92
|
+
|
|
93
|
+
if errors:
|
|
94
|
+
variables = ', '.join(values)
|
|
95
|
+
raise ExceptionGroup(
|
|
96
|
+
f"{self._name} is missing one or many dependencies: {variables}", errors)
|
|
97
|
+
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def to_be(self, expected: TypeAny) -> Self:
|
|
101
|
+
"""
|
|
102
|
+
Checks if the value of the environment variable is equal to the expected value.
|
|
103
|
+
|
|
104
|
+
Example usage::
|
|
105
|
+
|
|
106
|
+
from turboenv import TurboEnv
|
|
107
|
+
|
|
108
|
+
env = TurboEnv()
|
|
109
|
+
env.load_envs('.env')
|
|
110
|
+
|
|
111
|
+
conditional = env.conditional("DATABASE_URL")
|
|
112
|
+
conditional.to_be("postgres://user:password@localhost:5432/dbname")
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
expected (T): The expected value to compare against.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ConditionalError: If the value of the environment variable is not equal to the expected value.
|
|
119
|
+
"""
|
|
120
|
+
value = self.value()
|
|
121
|
+
if value != expected:
|
|
122
|
+
raise exceptions.ConditionalError(value, expected, "to be")
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def not_to_be(self, expected: TypeAny) -> Self:
|
|
126
|
+
"""
|
|
127
|
+
Checks if the value of the environment variable is not equal to the expected value.
|
|
128
|
+
|
|
129
|
+
Example usage::
|
|
130
|
+
|
|
131
|
+
from turboenv import TurboEnv
|
|
132
|
+
|
|
133
|
+
env = TurboEnv()
|
|
134
|
+
env.load_envs('.env')
|
|
135
|
+
|
|
136
|
+
conditional = env.conditional("DATABASE_URL")
|
|
137
|
+
conditional.not_to_be("postgres://user:password@localhost:5432/dbname")
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
expected (T): The expected value to compare against.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ConditionalError: If the value of the environment variable is equal to the expected value.
|
|
144
|
+
"""
|
|
145
|
+
value = self.value()
|
|
146
|
+
if value == expected:
|
|
147
|
+
raise exceptions.ConditionalError(
|
|
148
|
+
value, expected, "not to be")
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def to_exist(self) -> Self:
|
|
152
|
+
"""
|
|
153
|
+
Checks if the value of the environment variable exists (is not None).
|
|
154
|
+
|
|
155
|
+
Example usage::
|
|
156
|
+
|
|
157
|
+
from turboenv import TurboEnv
|
|
158
|
+
|
|
159
|
+
env = TurboEnv()
|
|
160
|
+
env.load_envs('.env')
|
|
161
|
+
|
|
162
|
+
conditional = env.conditional("DATABASE_URL")
|
|
163
|
+
conditional.to_exist()
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ConditionalError: If the value of the environment variable is None.
|
|
167
|
+
"""
|
|
168
|
+
value = self.value()
|
|
169
|
+
if value is None:
|
|
170
|
+
raise exceptions.ConditionalError(value, None, "exist")
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def to_not_be_empty(self) -> Self:
|
|
174
|
+
"""
|
|
175
|
+
Checks if the value of the environment variable is not empty.
|
|
176
|
+
|
|
177
|
+
Example usage::
|
|
178
|
+
|
|
179
|
+
from turboenv import TurboEnv
|
|
180
|
+
|
|
181
|
+
env = TurboEnv()
|
|
182
|
+
env.load_envs('.env')
|
|
183
|
+
|
|
184
|
+
conditional = env.conditional("DATABASE_URL")
|
|
185
|
+
conditional.to_not_be_empty()
|
|
186
|
+
|
|
187
|
+
Raises:
|
|
188
|
+
ConditionalError: If the value of the environment variable is None or an empty string.
|
|
189
|
+
"""
|
|
190
|
+
value = self.value()
|
|
191
|
+
if value is None or value == "":
|
|
192
|
+
raise exceptions.ConditionalError(value, None, "not be empty")
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def to_contain(self, expected: TypeAny) -> Self:
|
|
196
|
+
"""
|
|
197
|
+
Checks if the value of the environment variable contains the expected value.
|
|
198
|
+
|
|
199
|
+
Example usage::
|
|
200
|
+
|
|
201
|
+
from turboenv import TurboEnv
|
|
202
|
+
|
|
203
|
+
env = TurboEnv()
|
|
204
|
+
env.load_envs('.env')
|
|
205
|
+
|
|
206
|
+
conditional = env.conditional("DATABASE_URL")
|
|
207
|
+
conditional.to_contain(["postgres"])
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ConditionalError: If the value of the environment variable does not contain the expected value.
|
|
211
|
+
TypeError: If the value of the environment variable is not a list or a string.
|
|
212
|
+
"""
|
|
213
|
+
value = self.value()
|
|
214
|
+
if not isinstance(value, (list, str)):
|
|
215
|
+
raise TypeError(
|
|
216
|
+
"Value must be a list or a string to use to_contain")
|
|
217
|
+
|
|
218
|
+
if str(expected) not in value:
|
|
219
|
+
raise exceptions.ConditionalError(value, expected, "contain")
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def path_to_exist(self) -> Self:
|
|
223
|
+
"""
|
|
224
|
+
Checks if the value of the environment variable, interpreted as a file path, exists.
|
|
225
|
+
|
|
226
|
+
Example usage::
|
|
227
|
+
|
|
228
|
+
from turboenv import TurboEnv
|
|
229
|
+
|
|
230
|
+
env = TurboEnv()
|
|
231
|
+
env.load_envs('.env')
|
|
232
|
+
|
|
233
|
+
conditional = env.conditional("DATABASE_PATH")
|
|
234
|
+
conditional.path_to_exist()
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ConditionalError: If the value of the environment variable is None.
|
|
238
|
+
TypeError: If the value of the environment variable is not a string or a pathlib.Path.
|
|
239
|
+
FileNotFoundError: If the path does not exist.
|
|
240
|
+
"""
|
|
241
|
+
value = self.value()
|
|
242
|
+
if not isinstance(value, (str, pathlib.Path)):
|
|
243
|
+
raise TypeError(
|
|
244
|
+
"Value must be a string or a pathlib.Path to use path_to_exist")
|
|
245
|
+
|
|
246
|
+
value = self.value()
|
|
247
|
+
if value is None:
|
|
248
|
+
raise exceptions.ConditionalError(
|
|
249
|
+
self._name, None, "exist as a path")
|
|
250
|
+
|
|
251
|
+
path = pathlib.Path(value)
|
|
252
|
+
if not path.exists():
|
|
253
|
+
raise FileNotFoundError(
|
|
254
|
+
f"Path from env variable {self._name} does not exist")
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class TurboEnv:
|
|
259
|
+
"""A class for managing environment variables with support for
|
|
260
|
+
loading from files, type casting, and conditional logic.
|
|
261
|
+
|
|
262
|
+
Example usage:
|
|
263
|
+
|
|
264
|
+
from main import TurboEnv
|
|
265
|
+
|
|
266
|
+
env = TurboEnv()
|
|
267
|
+
env.load_envs('.env')
|
|
268
|
+
|
|
269
|
+
debug_mode = env.boolean('DEBUG_MODE', default=False)
|
|
270
|
+
database_url = env.string('DATABASE_URL')
|
|
271
|
+
allowed_hosts = env.str_list('ALLOWED_HOSTS', default=[])
|
|
272
|
+
|
|
273
|
+
Variables can separated by namespaces using the `namespace` method. Variables that do not specify a namespace
|
|
274
|
+
are loaded in the global namespace and are accessible directly from the main instance.
|
|
275
|
+
|
|
276
|
+
from main import TurboEnv
|
|
277
|
+
|
|
278
|
+
env = TurboEnv()
|
|
279
|
+
env.load_envs(('test', '.env'), ('prod', '.env.prod'))
|
|
280
|
+
|
|
281
|
+
test_envs = env.namespace('test')
|
|
282
|
+
database_url = test_envs.str('URL')
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
fail_on_missing (bool): If True, raises a FileNotFoundError if any of the specified files do not exist. Defaults to False.
|
|
286
|
+
only (str): If specified, only loads environment variables that start with this prefix. Defaults to None.
|
|
287
|
+
skip_empty (bool): If True, skips empty lines in the environment files. Defaults to False.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
_cache: OrderedDict[str, str] = OrderedDict()
|
|
291
|
+
|
|
292
|
+
def __init__(self, fail_on_missing: bool = False, skip_empty: bool = False):
|
|
293
|
+
self.fail_on_missing = fail_on_missing
|
|
294
|
+
self.skip_empty = skip_empty
|
|
295
|
+
self._files: set[pathlib.Path] = set()
|
|
296
|
+
|
|
297
|
+
def __call__(self, **defaults: str) -> "TurboEnv":
|
|
298
|
+
self._cache.update(defaults)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __repr__(self) -> str:
|
|
302
|
+
return f"TurboEnv(cache={len(self._cache)}, files={len(self._files)})"
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def has_files(self) -> bool:
|
|
306
|
+
return len(self._files) > 0
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def new(cls, **envs: str) -> "TurboEnv":
|
|
310
|
+
instance = cls()
|
|
311
|
+
instance._cache.update(envs)
|
|
312
|
+
return instance
|
|
313
|
+
|
|
314
|
+
def _exists(self, name: str) -> bool:
|
|
315
|
+
"""Checks if the specified environment variable exists in the cache.
|
|
316
|
+
Used for internal checks and conditional logic."""
|
|
317
|
+
return name in self._cache
|
|
318
|
+
|
|
319
|
+
def load_envs(self, *args: str):
|
|
320
|
+
"""Loads environment variables from the specified files.
|
|
321
|
+
If no files are specified, it defaults to loading from a
|
|
322
|
+
file named `.env` in the current directory.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
*args (str): A variable number of string arguments representing the file paths to load.
|
|
326
|
+
|
|
327
|
+
Raises:
|
|
328
|
+
FileNotFoundError: If `fail_on_missing` is set to True and any of the specified files do not exist.
|
|
329
|
+
"""
|
|
330
|
+
if not args:
|
|
331
|
+
args = ('.env',)
|
|
332
|
+
|
|
333
|
+
# To avoid loading the same files multiple times, we can check if the files
|
|
334
|
+
# have already been loaded by looking for a specific environment variable that we set
|
|
335
|
+
# after loading the files -; this is for performance optimization
|
|
336
|
+
load_completed = os.environ.get('TURBO_ENV_LOADED_FILES', 'False') == 'True'
|
|
337
|
+
if load_completed:
|
|
338
|
+
# FIXME: For whatever reason, in Django when the settings
|
|
339
|
+
# module is reloaded the environment variables from the
|
|
340
|
+
# file are lost and only the system environment variables are preserved.
|
|
341
|
+
# This is a workaround to reload the environment variables from
|
|
342
|
+
# the system environment in case they are lost after the initial load.
|
|
343
|
+
for key, value in os.environ.items():
|
|
344
|
+
self._cache[key] = value
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
for filename in args:
|
|
348
|
+
path = pathlib.Path(filename)
|
|
349
|
+
|
|
350
|
+
if self.fail_on_missing:
|
|
351
|
+
if not path.exists():
|
|
352
|
+
raise FileNotFoundError(f"File {filename} does not exist")
|
|
353
|
+
|
|
354
|
+
if path.exists():
|
|
355
|
+
self._files.add(path)
|
|
356
|
+
|
|
357
|
+
with _load_file(path) as lines:
|
|
358
|
+
for line in lines:
|
|
359
|
+
if line == "\n":
|
|
360
|
+
continue
|
|
361
|
+
|
|
362
|
+
variable_match = re.match(r'^([A-Z0-9\_]+)\s?\=\s?(.*)$', line)
|
|
363
|
+
if not variable_match:
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# Set the values that we read from the
|
|
367
|
+
# file into the cache
|
|
368
|
+
key, value = variable_match.groups()
|
|
369
|
+
self._cache[key] = value
|
|
370
|
+
|
|
371
|
+
# Once the files are loaded, check the system environment
|
|
372
|
+
# variables. They will override the values from the files
|
|
373
|
+
# if they exists in the system environment.
|
|
374
|
+
system_environ = os.environ
|
|
375
|
+
skip_keys = list(system_environ.keys())
|
|
376
|
+
# Set the environment variables from the local cache into the system environment
|
|
377
|
+
# This is necessary for the environment variables to be accessible from
|
|
378
|
+
# the system environment
|
|
379
|
+
for key, value in self._cache.items():
|
|
380
|
+
if key in skip_keys:
|
|
381
|
+
continue
|
|
382
|
+
os.environ.setdefault(key, value)
|
|
383
|
+
|
|
384
|
+
# In the specific case of Docker environments for example,
|
|
385
|
+
# since .env files are not used, load all the environment variables
|
|
386
|
+
# from the system environment
|
|
387
|
+
for key, value in system_environ.items():
|
|
388
|
+
self._cache[key] = value
|
|
389
|
+
|
|
390
|
+
os.environ.setdefault('TURBO_ENV_LOADED_FILES', 'True')
|
|
391
|
+
|
|
392
|
+
def get(self, name: str) -> TypeAny:
|
|
393
|
+
"""A strict version of the `get` method that raises a KeyError
|
|
394
|
+
if the specified environment variable does not exist."""
|
|
395
|
+
try:
|
|
396
|
+
return self._cache[name]
|
|
397
|
+
except KeyError as e:
|
|
398
|
+
raise exceptions.MissingEnvVariableError(name) from e
|
|
399
|
+
|
|
400
|
+
def boolean(self, name: str, default: bool | None = None) -> bool | None:
|
|
401
|
+
"""Returns the value of the specified environment variable as a boolean.
|
|
402
|
+
|
|
403
|
+
.. code-block:: python
|
|
404
|
+
|
|
405
|
+
turbo_env.boolean("MY_BOOLEAN_ENV_VAR", default=True)
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
name (str): The name of the environment variable to retrieve.
|
|
409
|
+
default (bool | None, optional): The default value to return if the environment variable is not set. Defaults to None.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
bool | None: The boolean value of the environment variable, or the default if not set.
|
|
413
|
+
"""
|
|
414
|
+
booleans = ['true', '1', 'yes', 'on', 'false', '0', 'no', 'off']
|
|
415
|
+
|
|
416
|
+
value = self._cache.get(name, None)
|
|
417
|
+
if value is None:
|
|
418
|
+
return default
|
|
419
|
+
|
|
420
|
+
if value.lower() in booleans[:4]:
|
|
421
|
+
return True
|
|
422
|
+
elif value.lower() in booleans[4:]:
|
|
423
|
+
return False
|
|
424
|
+
else:
|
|
425
|
+
raise ValueError(
|
|
426
|
+
f"Value for {name} is not a valid boolean: {value}")
|
|
427
|
+
|
|
428
|
+
def string(self, name: str, default: str | None = None) -> str:
|
|
429
|
+
"""Returns the value of the specified environment variable as a string.
|
|
430
|
+
|
|
431
|
+
.. code-block:: python
|
|
432
|
+
|
|
433
|
+
turbo_env.string("MY_STRING_ENV_VAR", default="default_value")
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
name (str): The name of the environment variable to retrieve.
|
|
437
|
+
default (str | None, optional): The default value to return if the environment variable is not set. Defaults to None.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
str: The string value of the environment variable, or the default if not set.
|
|
441
|
+
"""
|
|
442
|
+
value = self._cache.get(name, None)
|
|
443
|
+
if value is None:
|
|
444
|
+
return str(default)
|
|
445
|
+
return str(value)
|
|
446
|
+
|
|
447
|
+
def array[T = str](self, name: str, default: Sequence[T] | None = None, cast_values: Callable[[str], T] = str) -> Sequence[T] | None:
|
|
448
|
+
"""Returns a list of values for the given environment variable name. The values are
|
|
449
|
+
expected to be comma-separated in the environment variable.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
name (str): The name of the environment variable to retrieve.
|
|
453
|
+
default (Sequence[T], optional): The default value to return if the environment variable is not set. Defaults to None.
|
|
454
|
+
cast_values (Callable[[str], TypeCast] | None, optional): A function to cast each value in the list. Defaults to str.
|
|
455
|
+
"""
|
|
456
|
+
value = self._cache.get(name, None)
|
|
457
|
+
if value is None:
|
|
458
|
+
return default
|
|
459
|
+
|
|
460
|
+
single_values = [item for item in value.split(',')]
|
|
461
|
+
|
|
462
|
+
casted_values: list[T] = []
|
|
463
|
+
for item in single_values:
|
|
464
|
+
casted_value = cast_values(item)
|
|
465
|
+
if isinstance(casted_value, str):
|
|
466
|
+
casted_value = casted_value.strip()
|
|
467
|
+
casted_values.append(casted_value)
|
|
468
|
+
return casted_values
|
|
469
|
+
|
|
470
|
+
def json(self, name: str, default: Sequence[str] | None = None, cast_values: dict[str, TypeCast] | None = None) -> dict[str, TypeAny]:
|
|
471
|
+
"""Returns the value of the specified environment variable parsed as JSON.
|
|
472
|
+
|
|
473
|
+
Example usage::
|
|
474
|
+
|
|
475
|
+
from turboenv import TurboEnv
|
|
476
|
+
|
|
477
|
+
env = TurboEnv()
|
|
478
|
+
env.load_envs('.env')
|
|
479
|
+
|
|
480
|
+
config = env.json('CONFIG')
|
|
481
|
+
# config will be the parsed JSON value of the CONFIG environment variable
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
name (str): The name of the environment variable to retrieve.
|
|
485
|
+
default (Sequence[str] | None, optional): The default value to return if the environment variable is not set. Defaults to None.
|
|
486
|
+
cast_values (dict[str, TypeCast] | None, optional): A dictionary specifying cast functions for specific keys in the JSON object. Defaults to None.
|
|
487
|
+
|
|
488
|
+
Raises:
|
|
489
|
+
ValueError: If the specified environment variable is not set or if its value is not a valid JSON string.
|
|
490
|
+
"""
|
|
491
|
+
values = self.array(name, default=default, cast_values=str)
|
|
492
|
+
if values is None:
|
|
493
|
+
raise ValueError(f"Environment variable '{name}' is not set or is empty.")
|
|
494
|
+
|
|
495
|
+
dict_values: dict[str, TypeAny] = {}
|
|
496
|
+
for value in values:
|
|
497
|
+
if not isinstance(value, str):
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
key, value = value.split('=', 1)
|
|
501
|
+
|
|
502
|
+
_value = value.strip()
|
|
503
|
+
if cast_values is not None and key in cast_values:
|
|
504
|
+
try:
|
|
505
|
+
_value = cast_values[key](_value)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
raise ValueError(
|
|
508
|
+
f"Value for {key} is not valid according to the provided cast function."
|
|
509
|
+
) from e
|
|
510
|
+
|
|
511
|
+
dict_values[key.strip().upper()] = _value
|
|
512
|
+
return json_module.loads(json_module.dumps(dict_values))
|
|
513
|
+
|
|
514
|
+
def str_list(self, name: str, default: Sequence[str] | None = None):
|
|
515
|
+
"""Returns a list of strings for the given environment variable name.
|
|
516
|
+
|
|
517
|
+
Example usage::
|
|
518
|
+
|
|
519
|
+
from turboenv import TurboEnv
|
|
520
|
+
|
|
521
|
+
env = TurboEnv()
|
|
522
|
+
env.load_envs('.env')
|
|
523
|
+
|
|
524
|
+
allowed_hosts = env.str_list('ALLOWED_HOSTS')
|
|
525
|
+
# allowed_hosts will be a list of strings, e.g. ["localhost", "example.com"]
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
name (str): The name of the environment variable to retrieve.
|
|
529
|
+
default (Sequence[str] | None, optional): The default value to return if the environment variable is not set. Defaults to None.
|
|
530
|
+
"""
|
|
531
|
+
return self.array(name, default=default, cast_values=str)
|
|
532
|
+
|
|
533
|
+
def int_list(self, name: str, default: Sequence[int] | None = None):
|
|
534
|
+
"""Returns a list of integers for the given environment variable name.
|
|
535
|
+
|
|
536
|
+
Example usage::
|
|
537
|
+
|
|
538
|
+
from turboenv import TurboEnv
|
|
539
|
+
|
|
540
|
+
env = TurboEnv()
|
|
541
|
+
env.load_envs('.env')
|
|
542
|
+
|
|
543
|
+
port_numbers = env.int_list('PORT_NUMBERS')
|
|
544
|
+
# port_numbers will be a list of integers, e.g. [80, 443, 8080]
|
|
545
|
+
"""
|
|
546
|
+
return self.array(name, default=default, cast_values=int)
|
|
547
|
+
|
|
548
|
+
def domain_list(self, name: str, default: Sequence[str] | None = None) -> Sequence[str]:
|
|
549
|
+
"""Returns a list of domains for the given environment variable name.
|
|
550
|
+
|
|
551
|
+
Example usage::
|
|
552
|
+
|
|
553
|
+
from turboenv import TurboEnv
|
|
554
|
+
|
|
555
|
+
env = TurboEnv()
|
|
556
|
+
env.load_envs('.env')
|
|
557
|
+
|
|
558
|
+
allowed_domains = env.domain_list('ALLOWED_DOMAINS')
|
|
559
|
+
# allowed_domains will be a list of domains, e.g. ["example.com", "example.org"]
|
|
560
|
+
"""
|
|
561
|
+
domains = self.array(name, cast_values=str)
|
|
562
|
+
if domains is None:
|
|
563
|
+
return []
|
|
564
|
+
|
|
565
|
+
for domain in domains:
|
|
566
|
+
parsed = urlparse(domain)
|
|
567
|
+
if parsed.scheme and parsed.path:
|
|
568
|
+
raise ValueError(f"Value for {name} is not a valid domain: {domain}")
|
|
569
|
+
return domains
|
|
570
|
+
|
|
571
|
+
def url_list(self, name: str, default: Sequence[str] | None = None, secured: bool = False) -> Sequence[str]:
|
|
572
|
+
"""Returns a list of URLs for the given environment variable name.
|
|
573
|
+
|
|
574
|
+
Example usage::
|
|
575
|
+
|
|
576
|
+
from turboenv import TurboEnv
|
|
577
|
+
|
|
578
|
+
env = TurboEnv()
|
|
579
|
+
env.load_envs('.env')
|
|
580
|
+
|
|
581
|
+
api_endpoints = env.url_list('API_ENDPOINTS')
|
|
582
|
+
# api_endpoints will be a list of URLs, e.g. ["https://api.example.com", "https://api.example.org"]
|
|
583
|
+
"""
|
|
584
|
+
urls = self.array(name, default=default, cast_values=str)
|
|
585
|
+
if urls is None:
|
|
586
|
+
return []
|
|
587
|
+
|
|
588
|
+
for url in urls:
|
|
589
|
+
parsed = urlparse(url)
|
|
590
|
+
if not parsed.scheme or not parsed.netloc:
|
|
591
|
+
raise ValueError(f"Value for {name} is not a valid URL: {url}")
|
|
592
|
+
|
|
593
|
+
if secured and parsed.scheme != "https":
|
|
594
|
+
raise ValueError(f"Value for {name} is not a secure URL: {url}")
|
|
595
|
+
|
|
596
|
+
return urls
|
|
597
|
+
|
|
598
|
+
def secret(self, name: str):
|
|
599
|
+
"""Returns the value of the specified secret environment variable.
|
|
600
|
+
The value is expected to be base64-encoded.
|
|
601
|
+
|
|
602
|
+
Example usage::
|
|
603
|
+
|
|
604
|
+
from turboenv import TurboEnv
|
|
605
|
+
|
|
606
|
+
env = TurboEnv()
|
|
607
|
+
env.load_envs('.env')
|
|
608
|
+
|
|
609
|
+
db_password = env.secret('DB_PASSWORD')
|
|
610
|
+
# db_password will be the decoded value of the DB_PASSWORD environment variable
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
name (str): The name of the secret environment variable to retrieve.
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
ValueError: If the specified secret environment variable is not set or if its value is not a valid base64-encoded string.
|
|
617
|
+
"""
|
|
618
|
+
value = self._cache.get(name, None)
|
|
619
|
+
if value is None:
|
|
620
|
+
raise ValueError(f"Secret {name} is not set")
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
# Decode the value from base64
|
|
624
|
+
decoded_value = base64.b64decode(value).decode('utf-8')
|
|
625
|
+
except Exception as e:
|
|
626
|
+
raise ValueError(f"Value for {name} is not a valid base64-encoded string: {value}") from e
|
|
627
|
+
else:
|
|
628
|
+
return decoded_value
|
|
629
|
+
|
|
630
|
+
def path(self, name: str, check: bool = True) -> pathlib.Path | None:
|
|
631
|
+
"""
|
|
632
|
+
Retrieves the value of the specified environment variable as a file system path.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
name (str): The name of the environment variable to retrieve.
|
|
636
|
+
check (bool): If True, raises an error if the environment variable is not set.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
pathlib.Path: The value of the environment variable as a path.
|
|
640
|
+
|
|
641
|
+
Raises:
|
|
642
|
+
ValueError: If the environment variable is not set and check is True.
|
|
643
|
+
"""
|
|
644
|
+
value = self._cache.get(name, None)
|
|
645
|
+
if value is not None:
|
|
646
|
+
for f in self._files:
|
|
647
|
+
parent = f.parent.absolute()
|
|
648
|
+
fullpath = parent.joinpath(value).absolute()
|
|
649
|
+
# Only work with paths that are within
|
|
650
|
+
# the current base directory when the
|
|
651
|
+
# .env file is located
|
|
652
|
+
# fullpath.relative_to(parent)
|
|
653
|
+
|
|
654
|
+
if check and not fullpath.exists():
|
|
655
|
+
raise ValueError(f"Path for environment variable {name} does not exist: {fullpath}")
|
|
656
|
+
|
|
657
|
+
return fullpath
|
|
658
|
+
|
|
659
|
+
def conditional(self, name: str):
|
|
660
|
+
"""
|
|
661
|
+
Returns a Conditionals instance for the specified environment variable name,
|
|
662
|
+
allowing you to apply conditional logic on the value.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
name (str): The name of the environment variable to apply the conditional logic on.
|
|
666
|
+
"""
|
|
667
|
+
self.get(name)
|
|
668
|
+
return Conditionals(self, name)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NamespaceModel(BaseModel):
|
|
9
|
+
name: str | None = Field(default=None)
|
|
10
|
+
cache: dict[str, str] = Field(default_factory=dict)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NamespaceValues:
|
|
14
|
+
_cache: defaultdict[str, NamespaceModel] = defaultdict(lambda: NamespaceModel())
|
|
15
|
+
|
|
16
|
+
def __init__(self, name: str, values: dict[str, Any]):
|
|
17
|
+
self.name = name
|
|
18
|
+
|
|
19
|
+
ns = self._cache[name]
|
|
20
|
+
ns.name = name
|
|
21
|
+
ns.cache.update(values)
|
|
22
|
+
|
|
23
|
+
# ".env" file that was used to load the values
|
|
24
|
+
# for this namespace, if any
|
|
25
|
+
self.file: Optional[pathlib.Path] = None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from turboenv.typings import TypeTurboEnv
|
|
2
|
+
|
|
3
|
+
CACHE_SCHEMES = {
|
|
4
|
+
'dbcache': 'django.core.cache.backends.db.DatabaseCache',
|
|
5
|
+
'dummycache': 'django.core.cache.backends.dummy.DummyCache',
|
|
6
|
+
'filecache': 'django.core.cache.backends.filebased.FileBasedCache',
|
|
7
|
+
'locmemcache': 'django.core.cache.backends.locmem.LocMemCache',
|
|
8
|
+
'memcache': 'django.core.cache.backends.memcached.MemcachedCache',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def django_plugin(instance: TypeTurboEnv):
|
|
13
|
+
"""A plugin for Django projects that loads environment variables from .env files
|
|
14
|
+
and sets them in the system environment.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Type
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from turboenv.main import TurboEnv
|
|
5
|
+
|
|
6
|
+
type TypeAny = str | bool | float | list[str] | dict[str, Any] | None
|
|
7
|
+
|
|
8
|
+
type TypeCast[T = str | int | float] = Type[T]
|
|
9
|
+
|
|
10
|
+
type TypeTurboEnv = 'TurboEnv'
|