Settings
This commit is contained in:
49
.env.template
Normal file
49
.env.template
Normal file
@ -0,0 +1,49 @@
|
||||
# App
|
||||
BUILD_NUMBER=0
|
||||
DEBUG=true
|
||||
ENVIRONMENT=local
|
||||
LOG_LEVEL=INFO
|
||||
NAME=addressbook
|
||||
|
||||
# Api
|
||||
API_CACHE_EXPIRATION=60
|
||||
API_DB_SESSION_DEPENDENCY_KEY=db_session
|
||||
API_DEFAULT_PAGINATION_LIMIT=100
|
||||
API_DEFAULT_USER_NAME="__default_user__"
|
||||
API_HEALTH_PATH=/health
|
||||
API_SECRET_KEY=super-secret-value
|
||||
API_USER_DEPENDENCY_KEY=user
|
||||
|
||||
# OpenAPI
|
||||
OPENAPI_CONTACT_EMAIL=some_human@email.com
|
||||
OPENAPI_CONTACT_NAME="Some Human"
|
||||
OPENAPI_TITLE="My Litestar App"
|
||||
OPENAPI_VERSION=1.0.0
|
||||
|
||||
# Database
|
||||
DB_ECHO=true
|
||||
DB_ECHO_POOL=false
|
||||
DB_POOL_DISABLE=false
|
||||
DB_POOL_MAX_OVERFLOW=10
|
||||
DB_POOL_SIZE=5
|
||||
DB_POOL_TIMEOUT=30
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=addressbook
|
||||
DB_USER=addressbook
|
||||
DB_PASSWORD=addressbook
|
||||
|
||||
# Server
|
||||
UVICORN_HOST=0.0.0.0
|
||||
UVICORN_KEEPALIVE=65
|
||||
UVICORN_LOG_LEVEL=info
|
||||
UVICORN_PORT=8000
|
||||
UVICORN_RELOAD=true
|
||||
UVICORN_TIMEOUT=65
|
||||
|
||||
# Email
|
||||
EMAIL_HOST=mailhog
|
||||
EMAIL_NEW_AUTHOR_SUBJECT="New Author Added"
|
||||
EMAIL_PORT=1025
|
||||
EMAIL_RECIPIENT=someone@somewhere.com
|
||||
EMAIL_SENDER=root@localhost
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
||||
/.venv
|
||||
|
||||
__pycache__
|
||||
/.env
|
||||
|
||||
110
app/lib/settings.py
Normal file
110
app/lib/settings.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""All configuration via environment.
|
||||
|
||||
Take note of the environment variable prefixes required for each
|
||||
settings class, except `AppSettings`.
|
||||
"""
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
__all__ = [
|
||||
"APISettings",
|
||||
"AppSettings",
|
||||
"DatabaseSettings",
|
||||
"EmailSettings",
|
||||
"OpenAPISettings",
|
||||
"ServerSettings",
|
||||
]
|
||||
|
||||
from pydantic import Extra
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class BaseEnvSettings(BaseSettings):
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
class AppSettings(BaseEnvSettings):
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
|
||||
BUILD_NUMBER: str = "0"
|
||||
DEBUG: bool = False
|
||||
ENVIRONMENT: str = "local"
|
||||
LOG_LEVEL: str = "INFO"
|
||||
NAME: str = "addressbook"
|
||||
|
||||
@property
|
||||
def slug(self) -> str:
|
||||
return "-".join(s.lower() for s in self.NAME.split())
|
||||
|
||||
|
||||
class APISettings(BaseEnvSettings):
|
||||
class Config:
|
||||
env_prefix = "API_"
|
||||
case_sensitive = True
|
||||
|
||||
CACHE_EXPIRATION: int = 60
|
||||
DB_SESSION_DEPENDENCY_KEY: str = "db_session"
|
||||
DEFAULT_PAGINATION_LIMIT: int = 100
|
||||
DEFAULT_USER_NAME: str = "__default_user__"
|
||||
HEALTH_PATH: str = "/health"
|
||||
SECRET_KEY: str = "abc123"
|
||||
USER_DEPENDENCY_KEY: str = "user"
|
||||
|
||||
|
||||
class OpenAPISettings(BaseEnvSettings):
|
||||
class Config:
|
||||
env_prefix = "OPENAPI_"
|
||||
case_sensitive = True
|
||||
|
||||
TITLE: Optional[str] = "My Litestar App"
|
||||
VERSION: str = "0.1.0"
|
||||
CONTACT_NAME: str = "My Name"
|
||||
CONTACT_EMAIL: str = "some_human@some_domain.com"
|
||||
|
||||
|
||||
class DatabaseSettings(BaseEnvSettings):
|
||||
class Config:
|
||||
env_prefix = "DB_"
|
||||
case_sensitive = True
|
||||
|
||||
ECHO: bool = False
|
||||
ECHO_POOL: Union[bool, Literal["debug"]] = False
|
||||
POOL_DISABLE: bool = False
|
||||
POOL_MAX_OVERFLOW: int = 10
|
||||
POOL_SIZE: int = 5
|
||||
POOL_TIMEOUT: int = 30
|
||||
HOST: str = "localhost"
|
||||
PORT: int = 5432
|
||||
NAME: str = "db-name"
|
||||
USER: str = "db-user"
|
||||
PASSWORD: str = "db-password"
|
||||
|
||||
|
||||
class ServerSettings(BaseEnvSettings):
|
||||
class Config:
|
||||
env_prefix = "UVICORN_"
|
||||
case_sensitive = True
|
||||
|
||||
HOST: str = "localhost"
|
||||
LOG_LEVEL: str = "info"
|
||||
PORT: int = 8000
|
||||
RELOAD: bool = True
|
||||
KEEPALIVE: int = 65
|
||||
|
||||
|
||||
class EmailSettings(BaseEnvSettings):
|
||||
class Config:
|
||||
env_prefix = "EMAIL_"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
# `.parse_obj()` thing is a workaround for pyright and pydantic interplay, see:
|
||||
# https://github.com/pydantic/pydantic/issues/3753#issuecomment-1087417884
|
||||
api = APISettings.parse_obj({})
|
||||
app = AppSettings.parse_obj({})
|
||||
db = DatabaseSettings.parse_obj({})
|
||||
openapi = OpenAPISettings.parse_obj({})
|
||||
server = ServerSettings.parse_obj({})
|
||||
@ -1,10 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, cast, Literal
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from uuid import UUID
|
||||
|
||||
import msgspec
|
||||
import sqlalchemy
|
||||
from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyInitPlugin
|
||||
from litestar.contrib.sqlalchemy.plugins.init.config import SQLAlchemyAsyncConfig
|
||||
from litestar.contrib.sqlalchemy.plugins.init.config.common import (
|
||||
@ -16,28 +16,7 @@ from sqlalchemy import event
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
|
||||
DB_HOST = "localhost"
|
||||
DB_PORT = 5432
|
||||
DB_NAME = "addressbook"
|
||||
DB_USER = "addressbook"
|
||||
DB_PASSWORD = "addressbook"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatabaseSettings:
|
||||
URL: str = f"postgresql+asyncpg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
ECHO: bool = True
|
||||
ECHO_POOL: bool | Literal["debug"] = False
|
||||
POOL_DISABLE: bool = False
|
||||
POOL_MAX_OVERFLOW: int = 10
|
||||
POOL_SIZE: int = 5
|
||||
POOL_TIMEOUT: int = 30
|
||||
DB_SESSION_DEPENDENCY_KEY: str = "db_session"
|
||||
|
||||
|
||||
settings = DatabaseSettings()
|
||||
|
||||
from app.lib import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
@ -58,15 +37,24 @@ def _default(val: Any) -> str:
|
||||
raise TypeError()
|
||||
|
||||
|
||||
db_connection_url = sqlalchemy.engine.URL.create(
|
||||
drivername="postgresql+asyncpg",
|
||||
username=settings.db.USER,
|
||||
password=settings.db.PASSWORD,
|
||||
host=settings.db.HOST,
|
||||
port=settings.db.PORT,
|
||||
database=settings.db.NAME,
|
||||
)
|
||||
|
||||
engine = create_async_engine(
|
||||
settings.URL,
|
||||
echo=settings.ECHO,
|
||||
echo_pool=settings.ECHO_POOL,
|
||||
db_connection_url,
|
||||
echo=settings.db.ECHO,
|
||||
echo_pool=settings.db.ECHO_POOL,
|
||||
json_serializer=msgspec.json.Encoder(enc_hook=_default),
|
||||
max_overflow=settings.POOL_MAX_OVERFLOW,
|
||||
pool_size=settings.POOL_SIZE,
|
||||
pool_timeout=settings.POOL_TIMEOUT,
|
||||
poolclass=NullPool if settings.POOL_DISABLE else None,
|
||||
max_overflow=settings.db.POOL_MAX_OVERFLOW,
|
||||
pool_size=settings.db.POOL_SIZE,
|
||||
pool_timeout=settings.db.POOL_TIMEOUT,
|
||||
poolclass=NullPool if settings.db.POOL_DISABLE else None,
|
||||
)
|
||||
"""Configure via DatabaseSettings.
|
||||
|
||||
@ -74,7 +62,9 @@ Overrides default JSON
|
||||
serializer to use `msgspec`. See [`create_async_engine()`][sqlalchemy.ext.asyncio.create_async_engine]
|
||||
for detailed instructions.
|
||||
"""
|
||||
async_session_factory = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
||||
async_session_factory = async_sessionmaker(
|
||||
engine, expire_on_commit=False, class_=AsyncSession
|
||||
)
|
||||
"""Database session factory.
|
||||
|
||||
See [`async_sessionmaker()`][sqlalchemy.ext.asyncio.async_sessionmaker].
|
||||
@ -124,10 +114,11 @@ async def before_send_handler(message: Message, scope: Scope) -> None:
|
||||
|
||||
Args:
|
||||
message: ASGI message
|
||||
_:
|
||||
scope: ASGI scope
|
||||
"""
|
||||
session = cast("AsyncSession | None", get_litestar_scope_state(scope, SESSION_SCOPE_KEY))
|
||||
session = cast(
|
||||
"AsyncSession | None", get_litestar_scope_state(scope, SESSION_SCOPE_KEY)
|
||||
)
|
||||
try:
|
||||
if session is not None and message["type"] == "http.response.start":
|
||||
if 200 <= message["status"] < 300:
|
||||
@ -141,7 +132,7 @@ async def before_send_handler(message: Message, scope: Scope) -> None:
|
||||
|
||||
|
||||
config = SQLAlchemyAsyncConfig(
|
||||
session_dependency_key=settings.DB_SESSION_DEPENDENCY_KEY,
|
||||
session_dependency_key=settings.api.DB_SESSION_DEPENDENCY_KEY,
|
||||
engine_instance=engine,
|
||||
session_maker=async_session_factory,
|
||||
before_send_handler=before_send_handler,
|
||||
|
||||
17
main.py
17
main.py
@ -1,5 +1,6 @@
|
||||
from typing import Any
|
||||
|
||||
import uvicorn
|
||||
from litestar import Litestar
|
||||
from litestar.contrib.repository.exceptions import (
|
||||
RepositoryError as RepositoryException,
|
||||
@ -7,10 +8,13 @@ from litestar.contrib.repository.exceptions import (
|
||||
from litestar.openapi import OpenAPIConfig
|
||||
|
||||
from app.controllers import create_router
|
||||
from app.lib import exceptions, sqlalchemy_plugin
|
||||
from app.lib import exceptions, settings, sqlalchemy_plugin
|
||||
from app.lib.service import ServiceError
|
||||
|
||||
|
||||
def create_app(**kwargs: Any) -> Litestar:
|
||||
kwargs.setdefault("debug", settings.app.DEBUG)
|
||||
|
||||
return Litestar(
|
||||
route_handlers=[create_router()],
|
||||
openapi_config=OpenAPIConfig(title="My API", version="1.0.0"),
|
||||
@ -20,9 +24,18 @@ def create_app(**kwargs: Any) -> Litestar:
|
||||
RepositoryException: exceptions.repository_exception_to_http_response, # type: ignore[dict-item]
|
||||
ServiceError: exceptions.service_exception_to_http_response, # type: ignore[dict-item]
|
||||
},
|
||||
debug=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.server.HOST,
|
||||
log_level=settings.server.LOG_LEVEL,
|
||||
port=settings.server.PORT,
|
||||
reload=settings.server.RELOAD,
|
||||
timeout_keep_alive=settings.server.KEEPALIVE,
|
||||
)
|
||||
|
||||
31
poetry.lock
generated
31
poetry.lock
generated
@ -664,6 +664,21 @@ files = [
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.0.3"
|
||||
description = "Settings management using Pydantic"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"},
|
||||
{file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2.0.1"
|
||||
python-dotenv = ">=0.21.0"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.2"
|
||||
@ -698,6 +713,20 @@ files = [
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.0"
|
||||
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.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
|
||||
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.1"
|
||||
@ -913,4 +942,4 @@ anyio = ">=3.0.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "3319e3383cccbf806026c4983b44d301eb3106d9afc729328a5adfce7332ef8e"
|
||||
content-hash = "28c7f9f4b78e7d602b076108d3138c854cd1a1bc22c447987a6aedbba6300dcd"
|
||||
|
||||
@ -12,6 +12,7 @@ sqlalchemy = "^2.0.20"
|
||||
uvicorn = "^0.23.2"
|
||||
asyncpg = "^0.28.0"
|
||||
pydantic = "^2.3.0"
|
||||
pydantic-settings = "^2.0.3"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
Reference in New Issue
Block a user