Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies = [
"python-dotenv>=1.2.2",
"python-multipart>=0.0.27",
"slowapi>=0.1.9",
"sqlmodel>=0.0.38",
"template-python @ git+https://github.com/javidahmed64592/template-python.git",
"uvicorn[standard]>=0.46.0",
]
Expand Down
1 change: 1 addition & 0 deletions python_template_server/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database manager classes for servers using this template."""
28 changes: 28 additions & 0 deletions python_template_server/db/base_database_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""SQLModel database module."""

import logging
from abc import ABC, abstractmethod

from sqlmodel import SQLModel, create_engine

from python_template_server.models import DatabaseConfig

logger = logging.getLogger(__name__)


class BaseDatabaseManager(ABC):
"""Manager class for database operations."""

def __init__(self, db_config: DatabaseConfig) -> None:
"""Initialize the database manager."""
self.db_config = db_config
self.db_config.db_directory.mkdir(parents=True, exist_ok=True)

logger.info("Initializing database with URL: %s", self.db_url)
self.engine = create_engine(self.db_url, echo=False)
SQLModel.metadata.create_all(self.engine)

@property
@abstractmethod
def db_url(self) -> str:
"""Get the database URL."""
13 changes: 13 additions & 0 deletions python_template_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ class JSONResponseConfigModel(BaseModel):
media_type: str = Field(default="application/json; charset=utf-8", description="Media type for JSON responses")


class DatabaseConfig(BaseModel):
"""Configuration for the database."""

db_directory: Path = Field(
default=Path("data"), description="The directory where the SQLite database files will be stored."
)

def db_url(self, filename: str) -> str:
"""Construct the database URL for SQLAlchemy."""
return f"sqlite:///{self.db_directory}/{filename}"


class TemplateServerConfig(BaseModel):
"""Template server configuration."""

Expand All @@ -84,6 +96,7 @@ class TemplateServerConfig(BaseModel):
rate_limit: RateLimitConfigModel = Field(default_factory=RateLimitConfigModel)
certificate: CertificateConfigModel = Field(default_factory=CertificateConfigModel)
json_response: JSONResponseConfigModel = Field(default_factory=JSONResponseConfigModel)
db: DatabaseConfig = Field(default_factory=DatabaseConfig, description="Database configuration")

def save_to_file(self, filepath: Path) -> None:
"""Save the configuration to a JSON file.
Expand Down
23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from python_template_server.models import (
CertificateConfigModel,
CORSConfigModel,
DatabaseConfig,
JSONResponseConfigModel,
RateLimitConfigModel,
SecurityConfigModel,
Expand Down Expand Up @@ -64,6 +65,12 @@ def mock_tmp_static_path(tmp_path: Path) -> Path:
return tmp_path / "static"


@pytest.fixture
def mock_tmp_db_path(tmp_path: Path) -> Path:
"""Provide a temporary database directory path."""
return tmp_path / "data"


# Template Server Configuration Models
@pytest.fixture
def mock_security_config_dict() -> dict:
Expand Down Expand Up @@ -120,6 +127,14 @@ def mock_json_response_config_dict() -> dict:
}


@pytest.fixture
def mock_db_config_dict(mock_tmp_db_path: Path) -> dict:
"""Provide a mock database configuration dictionary."""
return {
"db_directory": mock_tmp_db_path,
}


@pytest.fixture
def mock_security_config(mock_security_config_dict: dict) -> SecurityConfigModel:
"""Provide a mock SecurityConfigModel instance."""
Expand Down Expand Up @@ -150,13 +165,20 @@ def mock_json_response_config(mock_json_response_config_dict: dict) -> JSONRespo
return JSONResponseConfigModel.model_validate(mock_json_response_config_dict)


@pytest.fixture
def mock_db_config(mock_db_config_dict: dict) -> DatabaseConfig:
"""Provide a mock DatabaseConfig instance."""
return DatabaseConfig.model_validate(mock_db_config_dict)


@pytest.fixture
def mock_template_server_config(
mock_security_config: SecurityConfigModel,
mock_cors_config: CORSConfigModel,
mock_rate_limit_config: RateLimitConfigModel,
mock_certificate_config: CertificateConfigModel,
mock_json_response_config: JSONResponseConfigModel,
mock_db_config: DatabaseConfig,
) -> TemplateServerConfig:
"""Provide a mock TemplateServerConfig instance."""
return TemplateServerConfig(
Expand All @@ -165,4 +187,5 @@ def mock_template_server_config(
rate_limit=mock_rate_limit_config,
certificate=mock_certificate_config,
json_response=mock_json_response_config,
db=mock_db_config,
)
42 changes: 42 additions & 0 deletions tests/db/test_base_database_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Unit tests for the python_template_server.db.base_database_manager module."""

