Releases: fastkit-org/fastkit-core
Release list
Version 0.4.4
[0.4.4] — 2026-06-26
This release is a metadata and contributor experience update. No breaking changes,
no API changes — purely improvements to package discoverability, contributor
onboarding, and project documentation.
Added
GitHub — Issue Templates (.github/ISSUE_TEMPLATE/)
Added three issue templates to standardize contributor interactions:
bug_report.md— structured bug reports with environment details, reproduction steps, and traceback sectionfeature_request.md— feature proposals with problem statement, proposed API, and alternativesgood_first_issue.md— template for maintainers to create well-defined tasks for new contributors
GitHub — CONTRIBUTING.md (.github/CONTRIBUTING.md)
Added a contributing guide that surfaces automatically when opening issues or PRs on GitHub.
Links to the full guide at fastkit.org/contribute and includes
a quick start using uv sync and uv run commands.
Changed
PyPI — Package Metadata (pyproject.toml)
- Description updated to include all key features: repository pattern, service layer,
i18n,TranslatableMixin, cache decorators, events & signals (pub-sub),AI_CONTEXT.md
support, and standardized responses - Keywords expanded with
i18n,internationalization,repository-pattern,
service-layer,translatable,cache,events,signals,pub-sub - Development Status changed from
4 - Betato5 - Production/Stable - Classifiers expanded with
Topic :: Database,Topic :: Software Development :: Code Generators,
Environment :: Web Environment,Operating System :: OS Independent,Typing :: Typed
Version 0.4.3
[0.4.3] — 2026-05-09
This release fixes a critical bug that made @cached unusable with composite
return types, extends the decorator with full recursive serialization support,
and adds the cursor_paginated_response() helper for consistency with the
existing paginated_response() API. No breaking changes.
Added
HTTP Responses — cursor_paginated_response() (fastkit_core/http/responses.py)
Added cursor_paginated_response() to standardize cursor-based pagination responses
and eliminate per-endpoint boilerplate. Mirrors the API of the existing
paginated_response() helper.
- Accepts
items,next_cursor,per_page, optionalmessage, andstatus_code. - All items are automatically serialized via the shared
_serialize()helper —
Pydantic models, SQLAlchemy instances, plain dicts, and nested structures all
work without manual.model_dump()calls. has_nextis derived automatically fromnext_cursor is not None.- Exported from
fastkit_core/http/__init__.pyalongside existing response helpers.
# Before — manual boilerplate on every endpoint
items, next_cursor = await service.cursor_paginate(cursor=cursor, per_page=per_page)
return success_response(data={
'items': [item.model_dump() for item in items],
'next_cursor': next_cursor,
'per_page': per_page,
'has_next': next_cursor is not None,
})
# After — one line, consistent shape
return cursor_paginated_response(items=items, next_cursor=next_cursor, per_page=per_page)Response shape:
{
"success": true,
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6IDIwfQ==",
"per_page": 20,
"has_next": true
}
}Fixed
Cache — @cached decorator crashes with tuple and Pydantic return values
@cached passed the raw Python return value directly to the cache backend without
serialization. Redis only accepts bytes, str, int, or float, so any function
returning a tuple, Pydantic model, or list[BaseModel] raised
redis.exceptions.DataError on the first cache write. The decorator was effectively
unusable on the most common FastKit service patterns (paginate, get_all, find).
Root cause — get_cache().set(cache_key, result) stored the raw object.
On cache hit, get_cache().get(cache_key) returned a raw string instead of the
original structure, so even if the write had succeeded, the caller would receive
a corrupted value.
Fix — introduced four serialization helpers in fastkit_core/cache/decorators.py:
_to_serializable(item)— recursively converts any value to a JSON-safe
structure. Pydantic models are converted viamodel_dump(). Tuples are wrapped
with a{"__tuple__": true, "items": [...]}sentinel that survives JSON
serialization. Lists and dicts are recursed into._serialize(value) -> str— thin wrapper overjson.dumps(_to_serializable(value)).
All cache writes now store a JSON string._from_serializable(item)— fully recursive counterpart to_to_serializable.
Reconstructstuplefrom the__tuple__sentinel at any nesting depth.
Previously_deserializeonly checked the top level, sotupleinsidetuple
was silently reconstructed aslist._deserialize(raw: str)— thin wrapper over_from_serializable(json.loads(raw)).
All cache reads now deserialize back to the original structure.
Supported return types and their cache-hit shapes:
| Return type | Cache-hit type |
|---|---|
str, int, float, bool |
unchanged |
dict |
dict |
list[dict] |
list[dict] |
tuple |
tuple (reconstructed via sentinel) |
tuple inside tuple |
fully reconstructed at all depths |
BaseModel |
dict (via model_dump()) |
list[BaseModel] |
list[dict] |
tuple(list[BaseModel], dict) |
tuple(list[dict], dict) — exact paginate() shape |
Backwards compatible — json.dumps("hello") → '"hello"' →
json.loads(...) → "hello". Plain str and int values are unaffected.
Version 0.4.2
[0.4.2] — 2026-05-04
Patch release. Fixes a bug that made RedisBackend unusable with any
password-protected Redis instance.
Fixed
Cache — RedisBackend missing password authentication support
RedisBackend did not accept a password parameter, and CacheManager did not
read the password key from the cache configuration. Any Redis instance configured
with requirepass raised AuthenticationError immediately on the first cache
operation, making RedisBackend unusable in production environments.
RedisBackend.__init__(fastkit_core/cache/backends/redis.py) — added
password: str | None = Noneparameter. The value is forwarded directly to
redis.asyncio.Redis. An empty string is normalised toNone(password or None).CacheManager._make_backend_instance(fastkit_core/cache/manager.py) — now
readspasswordfromcache.DEFAULTconfig and passes it toRedisBackend.
Defaults toNonewhen the key is absent, preserving backward compatibility with
unauthenticated Redis setups.
# config/cache.py
DEFAULT = {
"driver": "redis",
"host": os.getenv("REDIS_HOST", "localhost"),
"port": int(os.getenv("REDIS_PORT", 6379)),
"password": os.getenv("REDIS_PASSWORD", None), # now respected
"ttl": int(os.getenv("CACHE_TTL", 300)),
}Backwards compatible — password defaults to None, matching the previous
behaviour for unauthenticated Redis instances. No existing code requires changes.
Version 0.4.1
[0.4.1] — 2026-04-21
This release eliminates sync/async code duplication in the repository layer,
adds missing feature parity between the two implementations, and introduces
automatic Pydantic and SQLAlchemy serialization in HTTP response helpers.
No breaking changes.
Changed
Repository — Shared Base Logic (fastkit_core/database/base_repository.py)
Extracted all non-I/O logic from Repository and AsyncRepository into a shared
_BaseRepositoryMixin. Both classes now inherit the same query-building helpers
and only differ in session type and await usage.
_parse_field_operator()— moved from both repository classes into the mixin.
Single source of truth for Django-stylefield__operatorparsing._has_soft_delete()andquery()— moved into the mixin. Eliminates
identical method declarations in both classes._build_pagination_meta()— new shared method. Calculatestotal_pages,
has_next, andhas_prevfrompage,per_page, andtotal. Bothpaginate()
implementations now delegate to this method instead of constructing the dict inline.
Repository — Feature Parity
filter_or()added toAsyncRepository. Previously only available on the sync
Repository. Supports OR groups, AND conditions,_load_relations, and_order_by
— identical API to the sync version.filter_or()inRepository— fixed incomplete implementation.and_filters
keyword arguments were accepted but silently ignored. Now correctly applied as
AND conditions on top of the OR clause. Added_load_relationsand_order_by
support consistent with other read methods.delete_many()inRepository— added missingforce: bool = Falseparameter.
Previously always performed a hard delete, ignoring soft delete even when available.
Now consistent withAsyncRepository.delete_many()andRepository.delete().
HTTP Responses — Automatic Serialization (fastkit_core/http/responses.py)
_serialize()— extended with recursive serialization and two new fallbacks:- SQLAlchemy model fallback — objects with
__table__are serialized to a
plaindictof column values. Passing ORM instances directly tosuccess_response()
orpaginated_response()no longer raisesTypeError. - Recursive dict/list handling — nested Pydantic models, ORM instances, or mixed
structures insidedictandlistvalues are now fully serialized. Previously only
the top-level object was processed.
- SQLAlchemy model fallback — objects with
Fixed
filter_or()inRepositorysilently dropped all**and_filterskeyword arguments.delete_many()inRepositoryalways performed a hard delete, bypassing soft delete._serialize()raisedTypeErrorwhen passed a SQLAlchemy model instance directly._serialize()did not recurse intodictorlistvalues, leaving nested objects
unserializable.
Version 0.4.0
[0.4.0] — 2026-04-11
This release focuses on infrastructure that every production application needs but
typically implements from scratch on every project: caching, rate limiting, health checks,
and event-driven communication between modules. All additions are backward compatible —
existing code requires no changes.
Added
Caching Layer (fastkit_core/cache/)
A unified, backend-agnostic caching layer with zero required new dependencies.
InMemoryBackend— in-process dict storage, lazy TTL expiry, wildcard pattern invalidation viafnmatch. Works out of the box with no additional install.RedisBackend— fully async Redis client viaredis.asyncio. TTL delegated to Redis natively. Available viapip install fastkit-core[redis].CacheManager— singleton interface that delegates to the configured backend. Initialize once at startup withsetup_cache(config), then import thecacheproxy anywhere.cacheproxy — module-level lazy proxy (from fastkit_core.cache import cache) eliminates the need for explicitget_cache()calls throughout application code.@cacheddecorator — cache async function results with a single line. Accepts a static string key or aCallable[..., str]lambda for dynamic keys derived from function arguments. Enforces async-only usage at decoration time.- TTL control at three levels: config default, per-call override, and no-expiry (
ttl=None). - Pattern invalidation (
cache.invalidate('users:*')) — clears entire key namespaces in one call.
from fastkit_core.cache import cache, cached
await cache.set('users:all', users, ttl=300)
data = await cache.get('users:all')
await cache.invalidate('users:*')
@cached(ttl=60, key=lambda user_id: f'user:{user_id}')
async def get_user(user_id: int) -> UserResponse: ...Multi-Field Ordering for Repository and Service Layer
Extended _order_by to accept a list of fields in addition to a single string. Applied
across all read methods on both sync and async classes.
_order_bynow acceptsstr | list[str]infilter(),get_all(),first(), andpaginate()onRepository,AsyncRepository,BaseCrudService, andAsyncBaseCrudService.- Prefix
-for descending, no prefix for ascending — consistent with existing single-field behaviour. - Fields are applied left-to-right. Unknown or invalid field names are silently ignored.
- Fully backward compatible — single string usage is unchanged.
# Two fields — newest first, then alphabetical
items = await repo.filter(
status='active',
_order_by=['-created_at', 'name']
)
# Three fields with pagination
items, meta = service.paginate(
page=1, per_page=20,
_order_by=['-priority', 'due_date', 'name']
)Cursor-Based Pagination (cursor_paginate)
Added cursor_paginate() as a performant alternative to offset-based paginate() for
large datasets and feed-style endpoints.
- Available on
Repository,AsyncRepository,BaseCrudService, andAsyncBaseCrudService. - Uses the
per_page + 1trick to detect last page without aCOUNT(*)query. - Cursor is a URL-safe base64-encoded string — opaque to the client, safe to pass as a query parameter.
datetimecursor values are serialized to ISO 8601 automatically.- Supports all existing Django-style filter operators (
field__gte,field__in, etc.). cursor_fielddefaults toid; any indexed column can be used.- Ordering is implicit from
cursor_field+direction— no separate_order_byparameter to avoid cursor inconsistency. - Response schema mapping applied automatically at service layer — returns
list[ResponseSchema], not raw model instances.
# First page
items, next_cursor = await repo.cursor_paginate(per_page=20)
# Next page
items, next_cursor = await repo.cursor_paginate(
per_page=20,
cursor=next_cursor,
cursor_field='created_at',
direction='desc',
status='active'
)
# next_cursor is None on the last pageSignal / Event System (fastkit_core/events/)
A lightweight, in-process signal system for decoupling side effects and cross-module
communication. Designed for forward compatibility with broker backends planned for 0.5.0.
Signal(name)— module-level signal instance. Receivers connect to the object, not the name string.@signal.connect— decorator that registers async or sync receivers. Returns the receiver unchanged.await signal.send(payload)— always async, even in-process, for forward compatibility with 0.5.0 broker backends.- Error isolation — a receiver exception is caught, logged with full traceback, and never propagates to the sender or to other receivers. All receivers always run.
signal.disconnect(receiver)— explicit unregistration.signal.connected_to(receiver)— context manager for temporary connections; receiver is disconnected on exit even if an exception is raised. Primarily useful in tests.BaseSignalBackend— abstract backend class already present inbackends/base.py, making the 0.5.0 broker backend a purely additive change.InProcessBackend— default backend; dispatches receivers in registration order within the current process.- Payload warning — a
UserWarningis emitted at send time if the payload is not adict,dataclass, or Pydantic model, signaling that the payload will not survive broker serialization in 0.5.0.
# users/signals.py
from fastkit_core.events import Signal
user_created = Signal('user.created')
user_deleted = Signal('user.deleted')
# users/listeners.py
from .signals import user_created
@user_created.connect
async def send_welcome_email(payload: dict) -> None:
await email_service.send_welcome(payload['email'])
# users/service.py
async def after_create(self, instance: User) -> None:
await user_created.send({'id': instance.id, 'email': instance.email})BaseSchema Improvements — BaseCreateSchema, BaseUpdateSchema, Serialization Helpers
Extended the validation base classes to standardize create/update/response schema patterns.
BaseSchema—from_attributes=Trueenabled by default viamodel_config. ORM instances can now be passed directly tomodel_validate()without explicit config on every subclass.BaseCreateSchema— new base class for POST request bodies. Setsextra='forbid'(returns422on unexpected fields) and auto-strips leading/trailing whitespace from all string fields via@field_validator('*', mode='before').BaseUpdateSchema— new base class for PATCH request bodies. Setsextra='forbid'. Convention: all fields declared asOptionalwithNonedefault so only explicitly provided fields are written to the database.to_dict(exclude_none)— convenience wrapper overmodel_dump()available on allBaseSchemasubclasses.to_json_str(exclude_none)— convenience wrapper overmodel_dump_json().config_exclude_none()andconfig_exclude_fields(fields)— class methods that returnConfigDictand serve as documentation anchors communicating serialization intent. (Pydantic v2 does not support exclusion atConfigDictlevel; actual exclusion usesto_dict(exclude_none=True)orField(exclude=True).)
class UserCreate(BaseCreateSchema):
name: str # whitespace stripped, extra fields forbidden
email: str
class UserUpdate(BaseUpdateSchema):
name: str | None = None # only sent fields are updated
email: str | None = None
class UserResponse(BaseSchema):
id: int
name: str
# from_attributes=True already set — works with ORM instances directlyRate Limiting (fastkit_core/http/rate_limit.py)
A FastAPI Depends()-based rate limiter with zero required new dependencies.
RateLimit(limit, per, key_func)— callable class that implements FastAPI's dependency protocol. Use withDepends()at router or endpoint level.- Fixed-window algorithm with in-memory per-instance storage.
- Default key is client IP from
request.client.hostwithX-Forwarded-Forheader support. key_func— optional callable(request) -> strfor per-user, per-API-key, or any custom rate limiting strategy.- Raises
TooManyRequestsException(HTTP 429) withRetry-After,X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Resetheaders. Response body uses FastKit's standarderror_response()format. TooManyRequestsExceptionadded to the exceptions hierarchy, inheriting fromFastKitException. The global exception handler passes itsheadersto theJSONResponseautomatically.
# Router-level — one line covers all endpoints
auth_router = APIRouter(
dependencies=[Depends(RateLimit(10, per='minute'))]
)
# Per-user rate limiting
@app.post('/export')
async def export(_: None = Depends(RateLimit(
10, per='day',
key_func=lambda req: req.state.user_id
))):
...Standardized Health Check Router (fastkit_core/http/health.py)
A create_health_router() factory that generates a ready-to-mount APIRouter with
liveness and readiness endpoints.
GET /health/— liveness probe. Always returns200 OKwhile the process is running. No checks performed.GET /health/ready— readiness probe. Runs all registered checks. Returns200 OKif all pass,503 Service Unavailableif any check hasstatus='error'.HealthCheckPydantic model:name,status('ok' | 'error' | 'degraded'),detail,latency_ms.HealthResponsePydantic model:status,version(fromimportlib.metadata, cached with@lru_cache),checks.- Custom checks registered as async callables that return
HealthCheck. D...