Repository navigation
release(v0.1.0-rc.3): composite primary keys, DI kwargs auto-wiring, keyset pagination, and route sorting - #144
Merged
Merged
Conversation
…ependencies outside active scope
…fault values (#134) Fixes #131 ### Problem `IoCContainer._auto_wire` collected all type-annotated constructor parameters into `dependencies` without checking whether `param.default is not inspect.Parameter.empty`. When a class constructor declared parameters with default values for primitive or unregistered types (e.g., `timeout: int = 30` or `client: Any = None`), `_auto_wire` attempted to resolve those types through the container, raising `TypeError` or unexpected resolution failures. ### Solution - Add `IoCContainer._is_registered` to check whether a dependency type is explicitly registered in `_singletons`, `_scoped_definitions`, `_factories`, or active `_scoped_instances`. - In `IoCContainer._auto_wire`, skip variadic parameters (`*args`, `**kwargs`) and parameters that define a default value (`param.default is not inspect.Parameter.empty`) when their type annotation is not explicitly registered in the container, allowing Python to fall back to the parameter's default value (consistent with `background_task._resolve_injections`). - Add unit tests in `tests/test_di.py` covering primitive/unregistered defaults, mixed required dependencies + defaults, and explicitly registered dependencies with defaults. ### Backward Compatibility 100% backward compatible; all required constructor dependencies and explicitly registered optional dependencies continue to be injected identically.
…ring - Added types import and _unwrap_type to recursively unwrap Annotated, Optional, and Union types - Added is_registered() public helper across all lifecycle stores - Dependency signature cache now maps parameter names to types instead of positional list - Auto-wiring constructs instances via keyword arguments (**resolved_kwargs) - Removed special BaseSettings singleton caching in auto-wire - Removed explicit AsyncSession checks in _auto_wire (handled via scoped definition check) - background_task injection now uses container._unwrap_type - Added tests for registered/unregistered Optional union injection
…gnature - Added _DependencySignature list subclass that preserves list-like inspection while exposing kwargs_map - is_registered() now also treats AsyncSession and active scoped instances as registered - Auto-wire handles both _DependencySignature (kwargs) and legacy list signatures (positional args) - get_settings now checks container.is_registered before resolving, raising DIException otherwise - Tests updated for Optional union registration behavior
…and scope boundary enforcement (#135) ## Description This PR resolves an issue in `IoCContainer._auto_wire` where constructor parameters were previously collected into dependencies purely based on type annotations, without verifying whether a parameter defined a default value (`param.default is not inspect.Parameter.empty`). This caused the container to attempt resolving primitive types (such as `int`, `str`) or unregistered optional dependencies, triggering instantiation failures (`TypeError: int() takes at most 2 arguments (0 given)`). Key architectural enhancements in this PR: 1. **Default Parameter Handling:** Skips unregistered constructor parameters that define default values, allowing Python to safely fall back to the declared default. If a dependency is explicitly registered in the container, it takes precedence and is injected. 2. **Keyword-Based Auto-Wiring (`**kwargs`):** Switched constructor auto-wiring from positional arguments (`*args`) to keyword mapping (`**kwargs`). This completely eliminates parameter misplacement when optional/default parameters precede registered ones or when keyword-only arguments (`KEYWORD_ONLY`) are utilized. 3. **Cache Backward Compatibility (`_DependencySignature`):** Implemented a specialized `list` subclass (`_DependencySignature`) to retain 100% backward compatibility for test assertions expecting a list (`assert cache[cls] == [...]` or `== []`), while internally maintaining the parameter-to-dependency mapping (`kwargs_map`). 4. **Type Unwrapping (`_unwrap_type`):** Added recursive unwrapping for `Annotated`, `typing.Union`, and Python 3.10+ `types.UnionType` (e.g. `IService | None = None`) to correctly evaluate underlying interface registrations. 5. **Scope Boundary Enforcement:** Enforces strict lifecycle boundaries by raising `DIException` when attempting to resolve scoped dependencies or `AsyncSession` outside an active scope context (`_current_scope_id is None`). 6. **Settings Fallback Alignment:** Updated `get_settings()` in `config.py` to check `container.is_registered()` prior to resolution, ensuring singleton registration upon cold starts. Fixes #131 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? The fix was verified against the full test suite using `hatch test`: - **Unit/Integration Tests:** Executed all 521 unit and integration tests across the framework (`tests/test_di.py`, `tests/test_config.py`, etc.). - **Specific Test Cases:** - `test_auto_wire_skips_unregistered_default_parameters`: Validates primitive defaults (`timeout: int = 30`) remain untouched and cache reflects `[]`. - `test_auto_wire_resolves_required_and_skips_unregistered_defaults`: Ensures required dependencies are wired while defaults are skipped. - `test_auto_wire_injects_registered_dependency_even_with_default`: Confirms container registrations take precedence over default parameter values. - `test_auto_wire_injects_registered_dependency_with_optional_union`: Verifies `Union` / `Optional` type unwrapping. - `test_resolve_scoped_outside_scope_raises_di_exception`: Asserts `DIException` is raised when resolving scoped instances outside active scopes. - `test_settings_get_settings_fallback`: Confirms settings fallback auto-registers as a singleton. - [x] Unit/Integration tests (`hatch test`) - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added cursor_field and order fields to CursorParams for per-request override - CursorPagination accepts optional order with 'desc' fallback - _encode_cursor now serializes date, UUID, and Decimal types to strings - Added type coercion for date, UUID, int, and float values in cursor boundary - Clears existing ORDER BY before applying cursor ordering - Repository get_list and search now derive cursor_field/order from pagination or search sort - BaseRouter propagates search sort field/order into CursorParams
…d tests - Consolidated UUID/Decimal cursor serialization into single isinstance branch - Added test_repo_search_with_cursor_pagination_and_sort for asc/desc sort via SearchRequest - Added MockCursorSearchService and test_router_search_with_cursor_pagination_and_sort - Verified cursor propagates search sort field/order through BaseRouter
…ic search sort (#136) ## Description Resolves a conflict between `CursorPagination` and `SearchRequest` sorting criteria. Previously, `SearchEngine.build_base_query` applied the client-specified `ORDER BY` clause while `CursorPagination.paginate` fell back strictly to filtering and sorting by the primary key (`id`). This caused duplicated `ORDER BY` clauses, corrupted compound cursors, and invalid `WHERE id < :val` boundary conditions. ### Key Changes: - **`zcore.db.pagination`:** - Extended `CursorParams` with optional runtime `cursor_field` and `order` attributes. - Added support for serializing `date`, `UUID`, and `Decimal` values in `_encode_cursor`. - Added type coercion for `date`, `uuid.UUID`, `int`, and `float` during cursor coordinate parsing. - Reset existing query ordering (`query = query.order_by(None)`) before applying synchronized keyset `ORDER BY` clauses. - Implemented dynamic compound sorting: `(col < val) OR (col == val AND pk < last_pk)` for `DESC` and `(col > val) OR (col == val AND pk > last_pk)` for `ASC`. - **`zcore.db.repository`:** - Updated `ReadRepositoryMixin.get_list` to respect runtime `cursor_field` and `order` on `CursorParams`. - Updated `SearchRepositoryMixin.search` to automatically extract the primary sort field and order from `SearchRequest.sort` and configure `CursorPagination` accordingly. - **`zcore.web.base_router`:** - Updated `search_endpoint` and `lookup_endpoint` to propagate `search_in.sort[0]` field and order into `CursorParams`. - **Tests:** - Added `test_repo_search_with_cursor_pagination_and_sort` to verify ASC/DESC multi-page keyset pagination with non-PK columns. - Added `test_router_search_with_cursor_pagination_and_sort` to verify end-to-end HTTP payload propagation through `BaseRouter`. Fixes # (issue number) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_repository.py::test_repo_search_with_cursor_pagination_and_sort[desc]` passed. - `tests/test_repository.py::test_repo_search_with_cursor_pagination_and_sort[asc]` passed. - `tests/test_web.py::test_router_search_with_cursor_pagination_and_sort` passed. - Verified no record collision or duplication occurs between successive cursor pages. - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added Date import from sqlalchemy.sql.sqltypes
- Date coercion now triggers on isinstance(col.type, Date), not only python_type
- Strips time component via val.split('T')[0] before date.fromisoformat
- Added test_repo_cursor_pagination_with_date_column_coercion with DateCursorModel
- Consolidated Date import into main sqlalchemy import in test_repository - Moved datetime.date import to top of imports - Reformatted pagination imports to multi-line in test_web
## Description
Resolves a driver-level type mismatch error
(`asyncpg.exceptions.DatatypeMismatchError: operator does not exist:
date < character varying`) when using `CursorPagination` over SQLAlchemy
`Date` columns against strictly-typed databases like PostgreSQL.
### Motivation & Context:
In SQLAlchemy, the `Date` type does not inherit from `DateTime`.
Previously, `CursorPagination.paginate` only verified
`isinstance(col.type, DateTime)`. As a result, cursor values for `Date`
fields remained unparsed string primitives (`str`), which drivers like
`asyncpg` bind as `varchar`/`text`, causing PostgreSQL query execution
failures.
### Key Changes:
- **`src/zcore/db/pagination.py`:**
- Imported `Date` from `sqlalchemy.sql.sqltypes`.
- Added explicit type inspection for `(col_type is date or
isinstance(col.type, Date))`.
- Stripped optional time fragments via `val.split("T")[0]` before
converting to `datetime.date` using `date.fromisoformat(...)`.
- **`tests/test_repository.py`:**
- Added `DateCursorModel`, `DateCursorCreateSchema`, and
`DateCursorRepository`.
- Added regression test
`test_repo_cursor_pagination_with_date_column_coercion` asserting
multi-page keyset pagination with actual `datetime.date` boundaries.
- **`tests/test_web.py`:**
- Cleaned up and formatted multi-line pagination imports.
Fixes #128
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update
## How Has This Been Tested?
- [x] Unit/Integration tests (`hatch test`)
- Ran
`tests/test_repository.py::test_repo_cursor_pagination_with_date_column_coercion`
verifying that parsed cursor values are instances of `datetime.date` and
sequential pages maintain proper ordering without data collisions.
- Ran the full test suite (`test_repository.py`, `test_search.py`,
`test_web.py`) with 0 regressions.
- [x] Static type check passing (`mypy src/`)
## Checklist:
- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Replaced pk/pk_name attributes with pks/pk_names tuples and is_composite_pk flag - Added pk and pk_name convenience properties that raise on composite models - Added _build_pk_condition for model instance, dict, tuple/list, or scalar targets - Added _build_ids_condition supporting scalar in_ and composite tuple_.in_ - Updated get_by_ids, update, update_multi, delete, delete_multi, restore, restore_multi to use composite-aware conditions - BaseRepository __init__ now inspects all primary key columns - Added CompositePartitionModel, CompositeCreateSchema, CompositeUpdateSchema, CompositeRepository test fixtures - Added tests for guarded properties and full composite CRUD cycle
- Added strict=False to zip calls in composite PK condition and update_multi - Collapsed multi-line test calls and parametrize decorators to single lines - Added trailing newline at EOF in repository.py
- Replaced pk/pk_name attributes with pks/pk_names tuples and is_composite_pk flag - Added pk and pk_name convenience properties that raise on composite models - Added _build_pk_condition for model instance, dict, tuple/list, or scalar targets - Added _build_ids_condition supporting scalar in_ and composite tuple_.in_ - Updated get_by_ids, update, update_multi, delete, delete_multi, restore, restore_multi to use composite-aware conditions - BaseRepository __init__ now inspects all primary key columns - Added CompositePartitionModel, CompositeCreateSchema, CompositeUpdateSchema, CompositeRepository test fixtures - Added tests for guarded properties and full composite CRUD cycle
- Added strict=False to zip calls in composite PK condition and update_multi - Collapsed multi-line test calls and parametrize decorators to single lines - Added trailing newline at EOF in repository.py
…b.com:Baseryn/zcore into feat/repository-composite-primary-key-support
#138) ## Description Adds native support for composite primary keys (e.g., `(id, operation_date)`) in `BaseRepository`. Previously, `BaseRepository.__init__` hardcoded single-column PK resolution (`primary_key[0]`), ignoring subsequent PK columns. On composite models and PostgreSQL partitioned tables (e.g., Range/List partitioning), this broke query and mutation methods (`get`, `get_by_ids`, `update`, `delete`, etc.) and disabled PostgreSQL partition pruning, triggering full scans across all partitions. ### Key Changes: - **`src/zcore/db/repository.py`:** - Replaced hardcoded single-column resolution in `__init__` with `self.pks = tuple(mapper.primary_key)` and `self.pk_names = tuple(col.name for col in self.pks)`. - Added automatic zero-configuration reflection flag `self.is_composite_pk = len(self.pks) > 1`. - Implemented guarded convenience properties (`@property def pk` and `@property def pk_name`) adhering to the Fail-Fast design pattern: returns the primary column for single-key models, and raises an explicit `AttributeError` on composite models to prevent silent partial-key queries. - Implemented `_build_pk_condition(target)` to resolve equality filters whether the target is an ORM model instance, a dictionary, a tuple/list, or a scalar. - Implemented `_build_ids_condition(ids)` generating `tuple_(*self.pks).in_(...)` expressions for composite entities, generating standard `(col1, col2) IN ((val1, val2), ...)` clauses that allow PostgreSQL to perform partition pruning. - Updated `exist`, `get`, `get_by_ids`, `update`, `update_multi`, `delete`, `delete_multi`, `restore`, and `restore_multi` to operate via composite-aware conditions. - **`tests/test_repository.py`:** - Added `CompositePartitionModel`, `CompositeCreateSchema`, `CompositeUpdateSchema`, and `CompositeRepository`. - Added `test_repo_composite_guarded_properties` testing Fail-Fast behavior on composite models and backward compatibility on single-key models. - Added `test_repo_composite_primary_key_crud` verifying full CRUD and batch operations across tuples, dicts, and model instances. - **`tests/test_web.py`:** - Applied formatting cleanups. Fixes #129 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_repository.py::test_repo_composite_guarded_properties` passed. - `tests/test_repository.py::test_repo_composite_primary_key_crud` passed (tested single-record and multi-record `get`, `update`, `update_multi`, `delete`, `delete_multi`, and `exist`). - Full test suite passing with 0 regressions. - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added _bind_router_sorting_hooks to wrap add_api_route and add_route, triggering _sort_routes after each registration - Added public sort_routes() wrapper for manual route sorting - Added register_custom_routes() hook called at end of _register_routes for subclass extensions - Updated __init__ docstring to mention auto-sorting hook binding
…uters - Removed duplicated DateCursorModel fixtures from test_repository - Added test_subclassed_router_custom_route_in_init_avoids_shadowing verifying post-super() route additions are re-sorted - Added test_subclassed_router_hook_register_custom_routes verifying the register_custom_routes hook
…shadowing (#139) ## Description Resolves route shadowing issues where custom endpoints registered in `BaseRouter` subclasses were shadowed by dynamic parameterized paths (e.g., `/{id:uuid}` shadowing static paths like `/summary` or `/reports`), causing Starlette to match the dynamic route first and fail with `422 RequestValidationError`. ### Motivation & Context: Previously, `_sort_routes()` was executed strictly once during `_register_routes()` inside `BaseRouter.__init__()`. Any custom endpoint registered by child routers after `super().__init__()` was appended to the tail of `self.router.routes` without participating in specificity sorting. ### Key Changes: - **`src/zcore/web/base_router.py`:** - Added `_bind_router_sorting_hooks()` which wraps `self.router.add_api_route` and `self.router.add_route` after base routes are registered, ensuring that whenever any subsequent route is registered, routes are automatically re-sorted based on path specificity. - Added `register_custom_routes()` hook method called at the end of `_register_routes()` to allow clean subclass extensions without overriding `__init__`. - Added public `sort_routes()` method exposing on-demand route sorting. - **`tests/test_repository.py`:** - Removed duplicate `DateCursorModel` fixture definitions, eliminating the SQLAlchemy declarative `SAWarning`. - **`tests/test_web.py`:** - Added `test_subclassed_router_custom_route_in_init_avoids_shadowing` verifying that routes added in `__init__` after `super().__init__()` are sorted before `/{id:uuid}` and return 200 OK. - Added `test_subclassed_router_hook_register_custom_routes` verifying the `register_custom_routes` lifecycle hook. Fixes #130 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_web.py::test_subclassed_router_custom_route_in_init_avoids_shadowing` passed. - `tests/test_web.py::test_subclassed_router_hook_register_custom_routes` passed. - Full test suite passed cleanly with 0 warnings (`529 passed in 19.46s`). - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added model_validator check in SearchRequest raising ValueError when cursor is non-empty and page > 1 - Added parametrized test_search_request_pagination_mutual_exclusivity covering empty, whitespace, and None cursors - Added test_search_endpoint_rejects_conflicting_pagination_params verifying 422 response via registered handlers
#140) ## Description Resolves an ambiguous API contract where `SearchRequest` allowed both offset pagination (`page > 1`) and keyset pagination (`cursor: str`) to be sent simultaneously. Previously, `BaseRouter.search_endpoint` silently discarded one parameter depending on the configured pagination class, causing hard-to-detect frontend state discrepancies. ### Motivation & Context: Enforces the **Fail-Fast** design principle at the schema validation boundary. When a client explicitly specifies both a cursor token and a page index greater than 1, `SearchRequest` now raises a validation error instead of silently dropping one parameter. Standard requests using default `page=1` with a `cursor`, or offset requests without a `cursor`, remain fully supported without breaking changes. ### Key Changes: - **`src/zcore/db/search.py`:** - Added a validation rule in `SearchRequest._validate_and_bound_size` raising a `ValueError` when `self.cursor` is non-empty/non-whitespace and `self.page > 1`. - **`tests/test_search.py`:** - Added parameterized unit test `test_search_request_pagination_mutual_exclusivity` verifying combinations of `None`, empty string, whitespace, and valid tokens against `page=1` and `page > 1`. - **`tests/test_web.py`:** - Added HTTP endpoint integration test `test_search_endpoint_rejects_conflicting_pagination_params` ensuring FastAPI/ZCore exception handlers return a standard 422 `RequestValidationError` envelope. Fixes #132 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_search.py::test_search_request_pagination_mutual_exclusivity` passed for all parameter combinations. - `tests/test_web.py::test_search_endpoint_rejects_conflicting_pagination_params` passed with HTTP 422 response assertions. - Full test suite passed cleanly with 0 warnings. - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added module-level threading.Lock to guard _ensure_eviction_task and close_cache - Double-checked locking ensures only one eviction task is created under concurrent calls - close_cache now extracts and nullifies _eviction_task inside the lock before awaiting cancellation - Added test_concurrent_ensure_eviction_task_single_instance with ThreadPoolExecutor
## Description Resolves a race condition in `zcore.cache.base._ensure_eviction_task` where concurrent cache operations across multiple thread contexts (e.g., AnyIO worker threads, background workers, or parallel test harness executions) could evaluate `_eviction_task is None` simultaneously and spawn redundant background eviction loops (`_start_eviction_loop`). This caused duplicate memory sweeps and repeated shutdown logs (`Cache memory eviction loop stopped`). ### Motivation & Context: Guarantees strict single-instance lifecycle management for background cache workers. By applying the **Double-Checked Locking** pattern with `threading.Lock()`, task instantiation is thread-safe while preserving high-performance throughput on subsequent calls without unneeded synchronization overhead. ### Key Changes: - **`src/zcore/cache/base.py`:** - Added module-level `_lock = threading.Lock()`. - Implemented double-checked locking in `_ensure_eviction_task` ensuring only one background asyncio task is spawned under concurrent multi-threaded execution. - Updated `close_cache()` to atomically extract and nullify `_eviction_task` within the lock before awaiting its cancellation. - **`tests/test_cache.py`:** - Added `test_concurrent_ensure_eviction_task_single_instance` using `ThreadPoolExecutor` (10 workers, 20 concurrent invocations) verifying that only a single background task is created and maintained. Fixes #133 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Refactoring (architectural or code cleanup with no functional changes) - [ ] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_cache.py::test_concurrent_ensure_eviction_task_single_instance` passed. - Ran full test suite with 0 regressions and 0 warnings. - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Added __version__ resolution via importlib.metadata with rc.3 fallback
- Exported __version__ from zcore package
- CLI --version and banner now use __version__ dynamically
- main.py template now imports and returns __version__
- Fixed repository template class name to use {ModelNameRepository}
- Updated README version references to rc.3
…ursor field - Made cursor_field a property/setter backed by _cursor_field with None default - cursor_field getter raises AttributeError on composite models without explicit override - _build_pk_condition now validates dict completeness, tuple/list length, and rejects scalars for composite models - _build_ids_condition validates composite dict completeness, tuple length, and rejects scalar items - update_multi validates composite dict/tuple keys with descriptive error messages - Removed redundant cursor_field class annotation
- Removed importlib.metadata lookup; __version__ is now a static '0.1.0-rc.3' string
- Fixed repository template class name from {ModelNameRepository} to {ModelName}Repository
- Updated CLI version test assertions to rc.3
- Collapsed multi-line missing-key list comprehension in update_multi
…ite key validation (#142) ## Description Prepares and packages the framework for the **`v0.1.0-rc.3`** release candidate. Centralizes version definitions across CLI utilities and scaffold templates, fixes repository scaffolding syntax, and hardens `BaseRepository` with strict validation rules and guarded properties for composite primary key models. ### Key Changes: - **Version Centralization & Release Bump (`0.1.0-rc.3`):** - Defined and exported static `__version__ = "0.1.0-rc.3"` in `src/zcore/__init__.py`. - Replaced hardcoded version strings in CLI banner (`ui.py`), CLI `--version` output (`cli/__init__.py`), scaffolded `main.py` template (`templates.py`), and `README.md`. - Updated CLI version test assertions in `tests/test_cli.py`. - **Repository Scaffolding Template Fix:** - Corrected repository class name generation in `src/zcore/cli/templates.py` from `{ModelNameRepository}` to `{ModelName}Repository`. - **Repository Composite Key Hardening & Guarded Properties:** - Converted `cursor_field` into a guarded property/setter backed by `_cursor_field = None`. Accessing `cursor_field` on composite primary key models without an explicit field override or request-level sort criteria raises a descriptive `AttributeError` (Fail-Fast pattern). - Enforced strict validation in `_build_pk_condition`: verifies dictionary completeness (supporting both `col.key` and `col.name`), validates exact tuple length matching (`strict=True`), and rejects scalar inputs for composite entities with clear error messages. - Hardened `_build_ids_condition`: validates composite dictionaries and tuples, preventing invalid single-item tuple fallbacks and raising `ValueError` on scalar items. - Hardened `update_multi`: validates composite key dictionaries/tuples and ensures complete primary key payloads are assembled for bulk DBAPI operations. Fixes # (issue number or release milestone) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [x] Refactoring (architectural or code cleanup with no functional changes) - [x] Documentation update ## How Has This Been Tested? - [x] Unit/Integration tests (`hatch test`) - `tests/test_cli.py::test_cli_version_output_rc2` (updated for rc.3) passed. - `tests/test_repository.py` composite CRUD and guarded property tests passed. - Ran the full test suite (`529 passed in 19.46s`) with zero warnings and zero regressions. - [x] Static type check passing (`mypy src/`) ## Checklist: - [x] My code follows the style guidelines and architectural rules of this project - [x] I have performed a self-review of my own code - [x] My changes pass strict static type analysis (`mypy src/` generates no warnings) - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation in `zcore-docs` if applicable - [x] My changes generate no new runtime warnings - [x] I have added tests that prove my fix is effective or that my feature works
- Rewrote overview with concrete architectural challenges and less marketing language - Rebranded ZShop as a reference implementation with factual capability descriptions - Reworked comparison table with neutral phrasing and added composite PK details - Added new details section on composite primary keys and keyset pagination - Renamed 'Core Architectural Patterns' to 'Core Architectural Components' with Problem/Solution structure - Simplified quick start endpoint list and CLI command reference - Reframed community testing section to focus on technical feedback - Updated footer attribution
…yOverride fixture - Track whether stub had an override and store its original value in setUp - tearDown restores the original override instead of unconditionally deleting
…ion endpoint
- Added RouteKey.SCHEMA and schema_path attribute (default '/_schema')
- Added schema_endpoint resolving create/update/view/lookup schemas with optional ?action= filter
- Schema endpoint uses optional user stub for authentication
- Removed expose_schemas, _get_openapi_extra, find_input_schema, find_output_schema, and inline schema generation from ZCoreAPIRoute
- All endpoints now declare explicit openapi_extra={'action': ...}
- get_route_action returns empty string for unmapped route keys
- Added tests for action-specific, full-resource, and invalid-action schema introspection
- Added test for RouteKey.SCHEMA exclusion and route sorting
- Updated README to document /_schema endpoint
…g.Lock (#133) (#143) ### Summary This PR addresses #133 by guarding background memory eviction task creation in `zcore.cache.base` with a `threading.Lock` double-checked initialization pattern. ### Changes 1. **Thread-Safe Eviction Task Scheduling**: Added `_eviction_lock = threading.Lock()` in `zcore.cache.base`. In `_ensure_eviction_task()`, the loop check and `loop.create_task(_start_eviction_loop(...))` assignment are guarded via `with _eviction_lock:` with double-checked verification. 2. **Synchronized Shutdown in `close_cache()`**: Wrapped `_eviction_task` retrieval and clearing inside `with _eviction_lock:` to prevent concurrent threads from scheduling a new task while shutdown is in flight. 3. **Regression Tests**: Added `test_ensure_eviction_task_concurrent_thread_safety` in `tests/test_cache.py` exercising concurrent invocation across 20 threads to guarantee exactly one eviction loop task is spawned. ### Verification - `uv run ruff format --check`: Passed cleanly. - `uv run ruff check`: Passed cleanly. - `uv run --all-extras pytest`: 516/516 passed. Closes #133.
- Collapsed loop.create_task call to a single line - Removed unused ThreadPoolExecutor import from test_cache - Added trailing newlines at EOF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This release candidate (
v0.1.0-rc.3) introduces critical architectural hardening, PostgreSQL partition pruning support, thread-safety guarantees, and robust routing mechanics to finalize framework stability ahead of the upcomingv1.0.0release.Summary
IoCContainer._auto_wireto assemble dependencies using keyword mappings (**kwargs) instead of positional arguments, eliminating argument misplacement when defaults orKEYWORD_ONLYparameters are present. Addedis_registered()across all lifecycle stores to safely skip unregistered parameters with declared defaults (e.g.timeout: int = 30), allowing Python defaults to apply cleanly._unwrap_typeto resolve underlying dependency contracts acrossAnnotated,typing.Union, and Python 3.10+types.UnionType(e.g.,IService | None = None). Introduced_DependencySignature(a specializedlistsubclass exposingkwargs_map) to preserve backward compatibility for legacy list-based reflection assertions.DIExceptionwhen attempting to resolve scoped dependencies orAsyncSessionoutside an active scope context (_current_scope_id is None). Alignedget_settings()inconfig.pyto checkis_registeredprior to resolution, ensuring cold-start singleton fallback registration.AbstractRepositoryandBaseRepositorywithpks,pk_names, andis_composite_pkreflection. Added_build_ids_conditionproducingtuple_(*self.pks).in_(...)expressions to enable native PostgreSQL partition pruning on partitioned tables. Implemented guarded fail-fast properties (pk,pk_name,cursor_field) that raise descriptiveAttributeErrors when accessed on composite entities without explicit overrides. Updated single and batch CRUD operations (get_by_ids,update,update_multi,delete,delete_multi,restore,restore_multi) to support composite tuples, dicts, and model instances.CursorParamswith runtimecursor_fieldandorderoverrides. Implemented dynamic compound keyset condition evaluation ((col < val) OR (col == val AND pk < last_pk)forDESC), reset conflicting query order clauses (order_by(None)), and propagatedSearchRequest.sort[0]fromBaseRouterthrough repository search queries.asyncpg: Resolved driver-levelDatatypeMismatchErrorexceptions against PostgreSQL by explicitly inspecting(col_type is date or isinstance(col.type, Date)), stripping ISO time fragments viaval.split('T')[0], and parsing values into nativedatetime.dateobjects.SearchRequestraising a validation error when both a keyset token (cursor) and offset page index (page > 1) are supplied simultaneously, returning a structured HTTP 422 envelope._bind_router_sorting_hooks) wrappingrouter.add_api_routeandrouter.add_routeinBaseRouter, ensuring subsequent route registrations on subclasses are automatically re-sorted to prevent dynamic parameterized paths (e.g./{id:uuid}) from shadowing static endpoints (e.g./summary). Added theregister_custom_routes()subclass lifecycle hook and exposed a publicsort_routes()utility.GET /_schemaIntrospection Endpoint: Replaced the?schema=truequery parameter interceptor inZCoreAPIRoutewith a dedicatedRouteKey.SCHEMAendpoint (GET /_schema) supporting optional?action=create|update|view|lookupquery filters and optional user authentication._ensure_eviction_taskandclose_cache()inzcore.cache.basewiththreading.Lockusing the double-checked locking pattern, eliminating race conditions that previously spawned redundant background eviction tasks under concurrent multi-threaded execution.DependencyOverrideintesting/fixtures.pyto record pre-existing dependency overrides duringsetUp()and restore them upontearDown(), preventing cross-test dependency override pollution.{ModelName}Repository), statically exported__version__ = "0.1.0-rc.3", and restructuredREADME.mdwith technical, neutral architecture and benchmark documentation.Impact
Eliminates driver-level query failures on PostgreSQL partitioned tables and date columns, prevents race conditions during background cache task initialization, resolves route shadowing on custom router extensions, and ensures safe constructor dependency injection when default parameters are declared.
Fixes #128, Fixes #129, Fixes #130, Fixes #131, Fixes #132, Fixes #133
🤝 Acknowledgements
Special thanks to @dwaddle for diagnosing and contributing solutions for the IoC container kwargs signature mapping (#131) and cache background task concurrency race conditions.
Type of change
?schema=truehas been deprecated and removed. Applications must now query the dedicatedGET /<resource>/_schemaendpoint (with optional?action=create|update|view|lookup).SearchRequestnow strictly rejects simultaneouscursorandpage > 1payloads with an HTTP 422 error instead of silently dropping one parameter..pkor.pk_nameon a repository whose model uses composite primary keys now raises anAttributeError. Use.pksor.pk_namesinstead.AsyncSessionor scoped classes outside of an active HTTP request orbackground_scopenow raisesDIException.How Has This Been Tested?
hatch test)IoCContainerskipping unregistered defaults, injecting registered defaults, resolvingUnion/Optionaltypes, and raisingDIExceptionoutside active scopes.pk,pk_name), and batch operations with tuple/dict keys.Datecolumn type coercion.SearchRequest(cursor=..., page=2).__init__and viaregister_custom_routes.GET /_schema._ensure_eviction_taskwithThreadPoolExecutor(20 workers) ensuring single-instance task creation.mypy src/)ruff check src/,ruff format --check src/).Checklist:
mypy src/generates no warnings)zcore-docsif applicable