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.
- zodiac_core-0.2.0/LICENSE +21 -0
- zodiac_core-0.2.0/PKG-INFO +127 -0
- zodiac_core-0.2.0/README.md +96 -0
- zodiac_core-0.2.0/pyproject.toml +120 -0
- zodiac_core-0.2.0/setup.cfg +4 -0
- zodiac_core-0.2.0/tests/test_build.py +71 -0
- zodiac_core-0.2.0/tests/test_config.py +146 -0
- zodiac_core-0.2.0/tests/test_exception_handlers.py +141 -0
- zodiac_core-0.2.0/tests/test_exceptions.py +55 -0
- zodiac_core-0.2.0/tests/test_http.py +85 -0
- zodiac_core-0.2.0/tests/test_logging.py +101 -0
- zodiac_core-0.2.0/tests/test_middleware.py +120 -0
- zodiac_core-0.2.0/tests/test_pagination.py +94 -0
- zodiac_core-0.2.0/tests/test_routing.py +173 -0
- zodiac_core-0.2.0/tests/test_schemas_live.py +51 -0
- zodiac_core-0.2.0/zodiac/__init__.py +1 -0
- zodiac_core-0.2.0/zodiac/commands/__init__.py +3 -0
- zodiac_core-0.2.0/zodiac/commands/new.py +81 -0
- zodiac_core-0.2.0/zodiac/main.py +26 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/README.md.jinja +70 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/router.py.jinja +17 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/routers/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/routers/item_router.py.jinja +36 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/schemas/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/api/schemas/item_schema.py.jinja +12 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/github_service.py.jinja +16 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/application/services/item_service.py.jinja +24 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/core/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/core/container.py.jinja +56 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/models/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/models/item_model.py.jinja +9 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/repositories/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/database/repositories/item_repository.py.jinja +19 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/external/__init__.py.jinja +0 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/app/infrastructure/external/github_client.py.jinja +22 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/config/app.develop.ini.jinja +3 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/config/app.ini.jinja +3 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/main.py.jinja +49 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/pyproject.toml.jinja +48 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/__init__.py.jinja +2 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/conftest.py.jinja +11 -0
- zodiac_core-0.2.0/zodiac/templates/standard-3tier/tests/test_health.py.jinja +9 -0
- zodiac_core-0.2.0/zodiac_core/__init__.py +104 -0
- zodiac_core-0.2.0/zodiac_core/config.py +192 -0
- zodiac_core-0.2.0/zodiac_core/context.py +34 -0
- zodiac_core-0.2.0/zodiac_core/db/__init__.py +38 -0
- zodiac_core-0.2.0/zodiac_core/db/repository.py +125 -0
- zodiac_core-0.2.0/zodiac_core/db/session.py +282 -0
- zodiac_core-0.2.0/zodiac_core/db/sql.py +113 -0
- zodiac_core-0.2.0/zodiac_core/exception_handlers.py +82 -0
- zodiac_core-0.2.0/zodiac_core/exceptions.py +51 -0
- zodiac_core-0.2.0/zodiac_core/http.py +79 -0
- zodiac_core-0.2.0/zodiac_core/logging.py +106 -0
- zodiac_core-0.2.0/zodiac_core/middleware.py +88 -0
- zodiac_core-0.2.0/zodiac_core/pagination.py +74 -0
- zodiac_core-0.2.0/zodiac_core/response.py +123 -0
- zodiac_core-0.2.0/zodiac_core/routing.py +110 -0
- zodiac_core-0.2.0/zodiac_core/schemas.py +81 -0
- zodiac_core-0.2.0/zodiac_core.egg-info/PKG-INFO +127 -0
- zodiac_core-0.2.0/zodiac_core.egg-info/SOURCES.txt +68 -0
- zodiac_core-0.2.0/zodiac_core.egg-info/dependency_links.txt +1 -0
- zodiac_core-0.2.0/zodiac_core.egg-info/entry_points.txt +2 -0
- zodiac_core-0.2.0/zodiac_core.egg-info/requires.txt +14 -0
- 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,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"
|