from collections.abc import Generator

import pytest
from sqlalchemy import Engine

from python_template_server.db.base_database_manager import BaseDatabaseManager
from python_template_server.models import DatabaseConfig

MOCK_DB_FILENAME = "test.db"


class MockDatabaseManager(BaseDatabaseManager):
"""Mock implementation of BaseDatabaseManager for testing."""

@property
def db_url(self) -> str:
"""Return a mock database URL."""
return self.db_config.db_url(MOCK_DB_FILENAME)


@pytest.fixture
def mock_database_manager(mock_db_config: DatabaseConfig) -> Generator[BaseDatabaseManager]:
"""Fixture for creating a mock database manager."""
db_manager = MockDatabaseManager(db_config=mock_db_config)
yield db_manager
db_manager.engine.dispose()


class TestBaseDatabaseManager:
"""Unit tests for the BaseDatabaseManager class."""

def test_initialization(self, mock_database_manager: BaseDatabaseManager) -> None:
"""Test that the database manager initializes correctly."""
assert isinstance(mock_database_manager.db_config, DatabaseConfig)
assert isinstance(mock_database_manager.engine, Engine)

def test_db_url_property(self, mock_database_manager: BaseDatabaseManager) -> None:
"""Test that the db_url property returns the correct URL."""
expected_url = mock_database_manager.db_config.db_url(MOCK_DB_FILENAME)
assert mock_database_manager.db_url == expected_url
17 changes: 17 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
CertificateConfigModel,
CORSConfigModel,
CustomJSONResponse,
DatabaseConfig,
GetHealthResponse,
GetLoginResponse,
JSONResponseConfigModel,
Expand Down Expand Up @@ -83,6 +84,20 @@ def test_model_dump(
assert mock_json_response_config.model_dump() == mock_json_response_config_dict


class TestDatabaseConfig:
"""Unit tests for the DatabaseConfig class."""

def test_model_dump(self, mock_db_config_dict: dict, mock_db_config: DatabaseConfig) -> None:
"""Test the model_dump method."""
assert mock_db_config.model_dump() == mock_db_config_dict

def test_db_url_method(self, mock_db_config: DatabaseConfig) -> None:
"""Test the db_url method."""
filename = "test.db"
expected_url = f"sqlite:///{mock_db_config.db_directory}/{filename}"
assert mock_db_config.db_url(filename) == expected_url


class TestTemplateServerConfig:
"""Unit tests for the TemplateServerConfig class."""

Expand All @@ -94,6 +109,7 @@ def test_model_dump(
mock_rate_limit_config_dict: dict,
mock_certificate_config_dict: dict,
mock_json_response_config_dict: dict,
mock_db_config_dict: dict,
) -> None:
"""Test the model_dump method."""
expected_dict = {
Expand All @@ -102,6 +118,7 @@ def test_model_dump(
"rate_limit": mock_rate_limit_config_dict,
"certificate": mock_certificate_config_dict,
"json_response": mock_json_response_config_dict,
"db": mock_db_config_dict,
}
assert mock_template_server_config.model_dump() == expected_dict

Expand Down
4 changes: 2 additions & 2 deletions tests/test_template_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


@pytest.fixture(autouse=True)
def mock_package_metadata() -> Generator[MagicMock]:
def mock_package_metadata() -> Generator[PackageMetadata]:
"""Mock importlib.metadata.metadata to return a mock PackageMetadata."""
with patch("python_template_server.template_server.metadata") as mock_metadata:
mock_pkg_metadata = MagicMock(spec=PackageMetadata)
Expand Down Expand Up @@ -61,7 +61,7 @@ def mock_verify_token() -> Generator[MagicMock]:
@pytest.fixture
def mock_template_server(
mock_template_server_config: TemplateServerConfig, mock_tmp_config_path: Path, mock_tmp_static_path: Path
) -> Generator[MockTemplateServer]:
) -> Generator[TemplateServer]:
"""Provide a MockTemplateServer instance for testing."""
with (
patch("python_template_server.template_server.CertificateHandler", return_value=MagicMock(), autospec=True),
Expand Down
Loading
Loading