From 6e9a7fa9686d0e131ea76d3f248b1f3b152c5e66 Mon Sep 17 00:00:00 2001
From: Ali Alf Ostovar
Date: Thu, 16 Jul 2026 12:57:17 +0330
Subject: [PATCH 1/4] refactor(schema): rename __db_name__ to __model__ and
centralize core exports (#67)
### Summary
* **Decimal JSON Serialization**: Changed the `CustomJSONEncoder`
decimal serialization from `float` to `str` within
`zcore/utils/helpers.py`. This preserves exact precision during JSON
serialization, resolving critical precision loss risks in high-precision
and financial API scenarios.
* **Zchema Semantic Attribute Rename**: Renamed the `__db_name__` class
variable to `__model__` in `Zchema` (`zcore/web/projection.py`) to
decouple the presentation schemas from database-specific nomenclature,
updating all internal resolution methods and templates.
* **Expose Core Constructs at Root Namespace**: Added `RouteKey` and
`Plugin` exports to the dynamic `__getattr__` and `__all__` blocks in
`src/zcore/__init__.py`. This enables lazy-loaded, clean root imports
(e.g., `from zcore import RouteKey, Plugin`) instead of requiring deep
internal import paths.
* **CLI & Test Alignment**: Updated CLI templates
(`src/zcore/cli/templates.py`), project tests (`tests/test_utils.py`,
`tests/test_web.py`), and code generation blueprints to properly use the
new `__model__` attribute and the centralized imports.
### Impact
This PR enhances ZCore's overall developer experience (DX) by exposing
highly utilized constructs directly at the root module namespace,
corrects an architectural naming coupling in `Zchema`, and safeguards
data integrity by eliminating decimal serialization precision loss.
---
Closes #61
Closes #63
Closes #64
---
src/zcore/__init__.py | 10 +++++++++-
src/zcore/cli/templates.py | 8 ++++----
src/zcore/utils/helpers.py | 2 +-
src/zcore/web/projection.py | 6 +++---
tests/test_utils.py | 6 +++---
tests/test_web.py | 2 +-
6 files changed, 21 insertions(+), 13 deletions(-)
diff --git a/src/zcore/__init__.py b/src/zcore/__init__.py
index ad8c7a0..40d5765 100644
--- a/src/zcore/__init__.py
+++ b/src/zcore/__init__.py
@@ -3,6 +3,7 @@
if TYPE_CHECKING:
from zcore.kernel.engine import Kernel
+ from zcore.kernel.plugins import Plugin
from zcore.kernel.di import container, Inject
from zcore.db.setup import Base, SessionDep, db_manager, get_db
from zcore.db.repository import BaseRepository
@@ -31,7 +32,8 @@
'RouteKey',
'ResponseWrapper',
'ZCoreAPIRoute',
- 'Zchema'
+ 'Zchema',
+ "Plugin"
]
@@ -81,5 +83,11 @@ def __getattr__(name: str) -> Any:
if name == "Zchema":
from zcore.web.projection import Zchema
return Zchema
+ if name == "RouteKey":
+ from zcore.web.base_router import RouteKey
+ return RouteKey
+ if name == "Plugin":
+ from zcore.kernel.plugins import Plugin
+ return Plugin
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
\ No newline at end of file
diff --git a/src/zcore/cli/templates.py b/src/zcore/cli/templates.py
index 0a4cf7d..c7c418e 100644
--- a/src/zcore/cli/templates.py
+++ b/src/zcore/cli/templates.py
@@ -129,7 +129,7 @@ class {ModelName}(Base):
import uuid
class {ModelName}Base(Zchema):
- __db_name__ = "{table_name}"
+ __model__ = "{table_name}"
# TODO: Add your shared model attributes
pass
@@ -167,8 +167,8 @@ def __init__(self, repository: {ModelName}Repository = Inject({ModelName}Reposit
"""
ROUTER_TEMPLATE = """from typing import Any
-from zcore import BaseRouter
-from zcore.web import RouteKey
+from zcore import BaseRouter, RouteKey
+
from .schemas import {ModelName}Create, {ModelName}Update, {ModelName}Response
from .services import {ModelName}Service
from .models import {ModelName}
@@ -202,7 +202,7 @@ def get_route_dependencies(self, route_key: RouteKey, action: str) -> list[Any]:
"""
PLUGIN_TEMPLATE = """from fastapi import FastAPI
-from zcore.kernel import Plugin
+from zcore import Plugin
class {ModelName}Plugin(Plugin):
name = "{app_name}"
diff --git a/src/zcore/utils/helpers.py b/src/zcore/utils/helpers.py
index eb8d36d..51ecb19 100644
--- a/src/zcore/utils/helpers.py
+++ b/src/zcore/utils/helpers.py
@@ -59,7 +59,7 @@ def default(self, obj: Any) -> Any:
if isinstance(obj, (datetime, date, time)):
return obj.isoformat()
if isinstance(obj, Decimal):
- return float(obj)
+ return str(obj)
try:
return super().default(obj)
except TypeError:
diff --git a/src/zcore/web/projection.py b/src/zcore/web/projection.py
index 85dd3cc..0757f0b 100644
--- a/src/zcore/web/projection.py
+++ b/src/zcore/web/projection.py
@@ -14,17 +14,17 @@
class Zchema(BaseModel):
"""Unified, domain-aware security schema base class.
- Subclasses specify their unique database domain mapping via the `__db_name__`
+ Subclasses specify their unique database domain mapping via the `__model__`
class attribute. This enables contextual, recursive pruning across schema generation,
input validation, and response serialization.
"""
- __db_name__: ClassVar[Optional[str]] = None
+ __model__: ClassVar[Optional[str]] = None
@classmethod
def _get_relative_restricted_paths(cls) -> Set[str]:
"""Extract and normalize restricted field paths mapped to this schema's domain."""
- db_name = getattr(cls, "__db_name__", None)
+ db_name = getattr(cls, "__model__", None)
if not db_name:
return set()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2a46483..5b04314 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,7 +1,7 @@
import datetime
from decimal import Decimal
import uuid
-from typing import Any, Type
+from typing import Any
import pytest
from zcore.exceptions.base import ValidationError
@@ -29,11 +29,11 @@
),
(
Decimal("123.45"),
- "123.45"
+ '"123.45"'
),
(
{"user_id": uuid.UUID("12345678-1234-5678-1234-567812345678"), "balance": Decimal("9.99")},
- '{"user_id": "12345678-1234-5678-1234-567812345678", "balance": 9.99}'
+ '{"user_id": "12345678-1234-5678-1234-567812345678", "balance": "9.99"}'
)
]
)
diff --git a/tests/test_web.py b/tests/test_web.py
index 468f87f..db9510b 100644
--- a/tests/test_web.py
+++ b/tests/test_web.py
@@ -31,7 +31,7 @@ class DummyUpdate(BaseModel):
name: str
class DummyOut(Zchema):
- __db_name__ = "dummy"
+ __model__ = "dummy"
id: str
name: str
password: str = ""
From 293f1dd41b6ecccca613522327fa4a10a396ead8 Mon Sep 17 00:00:00 2001
From: Ali Alf Ostovar
Date: Thu, 16 Jul 2026 13:04:36 +0330
Subject: [PATCH 2/4] fix(db): improve datetime timezone handling and remove
legacy resource prefix (#68)
### Summary
* **Timezone-Aware Cursor Encoding**: Refactored `_encode_cursor` inside
`zcore/db/pagination.py` to normalize naive `datetime` objects to UTC
timezone before ISO formatting, preventing cursor misalignment issues
across multi-region deployments.
* **Native Python 3.11+ ISO Datetime Parsing**: Eliminated the redundant
`.replace("Z", "+00:00")` string replacement workaround in
`_coerce_value` (`zcore/db/search.py`), shifting database datetime
coercion to use Python 3.11's out-of-the-box support for native ISO
formats.
* **Eliminated Legacy Prefix Stripping**: Removed the buggy global
string replacement `.replace("resource.", "")` from
`_is_path_restricted` (`zcore/db/search.py`) and
`_get_relative_restricted_paths` (`zcore/web/projection.py`).
Normalization now strictly checks matching paths without corrupting
valid relation paths containing the word `"resource"` (e.g.,
`project.resource.status`).
* **Zchema Docstring Cleanup**: Cleaned up repetitive metadata in
validation and serialization levels within `Zchema`'s docstrings to
improve readability and code quality.
### Impact
This PR safeguards security and data integrity by correcting a critical
vulnerability where valid nested security paths could be misaligned
during prefix normalization. It also introduces proper timezone
normalization for pagination states and cleans up redundant, outdated
datetime parsing logic to benefit from modern Python 3.11 native
capabilities.
---
Closes #62
Closes #65
---
src/zcore/db/pagination.py | 6 +++++-
src/zcore/db/search.py | 6 +++---
src/zcore/web/projection.py | 21 +++++++++------------
3 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/src/zcore/db/pagination.py b/src/zcore/db/pagination.py
index 9cae3b5..75d36cd 100644
--- a/src/zcore/db/pagination.py
+++ b/src/zcore/db/pagination.py
@@ -10,7 +10,7 @@
import math
import base64
from typing import Any, Sequence, TypeVar, Generic, Optional, Type, Literal
-from datetime import datetime
+from datetime import datetime, timezone
from pydantic import BaseModel, Field
from sqlalchemy.sql.sqltypes import DateTime
@@ -224,6 +224,10 @@ def _encode_cursor(self, last_item: Any) -> str:
"""
value = getattr(last_item, self.cursor_field, None)
if isinstance(value, datetime):
+ if value.tzinfo is None:
+ value = value.replace(tzinfo=timezone.utc)
+ else:
+ value = value.astimezone(timezone.utc)
value = value.isoformat()
payload = {
diff --git a/src/zcore/db/search.py b/src/zcore/db/search.py
index a5005a5..0b380d2 100644
--- a/src/zcore/db/search.py
+++ b/src/zcore/db/search.py
@@ -127,9 +127,9 @@ def _is_path_restricted(self, path: str, restricted_set: Set[str]) -> bool:
if not restricted_set:
return False
- normalized_path = path.replace("resource.", "").lower()
+ normalized_path = path.lower()
for restricted in restricted_set:
- normalized_restricted = restricted.replace("resource.", "").lower()
+ normalized_restricted = restricted.lower()
if normalized_path == normalized_restricted:
return True
@@ -351,7 +351,7 @@ def _coerce_value(self, col: Any, value: Any) -> Any:
return date.fromisoformat(value.split("T")[0])
if python_type is datetime and isinstance(value, str):
- return datetime.fromisoformat(value.replace("Z", "+00:00"))
+ return datetime.fromisoformat(value)
if python_type is uuid.UUID and isinstance(value, str):
return uuid.UUID(value)
diff --git a/src/zcore/web/projection.py b/src/zcore/web/projection.py
index 0757f0b..d654255 100644
--- a/src/zcore/web/projection.py
+++ b/src/zcore/web/projection.py
@@ -24,21 +24,18 @@ class attribute. This enables contextual, recursive pruning across schema genera
@classmethod
def _get_relative_restricted_paths(cls) -> Set[str]:
"""Extract and normalize restricted field paths mapped to this schema's domain."""
- db_name = getattr(cls, "__model__", None)
- if not db_name:
+ model_name = getattr(cls, "__model__", None)
+ if not model_name:
return set()
- prefix = f"{db_name}."
+ prefix = f"{model_name}."
restricted = get_restricted_fields()
relative_paths = set()
for path in restricted:
- # Remove common resource prefix for robust backwards compatibility
- normalized = path.replace("resource.", "")
- if normalized.startswith(prefix):
- relative_paths.add(normalized[len(prefix):])
- elif normalized == db_name:
- # If the entire domain is restricted, restrict all attributes
+ if path.startswith(prefix):
+ relative_paths.add(path[len(prefix):])
+ elif path == model_name:
relative_paths.add("*")
return relative_paths
@@ -80,7 +77,7 @@ def _prune_data(cls, data: Any, relative_paths: Set[str]) -> Any:
def __get_pydantic_json_schema__(
cls, core_schema: Any, handler: Any
) -> dict[str, Any]:
- """Level 1: Customize dynamic JSON schema generation under context boundaries."""
+ """Customize dynamic JSON schema generation under context boundaries."""
json_schema = handler(core_schema)
relative_paths = cls._get_relative_restricted_paths()
if not relative_paths:
@@ -121,7 +118,7 @@ def prune_schema(schema: dict[str, Any], paths: Set[str]) -> None:
@model_validator(mode="before")
@classmethod
def filter_restricted_inputs(cls, data: Any) -> Any:
- """Level 2: Silently strip restricted fields from input payloads to prevent Mass Assignment."""
+ """Silently strip restricted fields from input payloads to prevent Mass Assignment."""
relative_paths = cls._get_relative_restricted_paths()
if not relative_paths or data is None:
return data
@@ -133,7 +130,7 @@ def filter_restricted_inputs(cls, data: Any) -> Any:
@model_serializer(mode="wrap")
def secure_serializer(self, handler: Any) -> Any:
- """Level 3: Securely intercept serialization to prune restricted attributes from response."""
+ """Securely intercept serialization to prune restricted attributes from response."""
serialized = handler(self)
relative_paths = self._get_relative_restricted_paths()
if not relative_paths or serialized is None:
From 09b1c1f06d1b33740915f9846366a7523d646e1a Mon Sep 17 00:00:00 2001
From: Ali Alf Ostovar
Date: Thu, 16 Jul 2026 13:11:47 +0330
Subject: [PATCH 3/4] docs: complete documentation overhaul, homepage redesign,
and quickstart guide (#69)
### Summary
* **Redesigned Homepage**: Completely overhauled `index.md` with a
modern, clean hero section, descriptive feature cards, and interactive
side-by-side comparisons highlighting ZCore's boilerplate reduction (35
lines, 7 endpoints in ZCore vs. 100+ lines, 3 endpoints in raw FastAPI).
* **Added 10-Minute Quickstart**: Introduced a comprehensive tutorial
(`quickstart.md`) focused on building a task manager API to quickly
onboard new developers. Reorganized `mkdocs.yml` navigation to feature
this guide prominently.
* **Overhauled README.md**: Upgraded the repository's main `README.md`
with professional badges (PyPI, License, CI, Python versions), clean
collapsible details for each architectural pillar, and an actionable
local installation guide.
* **Interactive Request Lifecycle Flow**: Integrated modern Mermaid
flowcharts and clean diagrams to map out the exact request lifecycle
across ZCore's middleware and DB routing layers.
* **Introduced Community Meta Files**: Formally introduced
`CONTRIBUTING.md` (covering local setup with Hatch, testing, and
semantic commit guidelines) and `SECURITY.md` (defining vulnerability
reporting procedures) to establish repo standards.
* **Theme & Configuration Cleanups**: Switched MkDocs configuration for
a sleek visual design, removed the heavy `mkdocstrings` plugin to
simplify build environments, and cleaned up navigation comments in
`mkdocs.yml`.
### Impact
This PR elevates ZCore's onboarding and developer experience (DX) to
professional standards. By providing comparative code examples, visual
flowcharts, a hands-on tutorial, and standardized community guidelines,
it drastically lowers the barrier to entry for new users and prepares
the project for healthy open-source contributions.
---
CONTRIBUTING.md | 167 ++++++
README.md | 245 ++++++--
SECURITY.md | 95 ++++
docs/assets/4-service.png | Bin 0 -> 542951 bytes
docs/assets/7-kernel.png | Bin 0 -> 656518 bytes
docs/assets/8-main.png | Bin 0 -> 500969 bytes
docs/assets/api-router.png | Bin 0 -> 1021156 bytes
docs/assets/auth-setup.png | Bin 0 -> 742444 bytes
docs/assets/cache.png | Bin 0 -> 1050284 bytes
docs/assets/context.png | Bin 0 -> 579488 bytes
docs/assets/db-events.png | Bin 0 -> 487354 bytes
docs/assets/di.png | Bin 0 -> 1207254 bytes
docs/assets/events.png | Bin 0 -> 832411 bytes
docs/assets/overview.png | Bin 0 -> 373041 bytes
docs/assets/search.png | Bin 0 -> 536696 bytes
docs/assets/security.png | Bin 0 -> 541517 bytes
docs/assets/storage.png | Bin 0 -> 550280 bytes
docs/assets/stream.png | Bin 0 -> 800748 bytes
docs/assets/zchema.png | Bin 0 -> 540209 bytes
docs/cheatsheet.md | 616 +++++++++------------
docs/features/database/db-events.md | 114 ++--
docs/features/database/db.md | 140 ++---
docs/features/database/pagination.md | 146 ++---
docs/features/database/repository.md | 151 +++--
docs/features/database/search.md | 167 +++---
docs/features/database/service.md | 181 +++---
docs/features/database/uow.md | 168 +++---
docs/features/ecosystem/cache.md | 172 +++---
docs/features/ecosystem/storage.md | 199 ++++---
docs/features/ecosystem/stream.md | 182 +++---
docs/features/infrastructure/cli.md | 130 +++--
docs/features/infrastructure/config.md | 143 ++---
docs/features/infrastructure/di.md | 168 +++---
docs/features/infrastructure/events.md | 185 +++----
docs/features/infrastructure/kernel.md | 132 +++--
docs/features/infrastructure/main.md | 193 +++----
docs/features/infrastructure/middleware.md | 127 ++---
docs/features/infrastructure/utils.md | 145 ++---
docs/features/security/api-router.md | 120 ++--
docs/features/security/context.md | 150 +++--
docs/features/security/exceptions.md | 156 +++---
docs/features/security/security.md | 160 +++---
docs/features/security/zchema.md | 191 +++----
docs/how-to/auth-setup.md | 218 ++++----
docs/how-to/base-router-extensibility.md | 202 +++----
docs/how-to/custom-storage-provider.md | 254 +++------
docs/how-to/field-masking.md | 227 +++-----
docs/how-to/multi-repo-transactions.md | 186 +++----
docs/how-to/websocket-pubsub.md | 612 ++++----------------
docs/index.md | 323 ++++++++---
docs/learn/1-db.md | 125 +++--
docs/learn/2-schemas.md | 149 ++---
docs/learn/3-repository.md | 125 +++--
docs/learn/4-service.md | 175 +++---
docs/learn/5-router.md | 197 +++----
docs/learn/6-plugin.md | 160 +++---
docs/learn/7-kernel.md | 132 +++--
docs/learn/8-main.md | 205 ++++---
docs/learn/installation.md | 143 ++---
docs/learn/overview.md | 111 +++-
docs/learn/quickstart.md | 431 ++++++++++++++
docs/overrides.css | 33 ++
mkdocs.yml | 107 ++--
63 files changed, 4659 insertions(+), 4199 deletions(-)
create mode 100644 CONTRIBUTING.md
create mode 100644 SECURITY.md
create mode 100644 docs/assets/4-service.png
create mode 100644 docs/assets/7-kernel.png
create mode 100644 docs/assets/8-main.png
create mode 100644 docs/assets/api-router.png
create mode 100644 docs/assets/auth-setup.png
create mode 100644 docs/assets/cache.png
create mode 100644 docs/assets/context.png
create mode 100644 docs/assets/db-events.png
create mode 100644 docs/assets/di.png
create mode 100644 docs/assets/events.png
create mode 100644 docs/assets/overview.png
create mode 100644 docs/assets/search.png
create mode 100644 docs/assets/security.png
create mode 100644 docs/assets/storage.png
create mode 100644 docs/assets/stream.png
create mode 100644 docs/assets/zchema.png
create mode 100644 docs/learn/quickstart.md
create mode 100644 docs/overrides.css
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bf991cd
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,167 @@
+# Contributing to ZCore
+
+First, thank you for considering contributing to ZCore! It is through developers like you that we can continue to refine this architectural chassis for FastAPI.
+
+Please take a moment to review this document to ensure your contributions align with our design philosophy, codebase standards, and development workflows.
+
+---
+
+## ποΈ Our Design Principles
+
+When proposing changes or writing code for ZCore, please keep these core pillars in mind:
+1. **The Complementary Promise:** ZCore must never obscure or lock developers out of native FastAPI/Pydantic capabilities. Your contributions should enhance, not restrict, developer freedom.
+2. **Strict Typings:** ZCore is designed with highly strict type safety. All public APIs must be fully annotated and pass static analysis checks without warnings.
+3. **Fail-Safe Security:** Security defaults (like magic byte verification or secure fallback JWT checks) should be enforced out-of-the-box, protecting developers from accidental vulnerabilities.
+4. **Performance Pragmatism:** Performance over-optimization is a trap, but unnecessary overhead (like repetitive reflection without caching) must be avoided.
+
+### β What We Don't Accept
+To preserve ZCore's lightweight, explicit, and highly predictable nature, we generally avoid:
+- **Tight Coupling:** Features that tightly bind ZCore to FastAPI internals or prevent independent unit testing of core components.
+- **Implicit "Magic" Behavior:** We prefer explicit, clear configuration over automatic, hard-to-debug "magic" conventions.
+- **Breaking Changes:** Any breaking changes submitted without a clearly documented and seamless migration path.
+- **Heavy Runtime Overhead:** Third-party dependencies or features that introduce unnecessary CPU/memory overhead (e.g., repetitive reflection without proper caching layers).
+
+---
+
+## π¦ Before You Contribute
+
+To help us maintain a high-quality codebase and manage our maintainer overhead, please follow these steps before opening an issue or a pull request:
+
+1. **Search Existing Issues:** Ensure the bug or feature request has not already been reported or discussed.
+2. **Provide a Minimal Reproducible Example (MRE):** When reporting bugs, please provide a self-contained, copy-pasteable script that reproduces the issue. Issues without a reproduction case may be closed.
+3. **Include Environment Information:** Always specify your Python, FastAPI, and ZCore versions, as well as your operating system.
+
+---
+
+## π οΈ Local Development Setup
+
+ZCore utilizes **Hatch** as its build backend and project manager. Follow these steps to prepare your local development environment:
+
+### 1. Fork and Clone
+Fork the repository on GitHub, then clone your fork locally:
+```bash
+git clone https://github.com/your-username/zcore.git
+cd zcore
+```
+
+### 2. Install Development Dependencies
+It is highly recommended to use a virtual environment. Install ZCore in editable mode along with all optional and development dependencies:
+```bash
+python -m venv .venv
+source .venv/bin/activate # On Windows: .venv\Scripts\activate
+
+# Install with editable mode and all dev extras
+pip install -e ".[all,dev]"
+```
+
+---
+
+## π§ͺ Testing Guidelines
+
+We aim for high test coverage on core architectural units. ZCore uses `pytest` and `pytest-asyncio` for asynchronous test suites.
+
+### Test Architecture
+ZCore's codebase is highly modular. To ensure that components like the Kernel, Plugins, Dependency Injection (DI), Unit of Work (UOW), Router, and Middleware interact correctly, we organize our tests as follows:
+
+```text
+tests/
+βββ unit/ # Tests for isolated, single architectural units with mocks
+β βββ test_repository.py
+β βββ test_zchema.py
+βββ integration/ # Tests verifying interactions between modules
+β βββ test_kernel_startup.py
+β βββ test_plugin_loading.py
+β βββ test_request_scope.py
+βββ e2e/ # Full end-to-end request/response flows using TestClient
+ βββ test_crud_flow.py
+```
+
+### Running Tests Locally
+Run the standard pytest suite using:
+```bash
+pytest
+```
+
+If you prefer to run tests inside Hatch-isolated environments:
+```bash
+hatch run test
+```
+
+### Writing Tests
+- **Asynchronous Tests:** Ensure your async tests are decorated with `@pytest.mark.asyncio`.
+- **Isolation:** Tests should not leave behind lingering SQLite databases, active sessions, or running background threads.
+- **Mocks:** When writing tests that require external services like Redis, implement mock fallbacks where appropriate to ensure the test suite can run fully offline.
+
+---
+
+## π Code Quality & Static Analysis
+
+Before submitting a Pull Request, your code must satisfy strict formatting, linting, and type-checking constraints.
+
+### 1. Type Checking
+ZCore enforces strict type checks via `mypy`. To verify your changes conform to the strict rules defined in `pyproject.toml`, run:
+```bash
+mypy src/
+```
+
+### 2. Linting & Formatting
+Ensure your code is clean, PEP-8 compliant, and structured consistently. We recommend using `ruff` for formatting and linting:
+```bash
+ruff check src/
+ruff format src/
+```
+
+---
+
+## π Commit Message Conventions
+
+ZCore uses the **Conventional Commits** specification to automate our changelog generation and semantic versioning. Your commit messages must follow this structure:
+
+```text
+():
+```
+
+Common types include:
+- `feat`: A new feature (corresponds to a minor version bump).
+- `fix`: A bug fix (corresponds to a patch version bump).
+- `docs`: Documentation updates.
+- `refactor`: Code changes that neither fix a bug nor add a feature.
+- `test`: Adding or correcting tests.
+- `ci`: Changes to our CI/CD workflows or scripts.
+
+*Examples:*
+- `feat(di): add redis plugin support`
+- `fix(uow): resolve transaction rollback issue on async commit`
+- `docs(zchema): update guide for dynamic schema pruning`
+
+---
+
+## π Pull Request Process
+
+We appreciate atomic, focused Pull Requests. To increase the chances of your PR being merged quickly, please follow this process:
+
+```mermaid
+flowchart LR
+ A[Create Feature Branch] --> B[Write Code & Tests]
+ B --> C[Pass Mypy & Pytest]
+ C --> D[Update Docs if needed]
+ D --> E[Submit PR]
+```
+
+1. **Branch Naming:** Use descriptive branch names (e.g., `feature/add-s3-provider` or `bugfix/fix-uow-flush`).
+2. **Atomic Commits:** Keep your commits focused on single logical changes and adhere to the conventional commits schema.
+3. **Documentation:** If you are adding a new feature or modifying an existing protocol, you **must** update the corresponding documentation page under the `docs/` folder.
+
+### π‘οΈ CI/CD & Branch Protection
+Our repository has strict branch protection rules configured on `master` and `develop`.
+- **Mandatory CI Checks:** Your Pull Request cannot be merged unless all status checks (Linting with Ruff, Type-checking with Mypy, and Test Suites with Pytest) pass successfully in our GitHub Actions pipeline.
+- **No Direct Pushes:** All code changes must go through a Pull Request. Direct pushes to protected branches are blocked.
+
+---
+
+## π Licensing & Copyright
+
+By contributing to ZCore, you agree that your contributions will be licensed under the project's **Apache License 2.0**.
+
+- **No CLA Required:** We do not currently require a signed Contributor License Agreement (CLA) for individual contributions.
+- **Certificate of Origin:** By submitting a Pull Request, you assert that you have the right to submit this code and that it conforms to an open-source contribution under the Apache 2.0 terms.
diff --git a/README.md b/README.md
index c465ccc..f08b5a1 100644
--- a/README.md
+++ b/README.md
@@ -1,103 +1,244 @@
-
-
- A modest and practical architectural layer built on top of FastAPI.
+
+ A pragmatic and complementary architectural layer built on top of FastAPI.
+ Standardize your structure, protect your data, and manage atomic transactionsβwithout losing your development freedom.
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
---
## What is ZCore?
-ZCore is not a replacement for FastAPI; it is a modest and practical architectural layer built on top of it. While FastAPI provides the high-performance engine for handling HTTP requests, ZCore provides the "chassis"βa structured environment that solves common challenges in medium-to-large scale applications such as dependency management, transaction integrity, and data leakage prevention.
-
-The framework focuses on **Engineered Simplicity**. It abstracts complex patterns like the *Unit of Work* and *Scoped Inversion of Control* into intuitive interfaces, allowing you to focus on your domain logic while the framework ensures that your database transactions are atomic and your sensitive data remains restricted based on the execution context.
-
----
-
-## Why Choose ZCore?
+**ZCore is not a framework that hides FastAPI β it is the chassis that stabilizes it.**
-ZCore was designed to bridge the gap between "writing an endpoint" and "building a maintainable system."
+While FastAPI gives you a high-performance engine for HTTP, it provides no blueprint for structuring medium-to-large applications. ZCore fills that gap with:
-| Feature | Standard FastAPI Challenge | The ZCore Approach |
-| :--- | :--- | :--- |
-| **Dependency Injection** | Manual wiring and complex `Depends` chains. | Automated **Scoped IoC** with constructor injection. |
-| **Data Security** | Manual filtering of Pydantic models for different users. | Context-aware **Response Pruning** via `ResponseProjector`. |
-| **Transactions** | Scatterred `.commit()` calls leading to partial failures. | Centralized **Unit of Work** (UOW) for atomic operations. |
-| **Project Structure** | Inconsistent layouts across different teams. | Modular **Plugin System** and standardized CLI scaffolding. |
-| **Search & Filter** | Writing repetitive boilerplate for every query. | A secure, dynamic **Search Engine** with depth-limit protection. |
+- **π Context-aware data masking** β Write one schema; sensitive fields are pruned automatically per-user.
+- **π Atomic Unit of Work** β Group operations into all-or-nothing transactions with delayed event dispatch.
+- **β‘ Scoped Dependency Injection** β Singleton, transient, and request-scoped dependencies resolved automatically.
+- **ποΈ Modular Plugin System** β Organize domains into isolated plugins with topological startup ordering.
+- **π Secure Dynamic Search Engine** β Nested JSON filters, cursor/offset pagination, and column-level security β zero boilerplate.
+- **π¦ CLI Scaffolding** β From `pip install` to running CRUD endpoints in under two minutes.
---
-## The Request Lifecycle
+## Why ZCore?
-Understanding how a request travels through ZCore is key to mastering its architecture. The following diagram illustrates the automated orchestration from the moment a request hits the server to the final pruned response.
-
-
-
-
+| Concern | Raw FastAPI | With ZCore |
+|---------|-------------|------------|
+| **Endpoint Scaffolding** | Manually write 7+ files per domain | One `BaseRouter` class β 7 secure endpoints |
+| **Data Leakage** | Multiple Pydantic models per role; manual conditionals | `Zchema` auto-prunes restricted fields per-user |
+| **Database Transactions** | Scattered `commit()` / `rollback()` calls | `UnitOfWork` guarantees atomicity + deferred events |
+| **Dependency Wiring** | Deeply nested `Depends()` chains | Constructor injection with `Inject()` β auto-resolved |
+| **Search & Pagination** | Hand-written SQL per endpoint | Declarative JSON filters + cursor/offset pagination |
+| **Project Layout** | No standard; every team reinvents the wheel | `zc init` + `zc startapp` β consistent domain modules |
+| **Startup Orchestration** | Manual `@app.on_event` spaghetti | `Plugin` with dependency graph β topological sorting |
---
## β‘ Quick Start
-### 1. Installation
-Set up a clean virtual environment and install ZCore with all optional dependencies:
+### 1. Install
```bash
-python -m venv .venv
-source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install fastapi-zcore-framework[all]
```
-### 2. Scaffold a Project
-Initialize your project and generate a structured domain module using our command-line utility:
+### 2. Scaffold
```bash
-# Initialize project workspace
-zc init product_api
-cd product_api
+zc init my_app && cd my_app
+zc startapp tasks --template
+```
+
+### 3. Define
+
+Open `app/tasks/models.py`:
-# Scaffold a domain app with boilerplate templates
-zc startapp products -t
+```python
+from zcore import Base
+from sqlalchemy.orm import Mapped, mapped_column
+
+class Task(Base):
+ __tablename__ = "tasks"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ title: Mapped[str]
+ is_completed: Mapped[bool] = mapped_column(default=False)
```
-### 3. Run the Development Server
-Launch the local Uvicorn development server:
+### 4. Serve
```bash
zc run
```
+Your API is live at **`http://127.0.0.1:8000`** with 7 CRUD + search endpoints ready.
+
+> π **Full walkthrough:** [10-Minute Quickstart](https://fastapi-zcore-framework.github.io/zcore/learn/quickstart/)
+
---
-## π Core Pillars at a Glance
+## ποΈ Core Pillars
+
+
+π Context Shielding (Zchema)
+
+
+Write a single schema. ZCore dynamically prunes input fields (preventing mass assignment) and output fields (preventing data leakage) based on the active user's permission scopes.
+
+```python
+from zcore import Zchema
-* **β‘ Scoped IoC Container:** Manage object lifecycles (Singleton, Transient, or Scoped) with ease. Scoped dependencies are automatically cleared at the end of every HTTP request to prevent memory pollution.
-* **π‘οΈ Secure Search Engine:** A dynamic query builder that supports nested filters and eager-loading, while automatically blocking access to restricted database columns based on security policies.
-* **π Unit of Work (UOW):** Ensures that business operations succeed or fail as a single unit. It coordinates database flushes and delays event dispatching until the transaction is successfully committed.
-* **ποΈ Modular Plugin System:** Organize your application into decoupled domains. Each plugin manages its own lifecycle hooks (`on_startup`, `on_shutdown`) and can declare dependencies on other plugins.
+class TaskCreate(Zchema):
+ __model__ = "tasks"
+ title: str
+ assignee_email: str | None = None # Auto-hidden from unauthorized users
+```
+
+When a user lacks `tasks:view_sensitive`, the `assignee_email` field vanishes from both validation and serialization β no `if` statements.
+
+
+
+π Atomic Transactions (Unit of Work)
+
+
+Group multiple repository operations into a single atomic unit. Events are queued and dispatched only after a successful commit. If any operation fails, the entire transaction rolls back.
+
+```python
+from zcore import UnitOfWork
+
+async with UnitOfWork(session, dispatcher) as uow:
+ await order_repo.create(order_data)
+ await inventory_repo.decrement_stock(product_id, quantity)
+
+ # Queue event for post-commit dispatch safely
+ uow.register_event("order.placed", {"id": order_data.id})
+```
+
+
+
+β‘ Scoped Dependency Injection
+
+
+Inject services, repositories, and infrastructure dependencies via constructor injection. ZCore's container resolves them automatically per-request and clears request-scoped instances after each response.
+
+```python
+from zcore import BaseService, Inject
+
+class TaskService(BaseService):
+ def __init__(self, repo: TaskRepository = Inject(TaskRepository)):
+ super().__init__(model=Task, repository=repo)
+```
+
+
+
+ποΈ Modular Plugin System
+
+
+Each domain is a self-contained `Plugin` with its own models, services, routers, and lifecycle hooks. The Kernel uses topological sorting to guarantee correct startup/shutdown order.
+
+```python
+from zcore import Plugin
+from .routers import router_instance
+
+class TaskPlugin(Plugin):
+ name = "tasks"
+ dependencies = ["auth"]
+
+ def setup(self, app):
+ app.include_router(router_instance.router)
+
+ async def on_startup(self):
+ await cache.warm()
+```
+
+
+
+π Secure Search Engine
+
+
+A dynamic query builder that translates nested JSON filters into safe SQL β with depth-limit protection against DoS attacks.
+
+```json
+{
+ "filters": [
+ { "field": "priority", "op": "eq", "value": "high" },
+ { "field": "is_completed", "op": "eq", "value": false }
+ ],
+ "sort_by": "created_at",
+ "sort_order": "desc",
+ "page": 1,
+ "size": 20
+}
+```
+
+
+
+π¦ CLI Scaffolding
+
+
+Stop copying boilerplate. The `zc` CLI generates consistent, production-ready domain modules with a single command.
+
+| Command | Purpose |
+|---------|---------|
+| `zc init ` | Create a new project with settings, `.env`, and `main.py` |
+| `zc startapp ` | Scaffold a domain module |
+| `zc startapp --template` | Scaffold with full CRUD boilerplate |
+| `zc run` | Launch Uvicorn with environment-aware config |
+| `zc gensecret` | Generate a cryptographically secure `SECRET_KEY` |
+
+
+---
+
+## π Documentation
+
+| Resource | Description |
+|----------|-------------|
+| [π 10-Minute Quickstart](https://fastapi-zcore-framework.github.io/zcore/learn/quickstart/) | Build a complete Task Manager API from scratch |
+| [π Tutorial (Step-by-Step)](https://fastapi-zcore-framework.github.io/zcore/learn/overview/) | Deep dive into each architectural layer |
+| [π§ How-To Guides](https://fastapi-zcore-framework.github.io/zcore/how-to/auth-setup/) | JWT auth, transactions, field masking, storage, WebSockets |
+| [ποΈ Architecture & Reference](https://fastapi-zcore-framework.github.io/zcore/features/infrastructure/di/) | Full API docs for DI, Kernel, Security, Caching, and more |
+| [π Cheat Sheet](https://fastapi-zcore-framework.github.io/zcore/cheatsheet/) | Pocket reference for quick syntax lookup |
---
-## π Documentation & Learning
+## π€ Contributing
-To explore the full capabilities of ZCore, please refer to our online documentation:
+Contributions are welcome! Please read our guidelines before submitting a PR.
-> βΉοΈ **NOTE:** **[Getting Started Guide]** - Step-by-step tutorial to build your first service.
+- **Issues:** Bug reports and feature requests via [GitHub Issues](https://github.com/fastapi-zcore-framework/zcore/issues)
+- **PRs:** Open a pull request with a clear description of the change
+- **Local Setup:** `pip install -e ".[all,dev]"` and run `pytest` to verify
-> π‘ **TIP:** **[Architectural Concepts]** - Understand the inner mechanics of our Scoped DI, UOW, and the Core Kernel.
+---
+
+## π License
-> β οΈ **IMPORTANT:** For production environments, remember to generate a secure secret key using the `zc gensecret` CLI command and update your `.env` configuration file.
+ZCore is licensed under the **Apache License 2.0**.
+See [LICENSE](https://github.com/fastapi-zcore-framework/zcore/blob/master/LICENSE) for details.
---
- ZCore is licensed under the Apache License 2.0. Built with β and architectural rigor.
-
+ Built with β and architectural rigor by Ali Alf Ostovar.
+
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..3183949
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,95 @@
+# Security Policy
+
+We take the security of ZCore, and the applications built on top of it, extremely seriously. Security is a core pillar of ZCore's design philosophy, integrated deeply into its architecture [security/jwt.py, security/hashing.py] rather than treated as an afterthought.
+
+If you believe you have found a security vulnerability in ZCore, please read this document to learn how to report it responsibly.
+
+---
+
+## Supported Versions
+
+ZCore is currently in **active beta development**. Security fixes and patches are prioritized and backported to the latest release branches.
+
+| Version | Status | Security Support Policy |
+| :--- | :--- | :--- |
+| **0.1.x (Beta)** | Active | Security patches are applied to the latest active beta release |
+| **< 0.1.0** | Obsolete | No security support |
+
+*Note: Once stable (1.y.z) releases are published, formal long-term support (LTS) security policies will be established exclusively for stable releases.*
+
+---
+
+## Severity Classification
+
+To help triage reports efficiently, we classify vulnerabilities according to the following severity levels:
+
+| Severity | Description / Examples |
+| :--- | :--- |
+| **Critical** | Remote Code Execution (RCE) in ZCore core components, SQL Injection caused by a vulnerability in ZCore query generation components. |
+| **High** | Authentication bypass, JWT signature forgery, or complete Scoped Context isolation failure. |
+| **Medium** | Information disclosure, scoped data leakage (bypass of Zchema projection), or session fixation. |
+| **Low** | Minor hardening issues, verbose error tracebacks in non-production environments. |
+
+---
+
+## Reporting a Vulnerability
+
+**Please do not report security vulnerabilities via public GitHub Issues.**
+
+To report a vulnerability responsibly, please follow this process:
+
+### 1. Send a Private Report
+We recommend using **GitHub Private Vulnerability Reporting** as your primary method:
+* Go to the "Security" tab of this repository, select "Advisories", and click "Report a vulnerability".
+
+Alternatively, you can contact the maintainer directly at **`alialfostovar@gmail.com`** (the security contact listed in our package metadata).
+
+### 2. What to Include
+To help us triage and patch the issue as quickly as possible, please include:
+- A clear description of the vulnerability and its potential impact.
+- Step-by-step instructions (or a minimal Proof of Concept script) to reproduce the issue.
+- Details of the environment where the issue was reproduced (Python version, ZCore version, database driver).
+
+### 3. Disclosure Policy
+We ask that you observe the principles of **Responsible Disclosure**:
+- Give us reasonable time to investigate and patch the issue before making any information public.
+- Do not exploit the vulnerability beyond what is strictly necessary to prove its existence.
+- When applicable, security advisories may be coordinated with CVE assignment channels.
+
+---
+
+## Our Resolution Process
+
+```mermaid
+flowchart LR
+ A[Report Received] --> B[Triage & Verify
Within 48 Hours]
+ B --> C[Private Patch Development]
+ C --> D[Release Patch
Minor/Patch Release]
+ D --> E[Public Advisory
GitHub Security Advisory]
+```
+
+1. **Acknowledgment:** We will acknowledge receipt of your report within **48 hours** and begin triaging the issue.
+2. **Investigation:** We will work privately to reproduce the vulnerability. If verified, we will develop a fix in a private branch.
+3. **Patch Release:** We will release a patch as a new minor or patch version (e.g., `0.1.1`).
+4. **Advisory:** We will publish a GitHub Security Advisory (GHSA) and coordinate CVE assignment if applicable.
+
+---
+
+## Out of Scope
+
+The following are generally **not considered** security vulnerabilities in ZCore:
+- **Application Misconfiguration:** Failing to configure secure environment variables (e.g., exposing database credentials).
+- **Intentionally Disabled Features:** Disabling built-in security middlewares or choosing not to use Zchema projection.
+- **Weak Credentials:** Brute-force or weak password choices by end-users (which are managed by application-level authentication logic).
+- **Debug Mode:** Leaving FastAPI's debug or reload mode enabled in production.
+
+---
+
+## Hardened Defaults in ZCore
+
+ZCore is built with strict, "secure-by-default" configurations [security/jwt.py, storage/local.py]:
+- **Production Secret Guard:** The framework will raise a fatal traceback and refuse to start in a production environment if the default fallback `SECRET_KEY` is detected [security/jwt.py].
+- **Argon2id default configuration:** We enforce high-entropy password hashing out-of-the-box [security/hashing.py].
+- **Path Traversal Protection:** The local storage provider strictly verifies resolved filesystem paths before running write or unlink operations [storage/local.py].
+
+*Note: ZCore only fails-fast (crashes) on fatal security configurations. Missing optional configuration elements (e.g., optional caching backends) will only raise non-fatal warnings.*
\ No newline at end of file
diff --git a/docs/assets/4-service.png b/docs/assets/4-service.png
new file mode 100644
index 0000000000000000000000000000000000000000..d47a565e8e6cd295856e8a4dac864469f194fb69
GIT binary patch
literal 542951
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