zodiac-core 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. zodiac_core-0.2.0/LICENSE +21 -0
  2. zodiac_core-0.2.0/PKG-INFO +127 -0
  3. zodiac_core-0.2.0/README.md +96 -0
  4. zodiac_core-0.2.0/pyproject.toml +120 -0
  5. zodiac_core-0.2.0/setup.cfg +4 -0
  6. zodiac_core-0.2.0/tests/test_build.py +71 -0
  7. zodiac_core-0.2.0/tests/test_config.py +146 -0
  8. zodiac_core-0.2.0/tests/test_exception_handlers.py +141 -0
  9. zodiac_core-0.2.0/tests/test_exceptions.py +55 -0
  10. zodiac_core-0.2.0/tests/test_http.py +85 -0
  11. zodiac_core-0.2.0/tests/test_logging.py +101 -0
  12. zodiac_core-0.2.0/tests/test_middleware.py +120 -0
  13. zodiac_core-0.2.0/tests/test_pagination.py +94 -0
  14. zodiac_core-0.2.0/tests/test_routing.py +173 -0
  15. zodiac_core-0.2.0/tests/test_schemas_live.py +51 -0
  16. zodiac_core-0.2.0/zodiac/__init__.py +1 -0
  17. zodiac_core-0.2.0/zodiac/commands/__init__.py +3 -0
  18. zodiac_core-0.2.0/zodiac/commands/new.py +81 -0
  19. zodiac_core-0.2.0/zodiac/main.py +26 -0
  20. zodiac_core-0.2.0/zodiac/templates/standard-3tier/README.md.jinja +70 -0
  21. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/__init__.py.jinja +0 -0
  22. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/__init__.py.jinja +0 -0
  23. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/router.py.jinja +17 -0
  24. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/routers/__init__.py.jinja +0 -0
  25. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/routers/item_router.py.jinja +36 -0
  26. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/schemas/__init__.py.jinja +0 -0
  27. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/schemas/item_schema.py.jinja +12 -0
  28. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/__init__.py.jinja +0 -0
  29. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/__init__.py.jinja +0 -0
  30. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/github_service.py.jinja +16 -0
  31. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/item_service.py.jinja +24 -0
  32. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/core/__init__.py.jinja +0 -0
  33. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/core/container.py.jinja +56 -0
  34. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/__init__.py.jinja +0 -0
  35. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/__init__.py.jinja +0 -0
  36. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/models/__init__.py.jinja +0 -0
  37. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/models/item_model.py.jinja +9 -0
  38. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/repositories/__init__.py.jinja +0 -0
  39. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/repositories/item_repository.py.jinja +19 -0
  40. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/external/__init__.py.jinja +0 -0
  41. zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/external/github_client.py.jinja +22 -0
  42. zodiac_core-0.2.0/zodiac/templates/standard-3tier/config/app.develop.ini.jinja +3 -0
  43. zodiac_core-0.2.0/zodiac/templates/standard-3tier/config/app.ini.jinja +3 -0
  44. zodiac_core-0.2.0/zodiac/templates/standard-3tier/main.py.jinja +49 -0
  45. zodiac_core-0.2.0/zodiac/templates/standard-3tier/pyproject.toml.jinja +48 -0
  46. zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/__init__.py.jinja +2 -0
  47. zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/conftest.py.jinja +11 -0
  48. zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/test_health.py.jinja +9 -0
  49. zodiac_core-0.2.0/zodiac_core/__init__.py +104 -0
  50. zodiac_core-0.2.0/zodiac_core/config.py +192 -0
  51. zodiac_core-0.2.0/zodiac_core/context.py +34 -0
  52. zodiac_core-0.2.0/zodiac_core/db/__init__.py +38 -0
  53. zodiac_core-0.2.0/zodiac_core/db/repository.py +125 -0
  54. zodiac_core-0.2.0/zodiac_core/db/session.py +282 -0
  55. zodiac_core-0.2.0/zodiac_core/db/sql.py +113 -0
  56. zodiac_core-0.2.0/zodiac_core/exception_handlers.py +82 -0
  57. zodiac_core-0.2.0/zodiac_core/exceptions.py +51 -0
  58. zodiac_core-0.2.0/zodiac_core/http.py +79 -0
  59. zodiac_core-0.2.0/zodiac_core/logging.py +106 -0
  60. zodiac_core-0.2.0/zodiac_core/middleware.py +88 -0
  61. zodiac_core-0.2.0/zodiac_core/pagination.py +74 -0
  62. zodiac_core-0.2.0/zodiac_core/response.py +123 -0
  63. zodiac_core-0.2.0/zodiac_core/routing.py +110 -0
  64. zodiac_core-0.2.0/zodiac_core/schemas.py +81 -0
  65. zodiac_core-0.2.0/zodiac_core.egg-info/PKG-INFO +127 -0
  66. zodiac_core-0.2.0/zodiac_core.egg-info/SOURCES.txt +68 -0
  67. zodiac_core-0.2.0/zodiac_core.egg-info/dependency_links.txt +1 -0
  68. zodiac_core-0.2.0/zodiac_core.egg-info/entry_points.txt +2 -0
  69. zodiac_core-0.2.0/zodiac_core.egg-info/requires.txt +14 -0
  70. zodiac_core-0.2.0/zodiac_core.egg-info/top_level.txt +2 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Legolas Bloom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: zodiac-core
