Compare commits
1
Commits
main
..
507d15436c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
507d15436c |
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM hugomods/hugo:0.161.1 AS build
|
FROM hugomods/hugo:0.160.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.103.0
|
git -C themes/blowfish/ checkout v2.98.0
|
||||||
RUN hugo
|
RUN hugo
|
||||||
|
|
||||||
# Publish Stage
|
# Publish Stage
|
||||||
FROM nginx:1.31-alpine
|
FROM nginx:1.29-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/
|
||||||
|
|||||||
+47
-56
@@ -31,13 +31,9 @@ GET /articles?page=2&items_per_page=20
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"items": [...],
|
"items": [...],
|
||||||
"pagination": {
|
"total": 143,
|
||||||
"total_count": 143,
|
"page": 2,
|
||||||
"items_per_page": 20,
|
"total_pages": 8
|
||||||
"page": 2,
|
|
||||||
"has_more": true,
|
|
||||||
"pages": 8
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,12 +52,8 @@ GET /articles?cursor=eyJpZCI6IjEyMyJ9&items_per_page=20
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"items": [...],
|
"items": [...],
|
||||||
"pagination": {
|
"next_cursor": "eyJpZCI6IjE0MyJ9",
|
||||||
"next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
|
"has_next": true
|
||||||
"prev_cursor": null,
|
|
||||||
"items_per_page": 20,
|
|
||||||
"has_more": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -215,19 +207,22 @@ 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,
|
dict[str, Any],
|
||||||
Depends(
|
Depends(ArticleCrud.offset_params(default_page_size=20, max_page_size=100)),
|
||||||
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,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -236,7 +231,7 @@ async def list_articles_offset(
|
|||||||
|
|
||||||
**Example request:**
|
**Example request:**
|
||||||
```
|
```
|
||||||
GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&order_by=created_at&order_dir=desc
|
GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=published&order_by=created_at&order_dir=desc
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example response:**
|
**Example response:**
|
||||||
@@ -264,16 +259,13 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&ord
|
|||||||
"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"]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -284,19 +276,22 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&ord
|
|||||||
async def list_articles_cursor(
|
async def list_articles_cursor(
|
||||||
session: SessionDep,
|
session: SessionDep,
|
||||||
params: Annotated[
|
params: Annotated[
|
||||||
dict,
|
dict[str, Any],
|
||||||
Depends(
|
Depends(ArticleCrud.cursor_params(default_page_size=20, max_page_size=100)),
|
||||||
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,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -305,7 +300,7 @@ async def list_articles_cursor(
|
|||||||
|
|
||||||
**Example request (first page):**
|
**Example request (first page):**
|
||||||
```
|
```
|
||||||
GET /articles/cursor?items_per_page=2&search=fastapi&status=published
|
GET /articles/cursor?items_per_page=2&search=fastapi&filter_by[status]=published
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example response:**
|
**Example response:**
|
||||||
@@ -339,9 +334,7 @@ GET /articles/cursor?items_per_page=2&search=fastapi&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"]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -359,19 +352,22 @@ 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,
|
dict[str, Any],
|
||||||
Depends(
|
Depends(ArticleCrud.paginate_params(default_page_size=20, max_page_size=100)),
|
||||||
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,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -380,7 +376,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&status=published
|
GET /articles/?pagination_type=offset&page=1&items_per_page=2&filter_by[status]=published
|
||||||
```
|
```
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -390,21 +386,18 @@ GET /articles/?pagination_type=offset&page=1&items_per_page=2&status=published
|
|||||||
"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&status=published
|
GET /articles/?pagination_type=cursor&items_per_page=2&filter_by[status]=published
|
||||||
```
|
```
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -419,9 +412,7 @@ GET /articles/?pagination_type=cursor&items_per_page=2&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"]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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, model mixins, logging):
|
The base package includes the core modules (CRUD, database, schemas, exceptions, fixtures, dependencies, 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]"
|
uv add "fastapi-toolsets[cli]" # CLI (typer)
|
||||||
uv add "fastapi-toolsets[metrics]"
|
uv add "fastapi-toolsets[metrics]" # Prometheus metrics (prometheus_client)
|
||||||
uv add "fastapi-toolsets[pytest]"
|
uv add "fastapi-toolsets[pytest]" # Pytest helpers (httpx, pytest-xdist)
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install everything:
|
Or install everything:
|
||||||
@@ -63,9 +63,7 @@ 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
|
||||||
- **Model Mixins**: SQLAlchemy mixins for common column patterns (`UUIDMixin`, `UUIDv7Mixin`, `CreatedAtMixin`, `UpdatedAtMixin`, `TimestampMixin`)
|
- **Standardized API Responses**: Consistent response format with `Response`, `PaginatedResponse`, and `PydanticBase`
|
||||||
- **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.
@@ -1,80 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Taskiq Deduplication"
|
|
||||||
date: 2026-05-02
|
|
||||||
slug: "taskiq-deduplication"
|
|
||||||
showAuthor: false
|
|
||||||
showWordCount: false
|
|
||||||
showReadingTime: false
|
|
||||||
showRelatedContent: false
|
|
||||||
showPagination: false
|
|
||||||
tags: ["python", "taskiq", "package"]
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
{{< github repo="d3vyce/taskiq-deduplication" >}}
|
|
||||||
|
|
||||||
> Redis-backed deduplication middleware for Taskiq that prevents duplicate tasks from being queued or executed concurrently
|
|
||||||
|
|
||||||
|
|
||||||
[](https://github.com/d3vyce/taskiq-deduplication/actions/workflows/ci.yml)
|
|
||||||
[](https://codecov.io/gh/d3vyce/taskiq-deduplication)
|
|
||||||
[](https://github.com/astral-sh/ty)
|
|
||||||
[](https://github.com/astral-sh/uv)
|
|
||||||
[](https://github.com/astral-sh/ruff)
|
|
||||||
[](https://www.python.org/downloads/)
|
|
||||||
[](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.
|
|
||||||
Reference in New Issue
Block a user