diff --git a/README.md b/README.md index 20dcac2..3d0c342 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Key features: - **Simple** — define HTTP requests as decorated async functions, no boilerplate. - **Typed** — full type annotations throughout; validate responses with Pydantic models. - **Logged** — colorful, structured request/response logs with timing, built-in. -- **Complete** — GET, POST, PUT, PATCH, DELETE, and GraphQL out of the box. +- **Complete** — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and GraphQL out of the box. - **Extensible** — middleware, dependency injection, routers, lifespan hooks. - **Interactive** — built-in Swagger UI via `app.web_run()` to browse and execute requests in the browser. - **HTTP/2** — optional HTTP/2 support, with automatic fallback to HTTP/1.1. @@ -220,11 +220,26 @@ async def put_data(resp: Response) -> dict: return resp.json() +@app.patch(url="https://httpbin.org/patch") +async def patch_data(resp: Response) -> dict: + return resp.json() + + @app.delete(url="https://httpbin.org/delete") async def delete_data(resp: Response) -> int: return resp.status_code +@app.head(url="https://httpbin.org/get") +async def head_data(resp: Response) -> int: + return resp.status + + +@app.options(url="https://httpbin.org/get") +async def options_data(resp: Response) -> dict: + return {"allow": resp.headers.get("allow", "")} + + if __name__ == "__main__": app.run() ``` diff --git a/docs/en/api-reference.md b/docs/en/api-reference.md index 52539cb..65701fa 100644 --- a/docs/en/api-reference.md +++ b/docs/en/api-reference.md @@ -38,6 +38,8 @@ app = FastHTTP( | `put_request` | `dict` | `{}` | PUT settings | | `patch_request` | `dict` | `{}` | PATCH settings | | `delete_request` | `dict` | `{}` | DELETE settings | +| `head_request` | `dict` | `{}` | HEAD settings | +| `options_request` | `dict` | `{}` | OPTIONS settings | | `middleware` | `list` | `[]` | Middleware list | | `security` | `bool` | `True` | Enable built-in security | | `lifespan` | `Callable` | `None` | Context manager for startup/shutdown | @@ -189,7 +191,34 @@ async def lifespan(app): response_model: type = None, request_model: type = None, responses: dict = None, - delete_request: dict = None, +) +``` + +### @app.head() + +```python +@app.head( + url: str, + params: dict = None, + tags: list = [], + dependencies: list = [], + response_model: type = None, + request_model: type = None, + responses: dict = None, +) +``` + +### @app.options() + +```python +@app.options( + url: str, + params: dict = None, + tags: list = [], + dependencies: list = [], + response_model: type = None, + request_model: type = None, + responses: dict = None, ) ``` @@ -481,7 +510,7 @@ The Route object represents a registered HTTP request. It contains all informati | Attribute | Type | Description | |-----------|------|-------------| -| `method` | `str` | HTTP method (GET, POST, PUT, PATCH, DELETE) | +| `method` | `str` | HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) | | `url` | `str` | Full URL of the request | | `params` | `dict` | Query parameters | | `json` | `dict` | JSON body sent with request | diff --git a/docs/en/dependencies.md b/docs/en/dependencies.md index 985cd17..a788a95 100644 --- a/docs/en/dependencies.md +++ b/docs/en/dependencies.md @@ -88,7 +88,7 @@ Both functions work the same way — the system automatically detects the functi The `route` object contains all information about the request: ```python -route.method # HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE" +route.method # HTTP method: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" route.url # Full request URL route.params # Query parameters (dictionary) route.json # JSON request body (dictionary) diff --git a/docs/en/index.md b/docs/en/index.md index 7dcb6ad..ae7e696 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,32 +1,70 @@ -# FastHTTP - -Asynchronous HTTP client for Python with a declarative approach, similar to FastAPI. - -## Features - -- **Declarative style** — define requests as functions with decorators -- **Asynchronous** — parallel request execution with asyncio -- **Dependencies** — modify requests before sending -- **Tags** — grouping and filtering requests -- **Middleware** — global logic for all requests -- **Pydantic** — response validation -- **HTTP/2** — support for modern protocol -- **CLI** — convenient command line +

+ +

+

+ Fast, simple HTTP client with decorator-based routing, async support, and beautiful logging. +

+

+ + Tests + + + Package version + + + Supported Python versions + + + CodSpeed + +

