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.
@@ -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'