Author SHA1 Message Date
renovate-bot 594b131d17 Update actions/checkout action to v7 2026-06-19 00:03:11 +00:00
d3vyce 61b144328d feat: add missing image to project
Build Blog Docker Image / build docker (push) Failing after 13m22s
2026-06-14 07:44:48 -04:00
d3vyce ef0048577d bump: blowfish/hugo/nginx version
Build Blog Docker Image / build docker (push) Successful in 2m0s
2026-05-14 06:02:00 -04:00
d3vyce a1bce5bb33 fix Taskiq Deduplication project date
Build Blog Docker Image / build docker (push) Successful in 1m14s
2026-05-03 04:50:19 -04:00
d3vyce 9a43ed9ae0 update FastAPI Toolsets project
Build Blog Docker Image / build docker (push) Successful in 1m26s
2026-05-03 04:47:03 -04:00
d3vyce f252d3bdd3 add Taskiq Deduplication project 2026-05-03 04:46:15 -04:00
d3vyce 38127874b5 bump: blowfish/hugo version 2026-05-03 04:44:18 -04:00
d3vyce a37cafc53c update fastapi pagination article
Build Blog Docker Image / build docker (push) Successful in 1m55s
2026-04-09 03:59:56 -04:00
9 changed files with 160 additions and 57 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: linux_amd runs-on: linux_amd
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
# with: # with:
# lfs: 'true' # lfs: 'true'
- name: Checkout LFS - name: Checkout LFS
+3 -3
View File
@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM hugomods/hugo:0.155.3 AS build FROM hugomods/hugo:0.161.1 AS build
ARG BLOWFISH_VERSION ARG BLOWFISH_VERSION
@@ -7,11 +7,11 @@ WORKDIR /opt/blog
COPY . /opt/blog/ COPY . /opt/blog/
RUN git submodule update --init --recursive && \ RUN git submodule update --init --recursive && \
git -C themes/blowfish/ checkout v2.98.0 git -C themes/blowfish/ checkout v2.103.0
RUN hugo RUN hugo
# Publish Stage # Publish Stage
FROM nginx:1.29-alpine FROM nginx:1.31-alpine
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
COPY --from=build /opt/blog/public /usr/share/nginx/html/ COPY --from=build /opt/blog/public /usr/share/nginx/html/
@@ -31,9 +31,13 @@ GET /articles?page=2&items_per_page=20
```json ```json
{ {
"items": [...], "items": [...],
"total": 143, "pagination": {
"page": 2, "total_count": 143,
"total_pages": 8 "items_per_page": 20,
"page": 2,
"has_more": true,
"pages": 8
}
} }
``` ```
@@ -52,8 +56,12 @@ GET /articles?cursor=eyJpZCI6IjEyMyJ9&items_per_page=20
```json ```json
{ {
"items": [...], "items": [...],
"next_cursor": "eyJpZCI6IjE0MyJ9", "pagination": {
"has_next": true "next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
"prev_cursor": null,
"items_per_page": 20,
"has_more": true
}
} }
``` ```
@@ -207,22 +215,19 @@ With the CRUD factory declared, routes become thin wrappers. Each route uses [Ar
async def list_articles_offset( async def list_articles_offset(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.offset_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.offset_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> OffsetPaginatedResponse[ArticleRead]: ) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate( return await ArticleCrud.offset_paginate(
session=session, session=session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -231,7 +236,7 @@ async def list_articles_offset(
**Example request:** **Example request:**
``` ```
GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=published&order_by=created_at&order_dir=desc GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&order_by=created_at&order_dir=desc
``` ```
**Example response:** **Example response:**
@@ -259,13 +264,16 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=pu
"total_count": 47, "total_count": 47,
"items_per_page": 2, "items_per_page": 2,
"page": 2, "page": 2,
"has_more": true "has_more": true,
"pages": 24
}, },
"pagination_type": "offset", "pagination_type": "offset",
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
@@ -276,22 +284,19 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=pu
async def list_articles_cursor( async def list_articles_cursor(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.cursor_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.cursor_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> CursorPaginatedResponse[ArticleRead]: ) -> CursorPaginatedResponse[ArticleRead]:
return await ArticleCrud.cursor_paginate( return await ArticleCrud.cursor_paginate(
session=session, session=session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -300,7 +305,7 @@ async def list_articles_cursor(
**Example request (first page):** **Example request (first page):**
``` ```
GET /articles/cursor?items_per_page=2&search=fastapi&filter_by[status]=published GET /articles/cursor?items_per_page=2&search=fastapi&status=published
``` ```
**Example response:** **Example response:**
@@ -334,7 +339,9 @@ GET /articles/cursor?items_per_page=2&search=fastapi&filter_by[status]=published
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
@@ -352,22 +359,19 @@ You can also expose a single endpoint that supports both strategies via a `pagin
async def list_articles( async def list_articles(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.paginate_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> PaginatedResponse[ArticleRead]: ) -> PaginatedResponse[ArticleRead]:
return await ArticleCrud.paginate( return await ArticleCrud.paginate(
session, session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -376,7 +380,7 @@ async def list_articles(
The response shape adapts to the chosen strategy. With `pagination_type=offset` (default): The response shape adapts to the chosen strategy. With `pagination_type=offset` (default):
``` ```
GET /articles/?pagination_type=offset&page=1&items_per_page=2&filter_by[status]=published GET /articles/?pagination_type=offset&page=1&items_per_page=2&status=published
``` ```
```json ```json
{ {
@@ -386,18 +390,21 @@ GET /articles/?pagination_type=offset&page=1&items_per_page=2&filter_by[status]=
"items_per_page": 2, "items_per_page": 2,
"page": 1, "page": 1,
"has_more": true "has_more": true
"pages": 24,
}, },
"pagination_type": "offset", "pagination_type": "offset",
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
With `pagination_type=cursor`: With `pagination_type=cursor`:
``` ```
GET /articles/?pagination_type=cursor&items_per_page=2&filter_by[status]=published GET /articles/?pagination_type=cursor&items_per_page=2&status=published
``` ```
```json ```json
{ {
@@ -412,7 +419,9 @@ GET /articles/?pagination_type=cursor&items_per_page=2&filter_by[status]=publish
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
Binary file not shown.
Binary file not shown.
+8 -6
View File
@@ -13,7 +13,7 @@ tags: ["python", "fastapi", "package", "toolsets"]
![overview](featured.png) ![overview](featured.png)
{{< github repo="d3vyce/fastapi-toolsets" >}} {{< github repo="d3vyce/fastapi-toolsets" >}}
> Production-ready utilities for FastAPI applications > Production-ready utilities for FastAPI applications
A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal. A modular collection of production-ready utilities for FastAPI. Install only what you need — from async CRUD and database helpers to CLI tooling, Prometheus metrics, and pytest fixtures. Each module is independently installable via optional extras, keeping your dependency footprint minimal.
@@ -35,7 +35,7 @@ A modular collection of production-ready utilities for FastAPI. Install only wha
## Installation ## Installation
The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, logging): The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, model mixins, logging):
```bash ```bash
uv add fastapi-toolsets uv add fastapi-toolsets
@@ -44,9 +44,9 @@ uv add fastapi-toolsets
Install only the extras you need: Install only the extras you need:
```bash ```bash
uv add "fastapi-toolsets[cli]" # CLI (typer) uv add "fastapi-toolsets[cli]"
uv add "fastapi-toolsets[metrics]" # Prometheus metrics (prometheus_client) uv add "fastapi-toolsets[metrics]"
uv add "fastapi-toolsets[pytest]" # Pytest helpers (httpx, pytest-xdist) uv add "fastapi-toolsets[pytest]"
``` ```
Or install everything: Or install everything:
@@ -63,7 +63,9 @@ uv add "fastapi-toolsets[all]"
- **Database**: Session management, transaction helpers, table locking, and polling-based row change detection - **Database**: Session management, transaction helpers, table locking, and polling-based row change detection
- **Dependencies**: FastAPI dependency factories (`PathDependency`, `BodyDependency`) for automatic DB lookups from path or body parameters - **Dependencies**: FastAPI dependency factories (`PathDependency`, `BodyDependency`) for automatic DB lookups from path or body parameters
- **Fixtures**: Fixture system with dependency management, context support, and pytest integration - **Fixtures**: Fixture system with dependency management, context support, and pytest integration
- **Standardized API Responses**: Consistent response format with `Response`, `PaginatedResponse`, and `PydanticBase` - **Model Mixins**: SQLAlchemy mixins for common column patterns (`UUIDMixin`, `UUIDv7Mixin`, `CreatedAtMixin`, `UpdatedAtMixin`, `TimestampMixin`)
- **Lifecycle Events**: Post-commit event system (`EventSession`, `listens_for`) that dispatches async/sync callbacks for insert, update, and delete operations
- **Standardized API Responses**: Consistent response format with `Response`, `ErrorResponse`, `PaginatedResponse`, `CursorPaginatedResponse` and `OffsetPaginatedResponse`.
- **Exception Handling**: Structured error responses with automatic OpenAPI documentation - **Exception Handling**: Structured error responses with automatic OpenAPI documentation
- **Logging**: Logging configuration with uvicorn integration via `configure_logging` and `get_logger` - **Logging**: Logging configuration with uvicorn integration via `configure_logging` and `get_logger`
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
---
title: "Taskiq Deduplication"
date: 2026-05-02
slug: "taskiq-deduplication"
showAuthor: false
showWordCount: false
showReadingTime: false
showRelatedContent: false
showPagination: false
tags: ["python", "taskiq", "package"]
---
![overview](featured.png)
{{< github repo="d3vyce/taskiq-deduplication" >}}
> Redis-backed deduplication middleware for Taskiq that prevents duplicate tasks from being queued or executed concurrently
[![CI](https://github.com/d3vyce/taskiq-deduplication/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/taskiq-deduplication/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/d3vyce/taskiq-deduplication/graph/badge.svg)](https://codecov.io/gh/d3vyce/taskiq-deduplication)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
---
**Documentation**: [https://taskiq-deduplication.d3vyce.fr](https://taskiq-deduplication.d3vyce.fr)
**Source Code**: [https://github.com/d3vyce/taskiq-deduplication](https://github.com/d3vyce/taskiq-deduplication)
---
## Installation
```bash
uv add taskiq-deduplication
```
## Quick Start
```python
from taskiq_redis import ListQueueBroker
from taskiq_deduplication import RedisDeduplicationMiddleware, DuplicateTaskError
broker = ListQueueBroker("redis://localhost:6379").with_middlewares(
RedisDeduplicationMiddleware(redis_url="redis://localhost:6379"),
)
@broker.task
async def send_report(user_id: int) -> None:
...
# First dispatch acquires the lock — succeeds.
await send_report.kiq(user_id=42)
# Second dispatch while the first is queued or running — raises.
try:
await send_report.kiq(user_id=42)
except DuplicateTaskError:
pass # already queued or running
```
## Features
- **Sender-side deduplication** — rejects duplicate tasks at dispatch time via a Redis queue lock, before they reach the broker.
- **Worker-side detection** — logs concurrent duplicate executions without raising, keeping `SmartRetryMiddleware` safe from retry storms.
- **Configurable TTL** — set a global default or override per task with the `deduplication_ttl` label.
- **Explicit lock key** — pin any task to a fixed Redis key with `deduplication_key`, bypassing fingerprint computation entirely.
- **Partial fingerprint** — deduplicate on a subset of kwargs with `deduplication_key_fields`, ignoring irrelevant arguments.
- **Per-task opt-out** — disable deduplication for individual tasks with the `deduplication` label.
## License
MIT License - see [LICENSE](LICENSE) for details.
## Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.