+ +--- + +**Documentation**: https://fasthttp.ndugram.dev/en/latest/ + +**Source Code**: https://github.com/ndugram/fasthttp + +--- + +FastHTTP is a modern **async HTTP client library** for Python, built on top of **httpx**. It brings a decorator-based API — similar to FastAPI, but for outgoing requests — with structured logging, middleware, Pydantic validation, and a built-in Swagger UI. + +The key features are: + +* **Fast**: built on httpx with full async support and parallel request execution. +* **Simple**: define HTTP requests as decorated async functions, no boilerplate. +* **Typed**: full type annotations throughout; validate responses with Pydantic models. +* **Logged**: colorful, structured request/response logs with timing, built-in. +* **Complete**: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and GraphQL out of the box. +* **Extensible**: middleware, dependency injection, routers, lifespan hooks. +* **Interactive**: built-in Swagger UI via `app.web_run()` to browse and execute requests in the browser. +* **HTTP/2**: optional HTTP/2 support, with automatic fallback to HTTP/1.1. + +## Requirements + +Python 3.10+ + +FastHTTP stands on the shoulders of giants: + +* httpx — async HTTP transport. +* pydantic — response model validation and serialization. +* orjson — fast JSON parsing. +* typer — CLI interface. +* uvicorn — ASGI server for `web_run()`. ## Installation -```bash -pip install fasthttp-client -``` - -For HTTP/2: +```console +$ pip install fasthttp-client -```bash -pip install fasthttp-client[http2] +---> 100% ``` +## Example -## Quick Example +### Create it + +Create a file `main.py`: ```python from fasthttp import FastHTTP @@ -35,8 +73,8 @@ from fasthttp.response import Response app = FastHTTP() -@app.get(url="https://jsonplaceholder.typicode.com/posts/1") -async def main(resp: Response) -> dict: +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() @@ -44,154 +82,358 @@ if __name__ == "__main__": app.run() ``` -:::tip Important -Handler functions must have a return type annotation (`-> dict`, `-> str`, `-> int`, etc.). -::: +### Run it -## Why FastHTTP? +```console +$ python main.py +``` -### The Problem +### Check it -Usually when working with HTTP requests in Python: +You will see output like: -```python -# Lots of boilerplate code -import aiohttp +``` +16:09:18.955 │ INFO │ fasthttp │ ✔ FastHTTP started +16:09:19.519 │ INFO │ fasthttp │ ✔ GET https://httpbin.org/get [200] 458.26ms +16:09:20.037 │ INFO │ fasthttp │ ✔ Done in 1.08s +``` -async def main(): - async with aiohttp.ClientSession() as session: - async with session.get("https://api.example.com/data") as resp: - data = await resp.json() - # ... handling +The `resp` object gives you access to status, headers, and body. `resp.json()` returns the parsed response: + +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "httpbin.org", + "User-Agent": "python-httpx/0.28.1" + }, + "origin": "...", + "url": "https://httpbin.org/get" +} ``` -### Solution with FastHTTP +### Interactive API docs + +Replace `app.run()` with `app.web_run()`: ```python -# Clean and clear code from fasthttp import FastHTTP from fasthttp.response import Response app = FastHTTP() -@app.get(url="https://api.example.com/data") -async def main(resp: Response) -> dict: + +@app.get(url="https://jsonplaceholder.typicode.com/users/1") +async def get_user(resp: Response) -> dict: return resp.json() + + +@app.post(url="https://jsonplaceholder.typicode.com/users") +async def create_user(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.web_run() ``` -## Documentation +Now go to http://127.0.0.1:8000/docs. -### Basics +You will see the automatic interactive API documentation: -- [Quick Start](quick-start.md) — start here -- [Configuration](configuration.md) — application settings -- [CLI](cli.md) — command line +![Swagger UI](../photo/swagger_ui_home.png) -### Advanced Topics +Expand any route to inspect parameters, schemas, and expected responses: -- [Dependencies](dependencies.md) — request modification -- [Middleware](middleware.md) — global logic -- [GraphQL](graphql.md) — GraphQL support -- [Pydantic](pydantic-validation.md) — validation -- [HTTP/2](http2-support.md) — HTTP/2 support -- [Security](security.md) — built-in protection +![Swagger UI expanded](../photo/swagger_ui_check_web.png) -### Additional +Click **Try it out** to execute the request directly from the browser and see the real response: -- [Examples](examples.md) — more code examples -- [API Reference](api-reference.md) — complete reference +![Swagger UI execute](../photo/swagger_ui_check_execute.png) -## Comparison with Other Libraries +### Upgrade the example -| Library | Style | Async | Dependencies | -|---------|-------|-------|--------------| -| **FastHTTP** | Declarative | ✅ Yes | ✅ Yes | -| requests | Imperative | ❌ No | ❌ No | -| aiohttp | Imperative | ✅ Yes | ❌ No | -| httpx | Imperative | ✅ Yes | ❌ No | +Now modify `main.py` to get more out of FastHTTP. Each upgrade below builds on the previous one. -## Usage Examples +
+With Pydantic response models... -### Multiple Parallel Requests +Declare a Pydantic model and pass it as `response_model`. FastHTTP will validate and parse the response automatically: ```python from fasthttp import FastHTTP from fasthttp.response import Response +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name: str + email: str + app = FastHTTP() -@app.get(url="https://jsonplaceholder.typicode.com/posts/1") -async def get_post(resp: Response) -> dict: +@app.get( + url="https://jsonplaceholder.typicode.com/users/1", + response_model=User, +) +async def get_user(resp: Response) -> User: + return User(**resp.json()) + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With multiple HTTP methods... + +Register as many routes as you need across all HTTP methods. FastHTTP runs them concurrently: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() -@app.get(url="https://jsonplaceholder.typicode.com/users/1") +@app.post(url="https://httpbin.org/post") +async def post_data(resp: Response) -> dict: + return resp.json() + + +@app.put(url="https://httpbin.org/put") +async def put_data(resp: Response) -> dict: + return resp.json() + + +@app.patch(url="https://httpbin.org/patch") +async def patch_data(resp: Response) -> dict: + return resp.json() + + +@app.delete(url="https://httpbin.org/delete") +async def delete_data(resp: Response) -> int: + return resp.status_code + + +@app.head(url="https://httpbin.org/get") +async def head_data(resp: Response) -> int: + return resp.status + + +@app.options(url="https://httpbin.org/get") +async def options_data(resp: Response) -> dict: + return {"allow": resp.headers.get("allow", "")} + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With routers... + +Group related routes into a `Router` with a shared prefix or base URL, then include it into the app: + +```python +from fasthttp import FastHTTP, Router +from fasthttp.response import Response + +users_router = Router(prefix="https://jsonplaceholder.typicode.com") + + +@users_router.get(url="/users/1") async def get_user(resp: Response) -> dict: return resp.json() -@app.get(url="https://jsonplaceholder.typicode.com/comments/1") -async def get_comment(resp: Response) -> dict: +@users_router.get(url="/users/2") +async def get_user_two(resp: Response) -> dict: return resp.json() -# All three requests execute in parallel! +@users_router.post(url="/users") +async def create_user(resp: Response) -> dict: + return resp.json() + + +app = FastHTTP() +app.include_router(users_router) + if __name__ == "__main__": app.run() ``` -### With Tags +
+ +
+With middleware... + +Intercept and modify requests before they are sent and responses after they are received: ```python from fasthttp import FastHTTP +from fasthttp.middleware import BaseMiddleware from fasthttp.response import Response -app = FastHTTP() +class LoggingMiddleware(BaseMiddleware): + __priority__ = 0 + __methods__ = None + __enabled__ = True -@app.get(url="https://api.example.com/users", tags=["users"]) -async def get_users(resp: Response) -> dict: - return resp.json() + async def request(self, method: str, url: str, kwargs: dict) -> dict: + print(f"→ {method} {url}") + return kwargs + async def response(self, response: Response) -> Response: + print(f"← {response.status}") + return response -@app.get(url="https://api.example.com/posts", tags=["posts"]) -async def get_posts(resp: Response) -> dict: + +app = FastHTTP(middleware=[LoggingMiddleware()]) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() -# Run only users -app.run(tags=["users"]) +if __name__ == "__main__": + app.run() ``` -### With Dependencies +
+ +
+With dependency injection... + +Use `Depends` to share logic across routes — auth tokens, computed headers, or any reusable setup: ```python from fasthttp import FastHTTP, Depends from fasthttp.response import Response +from fasthttp.types import RequestsOptinal -app = FastHTTP() +def auth_headers() -> RequestsOptinal: + return {"headers": {"Authorization": "Bearer my-token"}} -async def add_auth(route, config): - config.setdefault("headers", {})["Authorization"] = "Bearer token" - return config + +app = FastHTTP() @app.get( - url="https://api.example.com/data", - dependencies=[Depends(add_auth)] + url="https://httpbin.org/get", + dependencies=[Depends(auth_headers)], ) -async def protected(resp: Response) -> dict: +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With lifespan... + +Run setup and teardown logic around your requests using an async context manager: + +```python +from contextlib import asynccontextmanager + +from fasthttp import FastHTTP +from fasthttp.response import Response + + +@asynccontextmanager +async def lifespan(app: FastHTTP): + print("Startup: loading credentials...") + app.token = "my-secret-token" # type: ignore[attr-defined] + yield + print("Shutdown: cleanup done.") + + +app = FastHTTP(lifespan=lifespan) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With GraphQL... + +Use `@app.graphql` to send queries and mutations. The handler returns the query body; FastHTTP sends it and gives you the parsed response: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + + +app = FastHTTP() + + +@app.graphql(url="https://countries.trevorblades.com/graphql") +async def get_countries(resp: Response) -> dict: + return { + "query": """ + { + countries { + name + code + capital + } + } + """ + } + + +if __name__ == "__main__": + app.run() ``` -## Community +
+ +## Optional dependencies + +* httpx[http2] — HTTP/2 protocol support. + +```console +$ pip install fasthttp-client[http2] +``` + +Enable HTTP/2 per app instance: + +```python +app = FastHTTP(http2=True) +``` -- GitHub: https://github.com/ndugram/fasthttp -- PyPI: https://pypi.org/project/fasthttp-client/ -- Documentation: https://fasthttp.ndugram.dev +Servers that don't support HTTP/2 fall back to HTTP/1.1 automatically. ## License -MIT License +This project is licensed under the terms of the MIT license. diff --git a/docs/en/openapi.md b/docs/en/openapi.md index 0d8bc66..3a43b87 100644 --- a/docs/en/openapi.md +++ b/docs/en/openapi.md @@ -45,7 +45,7 @@ After running, open in your browser: ### Viewing Documentation All your HTTP requests are automatically displayed in Swagger UI with: -- HTTP method (GET, POST, PUT, DELETE) +- HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) - URL address - Description from docstring - Request parameters diff --git a/docs/en/quick-start.md b/docs/en/quick-start.md index c98f210..98e586a 100644 --- a/docs/en/quick-start.md +++ b/docs/en/quick-start.md @@ -169,6 +169,20 @@ async def patch_user(resp: Response): async def delete_user(resp: Response): """Delete user.""" return resp.status + + +# HEAD — check endpoint headers +@app.head(url="https://api.example.com/users") +async def check_users(resp: Response): + """Check if endpoint exists.""" + return resp.status + + +# OPTIONS — get allowed methods +@app.options(url="https://api.example.com/users") +async def allowed_methods(resp: Response): + """Get allowed HTTP methods.""" + return {"allow": resp.headers.get("allow", "")} ``` ## Request Parameters diff --git a/docs/en/tutorial/http-methods.md b/docs/en/tutorial/http-methods.md index 2c75b29..60b2192 100644 --- a/docs/en/tutorial/http-methods.md +++ b/docs/en/tutorial/http-methods.md @@ -57,6 +57,26 @@ async def delete_user(resp: Response) -> dict: return resp.status ``` +## HEAD - Check Endpoint + +Returns only headers, no body. Useful for checking if a resource exists or inspecting metadata. + +```python +@app.head(url="https://api.example.com/users") +async def check_users(resp: Response) -> int: + return resp.status +``` + +## OPTIONS - Allowed Methods + +Returns the HTTP methods supported by the endpoint. + +```python +@app.options(url="https://api.example.com/users") +async def allowed_methods(resp: Response) -> dict: + return {"allow": resp.headers.get("allow", "")} +``` + ## Decorator Parameters | Parameter | Description | diff --git a/docs/en/tutorial/index.md b/docs/en/tutorial/index.md index b4fd3b2..48f5fb0 100644 --- a/docs/en/tutorial/index.md +++ b/docs/en/tutorial/index.md @@ -8,7 +8,7 @@ The tutorial is divided into several sections: ### Getting Started - [First Steps](first-steps.md) - Installation and basic concepts -- [HTTP Methods](http-methods.md) - GET, POST, PUT, PATCH, DELETE +- [HTTP Methods](http-methods.md) - GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS - [Request Parameters](request-parameters.md) - Query, JSON, headers - [Response Handling](response-handling.md) - Working with responses diff --git a/docs/index.md b/docs/index.md index b6edc23..8b4cd41 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,32 +1,73 @@ -# FastHTTP +

+ +

+

+ Fast, simple HTTP client with decorator-based routing, async support, and beautiful logging. +

+

+ + Tests + + + Package version + + + Supported Python versions + + + CodSpeed + + + GitHub Stars + +

