pureenv 0.1.1__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.
- pureenv-0.1.1/PKG-INFO +10 -0
- pureenv-0.1.1/README.md +59 -0
- pureenv-0.1.1/pureenv/__init__.py +148 -0
- pureenv-0.1.1/pureenv.egg-info/PKG-INFO +10 -0
- pureenv-0.1.1/pureenv.egg-info/SOURCES.txt +8 -0
- pureenv-0.1.1/pureenv.egg-info/dependency_links.txt +1 -0
- pureenv-0.1.1/pureenv.egg-info/top_level.txt +1 -0
- pureenv-0.1.1/pyproject.toml +24 -0
- pureenv-0.1.1/setup.cfg +4 -0
- pureenv-0.1.1/tests/test_pureenv.py +89 -0
pureenv-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pureenv
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Typed environment variable parsing for Python. Zero dependencies. Pure stdlib.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/abiget/pureenv
|
|
7
|
+
Project-URL: Repository, https://github.com/abiget/pureenv
|
|
8
|
+
Project-URL: Issues, https://github.com/abiget/pureenv/issues
|
|
9
|
+
Keywords: environment,env,config,settings,typed
|
|
10
|
+
Requires-Python: >=3.8
|
pureenv-0.1.1/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# pureenv
|
|
2
|
+
|
|
3
|
+
Typed environment variable parsing for Python. Zero dependencies. Pure stdlib.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Every Python project ends up writing this:
|
|
10
|
+
```python
|
|
11
|
+
PORT = int(os.environ.get("PORT", 8080))
|
|
12
|
+
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
|
|
13
|
+
DB_URL = os.environ["DATABASE_URL"] # KeyError with no helpful message
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Manual casting. No validation. Cryptic error. Every. Single. Project.
|
|
17
|
+
|
|
18
|
+
## The solution
|
|
19
|
+
```python
|
|
20
|
+
from pureenv import env
|
|
21
|
+
PORT = env.int("PORT", default=8080)
|
|
22
|
+
DEBUG = env.bool("DEBUG", default=False)
|
|
23
|
+
DB_URL = env.str("DATABASE_URL", required=True)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Typed. Validated. Clear errors when something is wrong.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
```bash
|
|
31
|
+
pip install pureenv
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
```python
|
|
36
|
+
from pureenv import env
|
|
37
|
+
|
|
38
|
+
# strings
|
|
39
|
+
NAME = env.str("APP_NAME", default="myapp")
|
|
40
|
+
|
|
41
|
+
# numbers
|
|
42
|
+
PORT = env.int("PORT", default=8080)
|
|
43
|
+
RATIO = env.float("RATIO", default=0.5)
|
|
44
|
+
|
|
45
|
+
# booleans - accepts "true/false", "1/0", "yes/no", "y/n", "on/off"
|
|
46
|
+
DEBUG = env.bool("DEBUG", default=False)
|
|
47
|
+
|
|
48
|
+
# required - raises a clear error if missing
|
|
49
|
+
DB_URL = env.str("DATABASE_URL", required=True)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Why not `environs` or `python-dotenv`?
|
|
53
|
+
|
|
54
|
+
| | pureenv | python-dotenv | environs |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| Typed casting | ✅ | ❌ | ✅ |
|
|
57
|
+
| Zero dependencies | ✅ | ✅ | ❌ |
|
|
58
|
+
| Required validation | ✅ | ❌ | ✅ |
|
|
59
|
+
| Loads .env file | ❌ | ✅ | ✅ |
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Store references to the original int and float functions to avoid issues
|
|
5
|
+
# if they are overridden in the environment
|
|
6
|
+
_int = int
|
|
7
|
+
_float = float
|
|
8
|
+
_bool = bool
|
|
9
|
+
_str = str
|
|
10
|
+
|
|
11
|
+
class Env:
|
|
12
|
+
def __init__(self, environ: dict = None):
|
|
13
|
+
self._environ = environ if environ is not None else os.environ
|
|
14
|
+
def str(self, key: str, default: str = None,
|
|
15
|
+
required: bool = False) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Get an environment variable as a string.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
key: The name of the environment variable.
|
|
21
|
+
default: The default value to return if the variable is not set (default: None).
|
|
22
|
+
required: If True, raises an error if the variable is not set (default: False).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The value of the environment variable as a string, or the default value if not set.
|
|
26
|
+
"""
|
|
27
|
+
value = self._environ.get(key, None)
|
|
28
|
+
|
|
29
|
+
if value is None:
|
|
30
|
+
if required:
|
|
31
|
+
if sys.platform.startswith("win"):
|
|
32
|
+
hint = f"set {key}=your_value"
|
|
33
|
+
else:
|
|
34
|
+
hint = f"export {key}=your_value"
|
|
35
|
+
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Required env var '{key}' is not set.\n"
|
|
38
|
+
f"→ To fix: {hint}"
|
|
39
|
+
)
|
|
40
|
+
return _str(default) if default is not None else None
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
def int(self, key: str, default: int = None,
|
|
44
|
+
required: bool = False) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Get an environment variable as an integer.
|
|
47
|
+
Args:
|
|
48
|
+
key: The name of the environment variable.
|
|
49
|
+
default: The default value to return if the variable is not set (default: None).
|
|
50
|
+
required: If True, raises an error if the variable is not set (default: False).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The value of the environment variable as an integer, or the default value if not set.
|
|
54
|
+
"""
|
|
55
|
+
value = self.str(key, default=default, required=required)
|
|
56
|
+
|
|
57
|
+
if value is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return _int(value)
|
|
62
|
+
except (ValueError, TypeError):
|
|
63
|
+
# did it come from an env var or the default value?
|
|
64
|
+
if os.environ.get(key):
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Env var '{key}' has value {value!r} which is not a valid integer."
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Default value for '{key}' is {value!r} which is not a valid integer."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def float(self, key: str, default: float = None,
|
|
74
|
+
required: bool = False) -> float:
|
|
75
|
+
"""
|
|
76
|
+
Get an environment variable as a float.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
key: The name of the environment variable.
|
|
80
|
+
default: The default value to return if the variable is not set (default: None).
|
|
81
|
+
required: If True, raises an error if the variable is not set (default: False).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The value of the environment variable as a float, or the default value if not set.
|
|
85
|
+
"""
|
|
86
|
+
value = self.str(key, default=default, required=required)
|
|
87
|
+
|
|
88
|
+
if value is None:
|
|
89
|
+
return None
|
|
90
|
+
try:
|
|
91
|
+
return _float(value)
|
|
92
|
+
except (ValueError, TypeError):
|
|
93
|
+
if os.environ.get(key):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Env var '{key}' has value {value!r} which is not a valid float."
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Default value for '{key}' is {value!r} which is not a valid float."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def bool(self, key: str, default: bool = None,
|
|
103
|
+
required: bool = False) -> bool:
|
|
104
|
+
"""
|
|
105
|
+
Get an environment variable as a boolean.
|
|
106
|
+
|
|
107
|
+
Accepted values (case-insensitive):
|
|
108
|
+
- Truthy: "true", "1", "yes", "y", "on"
|
|
109
|
+
- Falsy: "false", "0", "no", "n", "off"
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
key: The name of the environment variable.
|
|
113
|
+
default: The default value to return if the variable is not set (default: None).
|
|
114
|
+
required: If True, raises an error if the variable is not set (default: False).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The value of the environment variable as a boolean, or the default value if not set.
|
|
118
|
+
"""
|
|
119
|
+
value = self.str(key, default=default, required=required)
|
|
120
|
+
|
|
121
|
+
if value is None:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
if isinstance(value, _bool):
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
if isinstance(value, (_int, _float)):
|
|
128
|
+
return _bool(value)
|
|
129
|
+
|
|
130
|
+
value_lower = value.lower()
|
|
131
|
+
if value_lower in ("true", "1", "yes", "y", "on"):
|
|
132
|
+
return True
|
|
133
|
+
elif value_lower in ("false", "0", "no", "n", "off"):
|
|
134
|
+
return False
|
|
135
|
+
else:
|
|
136
|
+
if os.environ.get(key):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Env var '{key}' has value {value!r} which is not a valid boolean.\n"
|
|
139
|
+
f"→ Use: true/false, 1/0, yes/no, y/n, on/off (case-insensitive)"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"The default value for '{key}' is {value!r} which is not a valid boolean.\n"
|
|
144
|
+
f"→ Use: true/false, 1/0, yes/no, y/n, on/off (case-insensitive)"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# module-level singleton instance of Env for convenience
|
|
148
|
+
env = Env()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pureenv
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Typed environment variable parsing for Python. Zero dependencies. Pure stdlib.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/abiget/pureenv
|
|
7
|
+
Project-URL: Repository, https://github.com/abiget/pureenv
|
|
8
|
+
Project-URL: Issues, https://github.com/abiget/pureenv/issues
|
|
9
|
+
Keywords: environment,env,config,settings,typed
|
|
10
|
+
Requires-Python: >=3.8
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pureenv
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pureenv"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Typed environment variable parsing for Python. Zero dependencies. Pure stdlib."
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
dependencies = []
|
|
11
|
+
keywords = ["environment", "env", "config", "settings", "typed"]
|
|
12
|
+
license = {text = "MIT"}
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
where = ["."]
|
|
16
|
+
include = ["pureenv*"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/abiget/pureenv"
|
|
20
|
+
Repository = "https://github.com/abiget/pureenv"
|
|
21
|
+
Issues = "https://github.com/abiget/pureenv/issues"
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
pureenv-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pureenv import Env
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_str_returns_value():
|
|
6
|
+
env = Env(environ={"NAME": "alice"})
|
|
7
|
+
assert env.str("NAME") == "alice"
|
|
8
|
+
|
|
9
|
+
def test_str_returns_default():
|
|
10
|
+
env = Env(environ={})
|
|
11
|
+
assert env.str("MISSING", default="fallback") == "fallback"
|
|
12
|
+
|
|
13
|
+
def test_str_returns_none_when_no_default():
|
|
14
|
+
env = Env(environ={})
|
|
15
|
+
assert env.str("MISSING") is None
|
|
16
|
+
|
|
17
|
+
def test_str_raises_when_required():
|
|
18
|
+
env = Env(environ={})
|
|
19
|
+
|
|
20
|
+
with pytest.raises(ValueError, match="MISSING"):
|
|
21
|
+
env.str("MISSING", required=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_int_returns_value():
|
|
25
|
+
env = Env(environ={"PORT": "8080"})
|
|
26
|
+
assert env.int("PORT") == 8080
|
|
27
|
+
|
|
28
|
+
def test_int_returns_default():
|
|
29
|
+
env = Env(environ={})
|
|
30
|
+
assert env.int("MISSING", default=3306) == 3306
|
|
31
|
+
|
|
32
|
+
def test_int_raises_on_invalid_value():
|
|
33
|
+
env = Env(environ={"PORT": "not_a_number"})
|
|
34
|
+
|
|
35
|
+
with pytest.raises(ValueError, match="PORT"):
|
|
36
|
+
env.int("PORT")
|
|
37
|
+
|
|
38
|
+
def test_int_raises_when_required():
|
|
39
|
+
env = Env(environ={})
|
|
40
|
+
|
|
41
|
+
with pytest.raises(ValueError, match="PORT"):
|
|
42
|
+
env.int("PORT", required=True)
|
|
43
|
+
|
|
44
|
+
def test_float_returns_value():
|
|
45
|
+
env = Env(environ={"PI": "3.14"})
|
|
46
|
+
assert env.float("PI") == 3.14
|
|
47
|
+
|
|
48
|
+
def test_float_returns_default():
|
|
49
|
+
env = Env(environ={})
|
|
50
|
+
assert env.float("MISSING", default=3.14) == 3.14
|
|
51
|
+
|
|
52
|
+
def test_float_raises_on_invalid_value():
|
|
53
|
+
env = Env(environ={"PI": "not_a_number"})
|
|
54
|
+
|
|
55
|
+
with pytest.raises(ValueError, match="PI"):
|
|
56
|
+
env.float("PI")
|
|
57
|
+
|
|
58
|
+
def test_float_raises_when_required():
|
|
59
|
+
env = Env(environ={})
|
|
60
|
+
|
|
61
|
+
with pytest.raises(ValueError, match="PI"):
|
|
62
|
+
env.float("PI", required=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize("value", ("true", "1", "yes", "y", "on"))
|
|
66
|
+
def test_bool_truthy_values(value):
|
|
67
|
+
env = Env(environ={"DEBUG": value})
|
|
68
|
+
assert env.bool("DEBUG") is True
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize("value", ("false", "0", "no", "n", "off"))
|
|
71
|
+
def test_bool_falsy_values(value):
|
|
72
|
+
env = Env(environ={"DEBUG": value})
|
|
73
|
+
assert env.bool("DEBUG") is False
|
|
74
|
+
|
|
75
|
+
def test_bool_returns_default():
|
|
76
|
+
env = Env(environ={})
|
|
77
|
+
assert env.bool("MISSING", default=False) is False
|
|
78
|
+
|
|
79
|
+
def test_bool_raises_on_invalid_value():
|
|
80
|
+
env = Env(environ={"DEBUG": "maybe"})
|
|
81
|
+
|
|
82
|
+
with pytest.raises(ValueError, match="DEBUG"):
|
|
83
|
+
env.bool("DEBUG")
|
|
84
|
+
|
|
85
|
+
def test_bool_raises_when_required():
|
|
86
|
+
env = Env(environ={})
|
|
87
|
+
|
|
88
|
+
with pytest.raises(ValueError, match="MISSING"):
|
|
89
|
+
env.bool("MISSING", required=True)
|