mmar-envops 1.0.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.
- mmar_envops-1.0.1/LICENSE +21 -0
- mmar_envops-1.0.1/PKG-INFO +62 -0
- mmar_envops-1.0.1/README.md +35 -0
- mmar_envops-1.0.1/pyproject.toml +69 -0
- mmar_envops-1.0.1/src/mmar_envops/__init__.py +13 -0
- mmar_envops-1.0.1/src/mmar_envops/aka_pydantic_settings.py +176 -0
- mmar_envops-1.0.1/src/mmar_envops/config.py +39 -0
- mmar_envops-1.0.1/src/mmar_envops/env_file_accessor.py +329 -0
- mmar_envops-1.0.1/src/mmar_envops/exec_context.py +127 -0
- mmar_envops-1.0.1/src/mmar_envops/fs.py +30 -0
- mmar_envops-1.0.1/src/mmar_envops/fs_utils.py +5 -0
- mmar_envops-1.0.1/src/mmar_envops/git.py +109 -0
- mmar_envops-1.0.1/src/mmar_envops/py.typed +0 -0
- mmar_envops-1.0.1/src/mmar_envops/repl.py +45 -0
- mmar_envops-1.0.1/src/mmar_envops/stdout.py +110 -0
- mmar_envops-1.0.1/src/mmar_envops/system.py +149 -0
- mmar_envops-1.0.1/src/mmar_envops/tunneling.py +396 -0
- mmar_envops-1.0.1/src/mmar_envops/utils.py +6 -0
- mmar_envops-1.0.1/src/mmar_envops/utils_collections.py +23 -0
- mmar_envops-1.0.1/src/mmar_envops/yaml_file_accessor.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 evjava
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mmar-envops
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Environment and configuration operations for Python applications
|
|
5
|
+
Keywords: environment,configuration,config,env,settings,dotenv
|
|
6
|
+
Author: Tagin
|
|
7
|
+
Author-email: Tagin <evjava@yandex.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Documentation
|
|
19
|
+
Classifier: Topic :: Software Development
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: pyyaml~=6.0 ; extra == 'all'
|
|
23
|
+
Requires-Dist: gitpython~=3.1 ; extra == 'all'
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# mmar-envops
|
|
29
|
+
|
|
30
|
+
Environment and configuration operations for Python applications.
|
|
31
|
+
|
|
32
|
+
A facade for managing environment variables, configuration files, and system operations in Python applications.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Environment file handling**: Read and parse `.env` files
|
|
37
|
+
- **Configuration management**: YAML-based configuration with environment-aware loading
|
|
38
|
+
- **Settings integration**: Pydantic settings support
|
|
39
|
+
- **System operations**: File system, git, and execution context utilities
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install mmar-envops
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from mmar_envops import EnvFileAccessor, Config
|
|
51
|
+
|
|
52
|
+
# Load environment variables from .env file
|
|
53
|
+
env = EnvFileAccessor.from_file(".env")
|
|
54
|
+
api_key = env.get("API_KEY")
|
|
55
|
+
|
|
56
|
+
# Load configuration from YAML
|
|
57
|
+
config = Config.from_yaml("config.yaml")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# mmar-envops
|
|
2
|
+
|
|
3
|
+
Environment and configuration operations for Python applications.
|
|
4
|
+
|
|
5
|
+
A facade for managing environment variables, configuration files, and system operations in Python applications.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Environment file handling**: Read and parse `.env` files
|
|
10
|
+
- **Configuration management**: YAML-based configuration with environment-aware loading
|
|
11
|
+
- **Settings integration**: Pydantic settings support
|
|
12
|
+
- **System operations**: File system, git, and execution context utilities
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install mmar-envops
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from mmar_envops import EnvFileAccessor, Config
|
|
24
|
+
|
|
25
|
+
# Load environment variables from .env file
|
|
26
|
+
env = EnvFileAccessor.from_file(".env")
|
|
27
|
+
api_key = env.get("API_KEY")
|
|
28
|
+
|
|
29
|
+
# Load configuration from YAML
|
|
30
|
+
config = Config.from_yaml("config.yaml")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mmar-envops"
|
|
3
|
+
# dynamic version is not supported yet on uv_build
|
|
4
|
+
version = "1.0.1"
|
|
5
|
+
description = "Environment and configuration operations for Python applications"
|
|
6
|
+
authors = [{name = "Tagin", email = "evjava@yandex.ru"}]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
keywords = ["environment", "configuration", "config", "env", "settings", "dotenv"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Documentation",
|
|
22
|
+
"Topic :: Software Development",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
all = [
|
|
30
|
+
"pyyaml~=6.0",
|
|
31
|
+
"gitpython~=3.1",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["uv_build>=0.8.14,<0.9.0"]
|
|
36
|
+
build-backend = "uv_build"
|
|
37
|
+
|
|
38
|
+
[tool.uv.build-backend]
|
|
39
|
+
module-name = "mmar_envops"
|
|
40
|
+
source-exclude = [".ruff_cache"]
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
ci = [
|
|
44
|
+
"ruff>=0.4",
|
|
45
|
+
"pytest>=8.2",
|
|
46
|
+
"pytest-asyncio>=1.0.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.uv]
|
|
50
|
+
default-groups = ["ci"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 120
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
|
|
58
|
+
[pytest]
|
|
59
|
+
python_files = [ "test_*.py" ]
|
|
60
|
+
testpaths = [ "tests" ]
|
|
61
|
+
|
|
62
|
+
# action:message_regex:warning_class:module_regex:line
|
|
63
|
+
filterwarnings = [ "error" ]
|
|
64
|
+
|
|
65
|
+
[mypy]
|
|
66
|
+
ignore_missing_imports = true
|
|
67
|
+
exclude = "tests/fixtures/"
|
|
68
|
+
warn_unused_ignores = true
|
|
69
|
+
show_error_codes = true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System operations package.
|
|
3
|
+
|
|
4
|
+
This package provides interfaces to system-level operations including:
|
|
5
|
+
- Shell command execution
|
|
6
|
+
- File system operations
|
|
7
|
+
- SSH tunnel management
|
|
8
|
+
- Environment file operations
|
|
9
|
+
- Environment variable parsing
|
|
10
|
+
- Interactive REPL utilities
|
|
11
|
+
- Output formatting and colors
|
|
12
|
+
- Execution context and configuration
|
|
13
|
+
"""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Auxiliary very lightweight pydantic-settings alternative"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, ClassVar, Literal, TypeVar, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
SupportedTypes = str | int | bool | float
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _str_to_bool(value: str) -> bool:
|
|
13
|
+
"""Convert string to boolean."""
|
|
14
|
+
normalized = value.lower()
|
|
15
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
16
|
+
return True
|
|
17
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
18
|
+
return False
|
|
19
|
+
raise ValueError(f"Invalid bool value: '{value}'")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _str_to_int(value: str) -> int:
|
|
23
|
+
"""Convert string to int."""
|
|
24
|
+
try:
|
|
25
|
+
return int(value)
|
|
26
|
+
except ValueError:
|
|
27
|
+
raise ValueError(f"Invalid int value: '{value}'") from None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _str_to_float(value: str) -> float:
|
|
31
|
+
"""Convert string to float."""
|
|
32
|
+
try:
|
|
33
|
+
return float(value)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return 0.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _str_to_literal(value: str, literal_values: tuple) -> Any:
|
|
39
|
+
"""Convert string to one of the allowed literal values."""
|
|
40
|
+
for lit_val in literal_values:
|
|
41
|
+
if isinstance(lit_val, str) and value == lit_val:
|
|
42
|
+
return lit_val
|
|
43
|
+
if isinstance(lit_val, bool) and _str_to_bool(value) == lit_val:
|
|
44
|
+
return lit_val
|
|
45
|
+
if isinstance(lit_val, int) and value.isdigit():
|
|
46
|
+
if int(value) == lit_val:
|
|
47
|
+
return lit_val
|
|
48
|
+
if isinstance(lit_val, float):
|
|
49
|
+
try:
|
|
50
|
+
if float(value) == lit_val:
|
|
51
|
+
return lit_val
|
|
52
|
+
except ValueError:
|
|
53
|
+
pass
|
|
54
|
+
raise ValueError(f"Invalid value '{value}' for Literal, expected one of {literal_values}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class AkaField:
|
|
59
|
+
"""Field descriptor with default value and alias support."""
|
|
60
|
+
|
|
61
|
+
default: Any = None
|
|
62
|
+
alias: str | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _FieldInfo:
|
|
66
|
+
"""Internal field metadata storage."""
|
|
67
|
+
|
|
68
|
+
__slots__ = ("name", "type", "default", "alias", "has_alias", "is_literal", "literal_values")
|
|
69
|
+
|
|
70
|
+
def __init__(self, name: str, type_: type, default: Any, alias: str | None = None):
|
|
71
|
+
self.name = name
|
|
72
|
+
self.type = type_
|
|
73
|
+
self.default = default
|
|
74
|
+
self.alias = alias
|
|
75
|
+
self.has_alias = alias is not None
|
|
76
|
+
self.is_literal = get_origin(type_) is Literal
|
|
77
|
+
self.literal_values = get_args(type_) if self.is_literal else ()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AkaBaseSettings:
|
|
81
|
+
"""Base settings class that reads from environment variables.
|
|
82
|
+
|
|
83
|
+
Features:
|
|
84
|
+
- Field aliases: e.g., debug with alias="d" can be set via DEBUG or D env var
|
|
85
|
+
- Type coercion for str/int/bool/float/Literal
|
|
86
|
+
- Default values
|
|
87
|
+
- Ignores extra environment variables
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_field_info: ClassVar[dict[str, _FieldInfo]] = {}
|
|
91
|
+
|
|
92
|
+
def __init_subclass__(cls, **kwargs: Any):
|
|
93
|
+
"""Parse class annotations and build field metadata."""
|
|
94
|
+
super().__init_subclass__(**kwargs)
|
|
95
|
+
|
|
96
|
+
fields = {}
|
|
97
|
+
|
|
98
|
+
for name, type_ in cls.__annotations__.items():
|
|
99
|
+
default_value = getattr(cls, name, None)
|
|
100
|
+
alias = None
|
|
101
|
+
|
|
102
|
+
if isinstance(default_value, AkaField):
|
|
103
|
+
alias = default_value.alias
|
|
104
|
+
default_value = default_value.default
|
|
105
|
+
elif default_value is None:
|
|
106
|
+
# For None defaults, check if it's Optional or required
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
fields[name] = _FieldInfo(name, type_, default_value, alias)
|
|
110
|
+
|
|
111
|
+
cls._field_info = fields
|
|
112
|
+
|
|
113
|
+
def __init__(self, **kwargs: Any):
|
|
114
|
+
"""Initialize settings from environment variables and keyword arguments.
|
|
115
|
+
|
|
116
|
+
Priority: environment variables > passed kwargs > default values.
|
|
117
|
+
Extra kwargs that don't match any field are ignored.
|
|
118
|
+
"""
|
|
119
|
+
self._set_values(**kwargs)
|
|
120
|
+
|
|
121
|
+
def _set_values(self, **kwargs: Any) -> None:
|
|
122
|
+
"""Set field values from environment, kwargs, or defaults."""
|
|
123
|
+
for info in self._field_info.values():
|
|
124
|
+
value = self._get_value(info, **kwargs)
|
|
125
|
+
object.__setattr__(self, info.name, value)
|
|
126
|
+
|
|
127
|
+
def _get_value(self, info: _FieldInfo, **kwargs: Any) -> Any:
|
|
128
|
+
"""Get value for a field from env, kwargs, or default.
|
|
129
|
+
|
|
130
|
+
Priority:
|
|
131
|
+
1. Environment variable (alias first, then field name)
|
|
132
|
+
2. Passed kwargs
|
|
133
|
+
3. Default value
|
|
134
|
+
"""
|
|
135
|
+
# Check environment by alias first, then by field name
|
|
136
|
+
env_value = None
|
|
137
|
+
|
|
138
|
+
if info.has_alias:
|
|
139
|
+
env_value = os.environ.get(info.alias.upper())
|
|
140
|
+
|
|
141
|
+
if env_value is None:
|
|
142
|
+
env_value = os.environ.get(info.name.upper())
|
|
143
|
+
|
|
144
|
+
if env_value is not None:
|
|
145
|
+
return self._convert_type(env_value, info.type, info.is_literal, info.literal_values)
|
|
146
|
+
|
|
147
|
+
# Check kwargs by alias first, then by field name
|
|
148
|
+
if info.has_alias and info.alias in kwargs:
|
|
149
|
+
kwarg_value = kwargs[info.alias]
|
|
150
|
+
return self._convert_type(str(kwarg_value), info.type, info.is_literal, info.literal_values)
|
|
151
|
+
|
|
152
|
+
if info.name in kwargs:
|
|
153
|
+
kwarg_value = kwargs[info.name]
|
|
154
|
+
return self._convert_type(str(kwarg_value), info.type, info.is_literal, info.literal_values)
|
|
155
|
+
|
|
156
|
+
return info.default
|
|
157
|
+
|
|
158
|
+
def _convert_type(self, value: str, target_type: type, is_literal: bool, literal_values: tuple) -> Any:
|
|
159
|
+
"""Convert string value to target type."""
|
|
160
|
+
|
|
161
|
+
if is_literal:
|
|
162
|
+
return _str_to_literal(value, literal_values)
|
|
163
|
+
|
|
164
|
+
if target_type is str:
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
if target_type is int:
|
|
168
|
+
return _str_to_int(value)
|
|
169
|
+
|
|
170
|
+
if target_type is float:
|
|
171
|
+
return _str_to_float(value)
|
|
172
|
+
|
|
173
|
+
if target_type is bool:
|
|
174
|
+
return _str_to_bool(value)
|
|
175
|
+
|
|
176
|
+
return value
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from mmar_envops.aka_pydantic_settings import AkaBaseSettings, AkaField
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExecConfig(AkaBaseSettings):
|
|
8
|
+
debug: bool = AkaField(default=False, alias="d")
|
|
9
|
+
time: bool = AkaField(default=False, alias="t")
|
|
10
|
+
autoyes: bool = AkaField(default=False)
|
|
11
|
+
dry: bool = False
|
|
12
|
+
color: bool = True
|
|
13
|
+
ripgrep_max_columns: int = 500
|
|
14
|
+
prefer_env_keys: Literal["upper", "lower", "no"] = "no"
|
|
15
|
+
|
|
16
|
+
def fix_key_case(self, key: str):
|
|
17
|
+
pref = self.prefer_env_keys
|
|
18
|
+
match pref:
|
|
19
|
+
case "upper":
|
|
20
|
+
return key.upper()
|
|
21
|
+
case "lower":
|
|
22
|
+
return key.lower()
|
|
23
|
+
case _:
|
|
24
|
+
return key
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_debug():
|
|
28
|
+
env = os.environ
|
|
29
|
+
return (env.get("debug") or env.get("d")) in {"1", "true", "yes"} or False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_force():
|
|
33
|
+
env = os.environ
|
|
34
|
+
return env.get("force") in {"1", "true", "yes"} or False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_autoyes():
|
|
38
|
+
env = os.environ
|
|
39
|
+
return (env.get("autoyes")) in {"1", "true", "yes"} or False
|