-FastHTTP is a lightweight asynchronous HTTP client built on top of httpx. It provides a clean declarative API for defining HTTP requests and handling responses. +--- -## Features +**Documentation**: https://fasthttp.ndugram.dev/en/latest/ -- **Declarative Style** - Define requests as functions with decorators -- **Async Support** - Parallel request execution with asyncio -- **Dependencies** - Modify requests before sending -- **Tags** - Group and filter requests -- **Middleware** - Global logic for all requests -- **Pydantic** - Response validation -- **HTTP/2** - Support for modern protocol -- **CLI** - Convenient command line interface -- **Built-in Security** - SSRF protection, circuit breaker +**Source Code**: https://github.com/ndugram/fasthttp + +--- + +FastHTTP is a modern **async HTTP client library** for Python, built on top of **httpx**. It brings a decorator-based API — similar to FastAPI, but for outgoing requests — with structured logging, middleware, Pydantic validation, and a built-in Swagger UI. + +The key features are: + +* **Fast**: built on httpx with full async support and parallel request execution. +* **Simple**: define HTTP requests as decorated async functions, no boilerplate. +* **Typed**: full type annotations throughout; validate responses with Pydantic models. +* **Logged**: colorful, structured request/response logs with timing, built-in. +* **Complete**: GET, POST, PUT, PATCH, DELETE, and GraphQL out of the box. +* **Extensible**: middleware, dependency injection, routers, lifespan hooks. +* **Interactive**: built-in Swagger UI via `app.web_run()` to browse and execute requests in the browser. +* **HTTP/2**: optional HTTP/2 support, with automatic fallback to HTTP/1.1. + +## Requirements + +Python 3.10+ + +FastHTTP stands on the shoulders of giants: + +* httpx — async HTTP transport. +* pydantic — response model validation and serialization. +* orjson — fast JSON parsing. +* typer — CLI interface. +* uvicorn — ASGI server for `web_run()`. ## Installation -```bash -pip install fasthttp-client +```console +$ pip install fasthttp-client + +---> 100% ``` -For HTTP/2 support: +## Example -```bash -pip install fasthttp-client[http2] -``` +### Create it -## Quick Example +Create a file `main.py`: ```python from fasthttp import FastHTTP @@ -35,8 +76,8 @@ from fasthttp.response import Response app = FastHTTP() -@app.get(url="https://jsonplaceholder.typicode.com/posts/1") -async def main(resp: Response) -> dict: +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() @@ -44,54 +85,343 @@ if __name__ == "__main__": app.run() ``` -Output: +### Run it +```console +$ python main.py ``` -INFO | fasthttp | FastHTTP started -INFO | fasthttp | Sending 1 requests -INFO | fasthttp | GET https://jsonplaceholder.typicode.com/posts/1 200 234.56ms -INFO | fasthttp | Done in 0.24s + +### Check it + +You will see output like: + +``` +16:09:18.955 │ INFO │ fasthttp │ ✔ FastHTTP started +16:09:19.519 │ INFO │ fasthttp │ ✔ GET https://httpbin.org/get [200] 458.26ms +16:09:20.037 │ INFO │ fasthttp │ ✔ Done in 1.08s +``` + +The `resp` object gives you access to status, headers, and body. `resp.json()` returns the parsed response: + +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "httpbin.org", + "User-Agent": "python-httpx/0.28.1" + }, + "origin": "...", + "url": "https://httpbin.org/get" +} ``` -## Why FastHTTP? +### Interactive API docs -Traditional HTTP clients require verbose code: +Replace `app.run()` with `app.web_run()`: ```python -# Lots of boilerplate code -import aiohttp +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.get(url="https://jsonplaceholder.typicode.com/users/1") +async def get_user(resp: Response) -> dict: + return resp.json() + + +@app.post(url="https://jsonplaceholder.typicode.com/users") +async def create_user(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.web_run() +``` + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation: + +![Swagger UI](photo/swagger_ui_home.png) + +Expand any route to inspect parameters, schemas, and expected responses: + +![Swagger UI expanded](photo/swagger_ui_check_web.png) + +Click **Try it out** to execute the request directly from the browser and see the real response: + +![Swagger UI execute](photo/swagger_ui_check_execute.png) + +### Upgrade the example + +Now modify `main.py` to get more out of FastHTTP. Each upgrade below builds on the previous one. + +
+With Pydantic response models... + +Declare a Pydantic model and pass it as `response_model`. FastHTTP will validate and parse the response automatically: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name: str + email: str + + +app = FastHTTP() + + +@app.get( + url="https://jsonplaceholder.typicode.com/users/1", + response_model=User, +) +async def get_user(resp: Response) -> User: + return User(**resp.json()) + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With multiple HTTP methods... + +Register as many routes as you need across all HTTP methods. FastHTTP runs them concurrently: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() + + +@app.post(url="https://httpbin.org/post") +async def post_data(resp: Response) -> dict: + return resp.json() + + +@app.put(url="https://httpbin.org/put") +async def put_data(resp: Response) -> dict: + return resp.json() + + +@app.delete(url="https://httpbin.org/delete") +async def delete_data(resp: Response) -> int: + return resp.status_code + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With routers... + +Group related routes into a `Router` with a shared prefix or base URL, then include it into the app: + +```python +from fasthttp import FastHTTP, Router +from fasthttp.response import Response -async def main(): - async with aiohttp.ClientSession() as session: - async with session.get("https://api.example.com/data") as resp: - data = await resp.json() +users_router = Router(prefix="https://jsonplaceholder.typicode.com") + + +@users_router.get(url="/users/1") +async def get_user(resp: Response) -> dict: + return resp.json() + + +@users_router.get(url="/users/2") +async def get_user_two(resp: Response) -> dict: + return resp.json() + + +@users_router.post(url="/users") +async def create_user(resp: Response) -> dict: + return resp.json() + + +app = FastHTTP() +app.include_router(users_router) + +if __name__ == "__main__": + app.run() ``` -FastHTTP simplifies this: +
+ +
+With middleware... + +Intercept and modify requests before they are sent and responses after they are received: ```python -# Clean and simple from fasthttp import FastHTTP +from fasthttp.middleware import BaseMiddleware +from fasthttp.response import Response + + +class LoggingMiddleware(BaseMiddleware): + __priority__ = 0 + __methods__ = None + __enabled__ = True + + async def request(self, method: str, url: str, kwargs: dict) -> dict: + print(f"→ {method} {url}") + return kwargs + + async def response(self, response: Response) -> Response: + print(f"← {response.status}") + return response + + +app = FastHTTP(middleware=[LoggingMiddleware()]) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With dependency injection... + +Use `Depends` to share logic across routes — auth tokens, computed headers, or any reusable setup: + +```python +from fasthttp import FastHTTP, Depends +from fasthttp.response import Response +from fasthttp.types import RequestsOptinal + + +def auth_headers() -> RequestsOptinal: + return {"headers": {"Authorization": "Bearer my-token"}} + app = FastHTTP() -@app.get(url="https://api.example.com/data") -async def main(resp): + +@app.get( + url="https://httpbin.org/get", + dependencies=[Depends(auth_headers)], +) +async def get_data(resp: Response) -> dict: return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With lifespan... + +Run setup and teardown logic around your requests using an async context manager: + +```python +from contextlib import asynccontextmanager + +from fasthttp import FastHTTP +from fasthttp.response import Response + + +@asynccontextmanager +async def lifespan(app: FastHTTP): + print("Startup: loading credentials...") + app.token = "my-secret-token" # type: ignore[attr-defined] + yield + print("Shutdown: cleanup done.") + + +app = FastHTTP(lifespan=lifespan) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+With GraphQL... + +Use `@app.graphql` to send queries and mutations. The handler returns the query body; FastHTTP sends it and gives you the parsed response: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + + +app = FastHTTP() + + +@app.graphql(url="https://countries.trevorblades.com/graphql") +async def get_countries(resp: Response) -> dict: + return { + "query": """ + { + countries { + name + code + capital + } + } + """ + } + + +if __name__ == "__main__": + app.run() ``` -## Documentation +
+ +## Optional dependencies -- [Learn](en/learn/index.md) - Fundamental concepts -- [Tutorial](en/tutorial/index.md) - User guide -- [CLI](en/cli/index.md) - Command line interface -- [Reference](en/reference/index.md) - API reference -- [About](en/about/index.md) - About the project +* httpx[http2] — HTTP/2 protocol support. -## GitHub +```console +$ pip install fasthttp-client[http2] +``` + +Enable HTTP/2 per app instance: + +```python +app = FastHTTP(http2=True) +``` -https://github.com/ndugram/fasthttp +Servers that don't support HTTP/2 fall back to HTTP/1.1 automatically. -## Documentation Site +## License -https://fasthttp.ndugram.dev +This project is licensed under the terms of the MIT license. diff --git a/docs/ru/api-reference.md b/docs/ru/api-reference.md index 313acb4..ae5fd1a 100644 --- a/docs/ru/api-reference.md +++ b/docs/ru/api-reference.md @@ -21,6 +21,8 @@ app = FastHTTP( put_request: dict = {}, patch_request: dict = {}, delete_request: dict = {}, + head_request: dict = {}, + options_request: dict = {}, middleware: list = [], security: bool = True, lifespan: Callable = None, @@ -38,6 +40,8 @@ app = FastHTTP( | `put_request` | `dict` | `{}` | Настройки для PUT | | `patch_request` | `dict` | `{}` | Настройки для PATCH | | `delete_request` | `dict` | `{}` | Настройки для DELETE | +| `head_request` | `dict` | `{}` | Настройки для HEAD | +| `options_request` | `dict` | `{}` | Настройки для OPTIONS | | `middleware` | `list` | `[]` | Список middleware | | `security` | `bool` | `True` | Включить встроенную защиту | | `lifespan` | `Callable` | `None` | Контекстный менеджер для startup/shutdown | @@ -189,7 +193,34 @@ async def lifespan(app): response_model: type = None, request_model: type = None, responses: dict = None, - delete_request: dict = None, +) +``` + +### @app.head() + +```python +@app.head( + url: str, + params: dict = None, + tags: list = [], + dependencies: list = [], + response_model: type = None, + request_model: type = None, + responses: dict = None, +) +``` + +### @app.options() + +```python +@app.options( + url: str, + params: dict = None, + tags: list = [], + dependencies: list = [], + response_model: type = None, + request_model: type = None, + responses: dict = None, ) ``` diff --git a/docs/ru/dependencies.md b/docs/ru/dependencies.md index 3f9cb11..2df4351 100644 --- a/docs/ru/dependencies.md +++ b/docs/ru/dependencies.md @@ -88,7 +88,7 @@ def my_dependency_sync(route, config): Объект `route` содержит всю информацию о запросе: ```python -route.method # HTTP метод: "GET", "POST", "PUT", "PATCH", "DELETE" +route.method # HTTP метод: "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" route.url # Полный URL запроса route.params # Query параметры (словарь) route.json # JSON тело запроса (словарь) diff --git a/docs/ru/index.md b/docs/ru/index.md index 07fdfdb..80a14e2 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -1,44 +1,121 @@ -# FastHTTP Client +

