diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9ad674b..64250ac 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,10 +11,22 @@ body: required: true - type: dropdown - id: project-type + id: workload attributes: - label: Which project type did you generate? - options: [library, cli, api, not applicable] + label: Which workload did you generate? + options: + - library + - cli + - api + - web + - tui + - mcp + - agent + - rag + - inference + - training + - hybrid + - repository or documentation validations: required: true @@ -22,11 +34,21 @@ body: id: answers attributes: label: Contents of .copier-answers.yml - description: From the generated project. This tells us exactly how it was generated. + description: From the generated project, with secrets removed. This identifies every selected layer. render: yaml + - type: input + id: version + attributes: + label: Template version or commit + placeholder: v0.5.0 or a commit SHA + validations: + required: true + - type: textarea id: logs attributes: label: Relevant output render: shell + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7bcee7b..0e20e24 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -2,6 +2,23 @@ name: Feature request description: Suggest an idea for the template labels: ["enhancement"] body: + - type: dropdown + id: layer + attributes: + label: Which layer does this affect? + options: + - workload or simple project + - framework or agent runtime + - interface + - model or embedding provider + - data engine or authentication + - training, serving, MLOps, or quality + - deployment or IaC + - agent skill + - repository tooling or documentation + validations: + required: true + - type: textarea id: problem attributes: @@ -21,3 +38,9 @@ body: id: alternatives attributes: label: What alternatives have you considered? + + - type: textarea + id: validation + attributes: + label: How could this be validated without credentials? + description: Describe an import, command, route, local fake, parsed artifact, or other representative test. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d60f72..711ea31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,24 @@ jobs: - name: Generate and test affected presets offline run: uv run pytest -m preset -n auto --basetemp="${{ runner.temp }}/pytest-presets" + docs: + name: Repository documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + + - uses: astral-sh/setup-uv@v9.0.0 + with: + python-version: "3.13" + enable-cache: true + + - run: uv sync --locked --only-group docs + + - name: Build Zensical site + run: uv run --only-group docs zensical build --clean --strict + toolchain: name: Generated project / ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -131,7 +149,7 @@ jobs: ci: name: CI runs-on: ubuntu-latest - needs: [lint, structure, presets, toolchain, zizmor] + needs: [lint, structure, presets, docs, toolchain, zizmor] if: always() steps: - name: Verify every job succeeded diff --git a/.gitignore b/.gitignore index 1adfb7a..aa6732e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Testing .pytest_cache/ .coverage +site/ # Tool caches .ruff_cache/ @@ -17,6 +18,7 @@ __pycache__/ # Editors .vscode/ .idea/ +.vercel/ # OS .DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5f0276..dab9045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,60 +1,82 @@ -# Python Template +# Contributing to Python Template -## Contributions are highly appreciated +Contributions are welcome, from a focused bug report to a complete component +integration. The repository generates many possible projects, so changes must +keep the catalog, templates, and executable tests aligned. -### All contributors will be granted credit on the following list +## Report an issue -* Leynier Gutiérrez González ([@leynier](https://github.com/leynier)) +Use the [issue forms](https://github.com/leynier/python-template/issues/new/choose). +For a generated-project bug, include the relevant `.copier-answers.yml`, the +command that failed, and its output. Remove credentials before posting. -You can help out by: +Security vulnerabilities belong in a private +[GitHub Security Advisory](https://github.com/leynier/python-template/security/advisories/new), +not a public issue. -* Reporting a bug -* Reviewing the code -* Submitting a fix -* Proposing new features -* Becoming a maintainer +## Propose a component -## Branches +Explain: -We're using the following branches to manage work: +- which architectural layer and workload it serves; +- how it differs from supported components in that role; +- its maintained Python versions and upstream stability; +- what functional source and configuration the template should generate; and +- how CI can validate it without paid credentials. -* `develop` is semi-stable and should be used as the branch to fork from -* `main` is stable and ready for prod (or it will be once we merge in the first release) -* `feature`, `bug` branches: unstable development +The project intentionally excludes ODMantic, AutoGOAL, archived FastUI, and +archived Reflex. A new proposal should meet the inclusion criteria in the +[support model](docs/concepts/support.md). -## How to report bugs +## Develop a change -We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/leynier/python-template/issues/new/choose); it's that easy! +Create a focused branch from `main` and install every repository group: -### Write bug reports with detail, background, and sample code +```bash +git switch main +git pull --ff-only +git switch -c feat/short-description +uv sync --all-groups +``` -**Great Bug Reports** tend to have: +For a catalog change, edit `catalog/components.yml` or `catalog/presets.yml`, +then compile deterministic outputs: -* A quick summary and/or background -* Steps to reproduce - * Be specific! - * Give sample code if you can. -* What you expected to happen -* What actually happens -* Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) +```bash +uv run python scripts/compile_catalog.py +uv run python scripts/compile_catalog.py --check +``` -We <3 thorough bug reports. +Do not edit `catalog/generated/`, `docs/reference/`, or +`template/_catalog_dependencies.jinja` by hand. Add functional source, +dependency, configuration, documentation, and tests for the selected component. -## How to contribute code +## Validate before opening a PR -We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. +```bash +uv run python scripts/compile_catalog.py --check +uv run ruff check . +uv run ruff format --check . +uv run pytest -n auto +uv run --group docs zensical build --clean --strict +npx skills add . --list +``` -Pull requests are the best way to propose changes to the codebase. We use a loose version of Git Flow -and actively welcome your pull requests: +Generate and exercise at least one representative project. A test that only +checks file presence is not enough when an import, CLI command, endpoint, tool, +training step, or container health route can be validated locally. -1. Create a new branch based on `develop`: - * Feature branches should start with `feature/` - * Bugfix branches should start with `bug/` -2. Implement your changes. -3. Add tests if applicable. -4. Make sure your code lints. -5. Issue that pull request! +Hosted CI repeats generated-project checks on Linux, macOS, and Windows. Keep +conditional source paths short enough for Windows temporary directories. -### Any contributions you make will be under the MIT License +## Pull requests -When you submit code changes, your submissions are understood to be under the same [MIT](LICENSE) that covers the project. +Keep each PR centered on one layer or cohesive outcome. Describe the supported +combination, commands run, external checks that were unavailable, and any +credential-gated behavior that remains operationally unverified. By submitting +a contribution, you agree that it is licensed under this repository's +[MIT license](LICENSE). + +## Contributors + +- Leynier Gutiérrez González ([@leynier](https://github.com/leynier)) diff --git a/docs/concepts/layers.md b/docs/concepts/layers.md new file mode 100644 index 0000000..3f5e86c --- /dev/null +++ b/docs/concepts/layers.md @@ -0,0 +1,46 @@ +# Composable layers + +The generator treats architecture as a set of roles with compatibility rules. +This avoids the combinatorial maintenance cost of keeping a separate template +for every framework/provider/database/deploy permutation. + +| Layer | Responsibility | Cardinality | +| --- | --- | --- | +| Workload | Primary shape and entry point | exactly one | +| Framework | Runtime or AI orchestration framework | zero or one | +| Interface | API, web, notebook, or terminal front end | zero or more | +| Model provider | Generative model access | zero or one | +| Embedding provider | Embedding model access | zero or one, independent | +| Data roles | SQL, document, vector, graph, cache | zero or one per role | +| SQL abstraction | SQLModel or SQLAlchemy | zero or one for eligible SQL stores | +| Auth | Request authentication | zero or one | +| Training extensions | Data, acceleration, tuning, optimization | zero or more | +| Serving | Model or gateway runtime | zero or one | +| MLOps and quality | Orchestration, evaluation, telemetry | zero or more | +| Deploy | Runtime destination | zero or one | +| IaC | Cloud resource definition | none, Pulumi, or Terraform | + +## Compatibility is resolved before rendering + +Copier validators reject combinations that cannot form a meaningful project. +Examples include using an agent framework for a plain library preset, adding a +SQL abstraction without a compatible SQL engine, or selecting IaC for a target +without a generated infrastructure contract. + +Python compatibility is also visible in the catalog. The general baseline is +Python 3.12 or newer; components whose upstream support is narrower constrain +the available versions. + +## Data roles stay explicit + +A single engine can fill several roles when it supports them. Beaver, for +example, can cover document, vector, graph, and cache in a local-first stack. +That does not collapse the roles: each answer remains explicit, so a project can +later move only vector retrieval to a hosted engine without changing its graph +store. + +## Portable deployment remains the base + +Selecting a deploy target creates its native configuration and keeps a Docker +path. Cloud-specific Pulumi or Terraform is additive. Application code does not +become dependent on the IaC choice. diff --git a/docs/concepts/support.md b/docs/concepts/support.md new file mode 100644 index 0000000..f14a2fa --- /dev/null +++ b/docs/concepts/support.md @@ -0,0 +1,31 @@ +# Support model + +Every catalog component has a support tier and a Python compatibility range. +The tier describes what this repository can verify; it is not a judgment about +the upstream project's general quality. + +## Stable + +The integration can be exercised locally or offline without a paid account. +Generated dependencies, imports, configuration, and representative behavior are +covered by the test suite. + +## Platform + +The integration depends on a hosted API or cloud account. The repository tests +the generated client boundary, configuration, parsability, and local contract. +Users still need to supply credentials, accept provider costs, and perform a +real deployment verification. + +## Experimental + +The upstream API or the integration surface is evolving. It remains useful, but +may support fewer combinations and carries a narrower compatibility promise. + +## Inclusion criteria + +A component should add a distinct architectural role, have active maintenance, +support the template's Python baseline, and permit a meaningful automated test. +ODMantic, AutoGOAL, archived FastUI, and archived Reflex are intentionally not +included. New proposals should explain which layer they add and how a generated +project can validate the integration without embedding credentials. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..4afcf9e --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,56 @@ +# Getting started + +## Requirements + +Install [`uv`](https://docs.astral.sh/uv). Copier itself can run ephemerally +through `uvx`, so no global Python environment is required. + +## Generate interactively + +```bash +uvx copier copy --trust gh:leynier/python-template my-project +``` + +`--trust` allows the template's post-copy tasks to initialize Git and run +`uv sync`. The prompts first offer an editable preset, then expose the layers +and normal project metadata. + +## Start from a preset + +Preselect a recipe and keep the remaining answers interactive: + +```bash +uvx copier copy --trust \ + -d preset=pydantic-ai-openai \ + gh:leynier/python-template my-agent +``` + +Use `-d key=value` repeatedly for automation. Keep the answer set in version +control through the generated `.copier-answers.yml` rather than maintaining a +second undocumented configuration. + +## Verify the result + +```bash +cd my-project +uv sync --all-groups +uv run ruff check . +uv run ruff format --check . +uv run pytest +``` + +The generated `readme.md` documents its primary entry point, environment +variables, Docker command, deployment adapter, and release flow when enabled. + +## Pull in later template releases + +Commit or stash local work before updating, then run: + +```bash +uvx copier update +uv sync --all-groups +uv run pytest +``` + +Copier performs a three-way update using `.copier-answers.yml`; review the diff +because template changes and project-specific edits can touch the same file. diff --git a/docs/guides/agent-skills.md b/docs/guides/agent-skills.md new file mode 100644 index 0000000..a3d8942 --- /dev/null +++ b/docs/guides/agent-skills.md @@ -0,0 +1,36 @@ +# Agent skills + +The repository follows the open Agent Skills layout under `skills/`, so it is +discoverable by the `skills` CLI and compatible coding agents. + +## Inspect and install + +```bash +npx skills add leynier/python-template --list +npx skills add leynier/python-template --skill compose-python-stack +``` + +Repository skills: + +- `compose-python-stack` translates a product goal into compatible layers and + generates the project. +- `maintain-python-template` keeps catalog, Copier questions, templates, + compiler outputs, and tests synchronized. +- `validate-python-stack` selects the appropriate repository or generated + project gates and reports unavailable checks honestly. + +## Skills inside generated projects + +Every generated project contains `project-workflow`. Projects with an AI or ML +capability add `operate-ai-stack`; projects with a deploy target add +`deploy-python-project`. A simple CLI therefore receives no cloud or AI +instructions it cannot use. + +From a generated project, confirm discovery with: + +```bash +npx skills add . --list +``` + +Each skill includes `agents/openai.yaml` metadata and is validated with the +skill-creator validator in this repository's release process. diff --git a/docs/guides/choose-a-stack.md b/docs/guides/choose-a-stack.md new file mode 100644 index 0000000..c76c676 --- /dev/null +++ b/docs/guides/choose-a-stack.md @@ -0,0 +1,44 @@ +# Choose a stack + +Use a preset when it expresses the same product shape, even if one provider or +interface will change. Use `custom` when the workload itself or several layers +differ. + +## Stay simple + +- Choose `python-library` for reusable importable behavior. +- Choose `typer-cli` for commands, automation, and local developer tools. +- Choose `fastapi-api` for a JSON service. Custom API projects can select Flask. + +These presets intentionally set AI capabilities to `none`. + +## Expose tools to models + +Choose `fastmcp-server` when the product boundary is MCP tools and resources. +It generates a FastMCP server and a tested local tool rather than wrapping an +agent around the server unnecessarily. + +## Build an agent + +Start with Pydantic AI, Google ADK, Strands Agents, LangGraph, or the local Lingo +recipe. Select the model provider separately. Pydantic AI can add selected +Pydantic AI Harness capabilities without making Harness mandatory. + +## Build RAG + +Choose the orchestration framework, model provider, embedding provider, vector +store, and user interface independently. Test ingestion and empty retrieval as +well as the successful answer path. + +## Train or serve models + +Training projects choose a framework plus optional datasets, acceleration, +fine-tuning, and experiment tooling. Hybrid projects use a `uv` workspace to +keep training and service dependencies separate. Serving can target BentoML, +LiteLLM, vLLM, Ollama, or Ray Serve. + +## Finish with operations + +Add only the evaluation, telemetry, orchestration, deployment, and IaC layers +the project will operate. An unused observability SDK is maintenance cost, not +production readiness. diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md new file mode 100644 index 0000000..ac6bfd9 --- /dev/null +++ b/docs/guides/deployment.md @@ -0,0 +1,37 @@ +# Deployment and IaC + +Choose one runtime destination. A deployed project retains a portable Docker +base and adds the selected platform's configuration. + +## Targets + +| Group | Targets | +| --- | --- | +| Portable and PaaS | Docker/Compose, Render, Fly.io, Vercel, Railway | +| AI platforms | Hugging Face Spaces, Modal, RunPod, BentoCloud | +| AWS | ECS, SageMaker | +| Google Cloud | Cloud Run, Vertex AI | +| Azure | Container Apps, Azure ML | + +Modal, RunPod, and BentoCloud receive native Python adapters. Managed inference +targets use consistent health and prediction routes so container and platform +checks exercise the same application contract. + +## Infrastructure as code + +Pulumi and Terraform are available for AWS ECS, SageMaker, Cloud Run, Vertex +AI, Azure Container Apps, and Azure ML. Select `none` when the platform's native +configuration or an external infrastructure repository owns those resources. + +Generated IaC is a starting resource graph, not permission to apply it. Review +account, region, naming, cost, secrets, state backend, and rollback before a +preview or plan becomes an update or apply. + +## Verification sequence + +1. Run the generated quality gates. +2. Build the Docker image and verify its health route locally. +3. Parse or validate the platform and IaC configuration. +4. Preview or plan cloud changes. +5. Deploy only with explicit authorization. +6. Verify the remote health route and one representative request. diff --git a/docs/guides/development.md b/docs/guides/development.md new file mode 100644 index 0000000..4b6871e --- /dev/null +++ b/docs/guides/development.md @@ -0,0 +1,44 @@ +# Develop the template + +## Set up + +```bash +uv sync --all-groups +``` + +Catalog sources live in `catalog/components.yml` and `catalog/presets.yml`. +Generated JSON, Copier choices, dependency mapping, and reference pages must not +be edited by hand. + +## Validate catalog changes + +```bash +uv run python scripts/compile_catalog.py +uv run python scripts/compile_catalog.py --check +``` + +Review every generated diff. Then add the functional template, dependency, +configuration, documentation, and tests for the component. + +## Run the gates + +```bash +uv run ruff check . +uv run ruff format --check . +uv run pytest -n auto +uv run --group docs zensical build --clean --strict +``` + +Focused tests shorten iteration, but delivery requires the full suite. Hosted +CI validates generated toolchains on Linux, macOS, and Windows and runs the +credential-free AI preset vertical slices on Linux. + +## Preview these docs + +```bash +uv run --group docs zensical serve +``` + +The repository documentation is independent of the optional documentation +generated inside a user's project. Vercel builds this root site from +`zensical.toml`; generated projects retain their own Zensical configuration. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..24000cc --- /dev/null +++ b/docs/index.md @@ -0,0 +1,34 @@ +# Build the Python stack you actually need + +Python Template generates production-ready Python and AI projects from +compatible layers. Start from one of 12 editable recipes or compose the +workload, framework, providers, data roles, interface, operations, and deploy +target yourself. + +```bash +uvx copier copy --trust gh:leynier/python-template my-project +cd my-project +uv run pytest +``` + +## Small projects stay small + +The Python Library, Typer CLI, and FastAPI API presets do not pull in an AI +framework. They receive the same tested packaging, CI, security, documentation, +and update path as larger stacks. + +## AI and ML are layers, not a separate template + +Choose an MCP server, agent, RAG app, inference service, training project, or +hybrid workspace. Model and embedding providers remain independent. SQL, +document, vector, graph, and cache roles each select their own engine. The +result is one coherent project rather than snippets from unrelated starters. + +## Start here + +- [Generate your first project](getting-started.md) +- [Choose between presets and custom layers](guides/choose-a-stack.md) +- [Understand the layer model](concepts/layers.md) +- [Install the agent skills](guides/agent-skills.md) +- [Review deployment and IaC targets](guides/deployment.md) +- [Browse all components](reference/components.md) diff --git a/pyproject.toml b/pyproject.toml index acf46f9..14ac5a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "python-template" version = "0.5.0" -description = "Composable Python and AI project template powered by Copier and uv" +description = "Generate production-ready Python and AI stacks by combining frameworks, data engines, tooling, and cloud deployment." readme = "readme.md" requires-python = ">=3.13" authors = [ @@ -29,6 +29,9 @@ dev = [ "pytest-xdist>=3.8.0", "ruff>=0.16.2", ] +docs = [ + "zensical>=0.0.53", +] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/readme.md b/readme.md index da5eb3c..7b271ff 100644 --- a/readme.md +++ b/readme.md @@ -2,122 +2,163 @@ [![CI](https://github.com/leynier/python-template/actions/workflows/ci.yml/badge.svg)](https://github.com/leynier/python-template/actions/workflows/ci.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/leynier/python-template/badge)](https://scorecard.dev/viewer/?uri=github.com/leynier/python-template) -[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Copier](https://img.shields.io/badge/template-copier-2ea44f)](https://copier.readthedocs.io) +[![Documentation](https://img.shields.io/badge/docs-python--template.leynier.dev-5c6ac4)](https://python-template.leynier.dev) +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +[![Copier](https://img.shields.io/badge/template-Copier-2ea44f)](https://copier.readthedocs.io) [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![Last commit](https://img.shields.io/github/last-commit/leynier/python-template.svg?style=flat)](https://github.com/leynier/python-template/commits) -[![Github Stars](https://img.shields.io/github/stars/leynier/python-template?style=flat&logo=github)](https://github.com/leynier/python-template/stargazers) +[![Last commit](https://img.shields.io/github/last-commit/leynier/python-template.svg)](https://github.com/leynier/python-template/commits) +[![GitHub stars](https://img.shields.io/github/stars/leynier/python-template?logo=github)](https://github.com/leynier/python-template/stargazers) -A modern Python project template with CI/CD ready for production. +Generate production-ready Python and AI stacks by combining frameworks, data engines, tooling, and cloud deployment. -## Usage +Start with a simple library, CLI, or API. Or compose an agent, RAG system, +training workspace, inference service, data layer, UI, quality stack, and deploy +target without assembling the project conventions yourself. -You need [uv](https://docs.astral.sh/uv). Nothing else. +## Quick start -```bash -uvx copier copy gh:leynier/python-template my-project -``` - -Answer the prompts and you get a working project: dependencies installed, git -initialised, tests passing. - -To pull later improvements to this template into a project you already -generated: +Install nothing globally beyond [`uv`](https://docs.astral.sh/uv): ```bash +uvx copier copy --trust gh:leynier/python-template my-project cd my-project -uvx copier update +uv run pytest ``` -That last command is the reason this template uses Copier instead of -Cookiecutter — Cookiecutter has no way to update a project after generating it. +Copier presents 12 editable recipes plus a custom layer-by-layer path. It +initializes Git and installs the selected dependencies after rendering. -## What you get +Want the small version? These remain first-class choices with no AI dependency: -One prompt, `project_type`, replaces what used to be three separate branches: +```text +Python Library typed, buildable package +Typer CLI tested command-line application +FastAPI API production-shaped JSON service +``` -| `project_type` | What it generates | -| -------------- | ---------------------------------------------------- | -| `library` | An importable package | -| `cli` | A command line app built with [Typer](https://typer.tiangolo.com) | -| `api` | A web API built with [FastAPI](https://fastapi.tiangolo.com) | +## Compose the stack you need + +The generator resolves each choice as a layer, so infrastructure can change +without replacing the application framework and model providers can change +without coupling them to the embedding provider. + +| Layer | Examples | +| --- | --- | +| Workload | library, CLI, API, web, TUI, MCP, agent, RAG, inference, training, hybrid | +| Framework | FastAPI, Flask, FastMCP, Pydantic AI, LangGraph, LlamaIndex, Lingo, Transformers | +| Interface | Streamlit, Gradio, Chainlit, Textual, NiceGUI, FastHTML, Violetear, JupyterLab | +| Model provider | OpenAI, Anthropic, Gemini, Bedrock, Azure OpenAI, Ollama, OpenRouter and more | +| Embeddings | hosted provider or Sentence Transformers, selected independently | +| Data | SQL, document, vector, graph, and cache roles with one engine per role | +| Auth | API key, OAuth/OIDC, or Supabase Auth | +| Training | Lightning, Datasets, Accelerate, PEFT, TRL, Optuna | +| Serving | BentoML, LiteLLM, vLLM, Ollama, Ray Serve | +| MLOps and quality | Prefect, Dagster, DVC, MLflow, Ragas, DeepEval, OpenTelemetry and more | +| Deploy | Docker plus 14 managed or cloud targets | +| IaC | none, Pulumi, or Terraform for supported cloud targets | + +The catalog currently contains 126 components with explicit workload, Python, +and support-tier metadata. See the complete [component +reference](https://python-template.leynier.dev/reference/components/). + +## The 12 presets + +Presets are useful starting points, not locked bundles. Every answer remains +editable during generation. + +| Preset | Starting stack | +| --- | --- | +| `python-library` | Typed publishable package | +| `typer-cli` | Typer command-line app | +| `fastapi-api` | FastAPI JSON service | +| `fastmcp-server` | FastMCP tool server | +| `pydantic-ai-openai` | Pydantic AI + Harness + OpenAI | +| `google-adk-gemini` | Google ADK + Gemini | +| `strands-bedrock` | Strands Agents + Bedrock | +| `langgraph-anthropic-api` | LangGraph + Anthropic + FastAPI | +| `llamaindex-rag` | LlamaIndex + OpenAI + Pinecone + Gradio | +| `local-lingo-app` | Lingo + Ollama + Beaver + Violetear | +| `litellm-gateway` | LiteLLM + Redis + Docker | +| `hf-finetuning` | Transformers + PEFT/TRL + MLflow + BentoML | + +Preselect one while keeping the remaining questions interactive: -### Tooling +```bash +uvx copier copy --trust \ + -d preset=fastmcp-server \ + gh:leynier/python-template my-tools +``` -- **[uv](https://docs.astral.sh/uv)** for dependencies, with a committed - `uv.lock`, PEP 621 metadata and PEP 735 dependency groups. -- **[Ruff](https://docs.astral.sh/ruff)** for linting and formatting — one tool - in place of flake8, black, isort and pyupgrade. -- **[ty](https://github.com/astral-sh/ty)** for type checking. -- **[deptry](https://deptry.com)** for undeclared and unused dependencies. -- **[pre-commit](https://pre-commit.com)** wiring all of the above into git - hooks. -- `src/` layout with a `py.typed` marker. +## Deploy without rebuilding the project -### CI/CD, via GitHub Actions +Every deployment choice keeps a portable Docker base. Choose one target among +Docker, Render, Fly.io, Vercel, Railway, Hugging Face Spaces, Modal, RunPod, +BentoCloud, AWS ECS, SageMaker, Cloud Run, Vertex AI, Azure Container Apps, or +Azure ML. The six cloud targets can additionally generate Pulumi or Terraform. -- Test matrix across Linux, macOS and Windows × every supported Python version. -- Least-privilege `permissions:` on every workflow, concurrency groups, and - actions pinned to full commit SHAs. -- **PyPI publishing with [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)** — - OIDC, no API tokens, with Sigstore attestations. The workflow refuses to - publish if the git tag does not match the project version. -- **[CodeQL](https://codeql.github.com)** scanning and - **[zizmor](https://github.com/zizmorcore/zizmor)** auditing the workflows - themselves. -- Dependabot covering `uv`, `github-actions` and Docker. +Managed inference variants expose consistent health and prediction contracts, +while Modal, RunPod, and BentoCloud receive native SDK adapters. -### Documentation +## Skills for AI coding agents -Built with **[Zensical](https://zensical.org)**, the successor to Material for -MkDocs from the same team, with **[mkdocstrings](https://mkdocstrings.github.io)** -generating an API reference from your docstrings. Deployed to GitHub Pages via -OIDC on every push to `main`. +This repository is directly discoverable by the open Agent Skills CLI: -### Optional extras +```bash +npx skills add leynier/python-template --list +npx skills add leynier/python-template --skill compose-python-stack +``` -A multi-stage `Dockerfile` running as a non-root user, a VS Code devcontainer, -and an `AGENTS.md` so AI coding agents pick up the project conventions. +Repository skills help an agent compose, maintain, and validate stacks. Every +generated project also includes a common workflow skill and conditionally adds +AI and deployment skills matching its selected layers. -## Two deliberate bets +## What every generated project gets -This template adopts two tools that are not yet 1.0. Both are used in a way -that fails soft: +- A `src/` layout, typed package marker, bounded dependencies, and committed + `uv.lock`. +- Ruff, pytest, deptry, advisory `ty`, pre-commit, coverage, and Poe tasks. +- CI across Linux, macOS, and Windows, with least-privilege permissions and + pinned actions. +- CodeQL, zizmor, Dependabot, issue forms, security policy, contribution guide, + and changelog. +- Optional Zensical docs, PyPI Trusted Publishing, Docker, devcontainer, and + agent instructions. +- A saved `.copier-answers.yml` so later template releases can be applied with + `uvx copier update`. -- **ty** is at `0.0.x` and has no plugin system. CI runs `ty check` with - `continue-on-error: true`, so it reports findings without gating your build. - When ty reaches 1.0, drop that line from `.github/workflows/ci.yml`. -- **Zensical** is at `0.0.x` and does not yet have full plugin parity with - Material for MkDocs. mkdocstrings is verified to work — the test suite asserts - the API reference is really rendered, not passed through — but other plugins - may not be. Zensical reads `mkdocs.yml`, so moving back is cheap. +## Support model -If you would rather not take those bets, answer `use_docs: false` and swap -`ty` for mypy in `pyproject.toml`. +Catalog entries use three intentionally visible tiers: -## File naming +- `stable`: open-source component exercised without external credentials. +- `platform`: hosted or cloud integration whose generated contract is tested + offline; real deployment still requires the user's account and secrets. +- `experimental`: useful but evolving integration with a narrower compatibility + promise. -Generated projects use lowercase filenames (`readme.md`), except for files that -GitHub or tooling matches by exact name, which stay uppercase: `LICENSE`, -`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `AGENTS.md`, -`Dockerfile` and `.github/ISSUE_TEMPLATE/`. +ODMantic, AutoGOAL, archived FastUI, and archived Reflex are intentionally not +offered. The catalog favors maintained projects that add a distinct layer or a +clear end-to-end recipe. -## Developing this template +## Develop the template ```bash uv sync --all-groups -uv run pytest -m "not slow" # structural tests: what gets rendered -uv run pytest -m slow # generates each variant and runs its toolchain -uv run pytest # everything +uv run python scripts/compile_catalog.py --check +uv run ruff check . +uv run ruff format --check . +uv run pytest -n auto +uv run --group docs zensical serve ``` -The slow suite is the important one: it generates every project type and runs -`uv sync`, Ruff, pytest, deptry, ty, `uv build` and the docs build inside each -generated project. The previous version of this template had no tests at all, -which is how it managed to sit broken for years without anyone noticing. +The test suite renders compatible combinations, runs real generated toolchains, +parses deployment artifacts, checks `copier update`, and exercises representative +AI/ML vertical slices. Hosted CI repeats the generated-project tests on Linux, +macOS, and Windows. + +Read the [documentation](https://python-template.leynier.dev), the +[contribution guide](CONTRIBUTING.md), or the [security policy](SECURITY.md). ## License -This project is collaborative and open source under the [MIT license](LICENSE). -Contributions are super appreciated. +Python Template is collaborative open source under the [MIT license](LICENSE). diff --git a/tests/test_repo_docs.py b/tests/test_repo_docs.py new file mode 100644 index 0000000..f42abf0 --- /dev/null +++ b/tests/test_repo_docs.py @@ -0,0 +1,63 @@ +"""Repository documentation and Vercel deployment contract.""" + +import json +import pathlib +import tomllib + +REPO = pathlib.Path(__file__).parents[1] + + +def _nav_targets(items: list[dict[str, object]]) -> list[str]: + targets: list[str] = [] + for item in items: + value = next(iter(item.values())) + if isinstance(value, str): + targets.append(value) + else: + targets.extend(_nav_targets(value)) + return targets + + +def test_zensical_navigation_points_to_real_repository_docs() -> None: + config = tomllib.loads((REPO / "zensical.toml").read_text()) + project = config["project"] + + assert project["site_dir"] == "site" + assert project["site_url"] == "https://python-template.leynier.dev/" + for target in _nav_targets(project["nav"]): + assert (REPO / "docs" / target).is_file(), target + + +def test_vercel_builds_the_zensical_static_output() -> None: + config = json.loads((REPO / "vercel.json").read_text()) + + assert config["outputDirectory"] == "site" + assert "zensical build" in config["buildCommand"] + assert "--strict" in config["buildCommand"] + assert "uv sync --locked" in config["installCommand"] + + +def test_ci_builds_repository_documentation() -> None: + workflow = (REPO / ".github" / "workflows" / "ci.yml").read_text() + + assert "Repository documentation" in workflow + assert "zensical build --clean --strict" in workflow + assert "needs: [lint, structure, presets, docs, toolchain, zizmor]" in workflow + + +def test_readme_presents_layers_simple_projects_and_agent_skills() -> None: + readme = (REPO / "readme.md").read_text() + + for marker in ( + "Generate production-ready Python and AI stacks", + "Typer CLI", + "FastAPI API", + "FastMCP", + "Pydantic AI", + "Lingo", + "Violetear", + "npx skills add leynier/python-template --list", + "Pulumi", + "Terraform", + ): + assert marker in readme diff --git a/uv.lock b/uv.lock index 07129c1..610ce1d 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -44,6 +56,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/bf/3adcb9b3091b36de729dad91c107179c8c7c51adb2b08c31177bb540bef1/copier-9.17.1-py3-none-any.whl", hash = "sha256:f6a48f296e2e536c635213e1000ea71099f9f58f3b464a60f2f0fd386e343628", size = 66364, upload-time = "2026-08-04T09:15:03.026Z" }, ] +[[package]] +name = "deepmerge" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, +] + [[package]] name = "deprecated" version = "1.3.1" @@ -129,6 +150,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -318,6 +348,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -391,6 +434,9 @@ dev = [ { name = "pyyaml" }, { name = "ruff" }, ] +docs = [ + { name = "zensical" }, +] [package.metadata] @@ -405,6 +451,7 @@ dev = [ { name = "pyyaml", specifier = ">=6.0.3,<7" }, { name = "ruff", specifier = ">=0.16.2" }, ] +docs = [{ name = "zensical", specifier = ">=0.0.53" }] [[package]] name = "pyyaml" @@ -551,6 +598,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -633,3 +716,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, ] + +[[package]] +name = "zensical" +version = "0.0.53" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/8b/d916d8226738421a847f039f71278fd07789744c32e9b40abcfa8b849ad8/zensical-0.0.53.tar.gz", hash = "sha256:61672d3e6389822b5738e099816dbc07416ea84db67c2b1cb7e6ea977d2e04d7", size = 3988318, upload-time = "2026-08-04T14:08:54.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/53/5db8c8e5a257db9a5fff0b77c8e05783283d6aaf42c05e34577f6b59f5d0/zensical-0.0.53-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:292cf9c7c323a50c6e3515d334ca08d9dcc517ce6d9d8ad1cd94d22befab1f56", size = 12835291, upload-time = "2026-08-04T14:08:16.746Z" }, + { url = "https://files.pythonhosted.org/packages/33/73/49a64c2c44aec251336a1cedcccbec7ba3d3eba9dd75d52ed24c09217d86/zensical-0.0.53-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0f4c1219c534d3cccc0b86093748dc009e0e9d80d4dad8d65e2150c846aa1123", size = 12719959, upload-time = "2026-08-04T14:08:20.279Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3a/2c08429f7c725d1a40d158b84d6aca4b5c4320d09a0a313e875d7dd3bfe5/zensical-0.0.53-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ca63b952b4961461b4376d61603adc2bf9d81b4df4946b2f27e20b2726f881f", size = 13169416, upload-time = "2026-08-04T14:08:23.474Z" }, + { url = "https://files.pythonhosted.org/packages/41/bc/ed057082989645d5ad3245bdf0b14c30334a315f866552c794c2413cf92f/zensical-0.0.53-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34f41b7f37a0430a1378ac13d9a72513fcc53db676c124378cf63cc6f6e22713", size = 13099720, upload-time = "2026-08-04T14:08:26.521Z" }, + { url = "https://files.pythonhosted.org/packages/a4/54/859cf2267ef853ff20eee2af37d898071f821bf30ec3df7d73061b391c78/zensical-0.0.53-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30ed22e9fcedda71888d9fe84f4fdb1aadd3b66cdb0223716f1eecce9ae22b07", size = 13482295, upload-time = "2026-08-04T14:08:29.618Z" }, + { url = "https://files.pythonhosted.org/packages/35/94/f73744d9f4b6107e2740aad58214285b84d4cf0997cde36bced43089b3d0/zensical-0.0.53-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b0cb72861b14bd985bc5ad0203c35b1da7a19c87c194df3189fab7a910db04", size = 13140985, upload-time = "2026-08-04T14:08:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ef/7557d859e25e4a74214d718a1528f2a123ff9d84823b49b32df9bc41ef17/zensical-0.0.53-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:15e9813f0f59db6cf1316414301139d030f1690b68af645f1bf68d78bc3defe0", size = 13344554, upload-time = "2026-08-04T14:08:35.924Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d9/3a1011bd4390e85a6f602afca6ff8b862454415a800e7b41471dadd9e6b1/zensical-0.0.53-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c203493598d6cad890d7cb48f9d75693f648fe0d2347b2f147406a99fd7bb101", size = 13373180, upload-time = "2026-08-04T14:08:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ee/f4faf3d66d1e854afa43fa5354c1e0c8414af3fc5c563233a3ca7f10e494/zensical-0.0.53-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:ec34844b3bc1855f5c10b99efbeebd27abcd983a9144dbad965609e65915c050", size = 13531133, upload-time = "2026-08-04T14:08:42.679Z" }, + { url = "https://files.pythonhosted.org/packages/5e/98/4a0272bb79bdd326714e552f685d58f31a501ffd49bdc17ae187e92b2581/zensical-0.0.53-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e016062c3299c84be811848d1e81ad0f3f711615f0bed87bb0a1b47f6968a5a4", size = 13480141, upload-time = "2026-08-04T14:08:45.97Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/8cbceea1e7f4d6d2ac078a0c34ccd06f1419401248d22f6d6ded4ac9a443/zensical-0.0.53-cp310-abi3-win32.whl", hash = "sha256:abb0af33bb646f15224045baa6c4118b59a2c9c3f80d7cd48edd66ee961c1985", size = 12410234, upload-time = "2026-08-04T14:08:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/65f0ced38274b6c1073a4b1c52ea41b8b43e7e972f5e3979c2f2aca5cc46/zensical-0.0.53-cp310-abi3-win_amd64.whl", hash = "sha256:8b609bc89717b6f276774651ea3a41df21b4813929d2a206ee161a294dc28cd1", size = 12646224, upload-time = "2026-08-04T14:08:51.945Z" }, +] diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..62682d0 --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "installCommand": "python3 -m pip install uv==0.12.3 && uv sync --locked --only-group docs", + "buildCommand": "uv run --only-group docs zensical build --clean --strict", + "outputDirectory": "site", + "cleanUrls": true, + "trailingSlash": false +} diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..8595f75 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,86 @@ +[project] +site_name = "Python Template" +site_url = "https://python-template.leynier.dev/" +site_description = "Generate production-ready Python and AI stacks by combining frameworks, data engines, tooling, and cloud deployment." +site_author = "Leynier Gutiérrez González" +copyright = "Copyright © 2026 Leynier Gutiérrez González" + +repo_url = "https://github.com/leynier/python-template" +repo_name = "leynier/python-template" +edit_uri = "edit/main/docs/" +site_dir = "site" + +nav = [ + { "Home" = "index.md" }, + { "Get started" = [ + { "Quick start" = "getting-started.md" }, + { "Choose a stack" = "guides/choose-a-stack.md" }, + ] }, + { "Concepts" = [ + { "Composable layers" = "concepts/layers.md" }, + { "Support model" = "concepts/support.md" }, + ] }, + { "Guides" = [ + { "Agent skills" = "guides/agent-skills.md" }, + { "Deployment and IaC" = "guides/deployment.md" }, + { "Develop the template" = "guides/development.md" }, + ] }, + { "Reference" = [ + { "Presets" = "reference/presets.md" }, + { "Components" = "reference/components.md" }, + ] }, +] + +[project.theme] +language = "en" +features = [ + "content.action.edit", + "content.code.annotate", + "content.code.copy", + "content.tooltips", + "navigation.footer", + "navigation.indexes", + "navigation.instant", + "navigation.instant.prefetch", + "navigation.sections", + "navigation.top", + "navigation.tracking", + "search.highlight", + "toc.follow", +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/leynier" + +[project.markdown_extensions] +abbr = {} +admonition = {} +attr_list = {} +def_list = {} +footnotes = {} +md_in_html = {} +toc = { permalink = true } +pymdownx.details = {} +pymdownx.highlight = { anchor_linenums = true, line_spans = "__span", pygments_lang_class = true } +pymdownx.inlinehilite = {} +pymdownx.superfences = {} +pymdownx.tabbed = { alternate_style = true, combine_header_slug = true } +pymdownx.tasklist = { custom_checkbox = true }