From fe4ef0a93510531f9c2cc5d798de27a214b6a561 Mon Sep 17 00:00:00 2001 From: Eduardo Firvida Date: Sat, 14 Jun 2025 22:07:37 -0400 Subject: [PATCH 1/5] Add support for multiple sqlalchemy bases --- papi/core/db/__init__.py | 6 ++-- papi/core/db/sql_utils.py | 30 ++++++++++++++++++ papi/core/init.py | 65 +++++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 papi/core/db/sql_utils.py diff --git a/papi/core/db/__init__.py b/papi/core/db/__init__.py index 407cb95..9a96616 100644 --- a/papi/core/db/__init__.py +++ b/papi/core/db/__init__.py @@ -1,7 +1,8 @@ +from .db_creation import create_database_if_not_exists +from .query_helper import query_helper from .redis import get_redis_client, get_redis_uri_with_db from .sql_session import get_sql_session, sql_session -from .query_helper import query_helper -from .db_creation import create_database_if_not_exists +from .sql_utils import extract_bases_from_models __all__ = [ "get_redis_client", @@ -10,4 +11,5 @@ "sql_session", "query_helper", "create_database_if_not_exists", + "extract_bases_from_models", ] diff --git a/papi/core/db/sql_utils.py b/papi/core/db/sql_utils.py new file mode 100644 index 0000000..0287e08 --- /dev/null +++ b/papi/core/db/sql_utils.py @@ -0,0 +1,30 @@ +from typing import Set, Type + +from sqlalchemy.orm import DeclarativeMeta + + +def extract_bases_from_models( + models: dict[str, Type[DeclarativeMeta]], +) -> Set[Type[DeclarativeMeta]]: + """ + Extracts unique SQLAlchemy Base classes from a dict of model classes. + + Args: + models: Dict[str, DeclarativeMeta] - model name to model class. + + Returns: + Set of unique Base classes (DeclarativeMeta subclasses). + """ + bases: Set[Type[DeclarativeMeta]] = set() + for model in models.values(): + # metadata suele estar en la clase Base, no directamente en el modelo + metadata = getattr(model, "metadata", None) + if metadata is None: + continue + + # Buscar en bases directas alguna clase que tenga metadata + base_class = next((b for b in model.__bases__ if hasattr(b, "metadata")), None) + if base_class is not None: + bases.add(base_class) + + return bases diff --git a/papi/core/init.py b/papi/core/init.py index 6e96215..5fc855e 100644 --- a/papi/core/init.py +++ b/papi/core/init.py @@ -1,6 +1,6 @@ import os from types import ModuleType -from typing import Callable, Type +from typing import Callable, Optional, Type from beanie import init_beanie from mcp.server.fastmcp import FastMCP @@ -8,6 +8,7 @@ from pymongo.errors import PyMongoError, ServerSelectionTimeoutError from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import ( + AsyncEngine, create_async_engine, ) from sqlalchemy.orm import DeclarativeMeta @@ -22,7 +23,11 @@ get_sqlalchemy_models_from_addon, load_and_import_all_addons, ) -from papi.core.db import create_database_if_not_exists, get_redis_client +from papi.core.db import ( + create_database_if_not_exists, + extract_bases_from_models, + get_redis_client, +) from papi.core.logger import logger from papi.core.mcp import create_sse_server from papi.core.router import MPCRouter @@ -106,25 +111,25 @@ async def init_sqlalchemy( config, modules: dict[str, ModuleType], create_tables: bool = True, -) -> dict[str, Type[DeclarativeMeta]] | None: +) -> Optional[dict[str, Type[DeclarativeMeta]]]: """ - Initializes SQLAlchemy if models are found and configuration is valid. + Initializes SQLAlchemy models from addon modules and optionally creates tables + for all detected Base metadata classes. Args: - config: Configuration object with database info. - modules: Dictionary of loaded addon modules. - create_tables: Whether to create tables automatically. + config: Configuration object with database URI. + modules: Dictionary of addon modules. + create_tables: Whether to automatically create tables in DB. Returns: - Dictionary of model names to model classes - Or None if no models were found. + Dict mapping model names to model classes, or None if no models found. Raises: - RuntimeError: If models are found but no DB URI is configured, or if connection fails. + RuntimeError: If DB URI missing or SQLAlchemy initialization fails. """ sqlalchemy_models: dict[str, Type[DeclarativeMeta]] = {} - logger.info("Searching for SQL models in active addons") + logger.info("Searching for SQLAlchemy models in active addons") for addon_id, module in modules.items(): addon_models = get_sqlalchemy_models_from_addon(module) if addon_models: @@ -132,39 +137,47 @@ async def init_sqlalchemy( logger.debug( f" → SQLAlchemy models from '{addon_id}': {', '.join(model_names)}" ) - sqlalchemy_models.update(dict(zip(model_names, addon_models))) + sqlalchemy_models.update({ + name: model for name, model in zip(model_names, addon_models) + }) if not sqlalchemy_models: - logger.info("No SQL models found. Skipping SQLAlchemy initialization.") + logger.info("No SQLAlchemy models found. Skipping SQLAlchemy initialization.") return None if not config.database.sql_uri: - logger.error("Found SQL models but SQL connection URI is not configured.") - raise RuntimeError("SQLAlchemy URI required to initialize SQLAlchemy models.") + logger.error("SQL models found but no SQL URI configured.") + raise RuntimeError("SQLAlchemy URI is required to initialize database models.") + + bases = extract_bases_from_models(sqlalchemy_models) try: - # Ensure database exists await create_database_if_not_exists(config.database.sql_uri) - # Create async engine and sessionmaker - engine = create_async_engine(config.database.sql_uri, echo=False, future=True) + engine: AsyncEngine = create_async_engine( + config.database.sql_uri, echo=False, future=True + ) logger.info( - f"SQLAlchemy engine and session initialized with {len(sqlalchemy_models)} models." + f"SQLAlchemy engine initialized with {len(sqlalchemy_models)} models." ) if create_tables: - # Get metadata from any model (they should all share the same metadata) - metadata = next(iter(sqlalchemy_models.values())).metadata - - async with engine.begin() as conn: - await conn.run_sync(metadata.create_all) - logger.info("All SQLAlchemy tables created.") + if not bases: + logger.warning( + "No Base classes detected to create tables for. Skipping table creation." + ) + else: + async with engine.begin() as conn: + for base in bases: + logger.debug(f"Creating tables for Base: {base}") + await conn.run_sync(base.metadata.create_all) + logger.info("All tables for specified Base(s) created successfully.") return sqlalchemy_models except SQLAlchemyError as exc: - logger.exception("SQLAlchemy initialization error.") + logger.exception("SQLAlchemy initialization failed.") raise RuntimeError(f"SQLAlchemy initialization error: {exc!r}") From f2ca086be91611ab160d332fef3d7106dcdbf520 Mon Sep 17 00:00:00 2001 From: Eduardo Firvida Date: Fri, 13 Jun 2025 12:34:21 -0400 Subject: [PATCH 2/5] Full refactor to include docs and move papi outside src folder --- mkdocs.yml | 1 + papi/base/user_auth_system/models/token.py | 54 +++++++++++++++++++ .../user_auth_system/security/__init__.py | 7 +++ papi/core/models/response.py | 4 +- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 papi/base/user_auth_system/models/token.py diff --git a/mkdocs.yml b/mkdocs.yml index bc5fef1..50d2621 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,7 @@ site_name: pAPI – Python/Pluggable API Framework site_description: pAPI – Official Documentación site_author: Eduardo Miguel Fírvida Donestevez +site_url: https://efirvida.github.io/papi/ theme: name: material diff --git a/papi/base/user_auth_system/models/token.py b/papi/base/user_auth_system/models/token.py new file mode 100644 index 0000000..fa5956d --- /dev/null +++ b/papi/base/user_auth_system/models/token.py @@ -0,0 +1,54 @@ +from datetime import datetime, timezone + +from sqlalchemy import Boolean, Column, DateTime, Index, String +from sqlalchemy.orm import declarative_base + +Base = declarative_base() + + +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + token = Column(String, primary_key=True, index=True) + subject = Column(String, nullable=False) + jti = Column(String, unique=True, nullable=False) + device_id = Column(String, nullable=True) + user_agent = Column(String, nullable=True) + revoked = Column(Boolean, default=False, nullable=False) + + expires_at = Column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + ) + created_at = Column( + DateTime(timezone=True), + nullable=False, + default=lambda: datetime.now(timezone.utc), + ) + + def __repr__(self): + return ( + f"" + ) + + +class AccessToken(Base): + __tablename__ = "access_tokens" + + jti = Column(String, primary_key=True, index=True, unique=True) + subject = Column(String, nullable=False, index=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + revoked_at = Column(DateTime(timezone=True), nullable=True) + blacklisted = Column(Boolean, default=False, nullable=False) + + __table_args__ = ( + Index("ix_access_tokens_subject_expires_at", "subject", "expires_at"), + ) + + def __repr__(self): + return ( + f"" + ) diff --git a/papi/base/user_auth_system/security/__init__.py b/papi/base/user_auth_system/security/__init__.py index e69de29..043dc6d 100644 --- a/papi/base/user_auth_system/security/__init__.py +++ b/papi/base/user_auth_system/security/__init__.py @@ -0,0 +1,7 @@ +from papi.core.settings import get_config +from user_auth_system.schemas import AuthSettings, BaseSecurity + +config = get_config() +main_config = config.addons.config.get("user_auth_system", {}) +auth_settings = AuthSettings(**main_config) +security: BaseSecurity = auth_settings.security diff --git a/papi/core/models/response.py b/papi/core/models/response.py index bb4f58d..3e1318d 100644 --- a/papi/core/models/response.py +++ b/papi/core/models/response.py @@ -1,6 +1,4 @@ -import uuid -from datetime import datetime -from typing import Any, Dict, Optional +from typing import Any, Optional from fastapi import status from pydantic import BaseModel, Field From 68fa104e96481c6214867ef95d141191103f3690 Mon Sep 17 00:00:00 2001 From: Eduardo Firvida Date: Fri, 13 Jun 2025 12:54:31 -0400 Subject: [PATCH 3/5] Automatic deploy docs --- .github/workflows/doc_deploy.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/doc_deploy.yml diff --git a/.github/workflows/doc_deploy.yml b/.github/workflows/doc_deploy.yml new file mode 100644 index 0000000..6c725ab --- /dev/null +++ b/.github/workflows/doc_deploy.yml @@ -0,0 +1,27 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + - develop + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + + - name: Install the latest version of rye + uses: eifinger/setup-rye@v4.2.9 + + - name: Install dependencies with Rye + run: rye sync + + - name: Deploy MkDocs site + run: rye run mkdocs gh-deploy --force + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 78af4752f21e62f41e4a58f78253221f1bd9a7ba Mon Sep 17 00:00:00 2001 From: Eduardo Firvida Date: Sat, 14 Jun 2025 13:41:30 -0400 Subject: [PATCH 4/5] Fix wrong imported create_response --- papi/base/user_auth_system/api/policies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/papi/base/user_auth_system/api/policies.py b/papi/base/user_auth_system/api/policies.py index 6738d6b..841d558 100644 --- a/papi/base/user_auth_system/api/policies.py +++ b/papi/base/user_auth_system/api/policies.py @@ -5,7 +5,8 @@ from papi.core.db import get_sql_session from papi.core.exceptions import APIException -from papi.core.models.response import APIResponse, create_response +from papi.core.models.response import APIResponse +from papi.core.response import create_response from papi.core.router import RESTRouter from user_auth_system.models.casbin import AuthRules from user_auth_system.schemas import PolicyCreate, PolicyInDB From b07677c3aa00edf9442c7ed3e9cf7a0de18ee3a5 Mon Sep 17 00:00:00 2001 From: Eduardo Firvida Date: Sun, 15 Jun 2025 14:56:57 -0400 Subject: [PATCH 5/5] Add first docs tutorials --- docs/index.md | 445 +++++++++++++++++++++++++++++++++- docs/reference/models.md | 1 - docs/tutorials/hello_world.md | 124 ++++++++++ docs/tutorials/weather.md | 363 +++++++++++++++++++++++++++ mkdocs.yml | 29 ++- 5 files changed, 937 insertions(+), 25 deletions(-) create mode 100644 docs/tutorials/hello_world.md create mode 100644 docs/tutorials/weather.md diff --git a/docs/index.md b/docs/index.md index 000ea34..cec8438 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,438 @@ -# Welcome to MkDocs +# pAPI — Pluggable Python API Framework -For full documentation visit [mkdocs.org](https://www.mkdocs.org). +> **GitHub**: [https://github.com/efirvida/papi](https://github.com/efirvida/papi) -## Commands +pAPI is a lightweight, modular micro-framework built on top of FastAPI. It leverages FastAPI’s full feature set—routing, dependency injection, async support—without altering its internals or impacting performance. Instead, it enhances FastAPI’s routing system with dynamic discovery, a plugin-based architecture, and first-class support for agent-native endpoints, such as tools exposed via the Model Context Protocol (MCP). This makes pAPI especially well-suited for building composable microservices, intelligent agent interfaces, and modular backends. -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. +--- -## Project layout +## ⚙️ What is pAPI? + +pAPI is designed to let you build composable APIs through reusable "addons" (self-contained units of logic). It handles: + +- Addon registration and lifecycle +- Auto-discovery of routers and models +- Dependency resolution between addons +- Consistent response formatting +- Database abstraction with async support +- Direct exposure of FastAPI routes as tools compatible with the **Model Context Protocol (MCP)** — enabling seamless integration with LLM-based agents + + +Aquí tienes una versión más clara, profesional y precisa de la sección, con estilo consistente y mejor redacción técnica: + +--- + +## 🔧 Core Features + +### 🧩 Modular Architecture + +* Addons are self-contained: they register their own routers, models, CLI commands, and lifecycle hooks. +* Route behavior can be extended or overridden by other addons. +* Each addon can expose tools through standard HTTP endpoints or as **Model Context Protocol (MCP)**-compatible tools for agent-based workflows. + +--- + +### 🤖 Agent-Native Interface + +pAPI is designed with LLM-driven agents in mind. It allows you to expose FastAPI endpoints as **Model Context Protocol (MCP)**-compatible tools with minimal effort, supporting both JSON-over-HTTP and **Server-Sent Events (SSE)** for streaming. + +✅ One-line tool exposure +✅ Seamless integration with LLM agents + +Expose a tool in a single line: + +```python +from papi.core.router import RESTRouter + +router = RESTRouter() + +@router.get("/tool", expose_as_mcp_tool=True) +async def my_tool(): + return {"result": "tool output"} +``` + +Once exposed, your tools are automatically discoverable via SSE: + +```json +{ + "name": "pAPI", + "type": "sse", + "url": "http://localhost:8000/sse" +} +``` + +If you need to expose MCP tools directly without API endpoints, you can use `MCPRouter`, which is a thin wrapper around `FastMCP`: + +```python +from papi.core.router import MCPRouter + +router = MCPRouter() + +@router.tool() +async def my_tool(): + return {"result": "tool output"} +``` + +--- + +Start the full API + MCP tools with: + +```bash +rye run python papi/cli.py webserver +``` + +Or launch just the MCP interface: + +```bash +rye run python papi/cli.py mcpserver +``` + +--- + +### 🗄️ Built-in Database Layer + +pAPI offers first-class support for multiple asynchronous databases, making it easy to combine different storage backends within a single application. The database layer is **declarative and dynamic** — connections are only initialized when relevant models are discovered at runtime. + +Supported backends: + +| Backend | Library | Typical Use Case | +| ---------- | ---------- | ---------------------------------- | +| MongoDB | Beanie | Document-oriented storage | +| PostgreSQL | SQLAlchemy | Relational databases | +| Redis | aioredis | Caching, queues, distributed locks | + +#### ✅ Smart and Flexible Integration + +* Models are **automatically discovered** during startup by inspecting enabled addons. +* Each addon can **define its own models** or **extend models from other addons** to add new fields, relations, or behaviors. +* No need for central registration — model discovery is fully modular. +* Unused databases are ignored, so you only pay for what you use. + +This architecture allows true decoupling between services. For example, one addon can provide core user models, and another can enhance those with metadata, analytics, or external account bindings — all without modifying the original addon code. + +> 💡 Tip: The system is ORM/ODM-agnostic — you can bring your own schema logic and override or compose behaviors freely. + +--- + +### 📦 Unified Response System + +pAPI provides an optional but powerful mechanism for standardizing responses across all endpoints. This promotes consistency, improves debuggability, and facilitates client integration—but does not constrain developers. You can continue to use raw FastAPI responses where desired. + +#### 🧱 Structured Response Format + +The framework offers a unified schema via the `APIResponse` model: + +```python +class APIResponse(BaseModel): + success: bool + message: Optional[str] + data: Optional[Any] + error: Optional[APIError] + meta: Meta +``` + +Key components include: + +* ✅ `success`: Boolean flag indicating request outcome +* 📦 `data`: Response payload (included only if successful) +* ⚠️ `error`: Structured error object (included only on failure) +* 💬 `message`: Optional human-readable message +* 📊 `meta`: Metadata (e.g., timestamp, request ID) + +#### ⚙️ `create_response`: Standardized Response Builder + +The `create_response()` helper generates well-formed `APIResponse` objects with minimal boilerplate: + +```python +from papi.core.response import create_response + +@app.get("/hello") +async def hello(): + return create_response(data={"message": "Hello, world!"}) +``` + +Internally, it wraps the payload with standardized metadata and, when applicable, error details. + +```python +def create_response( + data: Any = None, + success: bool = True, + message: Optional[str] = None, + error: Optional[Dict[str, Any]] = None, +) -> APIResponse: + ... +``` + +Error objects can include: + +* `code`: Application-specific error identifier +* `message`: Short description +* `detail`: Arbitrary contextual data +* `status_code`: HTTP status code to be returned + +#### 🚨 Unified Exception Handling + +The optional `APIException` class makes it easy to raise consistent, structured errors: + +```python +raise APIException( + status_code=403, + message="Access denied", + code="PERMISSION_DENIED", + detail={"required_role": "admin"} +) +``` + +All `APIException`s are automatically serialized into the same response format used by `create_response`, ensuring uniformity across success and error cases. + +> ℹ️ This system is **opt-in**: developers retain full control and can use native FastAPI `JSONResponse`, `Response`, or any custom return logic as needed. + +--- + +### 🛠️ Developer-Friendly CLI + +pAPI ships with a powerful, extensible CLI designed to streamline development, introspection, and deployment workflows. + +```bash +$ papi_cli start webserver # Launch the FastAPI server with all registered addons +$ papi_cli start mcpserver # Run the standalone MCP (agent) server +$ papi_cli shell # Open an interactive, async-ready developer shell +``` + +#### 🧠 Key Features + +* ✅ **Async-aware interactive shell** (with full `await` support), powered by IPython if available. +* 🧩 **Addon-aware**: all loaded addons can inject their own CLI commands. +* ⚙️ **Config-aware**: automatically loads `config.yaml` and injects environment context. + +--- + +#### 🧪 CLI Overview + +```bash +$ rye run python papi/cli.py --help +``` + +```text + _ ____ ___ + ___ / \ | _ \ |_ _| +| _ \ / _ \ | |_) | | | +| |_) | / ___ \ | __/ | | +| __/ /_/ \_\|_| |___| +|_| Version: v0.0.1 + +Usage: cli.py [OPTIONS] COMMAND [ARGS]... + + Main entry point for pAPI service management CLI. + +Options: + --config TEXT Path to configuration file + -h, --help Show this message and exit. + +Commands: + webserver* Start the production FastAPI web server. + mcpserver Start the standalone MCP server. + shell Launch an interactive IPython shell with the initialized... +``` + +--- + +### 🐚 Interactive Shell Utilities + +The `shell` command loads a fully-initialized runtime context, including all registered database models and configurations. + +#### MongoDB Integration + +MongoDB documents are registered and accessible via the `mongo_documents` dictionary: + +```python +await mongo_documents["Image"].find().to_list() +``` + +Each key is the document class name, and the value is the corresponding `Beanie` model class. + +#### SQLAlchemy Integration + +SQL models are exposed via the `sql_models` dictionary, enabling expressive, async queries with full SQLAlchemy support: + +```python +from sqlalchemy import select + +stmt = select(sql_models["User"]).where(sql_models["User"].email == "admin@example.com") +result = await db_session.execute(stmt) +user = result.scalar_one_or_none() +``` + +> This makes the shell ideal for advanced debugging, quick prototyping, and admin scripting—all with full access to the live database and application context. + + +--- + +## 📦 Addon System + +pAPI provides a robust, modular plugin system via **addons**—isolated Python modules that encapsulate logic, routes, models, CLI commands, configuration, and static assets. This architecture promotes separation of concerns, extensibility, and reusability. + +--- + +### 🧬 Anatomy of an Addon + +``` +addons/ +└── user_auth_system/ + ├── static/ + │ └── static_files/ + ├── __init__.py + ├── manifest.yaml + ├── routers.py + ├── models.py + └── cli.py +``` + +Each addon behaves as a self-contained package and is dynamically discovered and registered at runtime based on the configuration. + +--- + +### 📑 `manifest.yaml` + +The mandatory `manifest.yaml` file defines metadata and dependencies for each addon: + +```yaml +name: user_auth_system +version: 0.1.0 +description: Built-in user authentication system +author: pAPI Team + +dependencies: + - image_storage + +python_dependencies: + - passlib + - pydantic[email] +``` + +#### 🔗 Addon Dependencies + +* The `dependencies` field declares other addons that must be loaded before this one. +* Dependency resolution ensures proper load order and allows addons to **extend or reuse** logic and models from other addons. + +#### 🐍 Python Dependencies + +* The `python_dependencies` section specifies required PyPI packages for this addon. +* These dependencies will be automatically installed with `pip install` on every startup. +* ⚠️ Use with care: untrusted or unnecessary packages may affect system stability or introduce security risks. + +--- + +## 🧪 Example Configuration (`config.yaml`) + +```yaml +logger: + level: "INFO" + log_file: ./papi.log + +info: # FastAPI settings + title: "Testing API Server" + version: "1.0.0" + description: "Test instance for local development" + +server: # uvicorn settings + host: "127.0.0.1" + port: 8000 + +database: + mongodb_uri: "mongodb://root:example@localhost:27017/testing_db?authSource=admin" + sql_uri: "postgresql+asyncpg://localhost/testing_db" + redis_uri: "redis://localhost:6379" + +addons: + extra_addons_path: "/path/to/local/addons" + enabled: + - user_auth_system # this will load image_storage as dependency + config: # Addons configuration + user_auth_system: + security: + access_token_expire_minutes: 60 + allow_weak_passwords: true + lockout_duration_minutes: 15 + bcrypt_rounds: 15 + hash_algorithm: "HS256" + max_login_attempts: 5 + secret_key: "your-secret-key" + token_audience: bohio.com + token_issuer: bohio.com + key_rotation: + rotation_interval_days: 30 + max_historical_keys: 3 + image_storage: + image_optimization: + max_dimension: 2048 + jpeg_quality: 85 + png_compression: 6 + webp_quality: 80 + force_format: WEBP + cache_ttl: 3600 + cache_prefix: "pAPI:image_storage:" + max_image_size: 10485760 + allowed_formats: + - JPEG + - PNG + - WEBP + - GIF + storage_backend: local + +storage: # mounted as static files to provide global static files foled + files: "path/to/local/storage/folder/files" + images: "path/to/local/storage/folder/images" # used for image_storage addon + +``` + +--- + +## 🚀 Quickstart + +```bash +git clone https://github.com/your-org/papi +cd papi +rye sync +rye run python papi/cli.py webserver +``` + +--- +## 🧠 Use Cases + +pAPI is designed to enable modern backend patterns with minimal boilerplate. Some typical use cases include: + +* **Agent-Integrated APIs** + Seamlessly expose tool-like endpoints to LLM agents and orchestration frameworks via native MCP support and SSE-compatible interfaces. + +* **Plugin-Driven Architectures** + Build extensible backends for **low-code/no-code platforms** or SaaS systems, where features are delivered as isolated, dynamically loaded addons. + +* **Multi-Tenant and Domain-Based Systems** + Architect modular applications where business logic, models, and routes are grouped by tenant, domain, or business unit. + +* **Internal Developer Tools & Microservices** + Rapidly prototype lightweight internal services or CLI-extended tools that benefit from a unified config, database access, and CLI environment. + +--- + +## 📬 Contributing + +We welcome pull requests! + +```bash +# fork and clone +git checkout -b feature/my-feature +# write tests if applicable +git commit -m "Add my-feature" +git push origin feature/my-feature +``` + +Then open a PR on GitHub. 🚀 + +--- + +## 🪪 License + +MIT License © 2025 — Eduardo Miguel Firvida Donestevez - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. diff --git a/docs/reference/models.md b/docs/reference/models.md index 664d02a..bac4284 100644 --- a/docs/reference/models.md +++ b/docs/reference/models.md @@ -23,4 +23,3 @@ - Meta - APIError - APIResponse - - create_response diff --git a/docs/tutorials/hello_world.md b/docs/tutorials/hello_world.md new file mode 100644 index 0000000..b746285 --- /dev/null +++ b/docs/tutorials/hello_world.md @@ -0,0 +1,124 @@ +## 🚀 Hello World! + +pAPI is built around the concept of **addons**—self-contained Python modules that define routes, models, CLI commands, and business logic. Creating your first addon is straightforward and does **not** require modifying the core system. + +### 1️⃣ Create Your Addon Structure + +In your configured `extra_addons_path` (e.g., `my_addons/`), create a new folder: + +``` +my_addons/ +└── hello_world/ + ├── __init__.py + ├── manifest.yaml + ├── routers.py +``` + +### 2️⃣ Define the `manifest.yaml` + +This file provides metadata and optional dependencies. At a minimum, define `name` and `version`: + +```yaml +name: hello_world +version: 0.1.0 +description: A minimal addon that says hello +author: Your Name +``` + +### 3️⃣ Add Routes with `RESTRouter` + +In `routers.py`, use the `RESTRouter` class and `create_response()` helper to define a basic endpoint: + +```python +from papi.core.router import RESTRouter +from papi.core.response import create_response + +router = RESTRouter() + +@router.get("/hello") +async def hello(): + return create_response(data={"message": "Hello from addon!"}) +``` + +Then, import the router in `__init__.py` to ensure it is discovered automatically: + +```python +from .routers import router +``` + +### 4️⃣ Enable Your Addon in `config.yaml` + +Add your addon to the global configuration file: + +```yaml +logger: + level: "INFO" + log_file: ./papi.log + +info: + title: "Testing API Server" + version: "1.0.0" + description: "This is a test API server for demonstration purposes." + +server: + host: "127.0.0.1" + port: 8080 + +addons: + extra_addons_path: "my_addons" + enabled: + - hello_world +``` + +### 5️⃣ Run the Server and Test + +Start the server with: + +```bash +rye run python papi/cli.py webserver +``` + +Then access your new endpoint at: +`http://localhost:8080/hello` + +Or test via `curl`: + +```bash +curl -X GET http://localhost:8080/hello -H "accept: application/json" +``` + +Expected response: + +```json +{ + "success": true, + "message": null, + "data": { + "message": "Hello from addon!" + }, + "error": null, + "meta": { + "timestamp": "2025-06-14T15:41:44+00:00", + "requestId": "207567c1-00b6-4b9b-8962-4e90b9a87beb" + } +} +``` + +Swagger docs are available at: +🔗 [http://localhost:8080/docs](http://localhost:8080/docs) + +--- + +> 💡 **What happens behind the scenes?** +> +> * The addon is discovered and loaded automatically +> * All `RESTRouter` routes are registered under the main router +> * The addon is fully integrated into the routing and dependency system + +--- + +### ✅ What's Next? + +* Add **Beanie** or **SQLAlchemy** models — pAPI will detect and initialize them automatically +* Add custom CLI commands in `cli.py` +* Implement `AddonSetupHook` for custom startup logic diff --git a/docs/tutorials/weather.md b/docs/tutorials/weather.md new file mode 100644 index 0000000..98b3797 --- /dev/null +++ b/docs/tutorials/weather.md @@ -0,0 +1,363 @@ +## 🌦️ Weather Addon Example + +This addon demonstrates how to build a basic weather data API using **pAPI**, with persistent storage in MongoDB via the integrated **Beanie** ODM. + +Through this example, you will learn how to: + +* Integrate MongoDB using **Beanie** +* Add Python dependencies to your addon +* Register and list weather stations +* Fetch real-time weather data from an external API + +> 💡 **Bonus**: Learn how to interact with MongoDB using the integrated shell. + +--- + +### 🗂️ Project Structure + +``` +my_addons/ +└── weather/ + ├── __init__.py + ├── manifest.yaml + ├── models.py + ├── crud.py + └── routers.py +``` + +--- + +### 📄 `manifest.yaml` + +Declares the addon and its required dependencies: + +```yaml +name: weather +version: 1.0.0 +description: Weather data API +author: Your Name + +python_dependencies: + - "requests>=2.28.0" +``` + +--- + +### 🧬 `models.py` + +Defines the database models for weather stations and readings: + +```python +from datetime import datetime, timezone +from beanie import Document, PydanticObjectId +from pydantic import BaseModel, Field + + +class Location(BaseModel): + latitude: float + longitude: float + + +class WeatherStation(Document): + name: str + location: Location + created_at: datetime = Field( + default_factory=lambda: datetime.now(tz=timezone.utc) + ) + + class Settings: + name = "weather_stations" + + +class WeatherReading(Document): + station_id: PydanticObjectId + temperature: float + windspeed: float + humidity: float + timestamp: datetime = Field(default_factory=lambda: datetime.now(tz=timezone.utc)) + + class Settings: + name = "weather_readings" +``` + +--- + +### 🔌 `routers.py` + +Handles the REST API endpoints: + +```python +from fastapi import HTTPException +from papi.core.response import create_response +from papi.core.router import RESTRouter + +from .crud import get_weather +from .models import WeatherReading, WeatherStation + +router = RESTRouter() + + +@router.get("/stations") +async def list_stations(): + """List all registered weather stations.""" + stations = await WeatherStation.find_all().to_list() + return create_response(data=stations) + + +@router.post("/stations") +async def create_station(station: WeatherStation): + """Register a new weather station.""" + await station.create() + return create_response(message="Station created successfully", data=station) + + +@router.get("/stations/{station_id}/weather") +async def get_current_weather_for_station(station_id: str): + """ + Fetch the current weather data for a given station. + Also saves the reading to the database. + """ + station = await WeatherStation.get(station_id) + if not station: + raise HTTPException(status_code=404, detail="Station not found") + + weather = get_weather(station.location.latitude, station.location.longitude) + + await WeatherReading(station_id=station.id, **weather).save() + + return create_response(data=weather) +``` + +--- + +### 🌐 `crud.py` + +Fetches weather data using the Open-Meteo public API: + +```python +import requests + +def get_weather(latitude: float, longitude: float) -> dict: + """ + Fetch current weather data using the Open-Meteo API (no API key required). + """ + url = ( + "https://api.open-meteo.com/v1/forecast" + f"?latitude={latitude}&longitude={longitude}" + "¤t=temperature_2m,wind_speed_10m,relative_humidity_2m" + ) + + try: + response = requests.get(url, timeout=5) + response.raise_for_status() + data = response.json() + + current = data.get("current") + if not current: + raise ValueError("Missing 'current' field in API response") + + return { + "temperature": current["temperature_2m"], + "windspeed": current["wind_speed_10m"], + "humidity": current["relative_humidity_2m"] + } + + except Exception as e: + return { + "temperature": None, + "windspeed": None, + "humidity": None, + "error": str(e) + } +``` + +--- + +### 📦 `__init__.py` + +This file registers your addon so it can be discovered and loaded by **pAPI**. + +It also registers the **Beanie documents** so they are initialized with the MongoDB connection during pAPI startup. + +```python +from .models import WeatherReading, WeatherStation +from .router import router + +# Register the API router and Beanie documents for MongoDB +__all__ = [ + "router", # Registers the API routes + "WeatherStation", # Registers the weather station document + "WeatherReading" # Registers the weather reading document +] +``` + +--- + +### ⚙️ Configuration (`config.yaml`) + +```yaml + +# base configuration check the hello world example +... + +# MongoDB connection settings +database: + mongodb_uri: "mongodb://root:example@localhost:27017/weather_db?authSource=admin" + +# Enable the weather addon +addons: + extra_addons_path: "my_addons" + enabled: + - weather +``` + +--- + +## 🧪 How to Use + +### 🚀 Start the API Server + +```bash +rye run python papi/cli.py webserver +``` + +--- + +### 📍 Create a Weather Station + +```bash +curl -X 'POST' \ + 'http://localhost:8080/stations' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "Santa Clara, Cuba", + "location": { + "latitude": 22.4067, + "longitude": -79.9531 + } +}' +``` + +#### ✅ Example Response + +```json +{ + "success": true, + "message": "Station created successfully", + "data": { + "_id": "684da177ebcda212e2ce8dac", + "name": "Santa Clara, Cuba", + "location": { + "latitude": 22.4067, + "longitude": -79.9531 + }, + "created_at": "2025-06-14T16:21:11.352782Z" + }, + "error": null, + "meta": { ... } +} +``` + +--- + +### 📋 List All Stations + +```bash +curl -X 'GET' \ + 'http://localhost:8080/stations' \ + -H 'accept: application/json' +``` + +#### ✅ Example Response +```json +{ + "success": true, + "message": null, + "data": [ + { + "_id": "684daa34dc94122d9d84bac9", + "name": "Santa Clara, Cuba", + "location": { + "latitude": 22.4067, + "longitude": -79.9531 + }, + "created_at": "2025-06-14T16:58:28.540000" + } + ], + "error": null, + "meta": { + "timestamp": "2025-06-14T17:17:01+00:00Z", + "requestId": "888ecf22-ea67-4f37-87e1-c13d6f32cfea" + } +} +``` + +--- + +### 🌡️ Get Weather for a Station + +```bash +curl -X 'GET' \ + 'http://localhost:8080/stations/684da177ebcda212e2ce8dac/weather' \ + -H 'accept: application/json' +``` + +#### ✅ Example Response + +```json +{ + "success": true, + "message": null, + "data": { + "temperature": 31.2, + "windspeed": 18.7, + "humidity": 56 + }, + "error": null, + "meta": { ... } +} +``` + +--- + +## 🛠️ MongoDB Shell Access (via pAPI Shell) + +Access the MongoDB shell: + +```bash +rye run python papi/cli.py shell +``` + +### 🔍 Available Documents + +```python +In [1]: mongo_documents +Out[1]: +{ + 'WeatherStation': weather.models.WeatherStation, + 'WeatherReading': weather.models.WeatherReading +} +``` + +### 🧾 List Stations in Shell + +```python +In [2]: await mongo_documents["WeatherStation"].find().to_list() +``` + +### ✏️ Rename a Station + +```python +In [3]: from beanie import BeanieObjectId as ObjectId +In [4]: station = await mongo_documents["WeatherStation"].get(ObjectId('684daa34dc94122d9d84bac9')) +In [5]: station.name = "New Santa Clara" +In [6]: await station.save() +``` + +Confirm the change: + +```python +In [7]: await mongo_documents["WeatherStation"].get(ObjectId('684daa34dc94122d9d84bac9')) +Out[7]: WeatherStation(name='New Santa Clara', ...) +``` diff --git a/mkdocs.yml b/mkdocs.yml index 50d2621..dd43c36 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,23 @@ site_description: pAPI – Official Documentación site_author: Eduardo Miguel Fírvida Donestevez site_url: https://efirvida.github.io/papi/ +nav: + - Home: + - Home: index.md + - Getting started: + - Your first Addon: tutorials/hello_world.md + - The weather example: tutorials/weather.md + - API Reference: + - pAPI Core: + - Router: reference/router.md + - Settings: reference/settings.md + - DB: reference/db.md + - CLI: reference/cli.md + - Models: reference/models.md + - build in addons: + - Image Storage: build-in-addons/image_storage.md + - User Authentication: build-in-addons/user_auth_system.md + theme: name: material language: en @@ -99,15 +116,3 @@ plugins: show_symbol_type_heading: true show_symbol_type_toc: true -nav: - - Home: index.md - - API Reference: - - pAPI Core: - - Router: reference/router.md - - Settings: reference/settings.md - - DB: reference/db.md - - CLI: reference/cli.md - - Models: reference/models.md - - build in addons: - - Image Storage: build-in-addons/image_storage.md - - User Authentication: build-in-addons/user_auth_system.md