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 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
@@ -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,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ pureenv/__init__.py
4
+ pureenv.egg-info/PKG-INFO
5
+ pureenv.egg-info/SOURCES.txt
6
+ pureenv.egg-info/dependency_links.txt
7
+ pureenv.egg-info/top_level.txt
8
+ tests/test_pureenv.py
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)