diff --git a/README.md b/README.md index cb7e56a..b884627 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ uv add "fastapi-toolsets[all]" ### 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 - **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 diff --git a/docs/examples/pagination-search.md b/docs/examples/pagination-search.md new file mode 100644 index 0000000..f288d09 --- /dev/null +++ b/docs/examples/pagination-search.md @@ -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], +) +``` diff --git a/docs/index.md b/docs/index.md index cb7e56a..36026d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,7 +44,7 @@ uv add "fastapi-toolsets[all]" ### 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 - **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 diff --git a/docs/module/crud.md b/docs/module/crud.md index 33eb88a..aa793c8 100644 --- a/docs/module/crud.md +++ b/docs/module/crud.md @@ -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). -| | Full-text search | Filter attributes | +| | Full-text search | Faceted search | |---|---|---| | Input | Free-text string | Exact column values | | Relationship support | Yes | Yes | @@ -242,7 +242,7 @@ async def get_users( ) ``` -### Filter attributes +### Faceted search !!! info "Added in `v1.2`" @@ -384,7 +384,7 @@ await UserCrud.upsert( ) ``` -## `schema` — typed response serialization +## Response serialization !!! info "Added in `v1.1`" diff --git a/docs_src/__init__.py b/docs_src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/examples/__init__.py b/docs_src/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/examples/pagination_search/__init__.py b/docs_src/examples/pagination_search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/examples/pagination_search/app.py b/docs_src/examples/pagination_search/app.py new file mode 100644 index 0000000..9dc7d6d --- /dev/null +++ b/docs_src/examples/pagination_search/app.py @@ -0,0 +1,6 @@ +from fastapi import FastAPI + +from .routes import router + +app = FastAPI() +app.include_router(router=router) diff --git a/docs_src/examples/pagination_search/crud.py b/docs_src/examples/pagination_search/crud.py new file mode 100644 index 0000000..a05b712 --- /dev/null +++ b/docs_src/examples/pagination_search/crud.py @@ -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() diff --git a/docs_src/examples/pagination_search/db.py b/docs_src/examples/pagination_search/db.py new file mode 100644 index 0000000..8826de6 --- /dev/null +++ b/docs_src/examples/pagination_search/db.py @@ -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)] diff --git a/docs_src/examples/pagination_search/models.py b/docs_src/examples/pagination_search/models.py new file mode 100644 index 0000000..83df37b --- /dev/null +++ b/docs_src/examples/pagination_search/models.py @@ -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") diff --git a/docs_src/examples/pagination_search/routes.py b/docs_src/examples/pagination_search/routes.py new file mode 100644 index 0000000..e4b6de0 --- /dev/null +++ b/docs_src/examples/pagination_search/routes.py @@ -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, + ) diff --git a/docs_src/examples/pagination_search/schemas.py b/docs_src/examples/pagination_search/schemas.py new file mode 100644 index 0000000..62b1d3b --- /dev/null +++ b/docs_src/examples/pagination_search/schemas.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 8d2a13e..8bbab11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "fastapi-toolsets" version = "1.2.0" -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" license = "MIT" license-files = ["LICENSE"] @@ -11,7 +11,7 @@ authors = [ ] keywords = ["fastapi", "sqlalchemy", "postgresql"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Framework :: FastAPI", "Framework :: Pydantic", diff --git a/tests/test_example_pagination_search.py b/tests/test_example_pagination_search.py new file mode 100644 index 0000000..33d59b4 --- /dev/null +++ b/tests/test_example_pagination_search.py @@ -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" diff --git a/zensical.toml b/zensical.toml index 9e932e3..46861b6 100644 --- a/zensical.toml +++ b/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] - -# 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" - -# 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." - -# 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" - -# 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" - -# 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 -""" - +copyright = "Copyright © 2026 d3vyce" 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] - -# 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" - -# 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" - -# Zensical provides a number of feature toggles that change the behavior -# of the documentation site. 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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", - - # 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]] scheme = "default" toggle.icon = "lucide/sun" @@ -270,43 +40,13 @@ scheme = "slate" toggle.icon = "lucide/moon" 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] text = "Inter" 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] -#logo = "lucide/smile" 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] inventories = ["https://docs.python.org/3/objects.inv"] paths = ["src"] @@ -316,3 +56,42 @@ docstring_style = "google" inherited_members = true show_source = false 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