3
+ Version: 0.2.0
4
+ Summary: ZodiacCore-Py: A high-performance core library for modern Python web services.
5
+ License-Expression: MIT
6
+ Keywords: fastapi,async,web,logging,middleware,sqlalchemy,pydantic
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Framework :: FastAPI
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
16
+ Requires-Python: <3.15,>=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: fastapi>=0.128.0
20
+ Requires-Dist: pydantic>=2.12.5
21
+ Requires-Dist: loguru>=0.7.2
22
+ Requires-Dist: httpx>=0.28.1
23
+ Provides-Extra: zodiac
24
+ Requires-Dist: click>=8.0; extra == "zodiac"
25
+ Requires-Dist: jinja2>=3.0; extra == "zodiac"
26
+ Provides-Extra: sql
27
+ Requires-Dist: sqlmodel>=0.0.31; extra == "sql"
28
+ Provides-Extra: mongo
29
+ Requires-Dist: motor>=3.3.0; extra == "mongo"
30
+ Dynamic: license-file
31
+
32
+ # ZodiacCore-Py
33
+
34
+ <p align="center">
35
+ <img src="https://img.shields.io/badge/Python-3.12+-blue.svg?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.12+">
36
+ <img src="https://img.shields.io/badge/FastAPI-0.109+-009688.svg?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI">
37
+ <img src="https://img.shields.io/badge/Pydantic-v2-e92063.svg?style=for-the-badge&logo=pydantic&logoColor=white" alt="Pydantic v2">
38
+ <img src="https://img.shields.io/badge/Async-First-purple.svg?style=for-the-badge" alt="Async First">
39
+ <img src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" alt="MIT License">
40
+ </p>
41
+
42
+ > **The opinionated, async-first core library for modern Python web services.**
43
+
44
+ ## 🎯 Mission
45
+
46
+ **Stop copy-pasting your infrastructure code.**
47
+
48
+ Every new FastAPI project starts the same way: setting up logging, error handling, database sessions, pagination... It's tedious, error-prone, and inconsistent across teams.
49
+
50
+ **ZodiacCore** solves this in two ways: a **library** you drop into any FastAPI app, and a **CLI** that scaffolds a full project so you can start coding in seconds.
51
+
52
+ ## ✨ Key Features
53
+
54
+ * **🔍 Observability First**: Built-in JSON structured logging with **Trace ID** injection across the entire request lifecycle (Middleware → Context → Log).
55
+ * **🛡️ Robust Error Handling**: Centralized exception handlers that map `ZodiacException` to standard HTTP 4xx/5xx JSON responses.
56
+ * **💾 Database Abstraction**: Async SQLAlchemy session management and `BaseSQLRepository` with pagination helpers (`paginate_query`).
57
+ * **🎁 Standard Response Wrapper**: Automatic wrapping of API responses into `code` / `data` / `message` via `APIRouter`.
58
+ * **📄 Standard Pagination**: `PageParams` and `PagedResponse[T]` with repository integration.
59
+ * **⚡ Async Ready**: Python 3.12+ async/await from the ground up.
60
+ * **⌨️ zodiac CLI**: Scaffold a 3-tier FastAPI project (DI, routers, config) with one command.
61
+
62
+ ## 📦 Quick Install
63
+
64
+ | Use case | Install |
65
+ |----------|--------|
66
+ | **Library only** (use in your app) | `uv add zodiac-core` |
67
+ | **Library + CLI** (scaffold new projects) | `uv add "zodiac-core[zodiac]"` |
68
+
69
+ Extras (combinable): `zodiac-core[sql]` (SQLModel), `zodiac-core[mongo]` (Motor, helpers planned), `zodiac-core[zodiac]` (CLI). See the [Installation Guide](https://ttwshell.github.io/ZodiacCore-Py/user-guide/installation/) for details.
70
+
71
+ ---
72
+
73
+ ## 🚀 Two ways to use ZodiacCore
74
+
75
+ ### 1. Scaffolding (fastest start)
76
+
77
+ Use the **zodiac** CLI to generate a full project: 3-tier architecture, dependency injection, config, and tests.
78
+
79
+ ```bash
80
+ uv add "zodiac-core[zodiac]"
81
+ zodiac new my_app --tpl standard-3tier -o ./projects
82
+ cd projects/my_app
83
+ uv sync --extra dev && uv run fastapi run --reload
84
+ ```
85
+
86
+ Open `http://127.0.0.1:8000/docs` and `http://127.0.0.1:8000/api/v1/health`. See [Getting started](https://ttwshell.github.io/ZodiacCore-Py/user-guide/getting-started/) and [CLI docs](https://ttwshell.github.io/ZodiacCore-Py/user-guide/cli/).
87
+
88
+ ### 2. Library (use in your own app)
89
+
90
+ Add **zodiac-core** to an existing FastAPI project and wire up logging, middleware, and response wrapping.
91
+
92
+ ```python
93
+ from fastapi import FastAPI
94
+ from zodiac_core.routing import APIRouter
95
+ from zodiac_core.logging import setup_loguru
96
+ from zodiac_core.middleware import register_middleware
97
+ from zodiac_core.exception_handlers import register_exception_handlers
98
+ from zodiac_core.exceptions import NotFoundException
99
+ from loguru import logger
100
+
101
+ setup_loguru(level="INFO", json_format=True)
102
+ app = FastAPI()
103
+ register_middleware(app)
104
+ register_exception_handlers(app)
105
+
106
+ router = APIRouter()
107
+ @router.get("/items/{item_id}")
108
+ async def read_item(item_id: int):
109
+ logger.info(f"request: item_id={item_id}")
110
+ if item_id == 0:
111
+ raise NotFoundException(message="Item not found")
112
+ return {"item_id": item_id}
113
+ app.include_router(router)
114
+ ```
115
+
116
+ ## 📚 Documentation
117
+
118
+ - **Online**: [https://ttwshell.github.io/ZodiacCore-Py/](https://ttwshell.github.io/ZodiacCore-Py/) (multiple versions via release).
119
+ - **Local**: `make docs-serve` (sources in `docs/`).
120
+
121
+ ## 🤝 Contributing
122
+
123
+ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and workflow.
124
+
125
+ ## 📄 License
126
+
127
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,96 @@
1
+ # ZodiacCore-Py
2
+
3
+ <p align="center">
4
+ <img src="https://img.shields.io/badge/Python-3.12+-blue.svg?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.12+">
5
+ <img src="https://img.shields.io/badge/FastAPI-0.109+-009688.svg?style=for-the-badge&logo=fastapi&logoColor=white" alt="FastAPI">
6
+ <img src="https://img.shields.io/badge/Pydantic-v2-e92063.svg?style=for-the-badge&logo=pydantic&logoColor=white" alt="Pydantic v2">
7
+ <img src="https://img.shields.io/badge/Async-First-purple.svg?style=for-the-badge" alt="Async First">
8
+ <img src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" alt="MIT License">
9
+ </p>
10
+
11
+ > **The opinionated, async-first core library for modern Python web services.**
12
+
13
+ ## 🎯 Mission
14
+
15
+ **Stop copy-pasting your infrastructure code.**
16
+
17
+ Every new FastAPI project starts the same way: setting up logging, error handling, database sessions, pagination... It's tedious, error-prone, and inconsistent across teams.
18
+
19
+ **ZodiacCore** solves this in two ways: a **library** you drop into any FastAPI app, and a **CLI** that scaffolds a full project so you can start coding in seconds.
20
+
21
+ ## ✨ Key Features
22
+
23
+ * **🔍 Observability First**: Built-in JSON structured logging with **Trace ID** injection across the entire request lifecycle (Middleware → Context → Log).
24
+ * **🛡️ Robust Error Handling**: Centralized exception handlers that map `ZodiacException` to standard HTTP 4xx/5xx JSON responses.
25
+ * **💾 Database Abstraction**: Async SQLAlchemy session management and `BaseSQLRepository` with pagination helpers (`paginate_query`).
26
+ * **🎁 Standard Response Wrapper**: Automatic wrapping of API responses into `code` / `data` / `message` via `APIRouter`.
27
+ * **📄 Standard Pagination**: `PageParams` and `PagedResponse[T]` with repository integration.
28
+ * **⚡ Async Ready**: Python 3.12+ async/await from the ground up.
29
+ * **⌨️ zodiac CLI**: Scaffold a 3-tier FastAPI project (DI, routers, config) with one command.
30
+
31
+ ## 📦 Quick Install
32
+
33
+ | Use case | Install |
34
+ |----------|--------|
35
+ | **Library only** (use in your app) | `uv add zodiac-core` |
36
+ | **Library + CLI** (scaffold new projects) | `uv add "zodiac-core[zodiac]"` |
37
+
38
+ Extras (combinable): `zodiac-core[sql]` (SQLModel), `zodiac-core[mongo]` (Motor, helpers planned), `zodiac-core[zodiac]` (CLI). See the [Installation Guide](https://ttwshell.github.io/ZodiacCore-Py/user-guide/installation/) for details.
39
+
40
+ ---
41
+
42
+ ## 🚀 Two ways to use ZodiacCore
43
+
44
+ ### 1. Scaffolding (fastest start)
45
+
46
+ Use the **zodiac** CLI to generate a full project: 3-tier architecture, dependency injection, config, and tests.
47
+
48
+ ```bash
49
+ uv add "zodiac-core[zodiac]"
50
+ zodiac new my_app --tpl standard-3tier -o ./projects
51
+ cd projects/my_app
52
+ uv sync --extra dev && uv run fastapi run --reload
53
+ ```
54
+
55
+ Open `http://127.0.0.1:8000/docs` and `http://127.0.0.1:8000/api/v1/health`. See [Getting started](https://ttwshell.github.io/ZodiacCore-Py/user-guide/getting-started/) and [CLI docs](https://ttwshell.github.io/ZodiacCore-Py/user-guide/cli/).
56
+
57
+ ### 2. Library (use in your own app)
58
+
59
+ Add **zodiac-core** to an existing FastAPI project and wire up logging, middleware, and response wrapping.
60
+
61
+ ```python
62
+ from fastapi import FastAPI
63
+ from zodiac_core.routing import APIRouter
64
+ from zodiac_core.logging import setup_loguru
65
+ from zodiac_core.middleware import register_middleware
66
+ from zodiac_core.exception_handlers import register_exception_handlers
67
+ from zodiac_core.exceptions import NotFoundException
68
+ from loguru import logger
69
+
70
+ setup_loguru(level="INFO", json_format=True)
71
+ app = FastAPI()
72
+ register_middleware(app)
73
+ register_exception_handlers(app)
74
+
75
+ router = APIRouter()
76
+ @router.get("/items/{item_id}")
77
+ async def read_item(item_id: int):
78
+ logger.info(f"request: item_id={item_id}")
79
+ if item_id == 0:
80
+ raise NotFoundException(message="Item not found")
81
+ return {"item_id": item_id}
82
+ app.include_router(router)
83
+ ```
84
+
85
+ ## 📚 Documentation
86
+
87
+ - **Online**: [https://ttwshell.github.io/ZodiacCore-Py/](https://ttwshell.github.io/ZodiacCore-Py/) (multiple versions via release).
88
+ - **Local**: `make docs-serve` (sources in `docs/`).
89
+
90
+ ## 🤝 Contributing
91
+
92
+ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and workflow.
93
+
94
+ ## 📄 License
95
+
96
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,120 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zodiac-core"
7
+ version = "0.2.0"
8
+ description = "ZodiacCore-Py: A high-performance core library for modern Python web services."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.12,<3.15"
12
+ keywords = [
13
+ "fastapi",
14
+ "async",
15
+ "web",
16
+ "logging",
17
+ "middleware",
18
+ "sqlalchemy",
19
+ "pydantic",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Framework :: FastAPI",
24
+ "Intended Audience :: Developers",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
31
+ ]
32
+ dependencies = [
33
+ "fastapi>=0.128.0",
34
+ "pydantic>=2.12.5",
35
+ "loguru>=0.7.2",
36
+ "httpx>=0.28.1",
37
+ ]
38
+
39
+ [project.optional-dependencies]
40
+ zodiac = ["click>=8.0", "jinja2>=3.0"]
41
+ sql = ["sqlmodel>=0.0.31"]
42
+ mongo = ["motor>=3.3.0"]
43
+
44
+ [project.scripts]
45
+ zodiac = "zodiac.main:main"
46
+
47
+ [tool.setuptools.packages.find]
48
+ include = ["zodiac*", "zodiac_core*"]
49
+ exclude = ["tests*", "benchmarks*"]
50
+
51
+ [tool.setuptools.package-data]
52
+ zodiac = ["templates/**/*.jinja"]
53
+
54
+ [tool.ruff]
55
+ line-length = 120
56
+ target-version = "py312"
57
+ extend-exclude = [".venv", ".tox", "demo*"]
58
+
59
+ [tool.ruff.lint]
60
+ select = ["E", "F", "I", "B"]
61
+ ignore = []
62
+
63
+ [tool.ruff.format]
64
+ quote-style = "double"
65
+ indent-style = "space"
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests", "benchmarks"]
69
+ addopts = """
70
+ --cov=zodiac_core
71
+ --cov=zodiac
72
+ --cov-report=term-missing
73
+ --no-cov-on-fail
74
+ -s -vv
75
+ -p no:warnings
76
+ --benchmark-warmup=on
77
+ --benchmark-warmup-iterations=10
78
+ --benchmark-min-rounds=5
79
+ """
80
+ log_cli = true
81
+ markers = ["serial: marks tests that must run serially (e.g., database tests)"]
82
+
83
+ [tool.coverage.run]
84
+ source = ["zodiac_core", "zodiac"]
85
+ omit = [
86
+ "*/tests/*",
87
+ "*/test_*.py",
88
+ "*/__pycache__/*",
89
+ "*/.*",
90
+ "*/benchmarks/*",
91
+ "*/templates/*",
92
+ "*/*.jinja",
93
+ ]
94
+
95
+ [dependency-groups]
96
+ dev = [
97
+ "ruff>=0.9.3",
98
+ "ipython>=9.9.0",
99
+ "pytest>=9.0.2",
100
+ "pytest-asyncio>=1.3.0",
101
+ "pytest-cov>=7.0.0",
102
+ "pytest-env>=1.2.0",
103
+ "pytest-benchmark>=5.1.0",
104
+ "py-cpuinfo>=9.0.0",
105
+ "respx>=0.22.0",
106
+ "psycopg2-binary>=2.9.9",
107
+ "pymysql>=1.1.0",
108
+ "aiomysql>=0.2.0",
109
+ "cryptography>=42.0.0",
110
+ "asyncpg>=0.31.0",
111
+ "aiosqlite>=0.22.1",
112
+ "uvicorn>=0.40.0",
113
+ ]
114
+
115
+ docs = [
116
+ "mkdocs>=1.6.0",
117
+ "mkdocs-material>=9.5.0",
118
+ "mkdocstrings[python]>=0.24.0",
119
+ "mike>=2.0.0",
120
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,71 @@
1
+ """Tests for verifying package build includes all required files."""
2
+
3
+ import subprocess
4
+ import zipfile
5
+ from pathlib import Path
6
+
7
+
8
+ class TestPackageBuild:
9
+ """Tests for verifying package build completeness."""
10
+
11
+ # Manually verified expected file counts
12
+ EXPECTED_ZODIAC_FILES = 33 # Python files + template files (.jinja)
13
+ EXPECTED_ZODIAC_CORE_FILES = 16 # Python files only
14
+
15
+ def test_build_includes_all_files(self, tmp_path):
16
+ """Verify that built package includes all required files from zodiac and zodiac_core."""
17
+ project_root = Path(__file__).parent.parent
18
+
19
+ # 1. Build the package
20
+ dist_dir = tmp_path / "dist"
21
+ dist_dir.mkdir()
22
+
23
+ result = subprocess.run(
24
+ ["uv", "build", "--out-dir", str(dist_dir)],
25
+ cwd=project_root,
26
+ capture_output=True,
27
+ text=True,
28
+ )
29
+
30
+ assert result.returncode == 0, f"Build failed: {result.stderr}"
31
+
32
+ # 2. Find the built wheel
33
+ wheel_files = list(dist_dir.glob("*.whl"))
34
+ assert len(wheel_files) == 1, f"Expected 1 wheel file, found {len(wheel_files)}"
35
+
36
+ wheel_path = wheel_files[0]
37
+
38
+ # 3. Extract and count files in the wheel (separately for each package)
39
+ actual_zodiac_files = self._count_wheel_files(wheel_path, "zodiac")
40
+ actual_zodiac_core_files = self._count_wheel_files(wheel_path, "zodiac_core")
41
+
42
+ # 4. Verify counts match manually verified expectations
43
+ assert actual_zodiac_files == self.EXPECTED_ZODIAC_FILES, (
44
+ f"zodiac file count mismatch: "
45
+ f"expected={self.EXPECTED_ZODIAC_FILES} (manually verified), "
46
+ f"actual={actual_zodiac_files}"
47
+ )
48
+ assert actual_zodiac_core_files == self.EXPECTED_ZODIAC_CORE_FILES, (
49
+ f"zodiac_core file count mismatch: "
50
+ f"expected={self.EXPECTED_ZODIAC_CORE_FILES} (manually verified), "
51
+ f"actual={actual_zodiac_core_files}"
52
+ )
53
+
54
+ def _count_wheel_files(self, wheel_path: Path, package_name: str) -> int:
55
+ """Count files for a specific package in the wheel."""
56
+ count = 0
57
+ with zipfile.ZipFile(wheel_path, "r") as wheel:
58
+ for name in wheel.namelist():
59
+ # Count files in the package directory
60
+ if name.startswith(f"{package_name}/") and not name.endswith("/"):
61
+ # Exclude metadata files and dist-info
62
+ if not any(
63
+ excluded in name
64
+ for excluded in (
65
+ ".egg-info/",
66
+ ".dist-info/",
67
+ "__pycache__/",
68
+ )
69
+ ):
70
+ count += 1
71
+ return count
@@ -0,0 +1,146 @@
1
+ import os
2
+ from types import SimpleNamespace
3
+
4
+ from loguru import logger
5
+ from pydantic import BaseModel
6
+
7
+ from zodiac_core import ConfigManagement, Environment
8
+
9
+
10
+ def test_get_config_files_base_only(tmp_path):
11
+ (tmp_path / "app.ini").touch()
12
+ (tmp_path / "settings.ini").touch()
13
+
14
+ # Use 'develop' as target_env
15
+ files = ConfigManagement.get_config_files([tmp_path], env_var="TEST_ENV", default_env="develop")
16
+ filenames = [os.path.basename(f) for f in files]
17
+ assert sorted(filenames) == ["app.ini", "settings.ini"]
18
+
19
+
20
+ def test_get_config_files_with_env_override(tmp_path):
21
+ (tmp_path / "app.ini").touch()
22
+ (tmp_path / "app.develop.ini").touch()
23
+ (tmp_path / "app.production.ini").touch() # Known env, should be skipped silently
24
+
25
+ # target_env is 'develop'
26
+ files = ConfigManagement.get_config_files([tmp_path], env_var="TEST_ENV_2", default_env="develop")
27
+
28
+ filenames = [os.path.basename(f) for f in files]
29
+ assert "app.ini" in filenames
30
+ assert "app.develop.ini" in filenames
31
+ assert "app.production.ini" not in filenames
32
+ assert filenames.index("app.ini") < filenames.index("app.develop.ini")
33
+
34
+
35
+ def test_get_config_files_logging_strict_env(tmp_path):
36
+ # Setup
37
+ (tmp_path / "app.production.ini").touch()
38
+ (tmp_path / "app.dev.ini").touch()
39
+ (tmp_path / "app.test.ini").touch()
40
+ (tmp_path / "app.weirdo.ini").touch()
41
+
42
+ logs = []
43
+ handler_id = logger.add(logs.append, format="{message}")
44
+
45
+ try:
46
+ # target_env is 'production'
47
+ ConfigManagement.get_config_files([tmp_path], env_var="TEST_ENV_LOG", default_env="production")
48
+ finally:
49
+ logger.remove(handler_id)
50
+
51
+ log_messages = [str(log) for log in logs]
52
+
53
+ # Check strict matches are NOT logged
54
+ assert not any("app.production.ini" in m for m in log_messages)
55
+
56
+ # Check non-strict or unknown aliases ARE logged
57
+ assert any("app.dev.ini" in m for m in log_messages)
58
+ assert any("app.test.ini" in m for m in log_messages)
59
+ assert any("app.weirdo.ini" in m for m in log_messages)
60
+
61
+
62
+ def test_environment_enum_values():
63
+ assert Environment.DEVELOP == "develop"
64
+ assert Environment.TESTING == "testing"
65
+ assert Environment.STAGING == "staging"
66
+ assert Environment.PRODUCTION == "production"
67
+ assert len(Environment) == 4
68
+
69
+
70
+ def test_provide_config_recursive_list():
71
+ config_dict = {
72
+ "services": [
73
+ {"name": "auth", "port": 8080},
74
+ {"name": "gateway", "port": 80},
75
+ ],
76
+ "meta": {"tags": ["api", "v1"]},
77
+ }
78
+ config_obj = ConfigManagement.provide_config(config_dict)
79
+
80
+ assert isinstance(config_obj.services[0], SimpleNamespace)
81
+ assert config_obj.services[0].name == "auth"
82
+ assert config_obj.services[1].port == 80
83
+ assert config_obj.meta.tags == ["api", "v1"]
84
+
85
+
86
+ def test_get_config_files_complex_filename(tmp_path):
87
+ # Test file with multiple dots
88
+ (tmp_path / "my.app.service.develop.ini").touch()
89
+
90
+ files = ConfigManagement.get_config_files([tmp_path], env_var="TEST_ENV_COMPLEX", default_env="develop")
91
+ filenames = [os.path.basename(f) for f in files]
92
+ assert "my.app.service.develop.ini" in filenames
93
+
94
+
95
+ def test_get_config_files_invalid_paths(tmp_path):
96
+ # Test with non-existent directory
97
+ invalid_path = tmp_path / "ghost_dir"
98
+
99
+ # Should not raise exception, just return empty list or skip
100
+ files = ConfigManagement.get_config_files([invalid_path], default_env="develop")
101
+ assert len(files) == 0
102
+
103
+
104
+ class DbConfig(BaseModel):
105
+ host: str
106
+ port: int = 5432
107
+
108
+
109
+ class AppConfig(BaseModel):
110
+ db: DbConfig
111
+ debug: bool = False
112
+
113
+
114
+ def test_provide_config_with_pydantic_model():
115
+ """Test provide_config with Pydantic model for type-safe config."""
116
+ config_dict = {"db": {"host": "localhost", "port": 3306}, "debug": True}
117
+
118
+ config = ConfigManagement.provide_config(config_dict, AppConfig)
119
+
120
+ assert isinstance(config, AppConfig)
121
+ assert isinstance(config.db, DbConfig)
122
+ assert config.db.host == "localhost"
123
+ assert config.db.port == 3306
124
+ assert config.debug is True
125
+
126
+
127
+ def test_provide_config_pydantic_model_with_defaults():
128
+ """Test that Pydantic model defaults are applied."""
129
+ config_dict = {"db": {"host": "localhost"}} # port and debug use defaults
130
+
131
+ config = ConfigManagement.provide_config(config_dict, AppConfig)
132
+
133
+ assert config.db.port == 5432 # default
134
+ assert config.debug is False # default
135
+
136
+
137
+ def test_provide_config_empty_with_model():
138
+ """Test provide_config with empty dict and model that has all defaults."""
139
+
140
+ class AllDefaultConfig(BaseModel):
141
+ name: str = "app"
142
+ version: str = "1.0.0"
143
+
144
+ config = ConfigManagement.provide_config({}, AllDefaultConfig)
145
+ assert config.name == "app"
146
+ assert config.version == "1.0.0"