diff --git a/examples/middleware/error_middleware.py b/examples/middleware/error_middleware.py index de995c7..d04f397 100644 --- a/examples/middleware/error_middleware.py +++ b/examples/middleware/error_middleware.py @@ -2,7 +2,7 @@ from fasthttp.middleware import BaseMiddleware from fasthttp.response import Response from fasthttp.routing import Route -from fasthttp.types import RequestsOptinal +from fasthttp.types import RequestsOptional class ErrorTrackingMiddleware(BaseMiddleware): @@ -15,7 +15,7 @@ def __init__(self) -> None: self.error_count = 0 async def on_error( - self, error: Exception, route: Route, config: RequestsOptinal + self, error: Exception, route: Route, config: RequestsOptional ) -> None: self.error_count += 1 print(f"Error #{self.error_count}: {error.__class__.__name__}") diff --git a/examples/oauth2/__init__.py b/examples/oauth2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/oauth2/client_credentials.py b/examples/oauth2/client_credentials.py new file mode 100644 index 0000000..3d1fbb5 --- /dev/null +++ b/examples/oauth2/client_credentials.py @@ -0,0 +1,20 @@ +from fasthttp import FastHTTP, OAuth2ClientCredentials +from fasthttp.response import Response + +app = FastHTTP(debug=True) + +auth = OAuth2ClientCredentials( + token_url="https://auth.example.com/oauth/token", + client_id="your-client-id", + client_secret="GAZAN", + scopes=["read", "write"] +) + + +@app.get("https://api.example.com/protected/resource", auth=auth) +async def get_resource(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() diff --git a/examples/oauth2/shared_auth.py b/examples/oauth2/shared_auth.py new file mode 100644 index 0000000..dc0301e --- /dev/null +++ b/examples/oauth2/shared_auth.py @@ -0,0 +1,32 @@ + + +from fasthttp import FastHTTP, OAuth2ClientCredentials +from fasthttp.response import Response + +auth = OAuth2ClientCredentials( + token_url= "https://auth.example.com/oauth/token", + client_id= "example-client", + client_secret="67", + scopes=["read", "write"], +) + +app = FastHTTP(debug=True) + + +@app.get("https://api.example.com/users", auth=auth) +async def get_users(resp: Response) -> dict: + return resp.json() + + +@app.post("https://api.example.com/users", auth=auth, json={"name": "John"}) +async def create_user(resp: Response) -> dict: + return resp.json() + + +@app.delete("https://api.example.com/users/1", auth=auth) +async def delete_user(resp: Response) -> dict: + return resp.json() + + +if __name__ == "__main__": + app.run() diff --git a/fasthttp/app.py b/fasthttp/app.py index de5a574..4ad76a7 100644 --- a/fasthttp/app.py +++ b/fasthttp/app.py @@ -54,7 +54,7 @@ from .auth import BasicAuth, BearerAuth, DigestAuth, OAuth2ClientCredentials from .response import Response - from .types import RequestsOptinal + from .types import HTTPMethod, RequestsOptional class FastHTTP: @@ -131,7 +131,7 @@ def __init__( ] = False, get_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for GET requests. @@ -144,7 +144,7 @@ def __init__( ) = None, post_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for POST requests. @@ -158,7 +158,7 @@ def __init__( ) = None, put_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for PUT requests. @@ -172,7 +172,7 @@ def __init__( ) = None, patch_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for PATCH requests. @@ -185,7 +185,7 @@ def __init__( ) = None, delete_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for DELETE requests. @@ -198,7 +198,7 @@ def __init__( ) = None, head_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for HEAD requests. @@ -211,7 +211,7 @@ def __init__( ) = None, options_request: ( Annotated[ - RequestsOptinal | None, + RequestsOptional | None, Doc( """ Default configuration for OPTIONS requests. @@ -368,7 +368,7 @@ async def lifespan(app: FastHTTP): ), ] = False, startup_uuid_version: Annotated[ - str, + Literal["v4", "v7"], Doc( """ The version of UUID to generate on startup if `generate_startup_uuid` is True. @@ -642,15 +642,15 @@ def _resolve_url(self, url: str) -> str: def _add_route( self, *, - method: Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], + method: HTTPMethod, url: str, - params: dict | None = None, - json: dict | None = None, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, data: object | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -717,7 +717,7 @@ def include_router( ), ] = None, dependencies: Annotated[ - list | None, + list[Any] | None, Doc( """ Optional dependencies prepended before router dependencies. @@ -778,11 +778,11 @@ def get( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -804,12 +804,12 @@ def post( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -832,12 +832,12 @@ def put( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -860,12 +860,12 @@ def patch( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -888,12 +888,12 @@ def delete( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -916,11 +916,11 @@ def head( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, @@ -942,11 +942,11 @@ def options( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, response_model: type | None = None, request_model: type[BaseModel] | None = None, tags: list[str] | None = None, - dependencies: list | None = None, + dependencies: list[Any] | None = None, raise_for_status: bool = False, auth: BasicAuth | DigestAuth | BearerAuth | OAuth2ClientCredentials | None = None, responses: dict[int, dict[Literal["model"], type[BaseModel]]] | None = None, diff --git a/fasthttp/auth.py b/fasthttp/auth.py index 4391de6..0dddb5d 100644 --- a/fasthttp/auth.py +++ b/fasthttp/auth.py @@ -8,6 +8,8 @@ if TYPE_CHECKING: from collections.abc import Generator + from .types import OAuth2Scope + class BasicAuth: """HTTP Basic authentication (username + password).""" @@ -53,7 +55,7 @@ def __init__( token_url: str, client_id: str, client_secret: str, - scopes: list[str] | None = None, + scopes: list[OAuth2Scope] | None = None, extra: dict[str, str] | None = None, ) -> None: self.token_url = token_url diff --git a/fasthttp/client.py b/fasthttp/client.py index e61db2a..e9ff8eb 100644 --- a/fasthttp/client.py +++ b/fasthttp/client.py @@ -44,7 +44,7 @@ class HTTPClient: def __init__( self, request_configs: Annotated[ - dict, + dict[str, dict[str, Any]], Doc( """ Dictionary mapping HTTP methods to default request configurations. diff --git a/fasthttp/exceptions/base.py b/fasthttp/exceptions/base.py index e8b51af..851607b 100644 --- a/fasthttp/exceptions/base.py +++ b/fasthttp/exceptions/base.py @@ -1,10 +1,13 @@ from __future__ import annotations import logging -from typing import Annotated, Any +from typing import TYPE_CHECKING, Annotated, Any from annotated_doc import Doc +if TYPE_CHECKING: + from fasthttp.types import HTTPMethod + logger = logging.getLogger("fasthttp.exceptions") @@ -48,7 +51,7 @@ def __init__( ) = None, method: ( Annotated[ - str, + HTTPMethod, Doc( """ HTTP method of the failed request. diff --git a/fasthttp/middleware/base.py b/fasthttp/middleware/base.py index b35453a..7910e38 100644 --- a/fasthttp/middleware/base.py +++ b/fasthttp/middleware/base.py @@ -9,7 +9,7 @@ from fasthttp.response import Response from fasthttp.routing import Route - from fasthttp.types import RequestsOptinal + from fasthttp.types import RequestsOptional class BaseMiddleware: @@ -110,7 +110,7 @@ async def on_error( Doc("The route that failed."), ], config: Annotated[ - RequestsOptinal, + RequestsOptional, Doc("Request configuration that was used."), ], ) -> Annotated[ @@ -199,7 +199,7 @@ async def process_before_request( Doc("The route being executed."), ], config: Annotated[ - RequestsOptinal, + RequestsOptional, Doc("Initial request configuration."), ], ) -> Annotated[ @@ -226,7 +226,7 @@ async def process_after_response( Doc("The route that was executed."), ], config: Annotated[ # noqa: ARG002 - RequestsOptinal, + RequestsOptional, Doc("Request configuration that was used."), ], ) -> Annotated[ @@ -250,7 +250,7 @@ async def process_on_error( Doc("The route that failed."), ], config: Annotated[ - RequestsOptinal, + RequestsOptional, Doc("Request configuration that was used."), ], ) -> Annotated[ diff --git a/fasthttp/middleware/cache.py b/fasthttp/middleware/cache.py index d2e125b..1fb8de7 100644 --- a/fasthttp/middleware/cache.py +++ b/fasthttp/middleware/cache.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from fasthttp.response import Response from fasthttp.routing import Route - from fasthttp.types import RequestsOptinal + from fasthttp.types import RequestsOptional class CacheEntry: @@ -136,7 +136,7 @@ async def on_error( self, error: Exception, # noqa: ARG002 route: Route, # noqa: ARG002 - config: RequestsOptinal, # noqa: ARG002 + config: RequestsOptional, # noqa: ARG002 ) -> None: key, _ = self._state.get() if key is not None: diff --git a/fasthttp/middleware/retry.py b/fasthttp/middleware/retry.py index a04a197..3067ba6 100644 --- a/fasthttp/middleware/retry.py +++ b/fasthttp/middleware/retry.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from fasthttp.response import Response from fasthttp.routing import Route - from fasthttp.types import RequestsOptinal + from fasthttp.types import RequestsOptional _retry_state: ContextVar[dict[str, Any] | None] = ContextVar( "retry_state", default=None @@ -152,7 +152,7 @@ async def on_error( self, error: Exception, route: Route, # noqa: ARG002 - config: RequestsOptinal, # noqa: ARG002 + config: RequestsOptional, # noqa: ARG002 ) -> None: state = _retry_state.get() if state is None: diff --git a/fasthttp/response.py b/fasthttp/response.py index 115b6b3..43a9733 100644 --- a/fasthttp/response.py +++ b/fasthttp/response.py @@ -41,11 +41,11 @@ def __init__( self, status: Annotated[int, Doc("HTTP status code (e.g. 200, 404, 500).")], text: Annotated[str, Doc("Raw response body as a string.")], - headers: Annotated[dict, Doc("HTTP response headers returned by the server.")], + headers: Annotated[dict[str, str], Doc("HTTP response headers returned by the server.")], method: Annotated[str | None, Doc("HTTP method used for the request.")] = None, - req_headers: Annotated[dict | None, Doc("HTTP headers sent with the request.")] = None, - query: Annotated[dict | None, Doc("Query parameters encoded into the request URL.")] = None, - req_json: Annotated[dict | None, Doc("JSON body sent with the request.")] = None, + req_headers: Annotated[dict[str, str] | None, Doc("HTTP headers sent with the request.")] = None, + query: Annotated[dict[str, Any] | None, Doc("Query parameters encoded into the request URL.")] = None, + req_json: Annotated[dict[str, Any] | None, Doc("JSON body sent with the request.")] = None, req_data: Annotated[object | None, Doc("Raw body or form data sent with the request.")] = None, content: Annotated[bytes | None, Doc("Raw response body as bytes.")] = None, ) -> None: @@ -80,21 +80,21 @@ def method(self, value: str | None) -> None: self._method = value @property - def req_headers(self) -> dict | None: + def req_headers(self) -> dict[str, str] | None: """HTTP headers sent with the request.""" return self._req_headers @req_headers.setter - def req_headers(self, value: dict | None) -> None: + def req_headers(self, value: dict[str, str] | None) -> None: self._req_headers = value @property - def query(self) -> dict | None: + def query(self) -> dict[str, Any] | None: """Query parameters encoded into the request URL.""" return self._query @query.setter - def query(self, value: dict | None) -> None: + def query(self, value: dict[str, Any] | None) -> None: self._query = value @property @@ -111,7 +111,7 @@ def json(self) -> Any: # noqa: ANN401 return self._response_model.model_validate_json(self.text) # type: ignore[union-attr] return orjson.loads(self.text) - def req_json(self) -> dict | None: + def req_json(self) -> dict[str, Any] | None: """Return the JSON body that was sent with the request.""" return self._req_json diff --git a/fasthttp/session.py b/fasthttp/session.py index a0f4ddb..fced36d 100644 --- a/fasthttp/session.py +++ b/fasthttp/session.py @@ -2,7 +2,7 @@ import secrets from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import httpx @@ -130,7 +130,7 @@ def __init__( ) base_config = {"headers": dict(self._session_headers), "timeout": timeout} - self._request_configs: dict[str, dict] = { + self._request_configs: dict[str, dict[str, Any]] = { method: dict(base_config) for method in ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS") } @@ -188,8 +188,8 @@ def _build_route( method: HTTPMethod, url: str, *, - params: dict | None = None, - json: dict | None = None, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -220,7 +220,7 @@ async def get( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, ) -> Response | None: @@ -235,7 +235,7 @@ async def post( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -251,7 +251,7 @@ async def put( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -267,7 +267,7 @@ async def patch( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -283,7 +283,7 @@ async def delete( self, url: str, *, - json: dict | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -299,7 +299,7 @@ async def head( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, ) -> Response | None: @@ -314,7 +314,7 @@ async def options( self, url: str, *, - params: dict | None = None, + params: dict[str, Any] | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, ) -> Response | None: @@ -327,11 +327,11 @@ async def options( async def request( self, - method: str, + method: HTTPMethod, url: str, *, - params: dict | None = None, - json: dict | None = None, + params: dict[str, Any] | None = None, + json: dict[str, Any] | None = None, data: object | None = None, headers: dict[str, str] | None = None, timeout: float | None = None, @@ -353,7 +353,7 @@ async def request( @asynccontextmanager async def stream( self, - method: str, + method: HTTPMethod, url: str, *, headers: dict[str, str] | None = None, diff --git a/fasthttp/types.py b/fasthttp/types.py index 993febe..2273236 100644 --- a/fasthttp/types.py +++ b/fasthttp/types.py @@ -6,6 +6,20 @@ "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS" ] +OAuth2Scope: TypeAlias = Literal[ + "openid", + "profile", + "email", + "address", + "phone", + "offline_access", + "read", + "write", + "admin", + "api", +] + + class JSONResponse: """ @@ -18,11 +32,11 @@ class JSONResponse: including primitive values, lists, and nested objects. """ - Primutive: TypeAlias = str | int | float | bool | None - Value: TypeAlias = Primutive | list["Value"] | dict[str, "Value"] + Primitive: TypeAlias = str | int | float | bool | None + Value: TypeAlias = Primitive | list["Value"] | dict[str, "Value"] -class RequestsOptinal(TypedDict, total=False): +class RequestsOptional(TypedDict, total=False): """ Optional request configuration. diff --git a/tests/test_types.py b/tests/test_types.py index 682e9b9..ba7fdb6 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,6 +1,6 @@ """Tests for types module.""" -from fasthttp.types import JSONResponse, RequestsOptinal +from fasthttp.types import JSONResponse, RequestsOptional class TestJSONResponse: @@ -9,7 +9,7 @@ class TestJSONResponse: def test_json_response_primitive_types(self) -> None: """Test JSONResponse primitive type aliases.""" # These are type aliases, so we can verify they exist - assert hasattr(JSONResponse, "Primutive") + assert hasattr(JSONResponse, "Primitive") assert hasattr(JSONResponse, "Value") def test_json_response_value_can_be_primitive(self) -> None: @@ -26,30 +26,30 @@ def test_json_response_docstring(self) -> None: assert "JSON response type definitions" in JSONResponse.__doc__ -class TestRequestsOptinal: - """Tests for RequestsOptinal TypedDict.""" +class TestRequestsOptional: + """Tests for RequestsOptional TypedDict.""" def test_requests_optional_exists(self) -> None: - """Test RequestsOptinal type exists.""" - assert RequestsOptinal is not None + """Test RequestsOptional type exists.""" + assert RequestsOptional is not None def test_requests_optional_has_headers(self) -> None: - """Test RequestsOptinal has headers field.""" + """Test RequestsOptional has headers field.""" # TypedDict fields are checked at type-check time # We can verify the class annotations exist - assert "headers" in RequestsOptinal.__annotations__ + assert "headers" in RequestsOptional.__annotations__ def test_requests_optional_has_timeout(self) -> None: - """Test RequestsOptinal has timeout field.""" - assert "timeout" in RequestsOptinal.__annotations__ + """Test RequestsOptional has timeout field.""" + assert "timeout" in RequestsOptional.__annotations__ def test_requests_optional_has_allow_redirects(self) -> None: - """Test RequestsOptinal has allow_redirects field.""" - assert "allow_redirects" in RequestsOptinal.__annotations__ + """Test RequestsOptional has allow_redirects field.""" + assert "allow_redirects" in RequestsOptional.__annotations__ def test_requests_optional_can_be_created(self) -> None: - """Test RequestsOptinal can be instantiated with fields.""" - config: RequestsOptinal = { + """Test RequestsOptional can be instantiated with fields.""" + config: RequestsOptional = { "headers": {"Content-Type": "application/json"}, "timeout": 30.0, "allow_redirects": True, @@ -60,23 +60,23 @@ def test_requests_optional_can_be_created(self) -> None: assert config["allow_redirects"] is True def test_requests_optional_partial_fields(self) -> None: - """Test RequestsOptinal can have partial fields (all optional).""" - config1: RequestsOptinal = {"timeout": 60.0} + """Test RequestsOptional can have partial fields (all optional).""" + config1: RequestsOptional = {"timeout": 60.0} assert config1["timeout"] == 60.0 - config2: RequestsOptinal = {"headers": {"Authorization": "Bearer token"}} + config2: RequestsOptional = {"headers": {"Authorization": "Bearer token"}} assert "Authorization" in config2["headers"] - config3: RequestsOptinal = {"allow_redirects": False} + config3: RequestsOptional = {"allow_redirects": False} assert config3["allow_redirects"] is False def test_requests_optional_docstring(self) -> None: - """Test RequestsOptinal has proper docstring.""" - assert RequestsOptinal.__doc__ is not None - assert "Optional request configuration" in RequestsOptinal.__doc__ + """Test RequestsOptional has proper docstring.""" + assert RequestsOptional.__doc__ is not None + assert "Optional request configuration" in RequestsOptional.__doc__ def test_requests_optional_total_false(self) -> None: - """Test that RequestsOptinal has total=False (all fields optional).""" + """Test that RequestsOptional has total=False (all fields optional).""" # Verify that all fields are indeed optional by creating empty dict - config: RequestsOptinal = {} + config: RequestsOptional = {} assert config == {}