mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-01 17:00:48 +01:00
Compare commits
2 Commits
8d57bf9525
...
117675d02f
| Author | SHA1 | Date | |
|---|---|---|---|
|
117675d02f
|
|||
|
|
d7ad7308c5 |
@@ -44,7 +44,7 @@ uv add "fastapi-toolsets[all]"
|
|||||||
|
|
||||||
### Core
|
### Core
|
||||||
|
|
||||||
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in search with relationship traversal
|
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in full-text/faceted search and Offset/Cursor pagination.
|
||||||
- **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
|
||||||
|
|||||||
134
docs/examples/pagination-search.md
Normal file
134
docs/examples/pagination-search.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Pagination & search
|
||||||
|
|
||||||
|
This example builds an articles listing endpoint that supports **offset pagination**, **cursor pagination**, **full-text search**, and **faceted filtering** — all from a single `CrudFactory` definition.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
```python title="models.py"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/models.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schemas
|
||||||
|
|
||||||
|
```python title="schemas.py"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/schemas.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Crud
|
||||||
|
|
||||||
|
Declare `facet_fields` and `searchable_fields` once on [`CrudFactory`](../reference/crud.md#fastapi_toolsets.crud.factory.CrudFactory). All endpoints built from this class share the same defaults and can override them per call.
|
||||||
|
|
||||||
|
```python title="crud.py"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/crud.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session dependency
|
||||||
|
|
||||||
|
```python title="db.py"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/db.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info "Deploy a Postgres DB with docker"
|
||||||
|
```bash
|
||||||
|
docker run -d --name postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres -p 5432:5432 postgres:18-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## App
|
||||||
|
|
||||||
|
```python title="app.py"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/app.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
### Offset pagination
|
||||||
|
|
||||||
|
Best for admin panels or any UI that needs a total item count and numbered pages.
|
||||||
|
|
||||||
|
```python title="routes.py:1:27"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/routes.py:1:27"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example request**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /articles/offset?page=2&items_per_page=10&search=fastapi&status=published
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"data": [
|
||||||
|
{ "id": "3f47ac69-...", "title": "FastAPI tips", "status": "published", ... }
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"total_count": 42,
|
||||||
|
"page": 2,
|
||||||
|
"items_per_page": 10,
|
||||||
|
"has_more": true
|
||||||
|
},
|
||||||
|
"filter_attributes": {
|
||||||
|
"status": ["archived", "draft", "published"],
|
||||||
|
"name": ["backend", "frontend", "python"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`filter_attributes` always reflects the values visible **after** applying the active filters. Use it to populate filter dropdowns on the client.
|
||||||
|
|
||||||
|
### Cursor pagination
|
||||||
|
|
||||||
|
Best for feeds, infinite scroll, or any high-throughput API where offset performance degrades.
|
||||||
|
|
||||||
|
```python title="routes.py:30:45"
|
||||||
|
--8<-- "docs_src/examples/pagination_search/routes.py:30:45"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example request**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /articles/cursor?items_per_page=10&status=published
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"data": [
|
||||||
|
{ "id": "3f47ac69-...", "title": "FastAPI tips", "status": "published", ... }
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"next_cursor": "eyJ2YWx1ZSI6ICIzZjQ3YWM2OS0uLi4ifQ==",
|
||||||
|
"prev_cursor": null,
|
||||||
|
"items_per_page": 10,
|
||||||
|
"has_more": true
|
||||||
|
},
|
||||||
|
"filter_attributes": {
|
||||||
|
"status": ["published"],
|
||||||
|
"name": ["backend", "python"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `next_cursor` as the `cursor` query parameter on the next request to advance to the next page.
|
||||||
|
|
||||||
|
## Search behaviour
|
||||||
|
|
||||||
|
Both endpoints inherit the same `searchable_fields` declared on `ArticleCrud`:
|
||||||
|
|
||||||
|
Search is **case-insensitive** and uses a `LIKE %query%` pattern. Pass a [`SearchConfig`](../reference/crud.md#fastapi_toolsets.crud.search.SearchConfig) instead of a plain string to control case sensitivity or switch to `match_mode="all"` (AND across all fields instead of OR).
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi_toolsets.crud import SearchConfig
|
||||||
|
|
||||||
|
# Both title AND body must contain "fastapi"
|
||||||
|
result = await ArticleCrud.offset_paginate(
|
||||||
|
session,
|
||||||
|
search=SearchConfig(query="fastapi", case_sensitive=True, match_mode="all"),
|
||||||
|
search_fields=[Article.title, Article.body],
|
||||||
|
)
|
||||||
|
```
|
||||||
@@ -44,7 +44,7 @@ uv add "fastapi-toolsets[all]"
|
|||||||
|
|
||||||
### Core
|
### Core
|
||||||
|
|
||||||
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in search with relationship traversal
|
- **CRUD**: Generic async CRUD operations with `CrudFactory`, built-in full-text/faceted search and offset/cursor pagination.
|
||||||
- **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
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ PostCrud = CrudFactory(model=Post, cursor_column=Post.created_at)
|
|||||||
|
|
||||||
Two search strategies are available, both compatible with [`offset_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate) and [`cursor_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate).
|
Two search strategies are available, both compatible with [`offset_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.offset_paginate) and [`cursor_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate).
|
||||||
|
|
||||||
| | Full-text search | Filter attributes |
|
| | Full-text search | Faceted search |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Input | Free-text string | Exact column values |
|
| Input | Free-text string | Exact column values |
|
||||||
| Relationship support | Yes | Yes |
|
| Relationship support | Yes | Yes |
|
||||||
@@ -242,7 +242,7 @@ async def get_users(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Filter attributes
|
### Faceted search
|
||||||
|
|
||||||
!!! info "Added in `v1.2`"
|
!!! info "Added in `v1.2`"
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ await UserCrud.upsert(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## `schema` — typed response serialization
|
## Response serialization
|
||||||
|
|
||||||
!!! info "Added in `v1.1`"
|
!!! info "Added in `v1.1`"
|
||||||
|
|
||||||
|
|||||||
0
docs_src/__init__.py
Normal file
0
docs_src/__init__.py
Normal file
0
docs_src/examples/__init__.py
Normal file
0
docs_src/examples/__init__.py
Normal file
0
docs_src/examples/pagination_search/__init__.py
Normal file
0
docs_src/examples/pagination_search/__init__.py
Normal file
6
docs_src/examples/pagination_search/app.py
Normal file
6
docs_src/examples/pagination_search/app.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from .routes import router
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(router=router)
|
||||||
19
docs_src/examples/pagination_search/crud.py
Normal file
19
docs_src/examples/pagination_search/crud.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
|
||||||
|
from .models import Article, Category
|
||||||
|
|
||||||
|
ArticleCrud = CrudFactory(
|
||||||
|
model=Article,
|
||||||
|
cursor_column=Article.created_at,
|
||||||
|
searchable_fields=[ # default fields for full-text search
|
||||||
|
Article.title,
|
||||||
|
Article.body,
|
||||||
|
(Article.category, Category.name),
|
||||||
|
],
|
||||||
|
facet_fields=[ # fields exposed as filter dropdowns
|
||||||
|
Article.status,
|
||||||
|
(Article.category, Category.name),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ArticleFilters = ArticleCrud.filter_params()
|
||||||
17
docs_src/examples/pagination_search/db.py
Normal file
17
docs_src/examples/pagination_search/db.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from fastapi_toolsets.db import create_db_context, create_db_dependency
|
||||||
|
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
|
||||||
|
|
||||||
|
engine = create_async_engine(url=DATABASE_URL, future=True)
|
||||||
|
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
get_db = create_db_dependency(session_maker=async_session_maker)
|
||||||
|
get_db_context = create_db_context(session_maker=async_session_maker)
|
||||||
|
|
||||||
|
|
||||||
|
SessionDep = Annotated[AsyncSession, Depends(get_db)]
|
||||||
36
docs_src/examples/pagination_search/models.py
Normal file
36
docs_src/examples/pagination_search/models.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Category(Base):
|
||||||
|
__tablename__ = "categories"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||||
|
name: Mapped[str] = mapped_column(String(64), unique=True)
|
||||||
|
|
||||||
|
articles: Mapped[list["Article"]] = relationship(back_populates="category")
|
||||||
|
|
||||||
|
|
||||||
|
class Article(Base):
|
||||||
|
__tablename__ = "articles"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||||
|
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now()
|
||||||
|
)
|
||||||
|
title: Mapped[str] = mapped_column(String(256))
|
||||||
|
body: Mapped[str] = mapped_column(Text)
|
||||||
|
status: Mapped[str] = mapped_column(String(32))
|
||||||
|
published: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
category_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
ForeignKey("categories.id"), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
category: Mapped["Category | None"] = relationship(back_populates="articles")
|
||||||
45
docs_src/examples/pagination_search/routes.py
Normal file
45
docs_src/examples/pagination_search/routes.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
|
from fastapi_toolsets.schemas import PaginatedResponse
|
||||||
|
|
||||||
|
from .crud import ArticleCrud
|
||||||
|
from .db import SessionDep
|
||||||
|
from .schemas import ArticleRead
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/articles")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/offset")
|
||||||
|
async def list_articles_offset(
|
||||||
|
session: SessionDep,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
items_per_page: int = Query(20, ge=1, le=100),
|
||||||
|
search: str | None = None,
|
||||||
|
filter_by: dict[str, list[str]] = Depends(ArticleCrud.filter_params()),
|
||||||
|
) -> PaginatedResponse[ArticleRead]:
|
||||||
|
return await ArticleCrud.offset_paginate(
|
||||||
|
session=session,
|
||||||
|
page=page,
|
||||||
|
items_per_page=items_per_page,
|
||||||
|
search=search,
|
||||||
|
filter_by=filter_by or None,
|
||||||
|
schema=ArticleRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cursor")
|
||||||
|
async def list_articles_cursor(
|
||||||
|
session: SessionDep,
|
||||||
|
cursor: str | None = None,
|
||||||
|
items_per_page: int = Query(20, ge=1, le=100),
|
||||||
|
search: str | None = None,
|
||||||
|
filter_by: dict[str, list[str]] = Depends(ArticleCrud.filter_params()),
|
||||||
|
) -> PaginatedResponse[ArticleRead]:
|
||||||
|
return await ArticleCrud.cursor_paginate(
|
||||||
|
session=session,
|
||||||
|
cursor=cursor,
|
||||||
|
items_per_page=items_per_page,
|
||||||
|
search=search,
|
||||||
|
filter_by=filter_by or None,
|
||||||
|
schema=ArticleRead,
|
||||||
|
)
|
||||||
13
docs_src/examples/pagination_search/schemas.py
Normal file
13
docs_src/examples/pagination_search/schemas.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi_toolsets.schemas import PydanticBase
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleRead(PydanticBase):
|
||||||
|
id: uuid.UUID
|
||||||
|
created_at: datetime.datetime
|
||||||
|
title: str
|
||||||
|
status: str
|
||||||
|
published: bool
|
||||||
|
category_id: uuid.UUID | None
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
description = "Reusable tools for FastAPI: async CRUD, fixtures, CLI, and standardized responses for SQLAlchemy + PostgreSQL"
|
description = "Production-ready utilities for FastAPI applications"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -11,7 +11,7 @@ authors = [
|
|||||||
]
|
]
|
||||||
keywords = ["fastapi", "sqlalchemy", "postgresql"]
|
keywords = ["fastapi", "sqlalchemy", "postgresql"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Framework :: AsyncIO",
|
"Framework :: AsyncIO",
|
||||||
"Framework :: FastAPI",
|
"Framework :: FastAPI",
|
||||||
"Framework :: Pydantic",
|
"Framework :: Pydantic",
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ Example usage:
|
|||||||
return Response(data={"user": user.username}, message="Success")
|
return Response(data={"user": user.username}, message="Success")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "1.2.0"
|
__version__ = "1.2.1"
|
||||||
|
|||||||
271
tests/test_example_pagination_search.py
Normal file
271
tests/test_example_pagination_search.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
"""Live test for the docs/examples/pagination-search.md example.
|
||||||
|
|
||||||
|
Spins up the exact FastAPI app described in the example (sourced from
|
||||||
|
docs_src/examples/pagination_search/) and exercises it through a real HTTP
|
||||||
|
client against a real PostgreSQL database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from docs_src.examples.pagination_search.db import get_db
|
||||||
|
from docs_src.examples.pagination_search.models import Article, Base, Category
|
||||||
|
from docs_src.examples.pagination_search.routes import router
|
||||||
|
|
||||||
|
from .conftest import DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
def build_app(session: AsyncSession) -> FastAPI:
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
async def override_get_db():
|
||||||
|
yield session
|
||||||
|
|
||||||
|
app.dependency_overrides[get_db] = override_get_db
|
||||||
|
app.include_router(router)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
async def ex_db_session():
|
||||||
|
"""Isolated session for the example models (separate tables from conftest)."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
session = session_factory()
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(ex_db_session: AsyncSession):
|
||||||
|
app = build_app(ex_db_session)
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=ASGITransport(app=app), base_url="http://test"
|
||||||
|
) as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
|
||||||
|
async def seed(session: AsyncSession):
|
||||||
|
"""Insert representative fixture data."""
|
||||||
|
python = Category(name="python")
|
||||||
|
backend = Category(name="backend")
|
||||||
|
session.add_all([python, backend])
|
||||||
|
await session.flush()
|
||||||
|
|
||||||
|
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
Article(
|
||||||
|
title="FastAPI tips",
|
||||||
|
body="Ten useful tips for FastAPI.",
|
||||||
|
status="published",
|
||||||
|
published=True,
|
||||||
|
category_id=python.id,
|
||||||
|
created_at=now,
|
||||||
|
),
|
||||||
|
Article(
|
||||||
|
title="SQLAlchemy async",
|
||||||
|
body="How to use async SQLAlchemy.",
|
||||||
|
status="published",
|
||||||
|
published=True,
|
||||||
|
category_id=backend.id,
|
||||||
|
created_at=now + datetime.timedelta(seconds=1),
|
||||||
|
),
|
||||||
|
Article(
|
||||||
|
title="Draft notes",
|
||||||
|
body="Work in progress.",
|
||||||
|
status="draft",
|
||||||
|
published=False,
|
||||||
|
category_id=None,
|
||||||
|
created_at=now + datetime.timedelta(seconds=2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAppSessionDep:
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_db_yields_async_session(self):
|
||||||
|
"""get_db yields a real AsyncSession when called directly."""
|
||||||
|
from docs_src.examples.pagination_search.db import get_db
|
||||||
|
|
||||||
|
gen = get_db()
|
||||||
|
session = await gen.__anext__()
|
||||||
|
assert isinstance(session, AsyncSession)
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestOffsetPagination:
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_returns_all_articles(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 3
|
||||||
|
assert len(body["data"]) == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_pagination_page_size(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?items_per_page=2&page=1")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert len(body["data"]) == 2
|
||||||
|
assert body["pagination"]["total_count"] == 3
|
||||||
|
assert body["pagination"]["has_more"] is True
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_full_text_search(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?search=fastapi")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 1
|
||||||
|
assert body["data"][0]["title"] == "FastAPI tips"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_search_traverses_relationship(
|
||||||
|
self, client: AsyncClient, ex_db_session
|
||||||
|
):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
# "python" matches Category.name, not Article.title or body
|
||||||
|
resp = await client.get("/articles/offset?search=python")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 1
|
||||||
|
assert body["data"][0]["title"] == "FastAPI tips"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_facet_filter_scalar(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?status=published")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 2
|
||||||
|
assert all(a["status"] == "published" for a in body["data"])
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_facet_filter_multi_value(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?status=published&status=draft")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 3
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_attributes_in_response(
|
||||||
|
self, client: AsyncClient, ex_db_session
|
||||||
|
):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
fa = body["filter_attributes"]
|
||||||
|
assert set(fa["status"]) == {"draft", "published"}
|
||||||
|
# "name" is unique across all facet fields — no prefix needed
|
||||||
|
assert set(fa["name"]) == {"backend", "python"}
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_filter_attributes_scoped_to_filter(
|
||||||
|
self, client: AsyncClient, ex_db_session
|
||||||
|
):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?status=published")
|
||||||
|
|
||||||
|
body = resp.json()
|
||||||
|
# draft is filtered out → should not appear in filter_attributes
|
||||||
|
assert "draft" not in body["filter_attributes"]["status"]
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_search_and_filter_combined(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/offset?search=async&status=published")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["pagination"]["total_count"] == 1
|
||||||
|
assert body["data"][0]["title"] == "SQLAlchemy async"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCursorPagination:
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_first_page(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/cursor?items_per_page=2")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert len(body["data"]) == 2
|
||||||
|
assert body["pagination"]["has_more"] is True
|
||||||
|
assert body["pagination"]["next_cursor"] is not None
|
||||||
|
assert body["pagination"]["prev_cursor"] is None
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_second_page(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
first = await client.get("/articles/cursor?items_per_page=2")
|
||||||
|
next_cursor = first.json()["pagination"]["next_cursor"]
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/articles/cursor?items_per_page=2&cursor={next_cursor}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert len(body["data"]) == 1
|
||||||
|
assert body["pagination"]["has_more"] is False
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_facet_filter(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/cursor?status=draft")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert len(body["data"]) == 1
|
||||||
|
assert body["data"][0]["status"] == "draft"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_full_text_search(self, client: AsyncClient, ex_db_session):
|
||||||
|
await seed(ex_db_session)
|
||||||
|
|
||||||
|
resp = await client.get("/articles/cursor?search=sqlalchemy")
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert len(body["data"]) == 1
|
||||||
|
assert body["data"][0]["title"] == "SQLAlchemy async"
|
||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -251,7 +251,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-toolsets"
|
name = "fastapi-toolsets"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
|
|||||||
301
zensical.toml
301
zensical.toml
@@ -1,265 +1,35 @@
|
|||||||
# ============================================================================
|
|
||||||
#
|
|
||||||
# The configuration produced by default is meant to highlight the features
|
|
||||||
# that Zensical provides and to serve as a starting point for your own
|
|
||||||
# projects.
|
|
||||||
#
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
|
|
||||||
# The site_name is shown in the page header and the browser window title
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/setup/basics/#site_name
|
|
||||||
site_name = "FastAPI Toolsets"
|
site_name = "FastAPI Toolsets"
|
||||||
|
|
||||||
# The site_description is included in the HTML head and should contain a
|
|
||||||
# meaningful description of the site content for use by search engines.
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/setup/basics/#site_description
|
|
||||||
site_description = "Production-ready utilities for FastAPI applications."
|
site_description = "Production-ready utilities for FastAPI applications."
|
||||||
|
|
||||||
# The site_author attribute. This is used in the HTML head element.
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/setup/basics/#site_author
|
|
||||||
site_author = "d3vyce"
|
site_author = "d3vyce"
|
||||||
|
|
||||||
# The site_url is the canonical URL for your site. When building online
|
|
||||||
# documentation you should set this.
|
|
||||||
# Read more: https://zensical.org/docs/setup/basics/#site_url
|
|
||||||
site_url = "https://fastapi-toolsets.d3vyce.fr"
|
site_url = "https://fastapi-toolsets.d3vyce.fr"
|
||||||
|
copyright = "Copyright © 2026 d3vyce"
|
||||||
# The copyright notice appears in the page footer and can contain an HTML
|
|
||||||
# fragment.
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/setup/basics/#copyright
|
|
||||||
copyright = """
|
|
||||||
Copyright © 2026 d3vyce
|
|
||||||
"""
|
|
||||||
|
|
||||||
repo_url = "https://github.com/d3vyce/fastapi-toolsets"
|
repo_url = "https://github.com/d3vyce/fastapi-toolsets"
|
||||||
|
|
||||||
# Zensical supports both implicit navigation and explicitly defined navigation.
|
|
||||||
# If you decide not to define a navigation here then Zensical will simply
|
|
||||||
# derive the navigation structure from the directory structure of your
|
|
||||||
# "docs_dir". The definition below demonstrates how a navigation structure
|
|
||||||
# can be defined using TOML syntax.
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/setup/navigation/
|
|
||||||
# nav = [
|
|
||||||
# { "Get started" = "index.md" },
|
|
||||||
# { "Markdown in 5min" = "markdown.md" },
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# With the "extra_css" option you can add your own CSS styling to customize
|
|
||||||
# your Zensical project according to your needs. You can add any number of
|
|
||||||
# CSS files.
|
|
||||||
#
|
|
||||||
# The path provided should be relative to the "docs_dir".
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/customization/#additional-css
|
|
||||||
#
|
|
||||||
#extra_css = ["stylesheets/extra.css"]
|
|
||||||
|
|
||||||
# With the `extra_javascript` option you can add your own JavaScript to your
|
|
||||||
# project to customize the behavior according to your needs.
|
|
||||||
#
|
|
||||||
# The path provided should be relative to the "docs_dir".
|
|
||||||
#
|
|
||||||
# Read more: https://zensical.org/docs/customization/#additional-javascript
|
|
||||||
#extra_javascript = ["javascripts/extra.js"]
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Section for configuring theme options
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
[project.theme]
|
[project.theme]
|
||||||
|
|
||||||
# change this to "classic" to use the traditional Material for MkDocs look.
|
|
||||||
#variant = "classic"
|
|
||||||
|
|
||||||
# Zensical allows you to override specific blocks, partials, or whole
|
|
||||||
# templates as well as to define your own templates. To do this, uncomment
|
|
||||||
# the custom_dir setting below and set it to a directory in which you
|
|
||||||
# keep your template overrides.
|
|
||||||
#
|
|
||||||
# Read more:
|
|
||||||
# - https://zensical.org/docs/customization/#extending-the-theme
|
|
||||||
#
|
|
||||||
custom_dir = "docs/overrides"
|
custom_dir = "docs/overrides"
|
||||||
|
|
||||||
# With the "favicon" option you can set your own image to use as the icon
|
|
||||||
# browsers will use in the browser title bar or tab bar. The path provided
|
|
||||||
# must be relative to the "docs_dir".
|
|
||||||
#
|
|
||||||
# Read more:
|
|
||||||
# - https://zensical.org/docs/setup/logo-and-icons/#favicon
|
|
||||||
# - https://developer.mozilla.org/en-US/docs/Glossary/Favicon
|
|
||||||
#
|
|
||||||
#favicon = "images/favicon.png"
|
|
||||||
|
|
||||||
# Zensical supports more than 60 different languages. This means that the
|
|
||||||
# labels and tooltips that Zensical's templates produce are translated.
|
|
||||||
# The "language" option allows you to set the language used. This language
|
|
||||||
# is also indicated in the HTML head element to help with accessibility
|
|
||||||
# and guide search engines and translation tools.
|
|
||||||
#
|
|
||||||
# The default language is "en" (English). It is possible to create
|
|
||||||
# sites with multiple languages and configure a language selector. See
|
|
||||||
# the documentation for details.
|
|
||||||
#
|
|
||||||
# Read more:
|
|
||||||
# - https://zensical.org/docs/setup/language/
|
|
||||||
#
|
|
||||||
language = "en"
|
language = "en"
|
||||||
|
|
||||||
# Zensical provides a number of feature toggles that change the behavior
|
|
||||||
# of the documentation site.
|
|
||||||
features = [
|
features = [
|
||||||
# Zensical includes an announcement bar. This feature allows users to
|
|
||||||
# dismiss it when they have read the announcement.
|
|
||||||
# https://zensical.org/docs/setup/header/#announcement-bar
|
|
||||||
"announce.dismiss",
|
"announce.dismiss",
|
||||||
|
|
||||||
# If you have a repository configured and turn on this feature, Zensical
|
|
||||||
# will generate an edit button for the page. This works for common
|
|
||||||
# repository hosting services.
|
|
||||||
# https://zensical.org/docs/setup/repository/#content-actions
|
|
||||||
#"content.action.edit",
|
|
||||||
|
|
||||||
# If you have a repository configured and turn on this feature, Zensical
|
|
||||||
# will generate a button that allows the user to view the Markdown
|
|
||||||
# code for the current page.
|
|
||||||
# https://zensical.org/docs/setup/repository/#content-actions
|
|
||||||
"content.action.view",
|
"content.action.view",
|
||||||
|
|
||||||
# Code annotations allow you to add an icon with a tooltip to your
|
|
||||||
# code blocks to provide explanations at crucial points.
|
|
||||||
# https://zensical.org/docs/authoring/code-blocks/#code-annotations
|
|
||||||
"content.code.annotate",
|
"content.code.annotate",
|
||||||
|
|
||||||
# This feature turns on a button in code blocks that allow users to
|
|
||||||
# copy the content to their clipboard without first selecting it.
|
|
||||||
# https://zensical.org/docs/authoring/code-blocks/#code-copy-button
|
|
||||||
"content.code.copy",
|
"content.code.copy",
|
||||||
|
|
||||||
# Code blocks can include a button to allow for the selection of line
|
|
||||||
# ranges by the user.
|
|
||||||
# https://zensical.org/docs/authoring/code-blocks/#code-selection-button
|
|
||||||
"content.code.select",
|
"content.code.select",
|
||||||
|
|
||||||
# Zensical can render footnotes as inline tooltips, so the user can read
|
|
||||||
# the footnote without leaving the context of the document.
|
|
||||||
# https://zensical.org/docs/authoring/footnotes/#footnote-tooltips
|
|
||||||
"content.footnote.tooltips",
|
"content.footnote.tooltips",
|
||||||
|
|
||||||
# If you have many content tabs that have the same titles (e.g., "Python",
|
|
||||||
# "JavaScript", "Cobol"), this feature causes all of them to switch to
|
|
||||||
# at the same time when the user chooses their language in one.
|
|
||||||
# https://zensical.org/docs/authoring/content-tabs/#linked-content-tabs
|
|
||||||
"content.tabs.link",
|
"content.tabs.link",
|
||||||
|
|
||||||
# With this feature enabled users can add tooltips to links that will be
|
|
||||||
# displayed when the mouse pointer hovers the link.
|
|
||||||
# https://zensical.org/docs/authoring/tooltips/#improved-tooltips
|
|
||||||
"content.tooltips",
|
"content.tooltips",
|
||||||
|
|
||||||
# With this feature enabled, Zensical will automatically hide parts
|
|
||||||
# of the header when the user scrolls past a certain point.
|
|
||||||
# https://zensical.org/docs/setup/header/#automatic-hiding
|
|
||||||
# "header.autohide",
|
|
||||||
|
|
||||||
# Turn on this feature to expand all collapsible sections in the
|
|
||||||
# navigation sidebar by default.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-expansion
|
|
||||||
# "navigation.expand",
|
|
||||||
|
|
||||||
# This feature turns on navigation elements in the footer that allow the
|
|
||||||
# user to navigate to a next or previous page.
|
|
||||||
# https://zensical.org/docs/setup/footer/#navigation
|
|
||||||
"navigation.footer",
|
"navigation.footer",
|
||||||
|
|
||||||
# When section index pages are enabled, documents can be directly attached
|
|
||||||
# to sections, which is particularly useful for providing overview pages.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#section-index-pages
|
|
||||||
"navigation.indexes",
|
"navigation.indexes",
|
||||||
|
|
||||||
# When instant navigation is enabled, clicks on all internal links will be
|
|
||||||
# intercepted and dispatched via XHR without fully reloading the page.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#instant-navigation
|
|
||||||
"navigation.instant",
|
"navigation.instant",
|
||||||
|
|
||||||
# With instant prefetching, your site will start to fetch a page once the
|
|
||||||
# user hovers over a link. This will reduce the perceived loading time
|
|
||||||
# for the user.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#instant-prefetching
|
|
||||||
"navigation.instant.prefetch",
|
"navigation.instant.prefetch",
|
||||||
|
|
||||||
# In order to provide a better user experience on slow connections when
|
|
||||||
# using instant navigation, a progress indicator can be enabled.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#progress-indicator
|
|
||||||
#"navigation.instant.progress",
|
|
||||||
|
|
||||||
# When navigation paths are activated, a breadcrumb navigation is rendered
|
|
||||||
# above the title of each page
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-path
|
|
||||||
"navigation.path",
|
"navigation.path",
|
||||||
|
|
||||||
# When pruning is enabled, only the visible navigation items are included
|
|
||||||
# in the rendered HTML, reducing the size of the built site by 33% or more.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-pruning
|
|
||||||
#"navigation.prune",
|
|
||||||
|
|
||||||
# When sections are enabled, top-level sections are rendered as groups in
|
|
||||||
# the sidebar for viewports above 1220px, but remain as-is on mobile.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-sections
|
|
||||||
"navigation.sections",
|
"navigation.sections",
|
||||||
|
|
||||||
# When tabs are enabled, top-level sections are rendered in a menu layer
|
|
||||||
# below the header for viewports above 1220px, but remain as-is on mobile.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-tabs
|
|
||||||
"navigation.tabs",
|
"navigation.tabs",
|
||||||
|
|
||||||
# When sticky tabs are enabled, navigation tabs will lock below the header
|
|
||||||
# and always remain visible when scrolling down.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#sticky-navigation-tabs
|
|
||||||
#"navigation.tabs.sticky",
|
|
||||||
|
|
||||||
# A back-to-top button can be shown when the user, after scrolling down,
|
|
||||||
# starts to scroll up again.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#back-to-top-button
|
|
||||||
"navigation.top",
|
"navigation.top",
|
||||||
|
|
||||||
# When anchor tracking is enabled, the URL in the address bar is
|
|
||||||
# automatically updated with the active anchor as highlighted in the table
|
|
||||||
# of contents.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#anchor-tracking
|
|
||||||
"navigation.tracking",
|
"navigation.tracking",
|
||||||
|
|
||||||
# When search highlighting is enabled and a user clicks on a search result,
|
|
||||||
# Zensical will highlight all occurrences after following the link.
|
|
||||||
# https://zensical.org/docs/setup/search/#search-highlighting
|
|
||||||
"search.highlight",
|
"search.highlight",
|
||||||
|
|
||||||
# When anchor following for the table of contents is enabled, the sidebar
|
|
||||||
# is automatically scrolled so that the active anchor is always visible.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#anchor-following
|
|
||||||
# "toc.follow",
|
|
||||||
|
|
||||||
# When navigation integration for the table of contents is enabled, it is
|
|
||||||
# always rendered as part of the navigation sidebar on the left.
|
|
||||||
# https://zensical.org/docs/setup/navigation/#navigation-integration
|
|
||||||
#"toc.integrate",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# In the "palette" subsection you can configure options for the color scheme.
|
|
||||||
# You can configure different color # schemes, e.g., to turn on dark mode,
|
|
||||||
# that the user can switch between. Each color scheme can be further
|
|
||||||
# customized.
|
|
||||||
#
|
|
||||||
# Read more:
|
|
||||||
# - https://zensical.org/docs/setup/colors/
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
[[project.theme.palette]]
|
[[project.theme.palette]]
|
||||||
scheme = "default"
|
scheme = "default"
|
||||||
toggle.icon = "lucide/sun"
|
toggle.icon = "lucide/sun"
|
||||||
@@ -270,43 +40,13 @@ scheme = "slate"
|
|||||||
toggle.icon = "lucide/moon"
|
toggle.icon = "lucide/moon"
|
||||||
toggle.name = "Switch to light mode"
|
toggle.name = "Switch to light mode"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# In the "font" subsection you can configure the fonts used. By default, fonts
|
|
||||||
# are loaded from Google Fonts, giving you a wide range of choices from a set
|
|
||||||
# of suitably licensed fonts. There are options for a normal text font and for
|
|
||||||
# a monospaced font used in code blocks.
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
[project.theme.font]
|
[project.theme.font]
|
||||||
text = "Inter"
|
text = "Inter"
|
||||||
code = "Jetbrains Mono"
|
code = "Jetbrains Mono"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# You can configure your own logo to be shown in the header using the "logo"
|
|
||||||
# option in the "icons" subsection. The logo can be a path to a file in your
|
|
||||||
# "docs_dir" or it can be a path to an icon.
|
|
||||||
#
|
|
||||||
# Likewise, you can customize the logo used for the repository section of the
|
|
||||||
# header. Zensical derives the default logo for this from the repository URL.
|
|
||||||
# See below...
|
|
||||||
#
|
|
||||||
# There are other icons you can customize. See the documentation for details.
|
|
||||||
#
|
|
||||||
# Read more:
|
|
||||||
# - https://zensical.org/docs/setup/logo-and-icons
|
|
||||||
# - https://zensical.org/docs/authoring/icons-emojis/#search
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
[project.theme.icon]
|
[project.theme.icon]
|
||||||
#logo = "lucide/smile"
|
|
||||||
repo = "fontawesome/brands/github"
|
repo = "fontawesome/brands/github"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# The "extra" section contains miscellaneous settings.
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
#[[project.extra.social]]
|
|
||||||
#icon = "fontawesome/brands/github"
|
|
||||||
#link = "https://github.com/user/repo"
|
|
||||||
|
|
||||||
|
|
||||||
[project.plugins.mkdocstrings.handlers.python]
|
[project.plugins.mkdocstrings.handlers.python]
|
||||||
inventories = ["https://docs.python.org/3/objects.inv"]
|
inventories = ["https://docs.python.org/3/objects.inv"]
|
||||||
paths = ["src"]
|
paths = ["src"]
|
||||||
@@ -316,3 +56,42 @@ docstring_style = "google"
|
|||||||
inherited_members = true
|
inherited_members = true
|
||||||
show_source = false
|
show_source = false
|
||||||
show_root_heading = true
|
show_root_heading = true
|
||||||
|
|
||||||
|
[project.markdown_extensions]
|
||||||
|
abbr = {}
|
||||||
|
admonition = {}
|
||||||
|
attr_list = {}
|
||||||
|
def_list = {}
|
||||||
|
footnotes = {}
|
||||||
|
md_in_html = {}
|
||||||
|
"pymdownx.arithmatex" = {generic = true}
|
||||||
|
"pymdownx.betterem" = {}
|
||||||
|
"pymdownx.caret" = {}
|
||||||
|
"pymdownx.details" = {}
|
||||||
|
"pymdownx.emoji" = {}
|
||||||
|
"pymdownx.inlinehilite" = {}
|
||||||
|
"pymdownx.keys" = {}
|
||||||
|
"pymdownx.magiclink" = {}
|
||||||
|
"pymdownx.mark" = {}
|
||||||
|
"pymdownx.smartsymbols" = {}
|
||||||
|
"pymdownx.tasklist" = {custom_checkbox = true}
|
||||||
|
"pymdownx.tilde" = {}
|
||||||
|
|
||||||
|
[project.markdown_extensions."pymdownx.highlight"]
|
||||||
|
anchor_linenums = true
|
||||||
|
line_spans = "__span"
|
||||||
|
pygments_lang_class = true
|
||||||
|
|
||||||
|
[project.markdown_extensions."pymdownx.superfences"]
|
||||||
|
custom_fences = [{name = "mermaid", class = "mermaid"}]
|
||||||
|
|
||||||
|
[project.markdown_extensions."pymdownx.tabbed"]
|
||||||
|
alternate_style = true
|
||||||
|
combine_header_slug = true
|
||||||
|
|
||||||
|
[project.markdown_extensions."toc"]
|
||||||
|
permalink = true
|
||||||
|
|
||||||
|
[project.markdown_extensions."pymdownx.snippets"]
|
||||||
|
base_path = ["."]
|
||||||
|
check_paths = true
|
||||||
|
|||||||
Reference in New Issue
Block a user