From 4eb6ecdaecbd6b8af7b8809ddc2d4a911f73da43 Mon Sep 17 00:00:00 2001 From: Javid Ahmed Date: Tue, 5 May 2026 22:40:00 +0100 Subject: [PATCH 1/5] Add DatabaseConfig model and related tests for database configuration management --- python_template_server/models.py | 13 +++++++++++++ tests/conftest.py | 23 +++++++++++++++++++++++ tests/test_models.py | 17 +++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/python_template_server/models.py b/python_template_server/models.py index a7de323..dbc4132 100644 --- a/python_template_server/models.py +++ b/python_template_server/models.py @@ -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.""" @@ -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. diff --git a/tests/conftest.py b/tests/conftest.py index 226ef26..34e8d63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from python_template_server.models import ( CertificateConfigModel, CORSConfigModel, + DatabaseConfig, JSONResponseConfigModel, RateLimitConfigModel, SecurityConfigModel, @@ -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: @@ -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.""" @@ -150,6 +165,12 @@ 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, @@ -157,6 +178,7 @@ def mock_template_server_config( 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( @@ -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, ) diff --git a/tests/test_models.py b/tests/test_models.py index 923f253..2775487 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -10,6 +10,7 @@ CertificateConfigModel, CORSConfigModel, CustomJSONResponse, + DatabaseConfig, GetHealthResponse, GetLoginResponse, JSONResponseConfigModel, @@ -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.""" @@ -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 = { @@ -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 From 13137b968968c0c851f4f459e095996066755968 Mon Sep 17 00:00:00 2001 From: Javid Ahmed Date: Tue, 5 May 2026 22:46:24 +0100 Subject: [PATCH 2/5] Add sqlmodel and greenlet dependencies to project --- pyproject.toml | 1 + uv.lock | 88 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 234742c..11028db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/uv.lock b/uv.lock index 335aeda..cc92968 100644 --- a/uv.lock +++ b/uv.lock @@ -479,6 +479,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, ] +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1157,6 +1188,7 @@ dependencies = [ { name = "python-dotenv" }, { name = "python-multipart" }, { name = "slowapi" }, + { name = "sqlmodel" }, { name = "template-python" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1182,8 +1214,9 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.2.0" }, { name = "pytest-env", marker = "extra == 'dev'", specifier = ">=1.6.0" }, { name = "python-dotenv", specifier = ">=1.2.2" }, - { name = "python-multipart", specifier = ">=0.0.26" }, + { name = "python-multipart", specifier = ">=0.0.27" }, { name = "slowapi", specifier = ">=0.1.9" }, + { name = "sqlmodel", specifier = ">=0.0.38" }, { name = "template-python", git = "https://github.com/javidahmed64592/template-python.git" }, { name = "template-python", extras = ["dev"], marker = "extra == 'dev'", git = "https://github.com/javidahmed64592/template-python.git" }, { name = "template-python", extras = ["docs"], marker = "extra == 'docs'", git = "https://github.com/javidahmed64592/template-python.git" }, @@ -1449,6 +1482,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/0d/26ec1329960ea9430131fe63f63a95ea4cb8971d49c891ff7e1f3255421c/sqlmodel-0.0.38.tar.gz", hash = "sha256:d583ec237b14103809f74e8630032bc40ab68cd6b754a610f0813c56911a547b", size = 86710, upload-time = "2026-04-02T21:03:55.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/c7/10c60af0607ab6fa136264f7f39d205932218516226d38585324ffda705d/sqlmodel-0.0.38-py3-none-any.whl", hash = "sha256:84e3fa990a77395461ded72a6c73173438ce8449d5c1c4d97fbff1b1df692649", size = 27294, upload-time = "2026-04-02T21:03:56.406Z" }, +] + [[package]] name = "starlette" version = "1.0.0" From 273f4436cd2f67cd4b5932fe603ec32f75187065 Mon Sep 17 00:00:00 2001 From: Javid Ahmed Date: Tue, 5 May 2026 22:46:35 +0100 Subject: [PATCH 3/5] Add base database manager and unit tests for database operations --- python_template_server/db/__init__.py | 1 + .../db/base_database_manager.py | 28 +++++++++++++ tests/db/test_base_database_manager.py | 40 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 python_template_server/db/__init__.py create mode 100644 python_template_server/db/base_database_manager.py create mode 100644 tests/db/test_base_database_manager.py diff --git a/python_template_server/db/__init__.py b/python_template_server/db/__init__.py new file mode 100644 index 0000000..275b2e5 --- /dev/null +++ b/python_template_server/db/__init__.py @@ -0,0 +1 @@ +"""Database manager classes for servers using this template.""" diff --git a/python_template_server/db/base_database_manager.py b/python_template_server/db/base_database_manager.py new file mode 100644 index 0000000..a390115 --- /dev/null +++ b/python_template_server/db/base_database_manager.py @@ -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.""" diff --git a/tests/db/test_base_database_manager.py b/tests/db/test_base_database_manager.py new file mode 100644 index 0000000..4b3d986 --- /dev/null +++ b/tests/db/test_base_database_manager.py @@ -0,0 +1,40 @@ +"""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 + + +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("test.db") + + +@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("test.db") + assert mock_database_manager.db_url == expected_url From e1fb5f644feea2e2845b7da3a1722dd2718666e1 Mon Sep 17 00:00:00 2001 From: Javid Ahmed Date: Tue, 5 May 2026 22:46:44 +0100 Subject: [PATCH 4/5] Fix type hint for mock_package_metadata and mock_template_server in unit tests --- tests/test_template_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_template_server.py b/tests/test_template_server.py index 91e6212..c6e8d58 100644 --- a/tests/test_template_server.py +++ b/tests/test_template_server.py @@ -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) @@ -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), From 7b7d861f50fc3b93fa6e54f1b57b01c4a7535f2c Mon Sep 17 00:00:00 2001 From: Javid Ahmed Date: Tue, 5 May 2026 22:47:06 +0100 Subject: [PATCH 5/5] Refactor mock database URL handling in tests for consistency --- tests/db/test_base_database_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/db/test_base_database_manager.py b/tests/db/test_base_database_manager.py index 4b3d986..f678062 100644 --- a/tests/db/test_base_database_manager.py +++ b/tests/db/test_base_database_manager.py @@ -8,6 +8,8 @@ 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.""" @@ -15,7 +17,7 @@ class MockDatabaseManager(BaseDatabaseManager): @property def db_url(self) -> str: """Return a mock database URL.""" - return self.db_config.db_url("test.db") + return self.db_config.db_url(MOCK_DB_FILENAME) @pytest.fixture @@ -36,5 +38,5 @@ def test_initialization(self, mock_database_manager: BaseDatabaseManager) -> Non 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("test.db") + expected_url = mock_database_manager.db_config.db_url(MOCK_DB_FILENAME) assert mock_database_manager.db_url == expected_url