diff --git a/README.md b/README.md index 5bca3f5..78db4c6 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ This is a comprehensive guide to [Fable.Python](https://github.com/fable-compile 4. **Interop** - Using existing Python libraries and Fable.Python bindings 5. **Bindings** - Creating your own type-safe bindings for Python libraries 6. **Compatibility** - Supported F# features and limitations -7. **Fable v5** - What's new in Fable v5 for Python -8. **Libraries** - Existing ecosystem (Thoth.Json, AsyncRx, Siren, etc.) *(coming soon)* -9. **Pydantic** - Pydantic interop with Decorate and ClassAttributes -10. **Units of Measure** - Compile-time dimensional analysis -11. **Testing** - Testing F# code with Python test runners -12. **Fable.Literate** - The self-documenting converter -13. **Summary** - Wrap-up, resources, and contributing +7. **Async Programming** - F# async and Python asyncio +8. **Testing** - Testing F# code with Python test runners +9. **Fable v5** - What's new in Fable v5 for Python +10. **Pydantic** - Pydantic interop with Decorate and ClassAttributes +11. **FastAPI** - Building type-safe web APIs with F# +12. **Units of Measure** - Compile-time dimensional analysis +13. **Fable.Literate** - The self-documenting converter +14. **Summary** - Wrap-up, resources, and contributing ## The Strange Loop diff --git a/chapters/FastAPI.fs b/chapters/FastAPI.fs new file mode 100644 index 0000000..58105f8 --- /dev/null +++ b/chapters/FastAPI.fs @@ -0,0 +1,339 @@ +(** +# FastAPI + +As an F# developer you are probably familiar with web frameworks like ASP.NET Core, Giraffe, or Oxpecker. But Fable.Python +also allows you to build web APIs that run in Python environments, using the popular FastAPI framework. + +## What is FastAPI? + +[FastAPI](https://fastapi.tiangolo.com/) is Python's most popular modern web framework. +It's fast, easy to use, and built on top of Pydantic for automatic request validation +and OpenAPI documentation. + +FastAPI gives you: + +- **High performance** - One of the fastest Python frameworks available +- **Automatic validation** - Request/response validation via Pydantic, that you already know from + the previous chapter +- **Type hints** - Leverages Python type hints for better editor support +- **OpenAPI docs** - Interactive Swagger UI and ReDoc generated automatically +- **Async support** - Native async/await for high concurrency + +Fable.Python includes bindings for FastAPI, allowing you to write type-safe APIs +using F# while leveraging Python's mature web ecosystem. + +## Setting Up + +Add FastAPI and uvicorn to your Python environment: + +```bash +uv add fastapi uvicorn +``` + +Then import the FastAPI module in your F# code: +*) + +(*** hide ***) +module FastAPI + +open System.Threading.Tasks +open Fable.Core +open Fable.Python.FastAPI +open Fable.Python.Pydantic + +(** +```fsharp +open Fable.Python.FastAPI +open Fable.Python.Pydantic +``` + +## Creating the Application + +Create a FastAPI application instance at the module level: +*) + +let app = FastAPI(title = "My API", version = "1.0.0") + +(** +This generates: + +```python +app = FastAPI(title="My API", version="1.0.0") +``` + +The `app` variable name is important - the route decorators reference it. + +## Defining Models + +Request and response models use Pydantic's `BaseModel` (covered in the previous chapter): +*) + +[] +type Item(Id: int, Name: string, Price: float, InStock: bool) = + inherit BaseModel() + member val Id: int = Id with get, set + member val Name: string = Name with get, set + member val Price: float = Price with get, set + member val InStock: bool = InStock with get, set + +[] +type CreateItemRequest(Name: string, Price: float, InStock: bool) = + inherit BaseModel() + member val Name: string = Name with get, set + member val Price: float = Price with get, set + member val InStock: bool = InStock with get, set + +(** +## Defining Endpoints + +### The APIClass Pattern + +FastAPI endpoints are defined using a class with decorated static methods: +*) + +let items = ResizeArray() + +[] +type API() = + /// GET /items - List all items + [] + static member get_items() : ResizeArray = + items + + /// GET /items/{item_id} - Get item by ID + [] + static member get_item(item_id: int) : Task = task { + match items |> Seq.tryFind (fun i -> i.Id = item_id) with + | Some item -> return item :> obj + | None -> return {| error = "Item not found" |} + } + + /// POST /items - Create a new item + [] + static member create_item(request: CreateItemRequest) : Task = task { + let newId = + if items.Count = 0 then 1 + else (items |> Seq.map (fun i -> i.Id) |> Seq.max) + 1 + let newItem = Item(newId, request.Name, request.Price, request.InStock) + items.Add(newItem) + return {| status = "created"; item = newItem |} + } + +(** +This generates Python with proper FastAPI decorators: +*) + +(*** include-python: API ***) + +(** +### Key Points + +- `[]` marks the class for FastAPI routing. We use a class because Fable + can only apply decorator attributes to types and methods, not standalone functions +- Route decorators: `[]`, `[]`, `[]`, `[]`, `[]` +- Path parameters use `{param_name}` syntax and map to function arguments +- Pydantic models in parameters are automatically validated +- Return types can be sync or async (`Task<'T>`) + +### Anonymous Records for Quick Responses + +F# anonymous records compile to Python dictionaries, perfect for JSON responses: +*) + +[] +type HealthAPI() = + [] + static member health() = + {| status = "healthy"; version = "1.0.0" |} + +(** +## Async Endpoints + +For I/O-bound operations, use `task { }` to create async endpoints: +*) + +[] +type AsyncAPI() = + [] + static member slow_operation() = task { + // Simulate async work (e.g., database query) + do! Task.Delay(100) + return {| message = "Done!" |} + } + +(** +The `task { }` computation expression compiles to Python's `async def`, +integrating naturally with FastAPI's async support. + +## Path and Query Parameters + +### Path Parameters + +Path parameters are extracted from the URL: +*) + +[] +type UsersAPI() = + [] + static member get_user(user_id: int) = + {| id = user_id; name = "User " + string user_id |} + + [] + static member get_user_post(user_id: int, post_id: int) = + {| user_id = user_id; post_id = post_id |} + +(** +### Query Parameters + +Query parameters are function arguments not in the path: +*) + +[] +type SearchAPI() = + [] + static member search(q: string, limit: int) = + {| query = q; limit = limit |} + +(** +A request to `/search?q=hello&limit=10` maps to `search("hello", 10)`. + +## Request Bodies + +POST/PUT/PATCH endpoints receive request bodies as Pydantic models: +*) + +[] +type CreateUserRequest(name: string, email: string) = + inherit BaseModel() + member val name: string = name with get, set + member val email: string = email with get, set + +[] +type UserCrudAPI() = + [] + static member create_user(request: CreateUserRequest) = + // FastAPI automatically validates the request body + {| status = "created"; name = request.name; email = request.email |} + +(** +FastAPI validates the incoming JSON against the Pydantic model and returns +a 422 error if validation fails. + +## HTTP Exceptions + +Return proper HTTP errors using `HTTPException`: +*) + +[] +type ErrorAPI() = + [] + static member protected_route() = + // Check authentication (simplified example) + let isAuthenticated = false + if not isAuthenticated then + raise (System.Exception("Not authenticated")) + {| message = "Secret data" |} + +(** +In practice, you would use FastAPI's dependency injection for authentication. +The `HTTPException` type is available for more idiomatic error handling: + +```fsharp +// For proper HTTP exceptions, use a helper that emits Python's raise +[] +let raiseHttp (code: int) (msg: string) : unit = nativeOnly + +// Then in your endpoint: +if not isAuthenticated then + raiseHttp 401 "Not authenticated" +``` + + +## Running the Application + +Compile with Fable and run with uvicorn: + +```bash +# Compile F# to Python +dotnet fable --lang python --outDir build + +# Run the server +cd build +uvicorn app:app --reload +``` + +Visit: + +- `http://localhost:8000` - Your API +- `http://localhost:8000/docs` - Interactive Swagger UI +- `http://localhost:8000/redoc` - ReDoc documentation + +## Development Workflow + +For hot-reloading during development, run Fable in watch mode: + +```bash +# Terminal 1: Watch F# files +dotnet fable --lang python --outDir build --watch + +# Terminal 2: Run uvicorn with reload +cd build +uvicorn app:app --reload +``` + +Changes to your F# code automatically recompile and uvicorn picks up the changes. + +## Complete Example + +Here's a minimal but complete FastAPI application: + +```fsharp +module App + +open System.Threading.Tasks +open Fable.Core +open Fable.Python.FastAPI +open Fable.Python.Pydantic + +// Create the app +let app = FastAPI(title = "Todo API", version = "1.0.0") + +// Define the model +[] +type Todo(id: int, title: string, completed: bool) = + inherit BaseModel() + member val id: int = id with get, set + member val title: string = title with get, set + member val completed: bool = completed with get, set + +// In-memory store +let todos = ResizeArray() + +// Define endpoints +[] +type TodoAPI() = + [] + static member root() = + {| message = "Welcome to Todo API" |} + + [] + static member list_todos() = todos + + [] + static member create_todo(title: string) = + let todo = Todo(todos.Count + 1, title, false) + todos.Add(todo) + todo +``` + +## Why F# + FastAPI? + +This combination gives you: + +1. **Compile-time safety** - F# catches errors before they reach Python +2. **Runtime validation** - Pydantic validates incoming requests +3. **Auto documentation** - OpenAPI specs generated from your types +4. **Familiar ecosystem** - Deploy with standard Python tools + +You write type-safe F# code, but deploy and run it like any Python web service. +*) diff --git a/chapters/GettingStarted.fs b/chapters/GettingStarted.fs index 9708f8d..4800abd 100644 --- a/chapters/GettingStarted.fs +++ b/chapters/GettingStarted.fs @@ -10,8 +10,22 @@ Let's set up a Fable.Python project from scratch and get our first F# code runni You'll need: - [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later. We recommend - installing the latest LTS version, currently .NET 10 + installing the latest LTS version, currently .NET 10) - [Python 3.12+](https://www.python.org/downloads/) (Fable targets Python 3.12 or higher) +- [uv](https://docs.astral.sh/uv/) (recommended) - A fast Python package manager written in Rust + +If you don't have `uv` installed: + +```bash +# macOS/Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +You can also use `pip` if you prefer, but `uv` is significantly faster and handles +virtual environments automatically. ## Project Setup @@ -37,6 +51,10 @@ dotnet add package Fable.Core --version 5.0.0-beta.4 Fable-generated Python code requires the `fable-library` runtime: ```bash +# Using uv (recommended) +uv add "fable-library==5.0.0a21" + +# Or with pip pip install "fable-library==5.0.0a21" ``` @@ -78,6 +96,10 @@ dotnet fable --lang python This creates `program.py` in your project directory. Run it: ```bash +# Using uv +uv run python program.py + +# Or directly with python python3 program.py ``` diff --git a/chapters/Introduction.fs b/chapters/Introduction.fs index 3178619..f35c169 100644 --- a/chapters/Introduction.fs +++ b/chapters/Introduction.fs @@ -7,7 +7,7 @@ module Introduction 2025](https://sergeytihon.com/2025/11/03/f-advent-calendar-in-english-2025/). Thank you, Sergey Tihon, for organizing this wonderful tradition that brings the F# community together every year! -Welcome to this guide on [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - +Welcome to this guide on [Fable](https://fable.io/) and [Fable.Python](https://github.com/fable-compiler/Fable.Python/) - a compiler that transforms F# code into Python. ## Table of Contents @@ -21,8 +21,9 @@ a compiler that transforms F# code into Python. 7. [Testing](#heading-testing-fablepython-projects) - Using pytest with F# code 8. [Fable v5](#heading-fable-v5-whats-new) - New features and the Rust core 9. [Pydantic Integration](#heading-pydantic-interop) - Type-safe data validation -10. [Units of Measure](#heading-units-of-measure) - Compile-time dimensional analysis -11. [Fable.Literate](#heading-fableliterate-the-strange-loop) - The tool that wrote this post +10. [FastAPI](#heading-fastapi) - Building type-safe web APIs +11. [Units of Measure](#heading-units-of-measure) - Compile-time dimensional analysis +12. [Fable.Literate](#heading-fableliterate-the-strange-loop) - The tool that wrote this post **A teaser:** the final chapter reveals how this entire blog post was generated. The converter that transforms F# literate files into Markdown is itself written in F#, compiled to Python with Fable, and documented using its own @@ -63,7 +64,7 @@ Fable.Python is a great choice when: - **Units of measure** - F#'s compile-time dimensional analysis prevents unit errors that Python can't catch -## When NOT to Use Fable.Python +## When Not to Use Fable.Python - When your F# code depends on .NET libraries without Fable support - Performance-critical code (Python has runtime overhead) diff --git a/fable-python.fsproj b/fable-python.fsproj index eeab6c0..74fab11 100644 --- a/fable-python.fsproj +++ b/fable-python.fsproj @@ -21,6 +21,7 @@ + diff --git a/justfile b/justfile index 98720d2..ffc82af 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # Chapter order for documentation generation # Edit this list to reorder or add chapters -chapters := "Introduction Python GettingStarted Interop Bindings Compatibility AsyncProgramming Testing FableV5 Pydantic UnitsOfMeasure FableLiterate Summary" +chapters := "Introduction Python GettingStarted Interop Bindings Compatibility AsyncProgramming Testing FableV5 Pydantic FastAPI UnitsOfMeasure FableLiterate Summary" # Default: show help default: @@ -38,7 +38,9 @@ generate: build format-python mkdir -p docs for name in {{chapters}}; do # Convert PascalCase to snake_case for Python file naming - pyname=$(echo "$name" | sed 's/\([A-Z]\)/_\1/g' | sed 's/^_//' | tr '[:upper:]' '[:lower:]') + # First, handle common acronyms (API, HTTP, etc.) by treating them as single units + # Then convert remaining PascalCase to snake_case + pyname=$(echo "$name" | sed 's/API/Api/g; s/HTTP/Http/g' | sed 's/\([A-Z]\)/_\1/g' | sed 's/^_//' | tr '[:upper:]' '[:lower:]') # FableLiterate uses different python output path (symlink to Fable.Literate/App.fs) if [ "$name" = "FableLiterate" ]; then pyfile="output/Fable.Literate/python.py" @@ -60,7 +62,8 @@ blogpost: build format-python first=true for name in {{chapters}}; do # Convert PascalCase to snake_case for Python file naming - pyname=$(echo "$name" | sed 's/\([A-Z]\)/_\1/g' | sed 's/^_//' | tr '[:upper:]' '[:lower:]') + # First, handle common acronyms (API, HTTP, etc.) by treating them as single units + pyname=$(echo "$name" | sed 's/API/Api/g; s/HTTP/Http/g' | sed 's/\([A-Z]\)/_\1/g' | sed 's/^_//' | tr '[:upper:]' '[:lower:]') # FableLiterate uses different python output path (symlink to Fable.Literate/App.fs) if [ "$name" = "FableLiterate" ]; then pyfile="output/Fable.Literate/python.py"