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
|
/.venv
|
||||||
|
|
||||||
__pycache__
|
__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 __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from typing import TYPE_CHECKING, cast
|
||||||
from typing import TYPE_CHECKING, cast, Literal
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
import sqlalchemy
|
||||||
from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyInitPlugin
|
from litestar.contrib.sqlalchemy.plugins.init import SQLAlchemyInitPlugin
|
||||||
from litestar.contrib.sqlalchemy.plugins.init.config import SQLAlchemyAsyncConfig
|
from litestar.contrib.sqlalchemy.plugins.init.config import SQLAlchemyAsyncConfig
|
||||||
from litestar.contrib.sqlalchemy.plugins.init.config.common import (
|
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.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
from sqlalchemy.pool import NullPool
|
from sqlalchemy.pool import NullPool
|
||||||
|
|
||||||
|
from app.lib import settings
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -58,15 +37,24 @@ def _default(val: Any) -> str:
|
|||||||
raise TypeError()
|
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(
|
engine = create_async_engine(
|
||||||
settings.URL,
|
db_connection_url,
|
||||||
echo=settings.ECHO,
|
echo=settings.db.ECHO,
|
||||||
echo_pool=settings.ECHO_POOL,
|
echo_pool=settings.db.ECHO_POOL,
|
||||||
json_serializer=msgspec.json.Encoder(enc_hook=_default),
|
json_serializer=msgspec.json.Encoder(enc_hook=_default),
|
||||||
max_overflow=settings.POOL_MAX_OVERFLOW,
|
max_overflow=settings.db.POOL_MAX_OVERFLOW,
|
||||||
pool_size=settings.POOL_SIZE,
|
pool_size=settings.db.POOL_SIZE,
|
||||||
pool_timeout=settings.POOL_TIMEOUT,
|
pool_timeout=settings.db.POOL_TIMEOUT,
|
||||||
poolclass=NullPool if settings.POOL_DISABLE else None,
|
poolclass=NullPool if settings.db.POOL_DISABLE else None,
|
||||||
)
|
)
|
||||||
"""Configure via DatabaseSettings.
|
"""Configure via DatabaseSettings.
|
||||||
|
|
||||||
@ -74,7 +62,9 @@ Overrides default JSON
|
|||||||
serializer to use `msgspec`. See [`create_async_engine()`][sqlalchemy.ext.asyncio.create_async_engine]
|
serializer to use `msgspec`. See [`create_async_engine()`][sqlalchemy.ext.asyncio.create_async_engine]
|
||||||
for detailed instructions.
|
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.
|
"""Database session factory.
|
||||||
|
|
||||||
See [`async_sessionmaker()`][sqlalchemy.ext.asyncio.async_sessionmaker].
|
See [`async_sessionmaker()`][sqlalchemy.ext.asyncio.async_sessionmaker].
|
||||||
@ -124,10 +114,11 @@ async def before_send_handler(message: Message, scope: Scope) -> None:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
message: ASGI message
|
message: ASGI message
|
||||||
_:
|
|
||||||
scope: ASGI scope
|
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:
|
try:
|
||||||
if session is not None and message["type"] == "http.response.start":
|
if session is not None and message["type"] == "http.response.start":
|
||||||
if 200 <= message["status"] < 300:
|
if 200 <= message["status"] < 300:
|
||||||
@ -141,7 +132,7 @@ async def before_send_handler(message: Message, scope: Scope) -> None:
|
|||||||
|
|
||||||
|
|
||||||
config = SQLAlchemyAsyncConfig(
|
config = SQLAlchemyAsyncConfig(
|
||||||
session_dependency_key=settings.DB_SESSION_DEPENDENCY_KEY,
|
session_dependency_key=settings.api.DB_SESSION_DEPENDENCY_KEY,
|
||||||
engine_instance=engine,
|
engine_instance=engine,
|
||||||
session_maker=async_session_factory,
|
session_maker=async_session_factory,
|
||||||
before_send_handler=before_send_handler,
|
before_send_handler=before_send_handler,
|
||||||
|
|||||||
17
main.py
17
main.py
@ -1,5 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
from litestar import Litestar
|
from litestar import Litestar
|
||||||
from litestar.contrib.repository.exceptions import (
|
from litestar.contrib.repository.exceptions import (
|
||||||
RepositoryError as RepositoryException,
|
RepositoryError as RepositoryException,
|
||||||
@ -7,10 +8,13 @@ from litestar.contrib.repository.exceptions import (
|
|||||||
from litestar.openapi import OpenAPIConfig
|
from litestar.openapi import OpenAPIConfig
|
||||||
|
|
||||||
from app.controllers import create_router
|
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
|
from app.lib.service import ServiceError
|
||||||
|
|
||||||
|
|
||||||
def create_app(**kwargs: Any) -> Litestar:
|
def create_app(**kwargs: Any) -> Litestar:
|
||||||
|
kwargs.setdefault("debug", settings.app.DEBUG)
|
||||||
|
|
||||||
return Litestar(
|
return Litestar(
|
||||||
route_handlers=[create_router()],
|
route_handlers=[create_router()],
|
||||||
openapi_config=OpenAPIConfig(title="My API", version="1.0.0"),
|
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]
|
RepositoryException: exceptions.repository_exception_to_http_response, # type: ignore[dict-item]
|
||||||
ServiceError: exceptions.service_exception_to_http_response, # type: ignore[dict-item]
|
ServiceError: exceptions.service_exception_to_http_response, # type: ignore[dict-item]
|
||||||
},
|
},
|
||||||
debug=True,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
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]
|
[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 = "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]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.4.2"
|
version = "7.4.2"
|
||||||
@ -698,6 +713,20 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
six = ">=1.5"
|
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]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.1"
|
version = "6.0.1"
|
||||||
@ -913,4 +942,4 @@ anyio = ">=3.0.0"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "3319e3383cccbf806026c4983b44d301eb3106d9afc729328a5adfce7332ef8e"
|
content-hash = "28c7f9f4b78e7d602b076108d3138c854cd1a1bc22c447987a6aedbba6300dcd"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ sqlalchemy = "^2.0.20"
|
|||||||
uvicorn = "^0.23.2"
|
uvicorn = "^0.23.2"
|
||||||
asyncpg = "^0.28.0"
|
asyncpg = "^0.28.0"
|
||||||
pydantic = "^2.3.0"
|
pydantic = "^2.3.0"
|
||||||
|
pydantic-settings = "^2.0.3"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|||||||
Reference in New Issue
Block a user