153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
import os.path as op
|
|
from typing import Optional, Callable, Any
|
|
from enum import Enum
|
|
from dotenv import dotenv_values
|
|
import json
|
|
|
|
NO_DEFAULT = object() # Used to indicate no default value was provided
|
|
|
|
class EnviromentKeyMissing(Exception):
|
|
def __init__(self, key: str):
|
|
super().__init__(f"Environment variable '{key}' not found.")
|
|
|
|
class Modes(Enum):
|
|
"""
|
|
Enum for different environment modes.
|
|
Note: they are what i commonly use, feel free to use a own enum or directly use strings.
|
|
"""
|
|
PRODUCTION = ".production"
|
|
DEVELOPMENT = ".development"
|
|
TESTING = ".testing"
|
|
STAGING = ".staging"
|
|
LOCAL = ""
|
|
|
|
class Environment:
|
|
"""
|
|
Environment class for managing environment variables.
|
|
This class loads environment variables from a file and provides access to them.
|
|
It supports different modes (e.g., production, development) by appending the mode to the file name.
|
|
It uses a singleton pattern to ensure only one instance exists.
|
|
The environment variables can be accessed directly or through a `get` method that allows for casting and default values.
|
|
|
|
Usage:
|
|
```python
|
|
env = Environment(env_file_name='.env', mode=Modes.DEVELOPMENT)
|
|
value = env.get('MY_VARIABLE', default='default_value', cast=str)
|
|
```
|
|
|
|
This will load the environment variables from '.env.development' and return the value of 'MY_VARIABLE'.
|
|
If 'MY_VARIABLE' is not found, it will return 'default_value'.
|
|
If the environment file does not exist, it raises a FileNotFoundError.
|
|
"""
|
|
_instance = None
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
return cls._instance
|
|
|
|
def __init__(self, *, env_file_name: Optional[str] = None, env_file_path: Optional[str] = None, mode: Optional[Enum|str] = Modes.LOCAL, suppress_file_not_found: bool = False):
|
|
self.__initialize(env_file_name, env_file_path, mode, suppress_file_not_found)
|
|
|
|
def __initialize(self, env_file_name: str, env_file_path: str, mode: Enum|str, suppress_file_not_found: bool):
|
|
if hasattr(self, '__initialized') and self.__initialized:
|
|
return
|
|
|
|
self.__env_file_path = self.__resolve_file_path(env_file_name, env_file_path, mode)
|
|
|
|
if not op.exists(self.__env_file_path) and not suppress_file_not_found:
|
|
raise FileNotFoundError(f"Environment file '{self.__env_file_path}' does not exist.")
|
|
|
|
self.__values = dotenv_values(self.__env_file_path)
|
|
self.__initialized = True
|
|
|
|
def __resolve_file_path(self, env_file_name: str, env_file_path: str, mode: Enum|str) -> str:
|
|
"""
|
|
Resolves the file path for the environment file.
|
|
"""
|
|
if isinstance(mode, Enum):
|
|
mode = mode.value
|
|
else:
|
|
mode = f".{mode}" if mode else ""
|
|
|
|
if mode != "" and not mode.startswith('.'):
|
|
mode = f".{mode}"
|
|
|
|
file = (env_file_name or '.env') + mode
|
|
|
|
if env_file_path:
|
|
return op.join(env_file_path, file)
|
|
|
|
return file
|
|
|
|
@property
|
|
def env_file_path(self) -> str:
|
|
"""
|
|
Returns the path to the environment file.
|
|
This is the file from which environment variables are loaded.
|
|
If no file was specified, it defaults to '.env'.
|
|
If a mode is specified, it appends the mode to the file name.
|
|
Example: '.env.production', '.env.development', etc.
|
|
If the file was not found, it raises a FileNotFoundError unless suppressed.
|
|
Returns None if no file was specified or found.
|
|
"""
|
|
return self.__env_file_path if hasattr(self, '_Environment__env_file_path') else None
|
|
|
|
@property
|
|
def values(self) -> dict:
|
|
"""
|
|
Direct access to environment variables.
|
|
Returns a dictionary of all environment variables loaded.
|
|
Use with caution as it bypasses any casting or default values.
|
|
Prefer using `get` method for safer access.
|
|
"""
|
|
return self.__values
|
|
|
|
def get(self, key: str, default: Optional[Any] = NO_DEFAULT, cast: Optional[Callable] = lambda x: x) -> Any:
|
|
"""
|
|
Get the value of an environment variable.
|
|
If the variable is not found, it returns the default value.
|
|
If no default is provided, it raises a KeyError.
|
|
If a cast function is provided, it applies the cast to the value before returning.
|
|
"""
|
|
|
|
if default is NO_DEFAULT and key not in self.__values:
|
|
raise EnviromentKeyMissing(key)
|
|
|
|
return cast(self.__values.get(key, default))
|
|
|
|
def __getattribute__(self, name: str) -> Any:
|
|
if name.startswith("_") or name in super().__dir__():
|
|
return super().__getattribute__(name)
|
|
return self.get(name)
|
|
|
|
def __getitem__(self, key: str):
|
|
return self.get(key)
|
|
|
|
|
|
def cast_bool(value: str) -> bool:
|
|
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
def cast_int(value: str) -> int:
|
|
return int(value.strip())
|
|
|
|
def cast_float(value: str) -> float:
|
|
return float(value.strip())
|
|
|
|
def cast_str(value: str) -> str:
|
|
return str(value)
|
|
|
|
def cast_list(value: str) -> list:
|
|
return [item.strip() for item in value.split(",")]
|
|
|
|
def cast_tuple(value: str) -> tuple:
|
|
return tuple(item.strip() for item in value.split(","))
|
|
|
|
def cast_dict(value: str) -> dict:
|
|
return json.loads(value)
|
|
|
|
def cast_none_or_str(value: str):
|
|
if value.strip().lower() in ("null", "none"):
|
|
return None
|
|
return value
|