+ +

+

+ Быстрый, простой HTTP-клиент с декораторным роутингом, поддержкой async и красивым логированием. +

+

+ + Tests + + + Package version + + + Supported Python versions + + + CodSpeed + +

-Быстрый и простой HTTP-клиент с поддержкой async и красивым логированием. +--- + +**Документация**: https://fasthttp.ndugram.dev/ru/latest/ -[![PyPI Version](https://img.shields.io/pypi/v/fasthttp-client?style=flat&label=PyPI)](https://pypi.org/project/fasthttp-client/) -[![Python Versions](https://img.shields.io/pypi/pyversions/fasthttp-client?style=flat)](https://pypi.org/project/fasthttp-client/) -[![License](https://img.shields.io/pypi/l/fasthttp-client?style=flat)](https://github.com/ndugram/fasthttp) -[![Downloads](https://img.shields.io/pypi/dm/fasthttp-client?style=flat)](https://pypi.org/project/fasthttp-client/) +**Исходный код**: https://github.com/ndugram/fasthttp --- -## Особенности +FastHTTP — это современная **асинхронная HTTP-клиентская библиотека** для Python, построенная поверх **httpx**. Она предоставляет API на основе декораторов — похожий на FastAPI, но для исходящих запросов — со структурированным логированием, middleware, валидацией через Pydantic и встроенным Swagger UI. -| | | -|:---|:---| -| **Декларативный стиль** | Определяйте запросы как функции с декораторами | -| **Асинхронность** | Параллельное выполнение запросов с asyncio | -| **Зависимости** | Модификация запросов перед отправкой | -| **Теги** | Группировка и фильтрация запросов | -| **Middleware** | Глобальная логика для всех запросов | -| **Pydantic** | Валидация ответов | -| **HTTP/2** | Поддержка современного протокола | -| **CLI** | Удобная командная строка | +Ключевые возможности: ---- +* **Быстрый**: построен на httpx с полной поддержкой async и параллельным выполнением запросов. +* **Простой**: определяйте HTTP-запросы как декорированные async-функции, без лишнего кода. +* **Типизированный**: полные аннотации типов; валидация ответов через Pydantic. +* **Логирует**: красочные структурированные логи запросов/ответов со временем выполнения, встроено. +* **Полный**: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS и GraphQL из коробки. +* **Расширяемый**: middleware, dependency injection, роутеры, lifespan-хуки. +* **Интерактивный**: встроенный Swagger UI через `app.web_run()` для просмотра и выполнения запросов в браузере. +* **HTTP/2**: опциональная поддержка HTTP/2 с автоматическим fallback на HTTP/1.1. + +## Требования + +Python 3.10+ + +FastHTTP стоит на плечах гигантов: + +* httpx — асинхронный HTTP-транспорт. +* pydantic — валидация и сериализация моделей ответов. +* orjson — быстрый парсинг JSON. +* typer — CLI-интерфейс. +* uvicorn — ASGI-сервер для `web_run()`. ## Установка -```bash -pip install fasthttp-client +```console +$ pip install fasthttp-client + +---> 100% +``` + +## Пример + +### Создайте файл + +Создайте файл `main.py`: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() ``` -Для HTTP/2: +### Запустите -```bash -pip install fasthttp-client[http2] +```console +$ python main.py ``` ---- +### Проверьте результат + +Вы увидите вывод такого вида: + +``` +16:09:18.955 │ INFO │ fasthttp │ ✔ FastHTTP started +16:09:19.519 │ INFO │ fasthttp │ ✔ GET https://httpbin.org/get [200] 458.26ms +16:09:20.037 │ INFO │ fasthttp │ ✔ Done in 1.08s +``` + +Объект `resp` даёт доступ к статусу, заголовкам и телу ответа. `resp.json()` возвращает распарсенный ответ: + +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "httpbin.org", + "User-Agent": "python-httpx/0.28.1" + }, + "origin": "...", + "url": "https://httpbin.org/get" +} +``` + +### Интерактивная документация API -## Быстрый пример +Замените `app.run()` на `app.web_run()`: ```python from fasthttp import FastHTTP @@ -47,106 +124,316 @@ from fasthttp.response import Response app = FastHTTP() -@app.get(url="https://jsonplaceholder.typicode.com/posts/1") -async def main(resp: Response) -> dict: +@app.get(url="https://jsonplaceholder.typicode.com/users/1") +async def get_user(resp: Response) -> dict: + return resp.json() + + +@app.post(url="https://jsonplaceholder.typicode.com/users") +async def create_user(resp: Response) -> dict: return resp.json() +if __name__ == "__main__": + app.web_run() +``` + +Перейдите на http://127.0.0.1:8000/docs. + +Вы увидите автоматическую интерактивную документацию API: + +![Swagger UI](../photo/swagger_ui_home.png) + +Раскройте любой маршрут для просмотра параметров, схем и ожидаемых ответов: + +![Swagger UI expanded](../photo/swagger_ui_check_web.png) + +Нажмите **Try it out**, чтобы выполнить запрос прямо из браузера и увидеть реальный ответ: + +![Swagger UI execute](../photo/swagger_ui_check_execute.png) + +### Расширение примера + +Измените `main.py`, чтобы использовать больше возможностей FastHTTP. Каждое расширение опирается на предыдущее. + +
+С Pydantic-моделями ответа... + +Объявите Pydantic-модель и передайте её как `response_model`. FastHTTP автоматически провалидирует и распарсит ответ: + +```python +from fasthttp import FastHTTP +from fasthttp.response import Response +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name: str + email: str + + +app = FastHTTP() + + +@app.get( + url="https://jsonplaceholder.typicode.com/users/1", + response_model=User, +) +async def get_user(resp: Response) -> User: + return User(**resp.json()) + + if __name__ == "__main__": app.run() ``` -!!! tip "Важно" - Функции-обработчики должны иметь аннотацию возвращаемого типа (`-> dict`, `-> str`, `-> int` и т.д.) +
---- +
+С несколькими HTTP-методами... -## Документация +Регистрируйте любое количество маршрутов для всех HTTP-методов. FastHTTP выполняет их конкурентно: -### Основы +```python +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() -- [Быстрый старт](quick-start.md) - Начните здесь -- [Конфигурация](configuration.md) - Настройки приложения -- [CLI](cli.md) - Командная строка -### Продвинутые темы +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() -- [Зависимости](dependencies.md) - Модификация запросов -- [Middleware](middleware.md) - Глобальная логика -- [GraphQL](graphql.md) - Поддержка GraphQL -- [Pydantic](pydantic-validation.md) - Валидация -- [HTTP/2](http2-support.md) - Поддержка HTTP/2 -- [Безопасность](security.md) - Встроенная защита -### Дополнительно +@app.post(url="https://httpbin.org/post") +async def post_data(resp: Response) -> dict: + return resp.json() -- [Примеры](examples.md) - Больше примеров кода -- [API Reference](api-reference.md) - Полная документация ---- +@app.put(url="https://httpbin.org/put") +async def put_data(resp: Response) -> dict: + return resp.json() + + +@app.patch(url="https://httpbin.org/patch") +async def patch_data(resp: Response) -> dict: + return resp.json() + + +@app.delete(url="https://httpbin.org/delete") +async def delete_data(resp: Response) -> int: + return resp.status_code + + +@app.head(url="https://httpbin.org/get") +async def head_data(resp: Response) -> int: + return resp.status + + +@app.options(url="https://httpbin.org/get") +async def options_data(resp: Response) -> dict: + return {"allow": resp.headers.get("allow", "")} + + +if __name__ == "__main__": + app.run() +``` + +
-## Зачем нужен FastHTTP? +
+С роутерами... -### Проблема +Группируйте связанные маршруты в `Router` с общим префиксом или базовым URL, затем подключайте к приложению: ```python -# Много boilerplate кода -import aiohttp +from fasthttp import FastHTTP, Router +from fasthttp.response import Response + +users_router = Router(prefix="https://jsonplaceholder.typicode.com") + + +@users_router.get(url="/users/1") +async def get_user(resp: Response) -> dict: + return resp.json() + + +@users_router.get(url="/users/2") +async def get_user_two(resp: Response) -> dict: + return resp.json() + + +@users_router.post(url="/users") +async def create_user(resp: Response) -> dict: + return resp.json() -async def main(): - async with aiohttp.ClientSession() as session: - async with session.get("https://api.example.com/data") as resp: - data = await resp.json() - # ... обработка + +app = FastHTTP() +app.include_router(users_router) + +if __name__ == "__main__": + app.run() ``` -### Решение с FastHTTP +
+ +
+С middleware... + +Перехватывайте и изменяйте запросы перед отправкой и ответы после получения: ```python -# Чистый и понятный код from fasthttp import FastHTTP +from fasthttp.middleware import BaseMiddleware from fasthttp.response import Response + +class LoggingMiddleware(BaseMiddleware): + __priority__ = 0 + __methods__ = None + __enabled__ = True + + async def request(self, method: str, url: str, kwargs: dict) -> dict: + print(f"→ {method} {url}") + return kwargs + + async def response(self, response: Response) -> Response: + print(f"← {response.status}") + return response + + +app = FastHTTP(middleware=[LoggingMiddleware()]) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+С dependency injection... + +Используйте `Depends` для переиспользования логики между маршрутами — auth-токены, вычисленные заголовки или любая подготовительная работа: + +```python +from fasthttp import FastHTTP, Depends +from fasthttp.response import Response +from fasthttp.types import RequestsOptinal + + +def auth_headers() -> RequestsOptinal: + return {"headers": {"Authorization": "Bearer my-token"}} + + app = FastHTTP() -@app.get(url="https://api.example.com/data") -async def main(resp: Response) -> dict: +@app.get( + url="https://httpbin.org/get", + dependencies=[Depends(auth_headers)], +) +async def get_data(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() +``` + +
+ +
+С lifespan... + +Выполняйте инициализацию и очистку вокруг ваших запросов с помощью async context manager: + +```python +from contextlib import asynccontextmanager + +from fasthttp import FastHTTP +from fasthttp.response import Response + + +@asynccontextmanager +async def lifespan(app: FastHTTP): + print("Startup: загружаем учётные данные...") + app.token = "my-secret-token" # type: ignore[attr-defined] + yield + print("Shutdown: очистка завершена.") + + +app = FastHTTP(lifespan=lifespan) + + +@app.get(url="https://httpbin.org/get") +async def get_data(resp: Response) -> dict: return resp.json() + + +if __name__ == "__main__": + app.run() ``` ---- +
-## Сравнение +
+С GraphQL... -| Функция | FastHTTP | requests | aiohttp | httpx | -|:---|:---:|:---:|:---:|:---:| -| Декларативный | Да | Нет | Нет | Нет | -| Async | Да | Нет | Да | Да | -| Зависимости | Да | Нет | Нет | Нет | -| Теги | Да | Нет | Нет | Нет | -| Middleware | Да | Нет | Нет | Нет | -| Pydantic | Да | Нет | Нет | Нет | -| HTTP/2 | Да | Нет | Нет | Да | -| CLI | Да | Нет | Нет | Нет | +Используйте `@app.graphql` для отправки запросов и мутаций. Обработчик возвращает тело запроса; FastHTTP отправляет его и передаёт распарсенный ответ: ---- +```python +from fasthttp import FastHTTP +from fasthttp.response import Response -## Ссылки -- [Документация](https://fasthttp.ndugram.dev) - Английская версия -- [GitHub](https://github.com/ndugram/fasthttp) - Репозиторий -- [PyPI](https://pypi.org/project/fasthttp-client/) - Скачать библиотеку +app = FastHTTP() ---- -## Язык +@app.graphql(url="https://countries.trevorblades.com/graphql") +async def get_countries(resp: Response) -> dict: + return { + "query": """ + { + countries { + name + code + capital + } + } + """ + } -- [English Documentation](../en/index.md) -- [Русская документация](index.md) ---- +if __name__ == "__main__": + app.run() +``` + +
+ +## Опциональные зависимости + +* httpx[http2] — поддержка протокола HTTP/2. + +```console +$ pip install fasthttp-client[http2] +``` + +Включение HTTP/2 для конкретного приложения: + +```python +app = FastHTTP(http2=True) +``` + +Серверы без поддержки HTTP/2 автоматически переходят на HTTP/1.1. ## Лицензия -MIT License +Этот проект лицензирован на условиях лицензии MIT. diff --git a/docs/ru/openapi.md b/docs/ru/openapi.md index fcac53e..87d0c25 100644 --- a/docs/ru/openapi.md +++ b/docs/ru/openapi.md @@ -45,7 +45,7 @@ app.web_run() ### Просмотр документации Все ваши HTTP-запросы автоматически отображаются в Swagger UI с: -- Методом HTTP (GET, POST, PUT, DELETE) +- Методом HTTP (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) - URL адресом - Описанием из docstring - Параметрами запроса diff --git a/docs/ru/quick-start.md b/docs/ru/quick-start.md index ead7e04..b63cd43 100644 --- a/docs/ru/quick-start.md +++ b/docs/ru/quick-start.md @@ -170,6 +170,20 @@ async def patch_user(resp: Response): async def delete_user(resp: Response): """Удалить пользователя.""" return resp.status + + +# HEAD — проверка заголовков endpoint'а +@app.head(url="https://api.example.com/users") +async def check_users(resp: Response): + """Проверить существование endpoint'а.""" + return resp.status + + +# OPTIONS — получение разрешённых методов +@app.options(url="https://api.example.com/users") +async def allowed_methods(resp: Response): + """Получить разрешённые HTTP-методы.""" + return {"allow": resp.headers.get("allow", "")} ``` ## Параметры запроса diff --git a/docs/ru/tutorial/http-methods.md b/docs/ru/tutorial/http-methods.md index 416fb43..002abbc 100644 --- a/docs/ru/tutorial/http-methods.md +++ b/docs/ru/tutorial/http-methods.md @@ -57,6 +57,26 @@ async def delete_user(resp: Response) -> int: return resp.status ``` +## HEAD - проверка endpoint'а + +Возвращает только заголовки, без тела. Удобно для проверки существования ресурса или получения метаданных. + +```python +@app.head(url="https://api.example.com/users") +async def check_users(resp: Response) -> int: + return resp.status +``` + +## OPTIONS - разрешённые методы + +Возвращает HTTP-методы, поддерживаемые endpoint'ом. + +```python +@app.options(url="https://api.example.com/users") +async def allowed_methods(resp: Response) -> dict: + return {"allow": resp.headers.get("allow", "")} +``` + ## Возвращаемые значения Обработчики могут возвращать разные типы: diff --git a/docs/ru/tutorial/index.md b/docs/ru/tutorial/index.md index 5c8f657..03271d4 100644 --- a/docs/ru/tutorial/index.md +++ b/docs/ru/tutorial/index.md @@ -8,7 +8,7 @@ ### Начало работы - [Первые шаги](first-steps.md) - Установка и основные понятия -- [HTTP методы](http-methods.md) - GET, POST, PUT, PATCH, DELETE +- [HTTP методы](http-methods.md) - GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS - [Параметры запроса](request-parameters.md) - Query, JSON, заголовки - [Обработка ответа](response-handling.md) - Работа с ответами diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 0000000..73f7289 --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,606 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,300;0,14..32,400;0,14..32,500;0,14..32,600;0,14..32,700;0,14..32,800;1,14..32,400&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap'); + +:root, +[data-md-color-scheme="slate"] { + --nf-brand-blue: #2e73ff; + --nf-brand-green: #30fc9d; + --nf-bg: #0f0e14; + --nf-surface: #17161e; + --nf-surface-2: #1e1d27; + --nf-border: rgba(255, 255, 255, 0.07); + --nf-border-blue: rgba(46, 115, 255, 0.2); + --nf-text: #eeeef2; + --nf-text-2: rgba(238, 238, 242, 0.72); + --nf-text-muted: rgba(238, 238, 242, 0.42); + --nf-radius-sm: 8px; + --nf-radius: 14px; + --nf-radius-lg: 20px; + --nf-radius-xl: 28px; + + --md-default-bg-color: #0f0e14; + --md-default-bg-color--light: rgba(23, 22, 30, 0.54); + --md-default-bg-color--lighter: rgba(23, 22, 30, 0.26); + --md-default-bg-color--lightest: rgba(23, 22, 30, 0.07); + + --md-default-fg-color: var(--nf-text); + --md-default-fg-color--light: var(--nf-text-2); + --md-default-fg-color--lighter: var(--nf-text-muted); + --md-default-fg-color--lightest: rgba(238, 238, 242, 0.07); + + --md-primary-fg-color: var(--nf-brand-blue); + --md-primary-fg-color--light: rgba(46, 115, 255, 0.14); + --md-primary-fg-color--dark: #1a56d6; + --md-primary-bg-color: #ffffff; + --md-primary-bg-color--light: rgba(255, 255, 255, 0.8); + + --md-accent-fg-color: var(--nf-brand-green); + --md-accent-fg-color--transparent: rgba(48, 252, 157, 0.1); + --md-accent-bg-color: #0f0e14; + + --md-code-bg-color: #0c0b12; + --md-code-fg-color: #c9d1d9; + + --md-typeset-color: var(--nf-text); + --md-typeset-a-color: var(--nf-brand-blue); + --md-typeset-mark-color: rgba(46, 115, 255, 0.18); + + --md-admonition-fg-color: var(--nf-text); + --md-admonition-bg-color: var(--nf-surface); + + --md-footer-bg-color: var(--nf-surface); + --md-footer-bg-color--dark: var(--nf-bg); + --md-footer-fg-color: var(--nf-text); + --md-footer-fg-color--light: var(--nf-text-2); + --md-footer-fg-color--lighter: var(--nf-text-muted); + + --md-text-font-family: "Inter", system-ui, -apple-system, sans-serif; + --md-code-font-family: "JetBrains Mono", "Fira Code", monospace; + + --md-shadow-z1: 0 2px 12px rgba(0, 0, 0, 0.45); + --md-shadow-z2: 0 4px 28px rgba(0, 0, 0, 0.55); + --md-shadow-z3: 0 8px 48px rgba(0, 0, 0, 0.65); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + font-family: var(--md-text-font-family) !important; + background-color: var(--nf-bg) !important; +} + +::selection { + background: rgba(46, 115, 255, 0.28); + color: var(--nf-text); +} + +::-webkit-scrollbar { width: 5px; height: 5px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(46, 115, 255, 0.3); border-radius: 99px; } +::-webkit-scrollbar-thumb:hover { background: var(--nf-brand-blue); } + +.md-header { + background: var(--nf-surface) !important; + box-shadow: 0 1px 0 var(--nf-border), 0 4px 20px rgba(0, 0, 0, 0.4) !important; + backdrop-filter: blur(12px); +} + +.md-header__title { + font-family: var(--md-text-font-family) !important; + font-weight: 700 !important; + letter-spacing: -0.025em; + font-size: 1rem !important; +} + +.md-header__button { + border-radius: var(--nf-radius-sm) !important; + transition: background 0.18s ease, color 0.18s ease !important; +} + +.md-header__button:hover { + background: rgba(46, 115, 255, 0.12) !important; + color: var(--nf-brand-blue) !important; + opacity: 1 !important; +} + +.md-tabs { + background: var(--nf-surface) !important; + border-bottom: 1px solid var(--nf-border); +} + +.md-tabs__link { + font-family: var(--md-text-font-family) !important; + font-weight: 600 !important; + font-size: 0.8rem !important; + opacity: 0.55 !important; + transition: opacity 0.18s ease, color 0.18s ease !important; + letter-spacing: 0.01em; +} + +.md-tabs__link:hover { opacity: 0.9 !important; } + +.md-tabs__link--active { + opacity: 1 !important; + color: var(--nf-brand-blue) !important; +} + +.md-sidebar { + background: transparent !important; +} + +.md-sidebar__scrollwrap { + scrollbar-width: none; +} +.md-sidebar__scrollwrap::-webkit-scrollbar { display: none; } + +.md-nav__title { + font-family: var(--md-text-font-family) !important; + font-weight: 700 !important; + font-size: 0.68rem !important; + letter-spacing: 0.09em !important; + text-transform: uppercase !important; + color: var(--nf-text-muted) !important; +} + +.md-nav__link { + font-family: var(--md-text-font-family) !important; + color: var(--nf-text-2) !important; + font-weight: 500 !important; + font-size: 0.8rem !important; + border-radius: var(--nf-radius-sm) !important; + margin: 1px 0 !important; + padding: 5px 10px !important; + transition: background 0.15s ease, color 0.15s ease !important; +} + +.md-nav__link:hover { + background: rgba(46, 115, 255, 0.09) !important; + color: var(--nf-brand-blue) !important; +} + +.md-nav__link--active { + background: rgba(46, 115, 255, 0.12) !important; + color: var(--nf-brand-blue) !important; + font-weight: 600 !important; +} + +.md-nav--secondary .md-nav__link { + font-size: 0.75rem !important; + color: var(--nf-text-muted) !important; + padding: 4px 10px !important; +} + +.md-nav--secondary .md-nav__link:hover, +.md-nav--secondary .md-nav__link--active { + color: var(--nf-brand-blue) !important; + background: rgba(46, 115, 255, 0.08) !important; +} + +.md-main { + background: var(--nf-bg) !important; +} + +.md-content__inner { + background: var(--nf-surface) !important; + border-radius: var(--nf-radius-xl) !important; + padding: 44px 52px !important; + margin-top: 24px !important; + margin-bottom: 24px !important; + border: 1px solid var(--nf-border) !important; + box-shadow: 0 4px 48px rgba(0, 0, 0, 0.35) !important; + animation: fade-in-up 0.38s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +@media (max-width: 960px) { + .md-content__inner { + padding: 28px 24px !important; + border-radius: var(--nf-radius-lg) !important; + } +} + +@media (max-width: 600px) { + .md-content__inner { + padding: 20px 16px !important; + border-radius: var(--nf-radius) !important; + } +} + +.md-typeset { + font-family: var(--md-text-font-family) !important; + line-height: 1.72 !important; + color: var(--nf-text-2) !important; +} + +.md-typeset h1 { + font-family: var(--md-text-font-family) !important; + font-weight: 800 !important; + letter-spacing: -0.04em !important; + line-height: 1.15 !important; + color: var(--nf-text) !important; + margin-bottom: 0.4em !important; +} + +.md-typeset h2 { + font-family: var(--md-text-font-family) !important; + font-weight: 700 !important; + letter-spacing: -0.025em !important; + color: var(--nf-text) !important; + border-bottom: 1px solid var(--nf-border) !important; + padding-bottom: 0.35em !important; + margin-top: 2em !important; +} + +.md-typeset h3 { + font-family: var(--md-text-font-family) !important; + font-weight: 600 !important; + letter-spacing: -0.02em !important; + color: var(--nf-text) !important; +} + +.md-typeset h4, +.md-typeset h5, +.md-typeset h6 { + font-family: var(--md-text-font-family) !important; + font-weight: 600 !important; + color: var(--nf-text) !important; + letter-spacing: -0.01em !important; +} + +.md-typeset p { + color: var(--nf-text-2); +} + +.md-typeset a { + color: var(--nf-brand-blue) !important; + text-decoration: none !important; + font-weight: 500 !important; + transition: color 0.15s ease !important; +} + +.md-typeset a:hover { + color: #6ba3ff !important; + text-decoration: underline !important; + text-underline-offset: 3px; +} + +.md-typeset strong { + color: var(--nf-text) !important; + font-weight: 700 !important; +} + +.md-typeset em { + color: var(--nf-text-2); +} + +.md-typeset code { + font-family: var(--md-code-font-family) !important; + background: rgba(46, 115, 255, 0.1) !important; + color: #82b4ff !important; + border-radius: var(--nf-radius-sm) !important; + padding: 1px 7px !important; + font-size: 0.86em !important; + border: 1px solid rgba(46, 115, 255, 0.16); + font-weight: 400 !important; +} + +.md-typeset pre { + border-radius: var(--nf-radius) !important; + border: 1px solid var(--nf-border) !important; + background: var(--md-code-bg-color) !important; + overflow: hidden; +} + +.md-typeset pre > code { + font-family: var(--md-code-font-family) !important; + background: transparent !important; + color: inherit !important; + border: none !important; + padding: 0 !important; + font-size: 0.84em !important; + font-weight: 400 !important; +} + +.highlight { + background: var(--md-code-bg-color) !important; + border-radius: var(--nf-radius) !important; + border: 1px solid var(--nf-border) !important; + overflow: hidden; +} + +.md-clipboard { + color: var(--nf-text-muted) !important; + transition: color 0.15s ease, background 0.15s ease !important; + border-radius: 7px !important; +} + +.md-clipboard:hover { + color: var(--nf-brand-blue) !important; + background: rgba(46, 115, 255, 0.1) !important; +} + +.md-typeset table:not([class]) { + border-radius: var(--nf-radius) !important; + overflow: hidden; + border: 1px solid var(--nf-border) !important; + box-shadow: none !important; + display: table; + width: 100%; +} + +.md-typeset table:not([class]) th { + background: rgba(46, 115, 255, 0.1) !important; + color: var(--nf-brand-blue) !important; + font-weight: 700 !important; + font-size: 0.72rem !important; + text-transform: uppercase !important; + letter-spacing: 0.07em !important; + padding: 11px 14px !important; + border-bottom: 1px solid rgba(46, 115, 255, 0.18) !important; +} + +.md-typeset table:not([class]) td { + padding: 10px 14px !important; + border-bottom: 1px solid var(--nf-border) !important; + color: var(--nf-text-2) !important; + font-size: 0.88rem !important; +} + +.md-typeset table:not([class]) tr:last-child td { + border-bottom: none !important; +} + +.md-typeset table:not([class]) tr:hover td { + background: rgba(46, 115, 255, 0.04) !important; +} + +.md-typeset .admonition, +.md-typeset details { + border-radius: var(--nf-radius) !important; + border: 1px solid var(--nf-border) !important; + background: var(--nf-surface-2) !important; + box-shadow: none !important; +} + +.md-typeset .admonition-title, +.md-typeset summary { + font-weight: 600 !important; + font-family: var(--md-text-font-family) !important; + font-size: 0.85rem !important; + border-radius: calc(var(--nf-radius) - 1px) calc(var(--nf-radius) - 1px) 0 0 !important; +} + +.md-typeset .tip, .md-typeset .success { + border-left-color: var(--nf-brand-green) !important; +} +.md-typeset .tip > .admonition-title, +.md-typeset .success > .admonition-title { + background: rgba(48, 252, 157, 0.07) !important; +} +.md-typeset .tip > .admonition-title::before, +.md-typeset .success > .admonition-title::before { + color: var(--nf-brand-green) !important; +} + +.md-typeset .note, .md-typeset .info, .md-typeset .abstract { + border-left-color: var(--nf-brand-blue) !important; +} +.md-typeset .note > .admonition-title, +.md-typeset .info > .admonition-title, +.md-typeset .abstract > .admonition-title { + background: rgba(46, 115, 255, 0.07) !important; +} + +.md-typeset .warning { + border-left-color: #f5a623 !important; +} +.md-typeset .warning > .admonition-title { + background: rgba(245, 166, 35, 0.07) !important; +} + +.md-typeset .danger, .md-typeset .error { + border-left-color: #ff5a5a !important; +} +.md-typeset .danger > .admonition-title, +.md-typeset .error > .admonition-title { + background: rgba(255, 90, 90, 0.07) !important; +} + +.md-typeset details > summary { + cursor: pointer; + color: var(--nf-brand-blue) !important; + font-weight: 600 !important; + transition: color 0.15s ease; +} + +.md-typeset details > summary:hover { + color: #6ba3ff !important; +} + +.md-search__form { + background: rgba(255, 255, 255, 0.06) !important; + border-radius: var(--nf-radius-sm) !important; + border: 1px solid transparent !important; + transition: border-color 0.18s ease, box-shadow 0.18s ease !important; +} + +.md-search__form:focus-within { + background: rgba(46, 115, 255, 0.07) !important; + border-color: rgba(46, 115, 255, 0.28) !important; + box-shadow: 0 0 0 3px rgba(46, 115, 255, 0.1) !important; +} + +.md-search__input { + font-family: var(--md-text-font-family) !important; + color: var(--nf-text) !important; + font-size: 0.85rem !important; +} + +.md-search__input::placeholder { + color: var(--nf-text-muted) !important; +} + +.md-search-result { + background: var(--nf-surface) !important; + border-radius: var(--nf-radius) !important; + border: 1px solid var(--nf-border) !important; +} + +.md-search-result__link:hover .md-search-result__article { + background: rgba(46, 115, 255, 0.07) !important; +} + +.md-search-result__teaser mark { + color: var(--nf-brand-blue) !important; + background: transparent !important; + font-weight: 700; +} + +.md-search-result__title { + font-family: var(--md-text-font-family) !important; + font-weight: 600 !important; +} + +.md-footer { + background: var(--nf-surface) !important; + border-top: 1px solid var(--nf-border) !important; + border-radius: var(--nf-radius-xl) var(--nf-radius-xl) 0 0; + margin-top: 12px; +} + +.md-footer-meta { + background: transparent !important; +} + +.md-footer-nav__link { + font-family: var(--md-text-font-family) !important; + transition: color 0.15s ease !important; +} + +.md-footer-nav__link:hover .md-footer-nav__title { + color: var(--nf-brand-blue) !important; +} + +.md-footer-nav__title { + font-weight: 600 !important; +} + +.md-typeset .md-button { + font-family: var(--md-text-font-family) !important; + font-weight: 600 !important; + font-size: 0.85rem !important; + border-radius: var(--nf-radius-sm) !important; + padding: 9px 20px !important; + letter-spacing: -0.01em; + transition: all 0.18s ease !important; + background: rgba(46, 115, 255, 0.12) !important; + color: var(--nf-brand-blue) !important; + border: 1px solid rgba(46, 115, 255, 0.25) !important; +} + +.md-typeset .md-button:hover { + background: var(--nf-brand-blue) !important; + color: #ffffff !important; + border-color: var(--nf-brand-blue) !important; + transform: translateY(-1px) !important; + box-shadow: 0 6px 20px rgba(46, 115, 255, 0.3) !important; +} + +.md-typeset .md-button--primary { + background: var(--nf-brand-blue) !important; + color: #ffffff !important; + border-color: var(--nf-brand-blue) !important; +} + +.md-typeset .md-button--primary:hover { + background: #1a56d6 !important; + border-color: #1a56d6 !important; +} + +.md-typeset hr { + border-color: var(--nf-border) !important; + margin: 2.2em 0 !important; +} + +.md-typeset blockquote { + border-left: 3px solid var(--nf-brand-blue) !important; + background: rgba(46, 115, 255, 0.05) !important; + border-radius: 0 var(--nf-radius-sm) var(--nf-radius-sm) 0 !important; + color: var(--nf-text-2) !important; + padding: 12px 18px !important; + margin-left: 0 !important; +} + +.md-typeset ul li::marker { color: var(--nf-brand-blue); } +.md-typeset ol li::marker { color: var(--nf-brand-blue); font-weight: 700; } + +.md-typeset kbd { + font-family: var(--md-code-font-family) !important; + background: var(--nf-surface-2) !important; + border-color: var(--nf-border) !important; + box-shadow: none !important; + color: var(--nf-text) !important; + font-size: 0.8em !important; +} + +.md-tag { + background: rgba(46, 115, 255, 0.1) !important; + color: var(--nf-brand-blue) !important; + border-radius: 6px !important; + font-weight: 600 !important; + font-size: 0.7rem !important; + padding: 2px 9px !important; +} + +[data-md-color-scheme="default"] { + --nf-bg: #f5f5f7; + --nf-surface: #ffffff; + --nf-surface-2: #f0f0f4; + --nf-border: rgba(0, 0, 0, 0.08); + --nf-text: #111117; + --nf-text-2: rgba(17, 17, 23, 0.72); + --nf-text-muted: rgba(17, 17, 23, 0.4); + + --md-default-bg-color: #f5f5f7; + --md-default-bg-color--light: rgba(240, 240, 244, 0.54); + --md-default-fg-color: #111117; + --md-default-fg-color--light: rgba(17, 17, 23, 0.72); + --md-default-fg-color--lighter: rgba(17, 17, 23, 0.38); + --md-default-fg-color--lightest: rgba(17, 17, 23, 0.07); + + --md-code-bg-color: #f0f0f5; + --md-code-fg-color: #1a1a2e; + + --md-footer-bg-color: #e8e8ee; + --md-footer-bg-color--dark: #d8d8e0; + --md-footer-fg-color: #111117; +} + +[data-md-color-scheme="default"] .md-typeset code { + background: rgba(46, 115, 255, 0.08) !important; + color: #1a56d6 !important; + border-color: rgba(46, 115, 255, 0.2) !important; +} + +[data-md-color-scheme="default"] .md-typeset blockquote { + background: rgba(46, 115, 255, 0.04) !important; +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/examples/basic/test_head.py b/examples/basic/test_head.py new file mode 100644 index 0000000..331d256 --- /dev/null +++ b/examples/basic/test_head.py @@ -0,0 +1,16 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.head(url="https://httpbin.org/get") +async def check_endpoint(resp: Response) -> int: + print(f"HEAD: {resp.status}") + print(f"Content-Type: {resp.headers.get('content-type', 'n/a')}") + print(f"Content-Length: {resp.headers.get('content-length', 'n/a')}") + return resp.status + + +if __name__ == "__main__": + app.run() diff --git a/examples/basic/test_options.py b/examples/basic/test_options.py new file mode 100644 index 0000000..070275c --- /dev/null +++ b/examples/basic/test_options.py @@ -0,0 +1,16 @@ +from fasthttp import FastHTTP +from fasthttp.response import Response + +app = FastHTTP() + + +@app.options(url="https://httpbin.org/get") +async def check_allowed_methods(resp: Response) -> dict: + allow = resp.headers.get("allow", "") + print(f"OPTIONS: {resp.status}") + print(f"Allowed methods: {allow}") + return {"status": resp.status, "allow": allow} + + +if __name__ == "__main__": + app.run() diff --git a/fasthttp/app.py b/fasthttp/app.py index 9559096..c06ea0e 100644 --- a/fasthttp/app.py +++ b/fasthttp/app.py @@ -45,7 +45,7 @@ class FastHTTP: web frameworks like FastAPI, but for outgoing requests. The application manages: - - Request routing via decorators (GET, POST, PUT, PATCH, DELETE) + - Request routing via decorators (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) - Per-method default request configuration - Async request execution - Structured and colorized logging @@ -80,7 +80,7 @@ def __init__( """ Enable debug mode. - When enabled, FastHTTP will print datailed + When enabled, FastHTTP will print detailed tracebacks and requests/response logs. """ ), @@ -153,8 +153,6 @@ def __init__( RequestsOptinal | None, Doc( """ - # Create the app - Default configuration for PATCH requests. Used to configure headers, timeout and @@ -170,19 +168,45 @@ def __init__( """ Default configuration for DELETE requests. - Allows defining haders, timeout, + Allows defining headers, timeout, and other options for DELETE requests. """ ), ] ) = None, + head_request: ( + Annotated[ + RequestsOptinal | None, + Doc( + """ + Default configuration for HEAD requests. + + Allows defining headers, timeout, + and other options for HEAD requests. + """ + ), + ] + ) = None, + options_request: ( + Annotated[ + RequestsOptinal | None, + Doc( + """ + Default configuration for OPTIONS requests. + + Allows defining headers, timeout, + and other options for OPTIONS requests. + """ + ), + ] + ) = None, security: Annotated[ bool, Doc( """ Enable built-in security features. - When enabled (default), FastHTTP automatically against: + When enabled (default), FastHTTP automatically protects against: - SSRF attacks (blocking localhost and private IPs) - Secret leakage in logs - Circuit breaker for failed hosts @@ -272,7 +296,7 @@ async def lifespan(app: FastHTTP): Doc( """ The version of UUID to generate on startup if `generate_startup_uuid` is True. - Supported versions: 'v4' (random UUID), 'v7' (time-based UUID with random component, requires Python 3.12+). + Supported versions: 'v4' (random UUID), 'v7' (time-based UUID with random component, requires Python 3.13+). **Example** ```python from fashttp import FastHTTP @@ -346,6 +370,8 @@ async def lifespan(app: FastHTTP): "PUT": put_request or {}, "PATCH": patch_request or {}, "DELETE": delete_request or {}, + "HEAD": head_request or {}, + "OPTIONS": options_request or {}, } self.security_enabled = security @@ -471,7 +497,7 @@ def _resolve_url(self, url: str) -> str: def _add_route( self, *, - method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"], + method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], url: str, params: dict | None = None, json: dict | None = None, @@ -716,6 +742,50 @@ def delete( responses=responses, ) + def head( + self, + url: str, + *, + params: dict | None = None, + response_model: type[BaseModel] | None = None, + request_model: type[BaseModel] | None = None, + tags: list[str] | None = None, + dependencies: list | None = None, + responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, + ) -> Callable[[Callable[..., object]], Callable[..., object]]: + return self._add_route( + method="HEAD", + url=url, + params=params, + response_model=response_model, + request_model=request_model, + tags=tags, + dependencies=dependencies, + responses=responses, + ) + + def options( + self, + url: str, + *, + params: dict | None = None, + response_model: type[BaseModel] | None = None, + request_model: type[BaseModel] | None = None, + tags: list[str] | None = None, + dependencies: list | None = None, + responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, + ) -> Callable[[Callable[..., object]], Callable[..., object]]: + return self._add_route( + method="OPTIONS", + url=url, + params=params, + response_model=response_model, + request_model=request_model, + tags=tags, + dependencies=dependencies, + responses=responses, + ) + def graphql( self, url: Annotated[ diff --git a/fasthttp/response.py b/fasthttp/response.py index 8e74079..8db929c 100644 --- a/fasthttp/response.py +++ b/fasthttp/response.py @@ -291,8 +291,5 @@ def assets( return result def __repr__(self) -> str: - """ - Return a debug-friendly string representation - of the response. - """ + """Return a debug-friendly string representation of the response.""" return f"" diff --git a/fasthttp/routing.py b/fasthttp/routing.py index bc488fb..712cf5b 100644 --- a/fasthttp/routing.py +++ b/fasthttp/routing.py @@ -27,15 +27,15 @@ def __init__( self, *, method: Annotated[ - Literal["GET", "POST", "PUT", "PATCH", "DELETE"], + Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], Doc( """ HTTP method for the request. Determines how the request will be sent to the server. - Suppoeted methods are: - GET, POST, PUT, PATCH, DELETE. + Supported methods are: + GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. """ ), ], @@ -57,7 +57,7 @@ def __init__( """ Response handler function. - This asyncfunction will be called with a Response object + This async function will be called with a Response object and can return: - str - Response @@ -215,7 +215,7 @@ class _RouteDef: Stored before URL/prefix/base_url resolution. """ - method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] + method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] url: str handler: Callable[..., object] params: dict | None @@ -394,7 +394,7 @@ def include_router( def _add_route( self, *, - method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"], + method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], url: str, params: dict | None = None, json: dict | None = None, @@ -683,6 +683,94 @@ def delete( responses=responses, ) + def head( + self, + url: Annotated[ + str, + Doc("Route URL or path for the HEAD request."), + ], + *, + params: Annotated[ + dict | None, + Doc("Query parameters for the HEAD request."), + ] = None, + response_model: Annotated[ + type[BaseModel] | None, + Doc("Optional Pydantic model for validating the handler result."), + ] = None, + request_model: Annotated[ + type[BaseModel] | None, + Doc("Optional Pydantic model for validating request data."), + ] = None, + tags: Annotated[ + list[str] | None, + Doc("Optional tags for grouping and filtering the route."), + ] = None, + dependencies: Annotated[ + list | None, + Doc("Optional dependencies executed before the request."), + ] = None, + responses: Annotated[ + dict[int, dict[Literal["model"], type[BaseModel]]] | None, + Doc("Optional response models for error status codes."), + ] = None, + ) -> Callable[[Callable[..., object]], Callable[..., object]]: + """Decorator for registering a HEAD route on the router.""" + return self._add_route( + method="HEAD", + url=url, + params=params, + response_model=response_model, + request_model=request_model, + tags=tags, + dependencies=dependencies, + responses=responses, + ) + + def options( + self, + url: Annotated[ + str, + Doc("Route URL or path for the OPTIONS request."), + ], + *, + params: Annotated[ + dict | None, + Doc("Query parameters for the OPTIONS request."), + ] = None, + response_model: Annotated[ + type[BaseModel] | None, + Doc("Optional Pydantic model for validating the handler result."), + ] = None, + request_model: Annotated[ + type[BaseModel] | None, + Doc("Optional Pydantic model for validating request data."), + ] = None, + tags: Annotated[ + list[str] | None, + Doc("Optional tags for grouping and filtering the route."), + ] = None, + dependencies: Annotated[ + list | None, + Doc("Optional dependencies executed before the request."), + ] = None, + responses: Annotated[ + dict[int, dict[Literal["model"], type[BaseModel]]] | None, + Doc("Optional response models for error status codes."), + ] = None, + ) -> Callable[[Callable[..., object]], Callable[..., object]]: + """Decorator for registering an OPTIONS route on the router.""" + return self._add_route( + method="OPTIONS", + url=url, + params=params, + response_model=response_model, + request_model=request_model, + tags=tags, + dependencies=dependencies, + responses=responses, + ) + def build_routes( self, *, diff --git a/fasthttp/types.py b/fasthttp/types.py index 9a21173..85c55eb 100644 --- a/fasthttp/types.py +++ b/fasthttp/types.py @@ -1,6 +1,6 @@ from typing import Annotated, Literal, TypeAlias, TypedDict -HTTPMethod: TypeAlias = Literal["GET", "POST", "PUT", "PATCH", "DELETE"] +HTTPMethod: TypeAlias = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] from annotated_doc import Doc diff --git a/mkdocs.yml b/mkdocs.yml index 91475e5..671178a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,15 +12,15 @@ theme: name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default - primary: deep orange - accent: orange + primary: custom + accent: custom toggle: icon: material/lightbulb name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: deep orange - accent: orange + primary: custom + accent: custom toggle: icon: material/lightbulb-outline name: Switch to system preference @@ -51,6 +51,9 @@ theme: repo_name: ndugram/fasthttp repo_url: https://github.com/ndugram/fasthttp +extra_css: + - stylesheets/extra.css + plugins: - search markdown_extensions: diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 6ffe660..c1c508a 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -781,7 +781,7 @@ def test_http_method_values(self): def test_http_method_count(self): from fasthttp.types import HTTPMethod from typing import get_args - assert len(get_args(HTTPMethod)) == 5 + assert len(get_args(HTTPMethod)) == 7 # ---------------------------------------------------------------------------