[rewrite] Added a config manager and some test code
This commit is contained in:
parent
807d23da51
commit
3c005dbfa4
@ -1,4 +1,3 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
files = app/
|
files = app/
|
||||||
plugins = sqlmypy
|
plugins = sqlmypy
|
||||||
ignore_missing_imports = True
|
|
@ -1,42 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class MySqlConfig:
|
|
||||||
MYSQL_USER = os.environ.get("MYSQL_USER")
|
|
||||||
MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE")
|
|
||||||
MYSQL_HOST = os.environ.get("MYSQL_HOST")
|
|
||||||
MYSQL_PORT = os.environ.get("MYSQL_PORT")
|
|
||||||
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD")
|
|
||||||
|
|
||||||
|
|
||||||
class RedisConfig:
|
|
||||||
REDIS_HOST = os.environ.get("REDIS_HOST")
|
|
||||||
REDIS_PORT = os.environ.get("REDIS_PORT")
|
|
||||||
REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD")
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskProduction:
|
|
||||||
DEBUG = False
|
|
||||||
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
|
||||||
SERVER_NAME = os.environ.get("HOST") + ":" + os.environ.get("PORT")
|
|
||||||
|
|
||||||
MAIL_SERVER = os.environ.get("MAIL_SERVER")
|
|
||||||
MAIL_PORT = os.environ.get("MAIL_PORT")
|
|
||||||
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
|
|
||||||
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
|
||||||
MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS")
|
|
||||||
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER")
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskTesting:
|
|
||||||
DEBUG = True
|
|
||||||
TESTING = True
|
|
||||||
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
|
||||||
SERVER_NAME = os.environ.get("HOST") + ":" + os.environ.get("PORT")
|
|
||||||
|
|
||||||
MAIL_SERVER = os.environ.get("MAIL_SERVER")
|
|
||||||
MAIL_PORT = os.environ.get("MAIL_PORT")
|
|
||||||
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
|
|
||||||
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
|
||||||
MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS")
|
|
||||||
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER")
|
|
3
backend/app/core/__init__.py
Normal file
3
backend/app/core/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .config import *
|
||||||
|
|
||||||
|
__all__ = [*config.__all__]
|
190
backend/app/core/config.py
Normal file
190
backend/app/core/config.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Type, Optional, Callable
|
||||||
|
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
from .exceptions import ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
class EnvConfigField():
|
||||||
|
"""
|
||||||
|
Represents a single config field to be parsed from the environment.
|
||||||
|
|
||||||
|
:param env_name: Name of the environment variable
|
||||||
|
:param data_type: Expected data type (int, str, etc.)
|
||||||
|
:param default: Default value if not set in environment
|
||||||
|
:param required: Whether the variable is required
|
||||||
|
:param choices: List of allowed values (for enums)
|
||||||
|
:param min_value: Minimum value (for integers)
|
||||||
|
:param max_value: Maximum value (for integers)
|
||||||
|
:param regex: Regex pattern (for string validation)
|
||||||
|
:param parser: Custom parser function for complex validation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
env_name: str,
|
||||||
|
data_type: Type[Any] = str,
|
||||||
|
default: Any = None,
|
||||||
|
required: bool = False,
|
||||||
|
choices: Optional[list[Any]] = None,
|
||||||
|
min_value: Optional[int] = None,
|
||||||
|
max_value: Optional[int] = None,
|
||||||
|
regex: Optional[str] = None,
|
||||||
|
parser: Optional[Callable[[str], Any]] = None,
|
||||||
|
):
|
||||||
|
self.value = None
|
||||||
|
self.env_name = env_name
|
||||||
|
self.data_type = data_type
|
||||||
|
self.default = default
|
||||||
|
self.required = required
|
||||||
|
self.choices = choices
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
self.regex = re.compile(regex) if regex else None
|
||||||
|
self.parser = parser
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""Loads the value from the environment and validates it."""
|
||||||
|
raw_value = os.environ.get(self.env_name)
|
||||||
|
|
||||||
|
if self.required and (raw_value is None or raw_value == ""):
|
||||||
|
raise ConfigError(
|
||||||
|
"Field %(env_name)s is required, but was not provided in the environment / .env",
|
||||||
|
env_name=self.env_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.value = self.validate(raw_value)
|
||||||
|
|
||||||
|
def validate(self, value: Any) -> Any:
|
||||||
|
"""Validates the value based on provided constraints."""
|
||||||
|
|
||||||
|
# Use the default value if available and skip validation
|
||||||
|
if (value is None or value == "") and self.default is not None:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
# Use custom parser if provided
|
||||||
|
if self.parser:
|
||||||
|
value = self.parser(value)
|
||||||
|
|
||||||
|
# Convert value to expected data type
|
||||||
|
if self.data_type == str:
|
||||||
|
value = self.__parse_to_str(value)
|
||||||
|
elif self.data_type == int:
|
||||||
|
value = self.__parse_to_int(value)
|
||||||
|
elif self.data_type == bool:
|
||||||
|
value = self.__parse_to_bool(value)
|
||||||
|
|
||||||
|
# Validate allowed choices
|
||||||
|
if self.choices and value not in self.choices:
|
||||||
|
raise ConfigError(
|
||||||
|
"Value for %(env_name)s - %(value)s is not one of the following allowed values: %(allowed_values)s",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value,
|
||||||
|
allowed_values=", ".join(map(str, self.choices)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, new):
|
||||||
|
raise TypeError("Cannot modify")
|
||||||
|
|
||||||
|
def __parse_to_str(self, value) -> str:
|
||||||
|
"""Parses and validates string values."""
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
if self.regex and not self.regex.match(value):
|
||||||
|
raise ConfigError(
|
||||||
|
"Value for %(env_name)s - %(value)s does not match the expected pattern: %(regex)s",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value,
|
||||||
|
regex=self.regex.pattern,
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __parse_to_int(self, value) -> int:
|
||||||
|
"""Parses and validates integer values."""
|
||||||
|
if not str(value).isdigit():
|
||||||
|
raise ConfigError(
|
||||||
|
"Value for %(env_name)s - %(value)s must be a numeric integer",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
if self.min_value is not None and value < self.min_value:
|
||||||
|
raise ConfigError(
|
||||||
|
"Value for %(env_name)s - %(value)d is too small. Must be greater than or equal to %(min_value)d",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value,
|
||||||
|
min_value=self.min_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.max_value is not None and value > self.max_value:
|
||||||
|
raise ConfigError(
|
||||||
|
"Value for %(env_name)s - %(value)d is too large. Must be smaller than or equal to %(max_value)d",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value,
|
||||||
|
max_value=self.max_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __parse_to_bool(self, value):
|
||||||
|
value = str(value).lower()
|
||||||
|
|
||||||
|
true_values = ["1", "true", "yes"]
|
||||||
|
false_values = ["0", "false", "no"]
|
||||||
|
|
||||||
|
if value in true_values:
|
||||||
|
return True
|
||||||
|
if value in false_values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise ConfigError("Value for %(env_name)s - %(value)s cannot be converted to a true / false value",
|
||||||
|
env_name=self.env_name,
|
||||||
|
value=value)
|
||||||
|
|
||||||
|
|
||||||
|
class EnvConfig():
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
self.fields = {}
|
||||||
|
|
||||||
|
def add_field(self, name: str, field: EnvConfigField):
|
||||||
|
if name in self.fields:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.fields[name] = field
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
for field in self.fields.values():
|
||||||
|
try:
|
||||||
|
field.load
|
||||||
|
except ConfigError as e:
|
||||||
|
self.logger.error("Configuration error: %s", e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __getitem__(self, key: str):
|
||||||
|
return self.fields["key"].value
|
||||||
|
|
||||||
|
def __contains__(self, key: str) -> bool:
|
||||||
|
return key in self.fields
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {attr: getattr(self, attr) for attr in self.fields}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["EnvConfig"]
|
12
backend/app/core/exceptions.py
Normal file
12
backend/app/core/exceptions.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class SwagShopError(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
class ConfigError(SwagShopError):
|
||||||
|
def __init__(self, message, **kwargs):
|
||||||
|
formatted_message = message % kwargs
|
||||||
|
super().__init__(formatted_message)
|
||||||
|
self.message = formatted_message
|
||||||
|
|
||||||
|
__all__ = ["SwagShopError", "ConfigError"]
|
@ -1,6 +1,7 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from .routes.cart_routes import router as cart_router
|
from .routes.cart_routes import router as cart_router
|
||||||
from .routes.user_routes import router as user_router
|
from .routes.user_routes import router as user_router
|
||||||
|
from .routes.shop_routes import router as shop_router
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@ -10,6 +11,7 @@ app = FastAPI(
|
|||||||
|
|
||||||
app.include_router(user_router)
|
app.include_router(user_router)
|
||||||
app.include_router(cart_router)
|
app.include_router(cart_router)
|
||||||
|
app.include_router(shop_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/shop",
|
||||||
|
tags=["Shop"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def get_shop_info():
|
||||||
|
raise NotImplementedError
|
13
backend/app/utils/jwt_utils.py
Normal file
13
backend/app/utils/jwt_utils.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from app.extensions import jwt_redis_blocklist
|
||||||
|
|
||||||
|
from . import jwt_manager
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
@jwt_manager.token_in_blocklist_loader
|
||||||
|
def check_if_token_is_revoked(jwt_header, jwt_payload: dict) -> bool:
|
||||||
|
jti = jwt_payload["jti"]
|
||||||
|
token_in_redis = jwt_redis_blocklist.get(jti)
|
||||||
|
|
||||||
|
return token_in_redis is not None
|
16
backend/poetry.lock
generated
16
backend/poetry.lock
generated
@ -381,6 +381,20 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.0.1"
|
||||||
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||||
|
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
@ -600,4 +614,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "b8a25a5b186862b7ea42474e850ed664aa26925d64f0a89e98bf67cd4fd862f8"
|
content-hash = "250084c97cedad74f83b3a3420c71f338eb85d309bf30aed8e84743a3c2b26e3"
|
||||||
|
@ -4,11 +4,13 @@ version = "0.1.0"
|
|||||||
description = ""
|
description = ""
|
||||||
authors = ["Thastertyn <thastertyn@gmail.com>"]
|
authors = ["Thastertyn <thastertyn@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.12"
|
||||||
fastapi = "^0.115.6"
|
fastapi = "^0.115.6"
|
||||||
sqlalchemy = "^2.0.37"
|
sqlalchemy = "^2.0.37"
|
||||||
|
python-dotenv = "^1.0.1"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user