Skip to content

Releases: fastkit-org/fastkit-core

Version 0.4.4

Choose a tag to compare

@radomirbrkovic radomirbrkovic released this 26 Jun 08:55
ef3fc88

[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 section
  • feature_request.md — feature proposals with problem statement, proposed API, and alternatives
  • good_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 - Beta to 5 - 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

Choose a tag to compare

@radomirbrkovic radomirbrkovic released this 09 May 08:52
0958d25

[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, optional message, and status_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_next is derived automatically from next_cursor is not None.
  • Exported from fastkit_core/http/__init__.py alongside 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 causeget_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 via model_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 over json.dumps(_to_serializable(value)).
    All cache writes now store a JSON string.
  • _from_serializable(item) — fully recursive counterpart to _to_serializable.
    Reconstructs tuple from the __tuple__ sentinel at any nesting depth.
    Previously _deserialize only checked the top level, so tuple inside tuple
    was silently reconstructed as list.
  • _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 compatiblejson.dumps("hello")'"hello"'
json.loads(...)"hello". Plain str and int values are unaffected.

Version 0.4.2

Choose a tag to compare

@radomirbrkovic radomirbrkovic released this 04 May 17:05
8389b9a

[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 = None parameter. The value is forwarded directly to
    redis.asyncio.Redis. An empty string is normalised to None (password or None).
  • CacheManager._make_backend_instance (fastkit_core/cache/manager.py) — now
    reads password from cache.DEFAULT config and passes it to RedisBackend.
    Defaults to None when 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 compatiblepassword defaults to None, matching the previous
behaviour for unauthenticated Redis instances. No existing code requires changes.

Version 0.4.1

Choose a tag to compare

@radomirbrkovic radomirbrkovic released this 21 Apr 06:14
a455d1d

[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-style field__operator parsing.
  • _has_soft_delete() and query() — moved into the mixin. Eliminates
    identical method declarations in both classes.
  • _build_pagination_meta() — new shared method. Calculates total_pages,
    has_next, and has_prev from page, per_page, and total. Both paginate()
    implementations now delegate to this method instead of constructing the dict inline.

Repository — Feature Parity

  • filter_or() added to AsyncRepository. 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() in Repository — 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_relations and _order_by
    support consistent with other read methods.
  • delete_many() in Repository — added missing force: bool = False parameter.
    Previously always performed a hard delete, ignoring soft delete even when available.
    Now consistent with AsyncRepository.delete_many() and Repository.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
      plain dict of column values. Passing ORM instances directly to success_response()
      or paginated_response() no longer raises TypeError.
    • Recursive dict/list handling — nested Pydantic models, ORM instances, or mixed
      structures inside dict and list values are now fully serialized. Previously only
      the top-level object was processed.

Fixed

  • filter_or() in Repository silently dropped all **and_filters keyword arguments.
  • delete_many() in Repository always performed a hard delete, bypassing soft delete.
  • _serialize() raised TypeError when passed a SQLAlchemy model instance directly.
  • _serialize() did not recurse into dict or list values, leaving nested objects
    unserializable.

Version 0.4.0

Choose a tag to compare

@radomirbrkovic radomirbrkovic released this 11 Apr 11:27

[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 via fnmatch. Works out of the box with no additional install.
  • RedisBackend — fully async Redis client via redis.asyncio. TTL delegated to Redis natively. Available via pip install fastkit-core[redis].
  • CacheManager — singleton interface that delegates to the configured backend. Initialize once at startup with setup_cache(config), then import the cache proxy anywhere.
  • cache proxy — module-level lazy proxy (from fastkit_core.cache import cache) eliminates the need for explicit get_cache() calls throughout application code.
  • @cached decorator — cache async function results with a single line. Accepts a static string key or a Callable[..., 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_by now accepts str | list[str] in filter(), get_all(), first(), and paginate() on Repository, AsyncRepository, BaseCrudService, and AsyncBaseCrudService.
  • 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, and AsyncBaseCrudService.
  • Uses the per_page + 1 trick to detect last page without a COUNT(*) query.
  • Cursor is a URL-safe base64-encoded string — opaque to the client, safe to pass as a query parameter.
  • datetime cursor values are serialized to ISO 8601 automatically.
  • Supports all existing Django-style filter operators (field__gte, field__in, etc.).
  • cursor_field defaults to id; any indexed column can be used.
  • Ordering is implicit from cursor_field + direction — no separate _order_by parameter 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 page

Signal / 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 in backends/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 UserWarning is emitted at send time if the payload is not a dict, 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.

  • BaseSchemafrom_attributes=True enabled by default via model_config. ORM instances can now be passed directly to model_validate() without explicit config on every subclass.
  • BaseCreateSchema — new base class for POST request bodies. Sets extra='forbid' (returns 422 on 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. Sets extra='forbid'. Convention: all fields declared as Optional with None default so only explicitly provided fields are written to the database.
  • to_dict(exclude_none) — convenience wrapper over model_dump() available on all BaseSchema subclasses.
  • to_json_str(exclude_none) — convenience wrapper over model_dump_json().
  • config_exclude_none() and config_exclude_fields(fields) — class methods that return ConfigDict and serve as documentation anchors communicating serialization intent. (Pydantic v2 does not support exclusion at ConfigDict level; actual exclusion uses to_dict(exclude_none=True) or Field(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 directly

Rate 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 with Depends() at router or endpoint level.
  • Fixed-window algorithm with in-memory per-instance storage.
  • Default key is client IP from request.client.host with X-Forwarded-For header support.
  • key_func — optional callable (request) -> str for per-user, per-API-key, or any custom rate limiting strategy.
  • Raises TooManyRequestsException (HTTP 429) with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Response body uses FastKit's standard error_response() format.
  • TooManyRequestsException added to the exceptions hierarchy, inheriting from FastKitException. The global exception handler passes its headers to the JSONResponse automatically.
# 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 returns 200 OK while the process is running. No checks performed.
  • GET /health/ready — readiness probe. Runs all registered checks. Returns 200 OK if all pass, 503 Service Unavailable if any check has status='error'.
  • HealthCheck Pydantic model: name, status ('ok' | 'error' | 'degraded'), detail, latency_ms.
  • HealthResponse Pydantic model: status, version (from importlib.metadata, cached with @lru_cache), checks.
  • Custom checks registered as async callables that return HealthCheck. D...
Read more