From 61c00ced479bd05123194bb2b18c26e5dddbeeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leynier=20Guti=C3=A9rrez=20Gonz=C3=A1lez?= Date: Sat, 8 Aug 2026 16:53:52 -0600 Subject: [PATCH] Build the composable v0.5 catalog core --- .github/workflows/ci.yml | 3 + catalog/components.yml | 318 ++++ catalog/generated/catalog.json | 1524 +++++++++++++++++ catalog/generated/choices.yml | 139 ++ catalog/presets.yml | 51 + copier.yml | 400 ++++- docs/reference/components.md | 132 ++ docs/reference/presets.md | 76 + pyproject.toml | 6 +- scripts/compile_catalog.py | 214 +++ template/pyproject.toml.jinja | 10 +- template/readme.md.jinja | 14 +- .../src/{{ module_name }}/__init__.py.jinja | 8 +- ...rkload == 'api' %}api.py{% endif %}.jinja} | 20 + ...rkload == 'cli' %}cli.py{% endif %}.jinja} | 0 .../tests/test_{{ module_name }}.py.jinja | 30 +- ..._instructions %}AGENTS.md{% endif %}.jinja | 8 +- ...f use_docker %}Dockerfile{% endif %}.jinja | 9 +- .../api.md.jinja | 4 +- .../index.md.jinja | 14 +- .../usage.md.jinja | 6 +- tests/test_catalog.py | 48 + tests/test_generation.py | 81 +- tests/test_toolchain.py | 18 +- tests/test_update.py | 4 +- uv.lock | 6 +- 26 files changed, 3069 insertions(+), 74 deletions(-) create mode 100644 catalog/components.yml create mode 100644 catalog/generated/catalog.json create mode 100644 catalog/generated/choices.yml create mode 100644 catalog/presets.yml create mode 100644 docs/reference/components.md create mode 100644 docs/reference/presets.md create mode 100644 scripts/compile_catalog.py rename template/src/{{ module_name }}/{{% if project_type == 'api' %}api.py{% endif %}.jinja => {% if workload == 'api' %}api.py{% endif %}.jinja} (58%) rename template/src/{{ module_name }}/{{% if project_type == 'cli' %}cli.py{% endif %}.jinja => {% if workload == 'cli' %}cli.py{% endif %}.jinja} (100%) create mode 100644 tests/test_catalog.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b7ad5a..21e11eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,9 @@ jobs: - name: Validate copier.yml run: uv run python -c "import yaml,pathlib; yaml.safe_load(pathlib.Path('copier.yml').read_text())" + - name: Validate generated catalog artifacts + run: uv run python scripts/compile_catalog.py --check + structure: name: Generation tests runs-on: ubuntu-latest diff --git a/catalog/components.yml b/catalog/components.yml new file mode 100644 index 0000000..f4c1352 --- /dev/null +++ b/catalog/components.yml @@ -0,0 +1,318 @@ +schema_version: 1 + +components: + # Workloads define the shape of a generated project. A hybrid project uses + # an uv workspace and combines training with a serving workload. + - id: library + name: Python library + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: cli + name: CLI application + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: api + name: API service + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: web + name: Python web application + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: tui + name: Terminal UI + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: mcp + name: MCP server + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: agent + name: AI agent + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: rag + name: RAG application + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: inference + name: Model inference service + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: training + name: Model training project + layer: workload + tier: stable + python: ">=3.11,<3.15" + - id: hybrid + name: Training and serving workspace + layer: workload + tier: stable + python: ">=3.11,<3.15" + + # Primary frameworks. Only one primary framework is selected per workload. + - id: typer + name: Typer + layer: framework + tier: stable + workloads: [cli] + python: ">=3.11,<3.15" + packages: ["typer>=0.27.1,<1"] + - id: fastapi + name: FastAPI + layer: framework + tier: stable + workloads: [api] + python: ">=3.11,<3.15" + packages: ["fastapi[standard]>=0.141.1,<1", "pydantic>=2.13.4,<3"] + - id: flask + name: Flask + layer: framework + tier: stable + workloads: [api] + python: ">=3.11,<3.15" + packages: ["flask>=3.1.2,<4"] + - id: fastmcp + name: FastMCP + layer: framework + tier: stable + workloads: [mcp] + python: ">=3.11,<3.15" + packages: ["fastmcp>=3.4.6,<4"] + - id: pydantic-ai + name: PydanticAI + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: langgraph + name: LangGraph + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: langchain + name: LangChain + layer: framework + tier: stable + workloads: [agent, rag] + python: ">=3.11,<3.15" + - id: llamaindex + name: LlamaIndex + layer: framework + tier: stable + workloads: [agent, rag] + python: ">=3.11,<3.15" + - id: crewai + name: CrewAI + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: smolagents + name: Hugging Face smolagents + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: dspy + name: DSPy + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: openai-agents + name: OpenAI Agents SDK + layer: framework + tier: platform + workloads: [agent] + python: ">=3.11,<3.15" + - id: google-adk + name: Google ADK + layer: framework + tier: platform + workloads: [agent] + python: ">=3.11,<3.15" + - id: strands-agents + name: Strands Agents + layer: framework + tier: platform + workloads: [agent] + python: ">=3.11,<3.15" + - id: autogen + name: Microsoft AutoGen + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: ag2 + name: AG2 + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: agno + name: Agno + layer: framework + tier: stable + workloads: [agent] + python: ">=3.11,<3.15" + - id: lingo + name: Lingo + layer: framework + tier: stable + workloads: [agent] + python: ">=3.12,<3.15" + - id: haystack + name: Haystack + layer: framework + tier: stable + workloads: [rag] + python: ">=3.11,<3.15" + - id: pytorch + name: PyTorch + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + - id: transformers + name: Hugging Face Transformers + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + - id: jax + name: JAX + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + - id: keras + name: Keras / TensorFlow + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + - id: scikit-learn + name: scikit-learn + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + - id: xgboost + name: XGBoost + layer: framework + tier: stable + workloads: [training, hybrid] + python: ">=3.11,<3.15" + + # Interfaces are composable and can expose the same application through + # multiple entrypoints. + - {id: streamlit, name: Streamlit, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: gradio, name: Gradio, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: chainlit, name: Chainlit, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: textual, name: Textual, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: nicegui, name: NiceGUI, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: fasthtml, name: FastHTML, layer: interface, tier: stable, python: ">=3.11,<3.15"} + - {id: violetear, name: Violetear, layer: interface, tier: stable, python: ">=3.12,<3.15"} + - {id: jupyterlab, name: JupyterLab, layer: interface, tier: stable, python: ">=3.11,<3.15"} + + # Providers are intentionally independent for generation and embeddings. + - {id: openai, name: OpenAI, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: anthropic, name: Anthropic, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: gemini, name: Google Gemini, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: azure-openai, name: Azure OpenAI, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: bedrock, name: AWS Bedrock, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: openrouter, name: OpenRouter, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: groq, name: Groq, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: mistral, name: Mistral, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: cohere, name: Cohere, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: huggingface, name: Hugging Face, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: ollama, name: Ollama, layer: model_provider, tier: stable, python: ">=3.11,<3.15"} + - {id: openai-compatible, name: OpenAI-compatible endpoint, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: xai, name: xAI, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: deepseek, name: DeepSeek, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: together, name: Together AI, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: fireworks, name: Fireworks AI, layer: model_provider, tier: platform, python: ">=3.11,<3.15"} + - {id: sentence-transformers, name: Sentence Transformers, layer: embedding_provider, tier: stable, python: ">=3.11,<3.15"} + + # Data engines. A project may select at most one provider for each role. + - {id: sqlite, name: SQLite, layer: data, tier: stable, roles: [sql], python: ">=3.11,<3.15"} + - {id: postgresql, name: PostgreSQL, layer: data, tier: stable, roles: [sql], python: ">=3.11,<3.15"} + - {id: mysql, name: MySQL, layer: data, tier: stable, roles: [sql], python: ">=3.11,<3.15"} + - {id: duckdb, name: DuckDB, layer: data, tier: stable, roles: [sql], python: ">=3.11,<3.15"} + - {id: supabase, name: Supabase, layer: data, tier: platform, roles: [sql, document], python: ">=3.11,<3.15"} + - {id: mongodb, name: MongoDB, layer: data, tier: stable, roles: [document], python: ">=3.11,<3.15"} + - {id: beaver, name: Beaver, layer: data, tier: stable, roles: [document, vector, graph, cache], python: ">=3.12,<3.15"} + - {id: pgvector, name: pgvector, layer: data, tier: stable, roles: [vector], python: ">=3.11,<3.15"} + - {id: qdrant, name: Qdrant, layer: data, tier: stable, roles: [vector], python: ">=3.11,<3.15"} + - {id: chroma, name: Chroma, layer: data, tier: stable, roles: [vector], python: ">=3.11,<3.15"} + - {id: lancedb, name: LanceDB, layer: data, tier: stable, roles: [vector], python: ">=3.11,<3.15"} + - {id: weaviate, name: Weaviate, layer: data, tier: platform, roles: [vector], python: ">=3.11,<3.15"} + - {id: pinecone, name: Pinecone, layer: data, tier: platform, roles: [vector], python: ">=3.11,<3.15"} + - {id: milvus, name: Milvus, layer: data, tier: stable, roles: [vector], python: ">=3.11,<3.15"} + - {id: neo4j, name: Neo4j, layer: data, tier: stable, roles: [graph], python: ">=3.11,<3.15"} + - {id: redis, name: Redis, layer: data, tier: stable, roles: [cache], python: ">=3.11,<3.15"} + + - {id: sqlmodel, name: SQLModel, layer: sql_abstraction, tier: stable, python: ">=3.11,<3.15"} + - {id: sqlalchemy, name: SQLAlchemy, layer: sql_abstraction, tier: stable, python: ">=3.11,<3.15"} + - {id: api-key, name: API key, layer: auth, tier: stable, python: ">=3.11,<3.15"} + - {id: oidc, name: OAuth / OIDC, layer: auth, tier: platform, python: ">=3.11,<3.15"} + - {id: supabase-auth, name: Supabase Auth, layer: auth, tier: platform, python: ">=3.11,<3.15"} + + - {id: bentoml, name: BentoML, layer: serving, tier: stable, python: ">=3.11,<3.15"} + - {id: litellm, name: LiteLLM, layer: serving, tier: stable, python: ">=3.11,<3.15"} + - {id: vllm, name: vLLM, layer: serving, tier: platform, python: ">=3.11,<3.15"} + - {id: ollama-serving, name: Ollama, layer: serving, tier: stable, python: ">=3.11,<3.15"} + - {id: ray-serve, name: Ray Serve, layer: serving, tier: stable, python: ">=3.11,<3.15"} + + - {id: lightning, name: Lightning, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: datasets, name: Hugging Face Datasets, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: accelerate, name: Accelerate, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: peft, name: PEFT, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: trl, name: TRL, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: optuna, name: Optuna, layer: training_extension, tier: stable, python: ">=3.11,<3.15"} + - {id: prefect, name: Prefect, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: dagster, name: Dagster, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: temporal, name: Temporal, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: celery, name: Celery, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: dvc, name: DVC, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: feast, name: Feast, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: evidently, name: Evidently, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: polars, name: Polars, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + - {id: pandera, name: Pandera, layer: mlops, tier: stable, python: ">=3.11,<3.15"} + + - {id: deepeval, name: DeepEval, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: ragas, name: Ragas, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: pydantic-evals, name: Pydantic Evals, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: opentelemetry, name: OpenTelemetry, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: langfuse, name: Langfuse, layer: quality, tier: platform, python: ">=3.11,<3.15"} + - {id: phoenix, name: Phoenix, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: mlflow, name: MLflow, layer: quality, tier: stable, python: ">=3.11,<3.15"} + - {id: langsmith, name: LangSmith, layer: quality, tier: platform, python: ">=3.11,<3.15"} + - {id: wandb, name: Weights & Biases, layer: quality, tier: platform, python: ">=3.11,<3.15"} + + - {id: docker, name: Docker / Compose, layer: deploy, tier: stable, python: ">=3.11,<3.15"} + - {id: render, name: Render, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: fly, name: Fly.io, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: vercel, name: Vercel, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: railway, name: Railway, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: hf-spaces, name: Hugging Face Spaces, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: modal, name: Modal, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: runpod, name: RunPod, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: bentocloud, name: BentoCloud, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: aws-ecs, name: AWS ECS, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: aws-sagemaker, name: AWS SageMaker, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: cloud-run, name: Google Cloud Run, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: vertex-ai, name: Vertex AI, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: azure-container-apps, name: Azure Container Apps, layer: deploy, tier: platform, python: ">=3.11,<3.15"} + - {id: azure-ml, name: Azure ML, layer: deploy, tier: platform, python: ">=3.11,<3.15"} diff --git a/catalog/generated/catalog.json b/catalog/generated/catalog.json new file mode 100644 index 0000000..8e431d5 --- /dev/null +++ b/catalog/generated/catalog.json @@ -0,0 +1,1524 @@ +{ + "schema_version": 1, + "components": [ + { + "id": "library", + "name": "Python library", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "cli", + "name": "CLI application", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "api", + "name": "API service", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "web", + "name": "Python web application", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "tui", + "name": "Terminal UI", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "mcp", + "name": "MCP server", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "agent", + "name": "AI agent", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "rag", + "name": "RAG application", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "inference", + "name": "Model inference service", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "training", + "name": "Model training project", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "hybrid", + "name": "Training and serving workspace", + "layer": "workload", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "typer", + "name": "Typer", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "cli" + ], + "roles": [], + "packages": [ + "typer>=0.27.1,<1" + ] + }, + { + "id": "fastapi", + "name": "FastAPI", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "api" + ], + "roles": [], + "packages": [ + "fastapi[standard]>=0.141.1,<1", + "pydantic>=2.13.4,<3" + ] + }, + { + "id": "flask", + "name": "Flask", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "api" + ], + "roles": [], + "packages": [ + "flask>=3.1.2,<4" + ] + }, + { + "id": "fastmcp", + "name": "FastMCP", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "mcp" + ], + "roles": [], + "packages": [ + "fastmcp>=3.4.6,<4" + ] + }, + { + "id": "pydantic-ai", + "name": "PydanticAI", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "langgraph", + "name": "LangGraph", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "langchain", + "name": "LangChain", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent", + "rag" + ], + "roles": [], + "packages": [] + }, + { + "id": "llamaindex", + "name": "LlamaIndex", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent", + "rag" + ], + "roles": [], + "packages": [] + }, + { + "id": "crewai", + "name": "CrewAI", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "smolagents", + "name": "Hugging Face smolagents", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "dspy", + "name": "DSPy", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "openai-agents", + "name": "OpenAI Agents SDK", + "layer": "framework", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "google-adk", + "name": "Google ADK", + "layer": "framework", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "strands-agents", + "name": "Strands Agents", + "layer": "framework", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "autogen", + "name": "Microsoft AutoGen", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "ag2", + "name": "AG2", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "agno", + "name": "Agno", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "lingo", + "name": "Lingo", + "layer": "framework", + "tier": "stable", + "python": ">=3.12,<3.15", + "workloads": [ + "agent" + ], + "roles": [], + "packages": [] + }, + { + "id": "haystack", + "name": "Haystack", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "rag" + ], + "roles": [], + "packages": [] + }, + { + "id": "pytorch", + "name": "PyTorch", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "transformers", + "name": "Hugging Face Transformers", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "jax", + "name": "JAX", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "keras", + "name": "Keras / TensorFlow", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "scikit-learn", + "name": "scikit-learn", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "xgboost", + "name": "XGBoost", + "layer": "framework", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [ + "training", + "hybrid" + ], + "roles": [], + "packages": [] + }, + { + "id": "streamlit", + "name": "Streamlit", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "gradio", + "name": "Gradio", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "chainlit", + "name": "Chainlit", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "textual", + "name": "Textual", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "nicegui", + "name": "NiceGUI", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "fasthtml", + "name": "FastHTML", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "violetear", + "name": "Violetear", + "layer": "interface", + "tier": "stable", + "python": ">=3.12,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "jupyterlab", + "name": "JupyterLab", + "layer": "interface", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "openai", + "name": "OpenAI", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "anthropic", + "name": "Anthropic", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "gemini", + "name": "Google Gemini", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "azure-openai", + "name": "Azure OpenAI", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "bedrock", + "name": "AWS Bedrock", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "openrouter", + "name": "OpenRouter", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "groq", + "name": "Groq", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "mistral", + "name": "Mistral", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "cohere", + "name": "Cohere", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "huggingface", + "name": "Hugging Face", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "ollama", + "name": "Ollama", + "layer": "model_provider", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "openai-compatible", + "name": "OpenAI-compatible endpoint", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "xai", + "name": "xAI", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "deepseek", + "name": "DeepSeek", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "together", + "name": "Together AI", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "fireworks", + "name": "Fireworks AI", + "layer": "model_provider", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "sentence-transformers", + "name": "Sentence Transformers", + "layer": "embedding_provider", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "sqlite", + "name": "SQLite", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "sql" + ], + "packages": [] + }, + { + "id": "postgresql", + "name": "PostgreSQL", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "sql" + ], + "packages": [] + }, + { + "id": "mysql", + "name": "MySQL", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "sql" + ], + "packages": [] + }, + { + "id": "duckdb", + "name": "DuckDB", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "sql" + ], + "packages": [] + }, + { + "id": "supabase", + "name": "Supabase", + "layer": "data", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "sql", + "document" + ], + "packages": [] + }, + { + "id": "mongodb", + "name": "MongoDB", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "document" + ], + "packages": [] + }, + { + "id": "beaver", + "name": "Beaver", + "layer": "data", + "tier": "stable", + "python": ">=3.12,<3.15", + "workloads": [], + "roles": [ + "document", + "vector", + "graph", + "cache" + ], + "packages": [] + }, + { + "id": "pgvector", + "name": "pgvector", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "qdrant", + "name": "Qdrant", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "chroma", + "name": "Chroma", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "lancedb", + "name": "LanceDB", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "weaviate", + "name": "Weaviate", + "layer": "data", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "pinecone", + "name": "Pinecone", + "layer": "data", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "milvus", + "name": "Milvus", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "vector" + ], + "packages": [] + }, + { + "id": "neo4j", + "name": "Neo4j", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "graph" + ], + "packages": [] + }, + { + "id": "redis", + "name": "Redis", + "layer": "data", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [ + "cache" + ], + "packages": [] + }, + { + "id": "sqlmodel", + "name": "SQLModel", + "layer": "sql_abstraction", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "sqlalchemy", + "name": "SQLAlchemy", + "layer": "sql_abstraction", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "api-key", + "name": "API key", + "layer": "auth", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "oidc", + "name": "OAuth / OIDC", + "layer": "auth", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "supabase-auth", + "name": "Supabase Auth", + "layer": "auth", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "bentoml", + "name": "BentoML", + "layer": "serving", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "litellm", + "name": "LiteLLM", + "layer": "serving", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "vllm", + "name": "vLLM", + "layer": "serving", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "ollama-serving", + "name": "Ollama", + "layer": "serving", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "ray-serve", + "name": "Ray Serve", + "layer": "serving", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "lightning", + "name": "Lightning", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "datasets", + "name": "Hugging Face Datasets", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "accelerate", + "name": "Accelerate", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "peft", + "name": "PEFT", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "trl", + "name": "TRL", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "optuna", + "name": "Optuna", + "layer": "training_extension", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "prefect", + "name": "Prefect", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "dagster", + "name": "Dagster", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "temporal", + "name": "Temporal", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "celery", + "name": "Celery", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "dvc", + "name": "DVC", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "feast", + "name": "Feast", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "evidently", + "name": "Evidently", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "polars", + "name": "Polars", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "pandera", + "name": "Pandera", + "layer": "mlops", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "deepeval", + "name": "DeepEval", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "ragas", + "name": "Ragas", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "pydantic-evals", + "name": "Pydantic Evals", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "opentelemetry", + "name": "OpenTelemetry", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "langfuse", + "name": "Langfuse", + "layer": "quality", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "phoenix", + "name": "Phoenix", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "mlflow", + "name": "MLflow", + "layer": "quality", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "langsmith", + "name": "LangSmith", + "layer": "quality", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "wandb", + "name": "Weights & Biases", + "layer": "quality", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "docker", + "name": "Docker / Compose", + "layer": "deploy", + "tier": "stable", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "render", + "name": "Render", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "fly", + "name": "Fly.io", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "vercel", + "name": "Vercel", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "railway", + "name": "Railway", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "hf-spaces", + "name": "Hugging Face Spaces", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "modal", + "name": "Modal", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "runpod", + "name": "RunPod", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "bentocloud", + "name": "BentoCloud", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "aws-ecs", + "name": "AWS ECS", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "aws-sagemaker", + "name": "AWS SageMaker", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "cloud-run", + "name": "Google Cloud Run", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "vertex-ai", + "name": "Vertex AI", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "azure-container-apps", + "name": "Azure Container Apps", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + }, + { + "id": "azure-ml", + "name": "Azure ML", + "layer": "deploy", + "tier": "platform", + "python": ">=3.11,<3.15", + "workloads": [], + "roles": [], + "packages": [] + } + ], + "presets": [ + { + "id": "python-library", + "name": "Python Library", + "description": "A typed, publishable Python package without AI dependencies.", + "answers": { + "workload": "library", + "ai_capabilities": "none", + "framework": "none" + } + }, + { + "id": "typer-cli", + "name": "Typer CLI", + "description": "A polished command-line application without AI dependencies.", + "answers": { + "workload": "cli", + "ai_capabilities": "none", + "framework": "typer" + } + }, + { + "id": "fastapi-api", + "name": "FastAPI API", + "description": "A production-ready JSON API without AI dependencies.", + "answers": { + "workload": "api", + "ai_capabilities": "none", + "framework": "fastapi" + } + }, + { + "id": "fastmcp-server", + "name": "FastMCP Tool Server", + "description": "An MCP server exposing a tested local tool.", + "answers": { + "workload": "mcp", + "ai_capabilities": "mcp", + "framework": "fastmcp" + } + }, + { + "id": "pydantic-ai-openai", + "name": "PydanticAI + Harness + OpenAI", + "description": "A typed OpenAI agent with optional Harness capabilities.", + "answers": { + "workload": "agent", + "ai_capabilities": "agents", + "framework": "pydantic-ai", + "model_provider": "openai" + } + }, + { + "id": "google-adk-gemini", + "name": "Google ADK + Gemini", + "description": "An agent built with Google's official agent framework and model.", + "answers": { + "workload": "agent", + "ai_capabilities": "agents", + "framework": "google-adk", + "model_provider": "gemini" + } + }, + { + "id": "strands-bedrock", + "name": "Strands Agents + Bedrock", + "description": "An AWS-native agent targeting Bedrock.", + "answers": { + "workload": "agent", + "ai_capabilities": "agents", + "framework": "strands-agents", + "model_provider": "bedrock" + } + }, + { + "id": "langgraph-anthropic-api", + "name": "LangGraph + Anthropic + FastAPI", + "description": "A stateful agent workflow exposed through FastAPI.", + "answers": { + "workload": "agent", + "ai_capabilities": "agents", + "framework": "langgraph", + "model_provider": "anthropic", + "interfaces": [ + "fastapi" + ] + } + }, + { + "id": "llamaindex-rag", + "name": "LlamaIndex RAG + OpenAI + Pinecone + Gradio", + "description": "A hosted RAG chat application with a browser UI.", + "answers": { + "workload": "rag", + "ai_capabilities": "rag", + "framework": "llamaindex", + "model_provider": "openai", + "embedding_provider": "openai", + "vector_store": "pinecone", + "interfaces": [ + "gradio" + ] + } + }, + { + "id": "local-lingo-app", + "name": "Lingo + Ollama + Beaver + Violetear", + "description": "A local-first conversational app with an embedded multimodel database.", + "answers": { + "workload": "agent", + "ai_capabilities": "agents", + "framework": "lingo", + "model_provider": "ollama", + "document_store": "beaver", + "vector_store": "beaver", + "graph_store": "beaver", + "cache_store": "beaver", + "interfaces": [ + "violetear" + ] + } + }, + { + "id": "litellm-gateway", + "name": "LiteLLM Gateway + Redis + Docker", + "description": "A model gateway with Redis-backed caching and Docker deployment.", + "answers": { + "workload": "inference", + "ai_capabilities": "inference", + "framework": "none", + "serving": "litellm", + "cache_store": "redis", + "deploy_target": "docker" + } + }, + { + "id": "hf-finetuning", + "name": "Hugging Face fine-tuning + MLflow + BentoML", + "description": "A workspace for fine-tuning, tracking and serving a model.", + "answers": { + "workload": "hybrid", + "ai_capabilities": "training", + "framework": "transformers", + "training_extensions": [ + "datasets", + "accelerate", + "peft", + "trl" + ], + "quality_tools": [ + "mlflow" + ], + "serving": "bentoml" + } + } + ] +} diff --git a/catalog/generated/choices.yml b/catalog/generated/choices.yml new file mode 100644 index 0000000..3d96652 --- /dev/null +++ b/catalog/generated/choices.yml @@ -0,0 +1,139 @@ +auth: + API key: api-key + OAuth / OIDC: oidc + Supabase Auth: supabase-auth +data: + Beaver: beaver + Chroma: chroma + DuckDB: duckdb + LanceDB: lancedb + Milvus: milvus + MongoDB: mongodb + MySQL: mysql + Neo4j: neo4j + Pinecone: pinecone + PostgreSQL: postgresql + Qdrant: qdrant + Redis: redis + SQLite: sqlite + Supabase: supabase + Weaviate: weaviate + pgvector: pgvector +deploy: + AWS ECS: aws-ecs + AWS SageMaker: aws-sagemaker + Azure Container Apps: azure-container-apps + Azure ML: azure-ml + BentoCloud: bentocloud + Docker / Compose: docker + Fly.io: fly + Google Cloud Run: cloud-run + Hugging Face Spaces: hf-spaces + Modal: modal + Railway: railway + Render: render + RunPod: runpod + Vercel: vercel + Vertex AI: vertex-ai +embedding_provider: + Sentence Transformers: sentence-transformers +framework: + AG2: ag2 + Agno: agno + CrewAI: crewai + DSPy: dspy + FastAPI: fastapi + FastMCP: fastmcp + Flask: flask + Google ADK: google-adk + Haystack: haystack + Hugging Face Transformers: transformers + Hugging Face smolagents: smolagents + JAX: jax + Keras / TensorFlow: keras + LangChain: langchain + LangGraph: langgraph + Lingo: lingo + LlamaIndex: llamaindex + Microsoft AutoGen: autogen + OpenAI Agents SDK: openai-agents + PyTorch: pytorch + PydanticAI: pydantic-ai + Strands Agents: strands-agents + Typer: typer + XGBoost: xgboost + scikit-learn: scikit-learn +interface: + Chainlit: chainlit + FastHTML: fasthtml + Gradio: gradio + JupyterLab: jupyterlab + NiceGUI: nicegui + Streamlit: streamlit + Textual: textual + Violetear: violetear +mlops: + Celery: celery + DVC: dvc + Dagster: dagster + Evidently: evidently + Feast: feast + Pandera: pandera + Polars: polars + Prefect: prefect + Temporal: temporal +model_provider: + AWS Bedrock: bedrock + Anthropic: anthropic + Azure OpenAI: azure-openai + Cohere: cohere + DeepSeek: deepseek + Fireworks AI: fireworks + Google Gemini: gemini + Groq: groq + Hugging Face: huggingface + Mistral: mistral + Ollama: ollama + OpenAI: openai + OpenAI-compatible endpoint: openai-compatible + OpenRouter: openrouter + Together AI: together + xAI: xai +quality: + DeepEval: deepeval + LangSmith: langsmith + Langfuse: langfuse + MLflow: mlflow + OpenTelemetry: opentelemetry + Phoenix: phoenix + Pydantic Evals: pydantic-evals + Ragas: ragas + Weights & Biases: wandb +serving: + BentoML: bentoml + LiteLLM: litellm + Ollama: ollama-serving + Ray Serve: ray-serve + vLLM: vllm +sql_abstraction: + SQLAlchemy: sqlalchemy + SQLModel: sqlmodel +training_extension: + Accelerate: accelerate + Hugging Face Datasets: datasets + Lightning: lightning + Optuna: optuna + PEFT: peft + TRL: trl +workload: + AI agent: agent + API service: api + CLI application: cli + MCP server: mcp + Model inference service: inference + Model training project: training + Python library: library + Python web application: web + RAG application: rag + Terminal UI: tui + Training and serving workspace: hybrid diff --git a/catalog/presets.yml b/catalog/presets.yml new file mode 100644 index 0000000..b9e1d41 --- /dev/null +++ b/catalog/presets.yml @@ -0,0 +1,51 @@ +schema_version: 1 + +presets: + - id: python-library + name: Python Library + description: A typed, publishable Python package without AI dependencies. + answers: {workload: library, ai_capabilities: none, framework: none} + - id: typer-cli + name: Typer CLI + description: A polished command-line application without AI dependencies. + answers: {workload: cli, ai_capabilities: none, framework: typer} + - id: fastapi-api + name: FastAPI API + description: A production-ready JSON API without AI dependencies. + answers: {workload: api, ai_capabilities: none, framework: fastapi} + - id: fastmcp-server + name: FastMCP Tool Server + description: An MCP server exposing a tested local tool. + answers: {workload: mcp, ai_capabilities: mcp, framework: fastmcp} + - id: pydantic-ai-openai + name: PydanticAI + Harness + OpenAI + description: A typed OpenAI agent with optional Harness capabilities. + answers: {workload: agent, ai_capabilities: agents, framework: pydantic-ai, model_provider: openai} + - id: google-adk-gemini + name: Google ADK + Gemini + description: An agent built with Google's official agent framework and model. + answers: {workload: agent, ai_capabilities: agents, framework: google-adk, model_provider: gemini} + - id: strands-bedrock + name: Strands Agents + Bedrock + description: An AWS-native agent targeting Bedrock. + answers: {workload: agent, ai_capabilities: agents, framework: strands-agents, model_provider: bedrock} + - id: langgraph-anthropic-api + name: LangGraph + Anthropic + FastAPI + description: A stateful agent workflow exposed through FastAPI. + answers: {workload: agent, ai_capabilities: agents, framework: langgraph, model_provider: anthropic, interfaces: [fastapi]} + - id: llamaindex-rag + name: LlamaIndex RAG + OpenAI + Pinecone + Gradio + description: A hosted RAG chat application with a browser UI. + answers: {workload: rag, ai_capabilities: rag, framework: llamaindex, model_provider: openai, embedding_provider: openai, vector_store: pinecone, interfaces: [gradio]} + - id: local-lingo-app + name: Lingo + Ollama + Beaver + Violetear + description: A local-first conversational app with an embedded multimodel database. + answers: {workload: agent, ai_capabilities: agents, framework: lingo, model_provider: ollama, document_store: beaver, vector_store: beaver, graph_store: beaver, cache_store: beaver, interfaces: [violetear]} + - id: litellm-gateway + name: LiteLLM Gateway + Redis + Docker + description: A model gateway with Redis-backed caching and Docker deployment. + answers: {workload: inference, ai_capabilities: inference, framework: none, serving: litellm, cache_store: redis, deploy_target: docker} + - id: hf-finetuning + name: Hugging Face fine-tuning + MLflow + BentoML + description: A workspace for fine-tuning, tracking and serving a model. + answers: {workload: hybrid, ai_capabilities: training, framework: transformers, training_extensions: [datasets, accelerate, peft, trl], quality_tools: [mlflow], serving: bentoml} diff --git a/copier.yml b/copier.yml index 42a33b9..fac2e2f 100644 --- a/copier.yml +++ b/copier.yml @@ -98,14 +98,394 @@ github_repo: # ------------------------------------------------------------------ shape -- -project_type: +preset: type: str - help: What kind of project is this? - default: library + help: Start from an editable recipe or configure every layer yourself + default: python-library choices: - Library - an importable package: library - CLI application - built with Typer: cli - Web API - built with FastAPI: api + Python Library: python-library + Typer CLI: typer-cli + FastAPI API: fastapi-api + FastMCP Tool Server: fastmcp-server + PydanticAI + Harness + OpenAI: pydantic-ai-openai + Google ADK + Gemini: google-adk-gemini + Strands Agents + Bedrock: strands-bedrock + LangGraph + Anthropic + FastAPI: langgraph-anthropic-api + LlamaIndex RAG + OpenAI + Pinecone + Gradio: llamaindex-rag + Lingo + Ollama + Beaver + Violetear: local-lingo-app + LiteLLM Gateway + Redis + Docker: litellm-gateway + Hugging Face fine-tuning + MLflow + BentoML: hf-finetuning + Custom - compose every layer: custom + +workload: + type: str + help: What is the primary workload? + default: >- + {%- if preset == 'typer-cli' -%}cli + {%- elif preset == 'fastapi-api' -%}api + {%- elif preset == 'fastmcp-server' -%}mcp + {%- elif preset in ['pydantic-ai-openai', 'google-adk-gemini', 'strands-bedrock', 'langgraph-anthropic-api', 'local-lingo-app'] -%}agent + {%- elif preset == 'llamaindex-rag' -%}rag + {%- elif preset == 'litellm-gateway' -%}inference + {%- elif preset == 'hf-finetuning' -%}hybrid + {%- else -%}library{%- endif -%} + choices: + Library - importable package: library + CLI application: cli + API service: api + Python web application: web + Terminal UI: tui + MCP server: mcp + AI agent: agent + RAG application: rag + Model inference service: inference + Model training project: training + Training and serving workspace: hybrid + +ai_capabilities: + type: str + help: Which AI capability drives this project? + default: >- + {%- if workload == 'mcp' -%}mcp + {%- elif workload == 'agent' -%}agents + {%- elif workload == 'rag' -%}rag + {%- elif workload == 'inference' -%}inference + {%- elif workload in ['training', 'hybrid'] -%}training + {%- else -%}none{%- endif -%} + choices: + None - keep the project simple: none + MCP tools and resources: mcp + AI agents: agents + Retrieval-augmented generation: rag + Model inference: inference + Training and fine-tuning: training + validator: >- + {% if workload in ['library', 'cli', 'api', 'web', 'tui'] and ai_capabilities != 'none' and preset in ['python-library', 'typer-cli', 'fastapi-api'] %} + The selected simple preset intentionally has no AI capabilities; choose Custom to add them + {% endif %} + +framework: + type: str + help: Primary framework for this workload + default: >- + {%- if preset == 'typer-cli' or workload == 'cli' -%}typer + {%- elif preset == 'fastapi-api' or workload == 'api' -%}fastapi + {%- elif preset == 'fastmcp-server' or workload == 'mcp' -%}fastmcp + {%- elif preset == 'pydantic-ai-openai' -%}pydantic-ai + {%- elif preset == 'google-adk-gemini' -%}google-adk + {%- elif preset == 'strands-bedrock' -%}strands-agents + {%- elif preset == 'langgraph-anthropic-api' -%}langgraph + {%- elif preset == 'llamaindex-rag' -%}llamaindex + {%- elif preset == 'local-lingo-app' -%}lingo + {%- elif preset == 'hf-finetuning' -%}transformers + {%- else -%}none{%- endif -%} + choices: + None: none + Typer: typer + FastAPI: fastapi + Flask: flask + FastMCP: fastmcp + PydanticAI: pydantic-ai + LangGraph: langgraph + LangChain: langchain + LlamaIndex: llamaindex + CrewAI: crewai + Hugging Face smolagents: smolagents + DSPy: dspy + OpenAI Agents SDK: openai-agents + Google ADK: google-adk + Strands Agents: strands-agents + Microsoft AutoGen: autogen + AG2: ag2 + Agno: agno + Lingo: lingo + Haystack: haystack + PyTorch: pytorch + Hugging Face Transformers: transformers + JAX: jax + Keras / TensorFlow: keras + scikit-learn: scikit-learn + XGBoost: xgboost + validator: >- + {% set allowed = { + 'library': ['none'], + 'cli': ['typer'], + 'api': ['fastapi', 'flask'], + 'web': ['none'], + 'tui': ['none'], + 'mcp': ['fastmcp'], + 'agent': ['pydantic-ai', 'langgraph', 'langchain', 'llamaindex', 'crewai', 'smolagents', 'dspy', 'openai-agents', 'google-adk', 'strands-agents', 'autogen', 'ag2', 'agno', 'lingo'], + 'rag': ['langchain', 'llamaindex', 'haystack'], + 'inference': ['none'], + 'training': ['pytorch', 'transformers', 'jax', 'keras', 'scikit-learn', 'xgboost'], + 'hybrid': ['pytorch', 'transformers', 'jax', 'keras', 'scikit-learn', 'xgboost'] + } %} + {% if framework not in allowed[workload] %}{{ framework }} is not compatible with the {{ workload }} workload{% endif %} + +pydantic_ai_harness: + type: str + multiselect: true + help: Optional PydanticAI Harness capabilities + default: "{{ ['skills'] if preset == 'pydantic-ai-openai' else [] }}" + choices: + Agent skills: skills + ACP: acp + Browser use: browser-use + Code mode: code-mode + DBOS durable execution: dbos + Dynamic workflows: dynamic-workflow + Exa search: exa + Logfire: logfire + Modal: modal + MongoDB: mongodb + StackOne: stackone + Temporal: temporal + when: "{{ framework == 'pydantic-ai' }}" + +model_provider: + type: str + help: Provider for generative models + default: >- + {%- if preset in ['pydantic-ai-openai', 'llamaindex-rag'] -%}openai + {%- elif preset == 'google-adk-gemini' -%}gemini + {%- elif preset == 'strands-bedrock' -%}bedrock + {%- elif preset == 'langgraph-anthropic-api' -%}anthropic + {%- elif preset == 'local-lingo-app' -%}ollama + {%- else -%}openai{%- endif -%} + choices: + OpenAI: openai + Anthropic: anthropic + Google Gemini: gemini + Azure OpenAI: azure-openai + AWS Bedrock: bedrock + OpenRouter: openrouter + Groq: groq + Mistral: mistral + Cohere: cohere + Hugging Face: huggingface + Ollama: ollama + OpenAI-compatible endpoint: openai-compatible + xAI: xai + DeepSeek: deepseek + Together AI: together + Fireworks AI: fireworks + when: "{{ ai_capabilities in ['agents', 'rag', 'inference'] }}" + +embedding_provider: + type: str + help: Provider for embeddings (independent from the generative model) + default: "{{ 'openai' if preset == 'llamaindex-rag' else 'sentence-transformers' }}" + choices: + Local Sentence Transformers: sentence-transformers + OpenAI: openai + Google Gemini: gemini + Cohere: cohere + Mistral: mistral + Hugging Face: huggingface + Ollama: ollama + when: "{{ ai_capabilities == 'rag' }}" + +interfaces: + type: str + multiselect: true + help: User-facing interfaces and additional entrypoints + default: >- + {% if preset == 'langgraph-anthropic-api' %}{{ ['fastapi'] }} + {% elif preset == 'llamaindex-rag' %}{{ ['gradio'] }} + {% elif preset == 'local-lingo-app' %}{{ ['violetear'] }} + {% elif workload == 'web' %}{{ ['nicegui'] }} + {% elif workload == 'tui' %}{{ ['textual'] }} + {% else %}{{ [] }}{% endif %} + choices: + FastAPI: fastapi + Flask: flask + Streamlit: streamlit + Gradio: gradio + Chainlit: chainlit + Textual: textual + NiceGUI: nicegui + FastHTML: fasthtml + Violetear: violetear + JupyterLab: jupyterlab + when: "{{ workload in ['web', 'tui', 'agent', 'rag', 'training', 'hybrid'] }}" + +sql_store: + type: str + help: SQL engine + default: none + choices: + None: none + SQLite: sqlite + PostgreSQL: postgresql + MySQL: mysql + DuckDB: duckdb + Supabase: supabase + +sql_abstraction: + type: str + help: SQL abstraction and migration layer + default: sqlmodel + choices: + SQLModel + Alembic: sqlmodel + SQLAlchemy + Alembic: sqlalchemy + when: "{{ sql_store != 'none' }}" + +document_store: + type: str + help: Document store + default: "{{ 'beaver' if preset == 'local-lingo-app' else 'none' }}" + choices: + None: none + MongoDB: mongodb + Supabase: supabase + Beaver local bundle: beaver + +vector_store: + type: str + help: Vector store + default: >- + {%- if preset == 'llamaindex-rag' -%}pinecone + {%- elif preset == 'local-lingo-app' -%}beaver + {%- else -%}none{%- endif -%} + choices: + None: none + Beaver local bundle: beaver + pgvector: pgvector + Qdrant: qdrant + Chroma: chroma + LanceDB: lancedb + Weaviate: weaviate + Pinecone: pinecone + Milvus: milvus + +graph_store: + type: str + help: Graph store + default: "{{ 'beaver' if preset == 'local-lingo-app' else 'none' }}" + choices: + None: none + Beaver local bundle: beaver + Neo4j: neo4j + +cache_store: + type: str + help: Cache and event store + default: >- + {%- if preset == 'local-lingo-app' -%}beaver + {%- elif preset == 'litellm-gateway' -%}redis + {%- else -%}none{%- endif -%} + choices: + None: none + Beaver local bundle: beaver + Redis: redis + +auth: + type: str + help: Authentication strategy + default: none + choices: + None: none + API key: api-key + Generic OAuth / OIDC: oidc + Supabase Auth: supabase-auth + when: "{{ workload in ['api', 'web', 'mcp', 'agent', 'rag', 'inference', 'hybrid'] }}" + +serving: + type: str + help: Model serving engine + default: >- + {%- if preset == 'litellm-gateway' -%}litellm + {%- elif preset == 'hf-finetuning' -%}bentoml + {%- else -%}none{%- endif -%} + choices: + None: none + BentoML: bentoml + LiteLLM: litellm + vLLM: vllm + Ollama: ollama-serving + Ray Serve: ray-serve + when: "{{ workload in ['inference', 'hybrid'] }}" + +training_extensions: + type: str + multiselect: true + help: Training and optimization extensions + default: >- + {{ ['datasets', 'accelerate', 'peft', 'trl'] if preset == 'hf-finetuning' else [] }} + choices: + Lightning: lightning + Hugging Face Datasets: datasets + Accelerate: accelerate + PEFT: peft + TRL: trl + Optuna: optuna + when: "{{ workload in ['training', 'hybrid'] }}" + +mlops_tools: + type: str + multiselect: true + help: Data and ML orchestration tools + default: [] + choices: + Prefect: prefect + Dagster: dagster + Temporal: temporal + Celery: celery + DVC: dvc + Feast: feast + Evidently: evidently + Polars: polars + Pandera: pandera + when: "{{ workload in ['training', 'hybrid'] }}" + +quality_tools: + type: str + multiselect: true + help: Evaluation and observability tools + default: "{{ ['mlflow'] if preset == 'hf-finetuning' else [] }}" + choices: + DeepEval: deepeval + Ragas: ragas + Pydantic Evals: pydantic-evals + OpenTelemetry: opentelemetry + Langfuse: langfuse + Phoenix: phoenix + MLflow: mlflow + LangSmith: langsmith + Weights & Biases: wandb + when: "{{ ai_capabilities != 'none' or workload in ['training', 'hybrid'] }}" + +deploy_target: + type: str + help: Primary deployment target; Docker remains the portable base for services + default: "{{ 'docker' if workload not in ['library', 'cli'] else 'none' }}" + choices: + None: none + Docker / Compose: docker + Render: render + Fly.io: fly + Vercel: vercel + Railway: railway + Hugging Face Spaces: hf-spaces + Modal: modal + RunPod: runpod + BentoCloud: bentocloud + AWS ECS: aws-ecs + AWS SageMaker: aws-sagemaker + Google Cloud Run: cloud-run + Vertex AI: vertex-ai + Azure Container Apps: azure-container-apps + Azure ML: azure-ml + +iac_provider: + type: str + help: Infrastructure as code for the primary deployment target + default: none + choices: + None: none + Pulumi with Python: pulumi + Terraform: terraform + when: "{{ deploy_target not in ['none', 'docker', 'vercel', 'hf-spaces'] }}" python_version: type: str @@ -119,6 +499,10 @@ python_version: - "3.12" - "3.13" - "3.14" + validator: >- + {% if (framework in ['lingo'] or 'violetear' in interfaces or 'beaver' in [document_store, vector_store, graph_store, cache_store]) and python_version == '3.11' %} + The selected stack requires Python 3.12 or newer + {% endif %} license: type: str @@ -154,12 +538,12 @@ use_docs: publish_to_pypi: type: bool help: Publish to PyPI on tag, using Trusted Publishing (no secrets)? - default: "{{ project_type != 'api' }}" + default: "{{ workload in ['library', 'cli'] }}" use_docker: type: bool help: Include a multi-stage Dockerfile? - default: "{{ project_type == 'api' }}" + default: "{{ workload not in ['library', 'cli'] }}" use_devcontainer: type: bool diff --git a/docs/reference/components.md b/docs/reference/components.md new file mode 100644 index 0000000..0b8dab4 --- /dev/null +++ b/docs/reference/components.md @@ -0,0 +1,132 @@ +# Component catalog + +This reference is generated from `catalog/components.yml`. Do not edit it by hand. + +| Layer | Component | Support | Python | +| --- | --- | --- | --- | +| `workload` | Python library (`library`) | `stable` | `>=3.11,<3.15` | +| `workload` | CLI application (`cli`) | `stable` | `>=3.11,<3.15` | +| `workload` | API service (`api`) | `stable` | `>=3.11,<3.15` | +| `workload` | Python web application (`web`) | `stable` | `>=3.11,<3.15` | +| `workload` | Terminal UI (`tui`) | `stable` | `>=3.11,<3.15` | +| `workload` | MCP server (`mcp`) | `stable` | `>=3.11,<3.15` | +| `workload` | AI agent (`agent`) | `stable` | `>=3.11,<3.15` | +| `workload` | RAG application (`rag`) | `stable` | `>=3.11,<3.15` | +| `workload` | Model inference service (`inference`) | `stable` | `>=3.11,<3.15` | +| `workload` | Model training project (`training`) | `stable` | `>=3.11,<3.15` | +| `workload` | Training and serving workspace (`hybrid`) | `stable` | `>=3.11,<3.15` | +| `framework` | Typer (`typer`) | `stable` | `>=3.11,<3.15` | +| `framework` | FastAPI (`fastapi`) | `stable` | `>=3.11,<3.15` | +| `framework` | Flask (`flask`) | `stable` | `>=3.11,<3.15` | +| `framework` | FastMCP (`fastmcp`) | `stable` | `>=3.11,<3.15` | +| `framework` | PydanticAI (`pydantic-ai`) | `stable` | `>=3.11,<3.15` | +| `framework` | LangGraph (`langgraph`) | `stable` | `>=3.11,<3.15` | +| `framework` | LangChain (`langchain`) | `stable` | `>=3.11,<3.15` | +| `framework` | LlamaIndex (`llamaindex`) | `stable` | `>=3.11,<3.15` | +| `framework` | CrewAI (`crewai`) | `stable` | `>=3.11,<3.15` | +| `framework` | Hugging Face smolagents (`smolagents`) | `stable` | `>=3.11,<3.15` | +| `framework` | DSPy (`dspy`) | `stable` | `>=3.11,<3.15` | +| `framework` | OpenAI Agents SDK (`openai-agents`) | `platform` | `>=3.11,<3.15` | +| `framework` | Google ADK (`google-adk`) | `platform` | `>=3.11,<3.15` | +| `framework` | Strands Agents (`strands-agents`) | `platform` | `>=3.11,<3.15` | +| `framework` | Microsoft AutoGen (`autogen`) | `stable` | `>=3.11,<3.15` | +| `framework` | AG2 (`ag2`) | `stable` | `>=3.11,<3.15` | +| `framework` | Agno (`agno`) | `stable` | `>=3.11,<3.15` | +| `framework` | Lingo (`lingo`) | `stable` | `>=3.12,<3.15` | +| `framework` | Haystack (`haystack`) | `stable` | `>=3.11,<3.15` | +| `framework` | PyTorch (`pytorch`) | `stable` | `>=3.11,<3.15` | +| `framework` | Hugging Face Transformers (`transformers`) | `stable` | `>=3.11,<3.15` | +| `framework` | JAX (`jax`) | `stable` | `>=3.11,<3.15` | +| `framework` | Keras / TensorFlow (`keras`) | `stable` | `>=3.11,<3.15` | +| `framework` | scikit-learn (`scikit-learn`) | `stable` | `>=3.11,<3.15` | +| `framework` | XGBoost (`xgboost`) | `stable` | `>=3.11,<3.15` | +| `interface` | Streamlit (`streamlit`) | `stable` | `>=3.11,<3.15` | +| `interface` | Gradio (`gradio`) | `stable` | `>=3.11,<3.15` | +| `interface` | Chainlit (`chainlit`) | `stable` | `>=3.11,<3.15` | +| `interface` | Textual (`textual`) | `stable` | `>=3.11,<3.15` | +| `interface` | NiceGUI (`nicegui`) | `stable` | `>=3.11,<3.15` | +| `interface` | FastHTML (`fasthtml`) | `stable` | `>=3.11,<3.15` | +| `interface` | Violetear (`violetear`) | `stable` | `>=3.12,<3.15` | +| `interface` | JupyterLab (`jupyterlab`) | `stable` | `>=3.11,<3.15` | +| `model_provider` | OpenAI (`openai`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Anthropic (`anthropic`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Google Gemini (`gemini`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Azure OpenAI (`azure-openai`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | AWS Bedrock (`bedrock`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | OpenRouter (`openrouter`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Groq (`groq`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Mistral (`mistral`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Cohere (`cohere`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Hugging Face (`huggingface`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Ollama (`ollama`) | `stable` | `>=3.11,<3.15` | +| `model_provider` | OpenAI-compatible endpoint (`openai-compatible`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | xAI (`xai`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | DeepSeek (`deepseek`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Together AI (`together`) | `platform` | `>=3.11,<3.15` | +| `model_provider` | Fireworks AI (`fireworks`) | `platform` | `>=3.11,<3.15` | +| `embedding_provider` | Sentence Transformers (`sentence-transformers`) | `stable` | `>=3.11,<3.15` | +| `data` | SQLite (`sqlite`) | `stable` | `>=3.11,<3.15` | +| `data` | PostgreSQL (`postgresql`) | `stable` | `>=3.11,<3.15` | +| `data` | MySQL (`mysql`) | `stable` | `>=3.11,<3.15` | +| `data` | DuckDB (`duckdb`) | `stable` | `>=3.11,<3.15` | +| `data` | Supabase (`supabase`) | `platform` | `>=3.11,<3.15` | +| `data` | MongoDB (`mongodb`) | `stable` | `>=3.11,<3.15` | +| `data` | Beaver (`beaver`) | `stable` | `>=3.12,<3.15` | +| `data` | pgvector (`pgvector`) | `stable` | `>=3.11,<3.15` | +| `data` | Qdrant (`qdrant`) | `stable` | `>=3.11,<3.15` | +| `data` | Chroma (`chroma`) | `stable` | `>=3.11,<3.15` | +| `data` | LanceDB (`lancedb`) | `stable` | `>=3.11,<3.15` | +| `data` | Weaviate (`weaviate`) | `platform` | `>=3.11,<3.15` | +| `data` | Pinecone (`pinecone`) | `platform` | `>=3.11,<3.15` | +| `data` | Milvus (`milvus`) | `stable` | `>=3.11,<3.15` | +| `data` | Neo4j (`neo4j`) | `stable` | `>=3.11,<3.15` | +| `data` | Redis (`redis`) | `stable` | `>=3.11,<3.15` | +| `sql_abstraction` | SQLModel (`sqlmodel`) | `stable` | `>=3.11,<3.15` | +| `sql_abstraction` | SQLAlchemy (`sqlalchemy`) | `stable` | `>=3.11,<3.15` | +| `auth` | API key (`api-key`) | `stable` | `>=3.11,<3.15` | +| `auth` | OAuth / OIDC (`oidc`) | `platform` | `>=3.11,<3.15` | +| `auth` | Supabase Auth (`supabase-auth`) | `platform` | `>=3.11,<3.15` | +| `serving` | BentoML (`bentoml`) | `stable` | `>=3.11,<3.15` | +| `serving` | LiteLLM (`litellm`) | `stable` | `>=3.11,<3.15` | +| `serving` | vLLM (`vllm`) | `platform` | `>=3.11,<3.15` | +| `serving` | Ollama (`ollama-serving`) | `stable` | `>=3.11,<3.15` | +| `serving` | Ray Serve (`ray-serve`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | Lightning (`lightning`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | Hugging Face Datasets (`datasets`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | Accelerate (`accelerate`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | PEFT (`peft`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | TRL (`trl`) | `stable` | `>=3.11,<3.15` | +| `training_extension` | Optuna (`optuna`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Prefect (`prefect`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Dagster (`dagster`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Temporal (`temporal`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Celery (`celery`) | `stable` | `>=3.11,<3.15` | +| `mlops` | DVC (`dvc`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Feast (`feast`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Evidently (`evidently`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Polars (`polars`) | `stable` | `>=3.11,<3.15` | +| `mlops` | Pandera (`pandera`) | `stable` | `>=3.11,<3.15` | +| `quality` | DeepEval (`deepeval`) | `stable` | `>=3.11,<3.15` | +| `quality` | Ragas (`ragas`) | `stable` | `>=3.11,<3.15` | +| `quality` | Pydantic Evals (`pydantic-evals`) | `stable` | `>=3.11,<3.15` | +| `quality` | OpenTelemetry (`opentelemetry`) | `stable` | `>=3.11,<3.15` | +| `quality` | Langfuse (`langfuse`) | `platform` | `>=3.11,<3.15` | +| `quality` | Phoenix (`phoenix`) | `stable` | `>=3.11,<3.15` | +| `quality` | MLflow (`mlflow`) | `stable` | `>=3.11,<3.15` | +| `quality` | LangSmith (`langsmith`) | `platform` | `>=3.11,<3.15` | +| `quality` | Weights & Biases (`wandb`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Docker / Compose (`docker`) | `stable` | `>=3.11,<3.15` | +| `deploy` | Render (`render`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Fly.io (`fly`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Vercel (`vercel`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Railway (`railway`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Hugging Face Spaces (`hf-spaces`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Modal (`modal`) | `platform` | `>=3.11,<3.15` | +| `deploy` | RunPod (`runpod`) | `platform` | `>=3.11,<3.15` | +| `deploy` | BentoCloud (`bentocloud`) | `platform` | `>=3.11,<3.15` | +| `deploy` | AWS ECS (`aws-ecs`) | `platform` | `>=3.11,<3.15` | +| `deploy` | AWS SageMaker (`aws-sagemaker`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Google Cloud Run (`cloud-run`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Vertex AI (`vertex-ai`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Azure Container Apps (`azure-container-apps`) | `platform` | `>=3.11,<3.15` | +| `deploy` | Azure ML (`azure-ml`) | `platform` | `>=3.11,<3.15` | diff --git a/docs/reference/presets.md b/docs/reference/presets.md new file mode 100644 index 0000000..cc54fee --- /dev/null +++ b/docs/reference/presets.md @@ -0,0 +1,76 @@ +# Presets + +These editable recipes are generated from `catalog/presets.yml`. + +## Python Library + +A typed, publishable Python package without AI dependencies. + +ID: `python-library` + +## Typer CLI + +A polished command-line application without AI dependencies. + +ID: `typer-cli` + +## FastAPI API + +A production-ready JSON API without AI dependencies. + +ID: `fastapi-api` + +## FastMCP Tool Server + +An MCP server exposing a tested local tool. + +ID: `fastmcp-server` + +## PydanticAI + Harness + OpenAI + +A typed OpenAI agent with optional Harness capabilities. + +ID: `pydantic-ai-openai` + +## Google ADK + Gemini + +An agent built with Google's official agent framework and model. + +ID: `google-adk-gemini` + +## Strands Agents + Bedrock + +An AWS-native agent targeting Bedrock. + +ID: `strands-bedrock` + +## LangGraph + Anthropic + FastAPI + +A stateful agent workflow exposed through FastAPI. + +ID: `langgraph-anthropic-api` + +## LlamaIndex RAG + OpenAI + Pinecone + Gradio + +A hosted RAG chat application with a browser UI. + +ID: `llamaindex-rag` + +## Lingo + Ollama + Beaver + Violetear + +A local-first conversational app with an embedded multimodel database. + +ID: `local-lingo-app` + +## LiteLLM Gateway + Redis + Docker + +A model gateway with Redis-backed caching and Docker deployment. + +ID: `litellm-gateway` + +## Hugging Face fine-tuning + MLflow + BentoML + +A workspace for fine-tuning, tracking and serving a model. + +ID: `hf-finetuning` + diff --git a/pyproject.toml b/pyproject.toml index 2937741..85e003d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "python-template" -version = "1.0.0" -description = "Modern Python project template: Copier, uv, Ruff, ty and Zensical" +version = "0.5.0" +description = "Composable Python and AI project template powered by Copier and uv" readme = "readme.md" requires-python = ">=3.13" authors = [ @@ -21,6 +21,8 @@ package = false [dependency-groups] dev = [ "copier>=9.17.1", + "pydantic>=2.13.4,<3", + "pyyaml>=6.0.3,<7", "pytest>=9.1.1", "pytest-copie>=0.3.1", "pytest-xdist>=3.8.0", diff --git a/scripts/compile_catalog.py b/scripts/compile_catalog.py new file mode 100644 index 0000000..b7e63cd --- /dev/null +++ b/scripts/compile_catalog.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Validate the component catalog and compile deterministic artifacts.""" + +from __future__ import annotations + +import argparse +import json +import sys +from collections import defaultdict +from pathlib import Path +from typing import Literal + +import yaml +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, model_validator + +ROOT = Path(__file__).resolve().parent.parent +CATALOG = ROOT / "catalog" +GENERATED = CATALOG / "generated" + +Layer = Literal[ + "workload", + "framework", + "interface", + "model_provider", + "embedding_provider", + "data", + "sql_abstraction", + "auth", + "serving", + "training_extension", + "mlops", + "quality", + "deploy", +] +Tier = Literal["stable", "platform", "experimental"] + + +class Component(BaseModel): + """One selectable building block.""" + + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=r"^[a-z][a-z0-9-]*$") + name: str + layer: Layer + tier: Tier + python: str + workloads: list[str] = Field(default_factory=list) + roles: list[Literal["sql", "document", "vector", "graph", "cache"]] = Field( + default_factory=list + ) + packages: list[str] = Field(default_factory=list) + + +class ComponentCatalog(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] + components: list[Component] + + @model_validator(mode="after") + def unique_ids(self) -> ComponentCatalog: + ids = [component.id for component in self.components] + duplicates = sorted({item for item in ids if ids.count(item) > 1}) + if duplicates: + raise ValueError(f"duplicate component ids: {', '.join(duplicates)}") + return self + + +class Preset(BaseModel): + model_config = ConfigDict(extra="forbid") + + id: str = Field(pattern=r"^[a-z][a-z0-9-]*$") + name: str + description: str + answers: dict[str, object] + + +class PresetCatalog(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: Literal[1] + presets: list[Preset] + + @model_validator(mode="after") + def exactly_twelve_unique_presets(self) -> PresetCatalog: + ids = [preset.id for preset in self.presets] + if len(ids) != 12: + raise ValueError(f"expected 12 presets, found {len(ids)}") + if len(ids) != len(set(ids)): + raise ValueError("preset ids must be unique") + return self + + +def load_yaml(path: Path) -> object: + return yaml.safe_load(path.read_text()) + + +def load_catalogs() -> tuple[ComponentCatalog, PresetCatalog]: + components = ComponentCatalog.model_validate(load_yaml(CATALOG / "components.yml")) + presets = PresetCatalog.model_validate(load_yaml(CATALOG / "presets.yml")) + component_ids = {component.id for component in components.components} + for preset in presets.presets: + selected = _selected_component_ids(preset.answers) + unknown = selected - component_ids - {"none"} + if unknown: + raise ValueError( + f"preset {preset.id!r} references unknown components: " + f"{', '.join(sorted(unknown))}" + ) + return components, presets + + +def _selected_component_ids(answers: dict[str, object]) -> set[str]: + metadata_values = { + "agents", + "inference", + "mcp", + "none", + "rag", + "training", + } + selected: set[str] = set() + for value in answers.values(): + if isinstance(value, str): + selected.add(value) + elif isinstance(value, list): + selected.update(TypeAdapter(list[str]).validate_python(value)) + return selected - metadata_values + + +def build_artifacts( + components: ComponentCatalog, presets: PresetCatalog +) -> dict[Path, str]: + by_layer: dict[str, list[Component]] = defaultdict(list) + for component in components.components: + by_layer[component.layer].append(component) + + public_catalog = { + "schema_version": 1, + "components": [ + component.model_dump(mode="json") for component in components.components + ], + "presets": [preset.model_dump(mode="json") for preset in presets.presets], + } + choices = { + layer: {component.name: component.id for component in items} + for layer, items in sorted(by_layer.items()) + } + + rows = [ + "# Component catalog", + "", + "This reference is generated from `catalog/components.yml`. Do not edit it by hand.", + "", + "| Layer | Component | Support | Python |", + "| --- | --- | --- | --- |", + ] + for component in components.components: + rows.append( + f"| `{component.layer}` | {component.name} (`{component.id}`) " + f"| `{component.tier}` | `{component.python}` |" + ) + + preset_rows = [ + "# Presets", + "", + "These editable recipes are generated from `catalog/presets.yml`.", + "", + ] + for preset in presets.presets: + preset_rows.extend( + [f"## {preset.name}", "", preset.description, "", f"ID: `{preset.id}`", ""] + ) + + return { + GENERATED / "catalog.json": json.dumps(public_catalog, indent=2) + "\n", + GENERATED / "choices.yml": yaml.safe_dump( + choices, sort_keys=True, allow_unicode=True + ), + ROOT / "docs" / "reference" / "components.md": "\n".join(rows) + "\n", + ROOT / "docs" / "reference" / "presets.md": "\n".join(preset_rows) + "\n", + } + + +def compile_catalog(*, check: bool) -> int: + components, presets = load_catalogs() + artifacts = build_artifacts(components, presets) + stale: list[Path] = [] + for path, expected in artifacts.items(): + if check: + if not path.is_file() or path.read_text() != expected: + stale.append(path.relative_to(ROOT)) + continue + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(expected) + if stale: + print("Generated catalog artifacts are stale:", file=sys.stderr) + for path in stale: + print(f" - {path}", file=sys.stderr) + print("Run: uv run python scripts/compile_catalog.py", file=sys.stderr) + return 1 + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + return compile_catalog(check=args.check) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 32c1309..967ee42 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -22,13 +22,15 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ -{%- if project_type == 'cli' %} +{%- if framework == 'typer' %} "typer>=0.27.1", -{%- elif project_type == 'api' %} +{%- elif framework == 'fastapi' %} "fastapi[standard]>=0.141.1", # Imported directly by the app, so declared directly rather than relying # on it being a transitive dependency of FastAPI. "pydantic>=2.13.4", +{%- elif framework == 'flask' %} + "flask>=3.1.2", {%- endif %} ] @@ -40,7 +42,7 @@ Issues = "https://github.com/{{ github_username }}/{{ github_repo }}/issues" Documentation = "https://{{ github_username }}.github.io/{{ github_repo }}" {%- endif %} -{% if project_type == 'cli' -%} +{% if workload == 'cli' -%} [project.scripts] {{ project_slug }} = "{{ module_name }}.cli:app" @@ -60,7 +62,7 @@ dev = [ "pre-commit>=4.6.1", "pytest>=9.1.1", "pytest-cov>=7.1.0", -{%- if project_type == 'api' %} +{%- if framework == 'fastapi' %} "httpx>=0.28.1", {%- endif %} "ruff>=0.16.2", diff --git a/template/readme.md.jinja b/template/readme.md.jinja index 8947a3d..10ae5be 100644 --- a/template/readme.md.jinja +++ b/template/readme.md.jinja @@ -19,7 +19,7 @@ ## Installation -{% if project_type == 'api' -%} +{% if workload == 'api' -%} ```bash git clone https://github.com/{{ github_username }}/{{ github_repo }}.git cd {{ github_repo }} @@ -33,22 +33,28 @@ uv add {{ project_slug }} ## Usage -{% if project_type == 'library' -%} +{% if workload == 'library' -%} ```python from {{ module_name }} import greet print(greet("world")) # Hello, world! ``` -{%- elif project_type == 'cli' -%} +{%- elif workload == 'cli' -%} ```bash {{ project_slug }} hello world ``` -{%- elif project_type == 'api' -%} +{%- elif workload == 'api' -%} +{% if framework == 'fastapi' -%} ```bash uv run fastapi dev src/{{ module_name }}/api.py ``` The interactive API documentation is then available at . +{%- else -%} +```bash +uv run flask --app {{ module_name }}.api run --debug +``` +{%- endif %} {%- endif %} ## Development diff --git a/template/src/{{ module_name }}/__init__.py.jinja b/template/src/{{ module_name }}/__init__.py.jinja index bc86835..e9a749f 100644 --- a/template/src/{{ module_name }}/__init__.py.jinja +++ b/template/src/{{ module_name }}/__init__.py.jinja @@ -2,22 +2,24 @@ __version__ = "0.1.0" -{% if project_type == 'library' -%} +{% if workload == 'library' -%} __all__ = ["greet"] def greet(name: str) -> str: """Return a friendly greeting for ``name``.""" return f"Hello, {name}!" -{%- elif project_type == 'cli' -%} +{%- elif workload == 'cli' -%} __all__ = ["greet"] def greet(name: str) -> str: """Return a friendly greeting for ``name``.""" return f"Hello, {name}!" -{%- elif project_type == 'api' -%} +{%- elif workload == 'api' -%} __all__ = ["app"] from {{ module_name }}.api import app +{%- else -%} +__all__: list[str] = [] {%- endif %} diff --git a/template/src/{{ module_name }}/{% if project_type == 'api' %}api.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if workload == 'api' %}api.py{% endif %}.jinja similarity index 58% rename from template/src/{{ module_name }}/{% if project_type == 'api' %}api.py{% endif %}.jinja rename to template/src/{{ module_name }}/{% if workload == 'api' %}api.py{% endif %}.jinja index 70de615..2086769 100644 --- a/template/src/{{ module_name }}/{% if project_type == 'api' %}api.py{% endif %}.jinja +++ b/template/src/{{ module_name }}/{% if workload == 'api' %}api.py{% endif %}.jinja @@ -1,3 +1,4 @@ +{% if framework == 'fastapi' -%} """FastAPI application for {{ project_name }}.""" from fastapi import FastAPI @@ -32,3 +33,22 @@ async def health() -> Health: async def greet(name: str) -> Greeting: """Greet the given name.""" return Greeting(message=f"Hello, {name}!") +{%- elif framework == 'flask' -%} +"""Flask application for {{ project_name }}.""" + +from flask import Flask, jsonify + +app = Flask(__name__) + + +@app.get("/health") +def health(): # type: ignore[no-untyped-def] + """Report service health.""" + return jsonify(status="ok") + + +@app.get("/greet/") +def greet(name: str): # type: ignore[no-untyped-def] + """Greet the given name.""" + return jsonify(message=f"Hello, {name}!") +{%- endif %} diff --git a/template/src/{{ module_name }}/{% if project_type == 'cli' %}cli.py{% endif %}.jinja b/template/src/{{ module_name }}/{% if workload == 'cli' %}cli.py{% endif %}.jinja similarity index 100% rename from template/src/{{ module_name }}/{% if project_type == 'cli' %}cli.py{% endif %}.jinja rename to template/src/{{ module_name }}/{% if workload == 'cli' %}cli.py{% endif %}.jinja diff --git a/template/tests/test_{{ module_name }}.py.jinja b/template/tests/test_{{ module_name }}.py.jinja index 6ad8f44..7d4b66f 100644 --- a/template/tests/test_{{ module_name }}.py.jinja +++ b/template/tests/test_{{ module_name }}.py.jinja @@ -1,4 +1,4 @@ -{% if project_type == 'library' -%} +{% if workload == 'library' -%} """Tests for {{ module_name }}.""" from {{ module_name }} import __version__, greet @@ -10,7 +10,7 @@ def test_version() -> None: def test_greet() -> None: assert greet("world") == "Hello, world!" -{%- elif project_type == 'cli' -%} +{%- elif workload == 'cli' -%} """Tests for the {{ module_name }} command line interface.""" from typer.testing import CliRunner @@ -37,7 +37,7 @@ def test_hello_named() -> None: result = runner.invoke(app, ["hello", "{{ author_name.split(' ')[0] or 'friend' }}"]) assert result.exit_code == 0 assert "Hello, {{ author_name.split(' ')[0] or 'friend' }}!" in result.stdout -{%- elif project_type == 'api' -%} +{%- elif workload == 'api' and framework == 'fastapi' -%} """Tests for the {{ module_name }} API.""" from fastapi.testclient import TestClient @@ -57,4 +57,28 @@ def test_greet() -> None: response = client.get("/greet/world") assert response.status_code == 200 assert response.json() == {"message": "Hello, world!"} +{%- elif workload == 'api' and framework == 'flask' -%} +"""Tests for the {{ module_name }} Flask API.""" + +from {{ module_name }}.api import app + + +def test_health() -> None: + response = app.test_client().get("/health") + assert response.status_code == 200 + assert response.get_json() == {"status": "ok"} + + +def test_greet() -> None: + response = app.test_client().get("/greet/world") + assert response.status_code == 200 + assert response.get_json() == {"message": "Hello, world!"} +{%- else -%} +"""Smoke tests for {{ module_name }}.""" + +from {{ module_name }} import __version__ + + +def test_version() -> None: + assert __version__ == "0.1.0" {%- endif %} diff --git a/template/{% if agent_instructions %}AGENTS.md{% endif %}.jinja b/template/{% if agent_instructions %}AGENTS.md{% endif %}.jinja index 82a6545..da44e09 100644 --- a/template/{% if agent_instructions %}AGENTS.md{% endif %}.jinja +++ b/template/{% if agent_instructions %}AGENTS.md{% endif %}.jinja @@ -6,10 +6,14 @@ Conventions for AI coding agents working in this repository. - **{{ project_name }}** — {{ description }} - Python {{ python_version }}+, `src/` layout, package `{{ module_name }}`. -{%- if project_type == 'cli' %} +{%- if workload == 'cli' %} - Typer CLI; the entry point is `{{ module_name }}.cli:app`. -{%- elif project_type == 'api' %} +{%- elif workload == 'api' %} +{%- if framework == 'fastapi' %} - FastAPI application; the ASGI app is `{{ module_name }}.api:app`. +{%- else %} +- Flask application; the WSGI app is `{{ module_name }}.api:app`. +{%- endif %} {%- endif %} ## Toolchain diff --git a/template/{% if use_docker %}Dockerfile{% endif %}.jinja b/template/{% if use_docker %}Dockerfile{% endif %}.jinja index e6518d3..fdb1ff3 100644 --- a/template/{% if use_docker %}Dockerfile{% endif %}.jinja +++ b/template/{% if use_docker %}Dockerfile{% endif %}.jinja @@ -36,12 +36,17 @@ USER app EXPOSE 8000 -{% if project_type == 'api' -%} +{% if workload == 'api' and framework == 'fastapi' -%} HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health').status == 200 else 1)" CMD ["uvicorn", "{{ module_name }}.api:app", "--host", "0.0.0.0", "--port", "8000"] -{%- elif project_type == 'cli' -%} +{%- elif workload == 'api' and framework == 'flask' -%} +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \ + CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health').status == 200 else 1)" + +CMD ["flask", "--app", "{{ module_name }}.api:app", "run", "--host", "0.0.0.0", "--port", "8000"] +{%- elif workload == 'cli' -%} ENTRYPOINT ["{{ project_slug }}"] {%- else -%} CMD ["python", "-c", "import {{ module_name }}; print({{ module_name }}.__version__)"] diff --git a/template/{% if use_docs %}docs{% endif %}/api.md.jinja b/template/{% if use_docs %}docs{% endif %}/api.md.jinja index 6247095..b6106fc 100644 --- a/template/{% if use_docs %}docs{% endif %}/api.md.jinja +++ b/template/{% if use_docs %}docs{% endif %}/api.md.jinja @@ -4,11 +4,11 @@ Generated from the docstrings in the source by [mkdocstrings](https://mkdocstrings.github.io). ::: {{ module_name }} -{% if project_type == 'cli' %} +{% if workload == 'cli' %} ## Command line interface ::: {{ module_name }}.cli -{%- elif project_type == 'api' %} +{%- elif workload == 'api' %} ## Application ::: {{ module_name }}.api diff --git a/template/{% if use_docs %}docs{% endif %}/index.md.jinja b/template/{% if use_docs %}docs{% endif %}/index.md.jinja index 523786b..e48d5ae 100644 --- a/template/{% if use_docs %}docs{% endif %}/index.md.jinja +++ b/template/{% if use_docs %}docs{% endif %}/index.md.jinja @@ -4,7 +4,7 @@ ## Installation -{% if project_type == 'api' -%} +{% if workload == 'api' -%} ```bash git clone https://github.com/{{ github_username }}/{{ github_repo }}.git cd {{ github_repo }} @@ -24,22 +24,28 @@ pip install {{ project_slug }} ## Quick start -{% if project_type == 'library' -%} +{% if workload == 'library' -%} ```python from {{ module_name }} import greet print(greet("world")) ``` -{%- elif project_type == 'cli' -%} +{%- elif workload == 'cli' -%} ```bash {{ project_slug }} hello world ``` -{%- elif project_type == 'api' -%} +{%- elif workload == 'api' -%} +{% if framework == 'fastapi' -%} ```bash uv run fastapi dev src/{{ module_name }}/api.py ``` Then open for the interactive API documentation. +{%- else -%} +```bash +uv run flask --app {{ module_name }}.api run --debug +``` +{%- endif %} {%- endif %} See [Usage](usage.md) for more. diff --git a/template/{% if use_docs %}docs{% endif %}/usage.md.jinja b/template/{% if use_docs %}docs{% endif %}/usage.md.jinja index 3dbb182..b7b907b 100644 --- a/template/{% if use_docs %}docs{% endif %}/usage.md.jinja +++ b/template/{% if use_docs %}docs{% endif %}/usage.md.jinja @@ -1,6 +1,6 @@ # Usage -{% if project_type == 'library' -%} +{% if workload == 'library' -%} ## `greet` ```python @@ -10,7 +10,7 @@ greet("Ada") # "Hello, Ada!" ``` Replace this page with the real documentation for your library. -{%- elif project_type == 'cli' -%} +{%- elif workload == 'cli' -%} ## Commands ```bash @@ -20,7 +20,7 @@ Replace this page with the real documentation for your library. ``` Replace this page with the real documentation for your commands. -{%- elif project_type == 'api' -%} +{%- elif workload == 'api' -%} ## Endpoints | Method | Path | Description | diff --git a/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 0000000..8ee55b2 --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,48 @@ +"""The declarative catalog is the single source of truth for v0.5.""" + +import json +import subprocess +import sys +from pathlib import Path + +import yaml + +REPO = Path(__file__).resolve().parent.parent + + +def test_catalog_compiles_without_drift() -> None: + result = subprocess.run( + [sys.executable, "scripts/compile_catalog.py", "--check"], + cwd=REPO, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + +def test_catalog_and_public_artifact_match() -> None: + source = yaml.safe_load((REPO / "catalog" / "components.yml").read_text()) + generated = json.loads( + (REPO / "catalog" / "generated" / "catalog.json").read_text() + ) + + assert [item["id"] for item in generated["components"]] == [ + item["id"] for item in source["components"] + ] + assert len(generated["presets"]) == 12 + + +def test_deliberately_excluded_projects_stay_excluded() -> None: + text = (REPO / "catalog" / "components.yml").read_text().lower() + for excluded in ("autogoal", "odmantic", "fastui", "reflex"): + assert excluded not in text + + +def test_every_preset_is_exposed_by_copier() -> None: + config = yaml.safe_load((REPO / "copier.yml").read_text()) + preset_choices = set(config["preset"]["choices"].values()) + source = yaml.safe_load((REPO / "catalog" / "presets.yml").read_text()) + preset_ids = {preset["id"] for preset in source["presets"]} + + assert preset_choices == preset_ids | {"custom"} diff --git a/tests/test_generation.py b/tests/test_generation.py index 2e0b578..899ce1b 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -5,23 +5,23 @@ import pytest from conftest import answers -PROJECT_TYPES = ["library", "cli", "api"] +SIMPLE_WORKLOADS = ["library", "cli", "api"] -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_generates_cleanly(copie, project_type: str) -> None: - """Each project type renders without error.""" - result = copie.copy(extra_answers=answers(project_type=project_type)) +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_generates_cleanly(copie, workload: str) -> None: + """Each workload renders without error.""" + result = copie.copy(extra_answers=answers(workload=workload)) assert result.exception is None, result.exception assert result.exit_code == 0 assert result.project_dir.is_dir() -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_core_layout(copie, project_type: str) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_core_layout(copie, workload: str) -> None: """The src layout, typing marker and answers file are always present.""" - result = copie.copy(extra_answers=answers(project_type=project_type)) + result = copie.copy(extra_answers=answers(workload=workload)) project = result.project_dir assert (project / "src" / "demo_project" / "__init__.py").is_file() @@ -32,10 +32,10 @@ def test_core_layout(copie, project_type: str) -> None: assert (project / ".copier-answers.yml").is_file() -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_lowercase_readme(copie, project_type: str) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_lowercase_readme(copie, workload: str) -> None: """Readme is lowercase, and the packaging metadata agrees with it.""" - result = copie.copy(extra_answers=answers(project_type=project_type)) + result = copie.copy(extra_answers=answers(workload=workload)) project = result.project_dir assert (project / "readme.md").is_file() @@ -44,10 +44,10 @@ def test_lowercase_readme(copie, project_type: str) -> None: assert 'readme = "readme.md"' in (project / "pyproject.toml").read_text() -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_convention_files_stay_uppercase(copie, project_type: str) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_convention_files_stay_uppercase(copie, workload: str) -> None: """Files that GitHub and tooling match by exact name keep their case.""" - result = copie.copy(extra_answers=answers(project_type=project_type)) + result = copie.copy(extra_answers=answers(workload=workload)) project = result.project_dir for name in ("LICENSE", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md"): @@ -55,10 +55,10 @@ def test_convention_files_stay_uppercase(copie, project_type: str) -> None: def test_entrypoints_per_type(copie) -> None: - """Each project type gets its own entry point module, and only that one.""" - library = copie.copy(extra_answers=answers(project_type="library")).project_dir - cli = copie.copy(extra_answers=answers(project_type="cli")).project_dir - api = copie.copy(extra_answers=answers(project_type="api")).project_dir + """Each workload gets its own entry point module, and only that one.""" + library = copie.copy(extra_answers=answers(workload="library")).project_dir + cli = copie.copy(extra_answers=answers(workload="cli")).project_dir + api = copie.copy(extra_answers=answers(workload="api")).project_dir assert not (library / "src" / "demo_project" / "cli.py").exists() assert not (library / "src" / "demo_project" / "api.py").exists() @@ -71,6 +71,39 @@ def test_entrypoints_per_type(copie) -> None: assert not (api / "src" / "demo_project" / "cli.py").exists() +def test_simple_presets_remain_first_class(copie) -> None: + """The three simple recipes must never require an AI component.""" + cases = { + "python-library": ("library", "none"), + "typer-cli": ("cli", "typer"), + "fastapi-api": ("api", "fastapi"), + } + for preset, (workload, framework) in cases.items(): + project = copie.copy(extra_answers=answers(preset=preset)).project_dir + saved = (project / ".copier-answers.yml").read_text() + assert f"workload: {workload}" in saved + assert f"framework: {framework}" in saved + assert "ai_capabilities: none" in saved + + +def test_flask_is_available_for_a_simple_api(copie) -> None: + project = copie.copy( + extra_answers=answers( + preset="custom", + workload="api", + ai_capabilities="none", + framework="flask", + ) + ).project_dir + + api = (project / "src" / "demo_project" / "api.py").read_text() + pyproject = (project / "pyproject.toml").read_text() + tests = (project / "tests" / "test_demo_project.py").read_text() + assert "from flask import Flask" in api + assert '"flask>=3.1.2"' in pyproject + assert "app.test_client()" in tests + + def test_optional_features_are_omitted(copie) -> None: """Turning features off removes their files entirely.""" result = copie.copy( @@ -102,7 +135,7 @@ def test_optional_features_are_included(copie) -> None: """Turning features on renders their files.""" result = copie.copy( extra_answers=answers( - project_type="api", + workload="api", use_docs=True, publish_to_pypi=True, use_docker=True, @@ -232,10 +265,10 @@ def test_zizmor_config_allows_tag_pins(copie) -> None: assert "ref-pin" in config -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_changelog_is_lowercase(copie, project_type: str) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_changelog_is_lowercase(copie, workload: str) -> None: """A starter changelog ships with every project, lowercase per convention.""" - project = copie.copy(extra_answers=answers(project_type=project_type)).project_dir + project = copie.copy(extra_answers=answers(workload=workload)).project_dir assert (project / "changelog.md").is_file() assert not (project / "CHANGELOG.md").exists() @@ -271,7 +304,7 @@ def test_publish_workflow_does_not_cache(copie) -> None: builds the published artifacts should not trust a shared cache. """ project = copie.copy( - extra_answers=answers(project_type="library", publish_to_pypi=True) + extra_answers=answers(workload="library", publish_to_pypi=True) ).project_dir publish = (project / ".github" / "workflows" / "publish.yml").read_text() @@ -282,7 +315,7 @@ def test_publish_workflow_does_not_cache(copie) -> None: def test_publish_workflow_uses_trusted_publishing(copie) -> None: """No PyPI passwords or API tokens anywhere; OIDC only.""" project = copie.copy( - extra_answers=answers(project_type="library", publish_to_pypi=True) + extra_answers=answers(workload="library", publish_to_pypi=True) ).project_dir publish = (project / ".github" / "workflows" / "publish.yml").read_text() diff --git a/tests/test_toolchain.py b/tests/test_toolchain.py index 387b577..53be8ca 100644 --- a/tests/test_toolchain.py +++ b/tests/test_toolchain.py @@ -11,16 +11,14 @@ import pytest from conftest import answers, assert_ok, run -PROJECT_TYPES = ["library", "cli", "api"] +SIMPLE_WORKLOADS = ["library", "cli", "api"] @pytest.mark.slow -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_generated_project_passes_its_own_checks( - copie, uv: str, project_type: str -) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_generated_project_passes_its_own_checks(copie, uv: str, workload: str) -> None: """Install the generated project and run the full quality gate on it.""" - result = copie.copy(extra_answers=answers(project_type=project_type)) + result = copie.copy(extra_answers=answers(workload=workload)) assert result.exception is None, result.exception project = result.project_dir @@ -44,21 +42,21 @@ def test_generated_project_passes_its_own_checks( ty = run([uv, "run", "ty", "check"], project) assert ty.returncode in (0, 1), f"ty crashed:\n{ty.stdout}\n{ty.stderr}" - if project_type == "cli": + if workload == "cli": hello = run([uv, "run", "demo-project", "hello", "Ada"], project) assert_ok(hello, "cli entry point") assert "Hello, Ada!" in hello.stdout @pytest.mark.slow -@pytest.mark.parametrize("project_type", PROJECT_TYPES) -def test_generated_project_builds(copie, uv: str, project_type: str) -> None: +@pytest.mark.parametrize("workload", SIMPLE_WORKLOADS) +def test_generated_project_builds(copie, uv: str, workload: str) -> None: """The project produces an sdist and a wheel. ``uv build`` provisions its own build environment, so this deliberately does not run ``uv sync`` first. """ - result = copie.copy(extra_answers=answers(project_type=project_type)) + result = copie.copy(extra_answers=answers(workload=workload)) project = result.project_dir assert_ok(run([uv, "build"], project), "uv build") diff --git a/tests/test_update.py b/tests/test_update.py index 376bc84..9c089ad 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -53,7 +53,7 @@ def test_update_pulls_in_template_changes(template_clone: Path, tmp_path: Path) copier.run_copy( str(template_clone), str(project), - data=BASE_ANSWERS | {"project_type": "library"}, + data=BASE_ANSWERS | {"workload": "library", "framework": "none"}, defaults=True, unsafe=True, vcs_ref="v1.0.0", @@ -91,7 +91,7 @@ def test_update_preserves_local_edits(template_clone: Path, tmp_path: Path) -> N copier.run_copy( str(template_clone), str(project), - data=BASE_ANSWERS | {"project_type": "library"}, + data=BASE_ANSWERS | {"workload": "library", "framework": "none"}, defaults=True, unsafe=True, vcs_ref="v1.0.0", diff --git a/uv.lock b/uv.lock index 9b95bca..c362fd5 100644 --- a/uv.lock +++ b/uv.lock @@ -355,15 +355,17 @@ wheels = [ [[package]] name = "python-template" -version = "1.0.0" +version = "0.5.0" source = { virtual = "." } [package.dev-dependencies] dev = [ { name = "copier" }, + { name = "pydantic" }, { name = "pytest" }, { name = "pytest-copie" }, { name = "pytest-xdist" }, + { name = "pyyaml" }, { name = "ruff" }, ] @@ -372,9 +374,11 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "copier", specifier = ">=9.17.1" }, + { name = "pydantic", specifier = ">=2.13.4,<3" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-copie", specifier = ">=0.3.1" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "pyyaml", specifier = ">=6.0.3,<7" }, { name = "ruff", specifier = ">=0.16.2" }, ]