Skip to content

Commit

Permalink
Drafting first http tools
Browse files Browse the repository at this point in the history
  • Loading branch information
apetenchea committed Jun 23, 2024
1 parent 322390a commit ba3d617
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 0 deletions.
145 changes: 145 additions & 0 deletions arangoasync/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# TODO __all__ = []

from abc import ABC, abstractmethod
from typing import Optional

from aiohttp import BaseConnector, BasicAuth, ClientSession, ClientTimeout
from request import Request
from response import Response


class Session(ABC): # pragma: no cover
"""Abstract base class for HTTP sessions."""

@abstractmethod
async def request(self, request: Request) -> Response:
"""Send an HTTP request.
This method must be overridden by the user.
:param request: HTTP request.
:type request: arangoasync.request.Request
:returns: HTTP response.
:rtype: arangoasync.response.Response
"""
raise NotImplementedError

@abstractmethod
async def close(self) -> None:
"""Close the session.
This method must be overridden by the user.
"""
raise NotImplementedError


class HTTPClient(ABC): # pragma: no cover
"""Abstract base class for HTTP clients."""

@abstractmethod
def create_session(self, host: str) -> Session:
"""Return a new requests session given the host URL.
This method must be overridden by the user.
:param host: ArangoDB host URL.
:type host: str
:returns: Requests session object.
:rtype: arangoasync.http.Session
"""
raise NotImplementedError

@abstractmethod
async def send_request(
self,
session: Session,
url: str,
request: Request,
) -> Response:
"""Send an HTTP request.
This method must be overridden by the user.
:param session: Session object.
:type session: arangoasync.http.Session
:param url: Request URL.
:type url: str
:param request: HTTP request.
:type request: arangoasync.request.Request
:returns: HTTP response.
:rtype: arango.response.Response
"""
raise NotImplementedError


class DefaultSession(Session):
"""Wrapper on top of an aiohttp.ClientSession."""

def __init__(
self,
host: str,
connector: BaseConnector,
timeout: ClientTimeout,
read_bufsize: int = 2**16,
auth: Optional[BasicAuth] = None,
) -> None:
"""Initialize the session.
:param host: ArangoDB coordinator URL (eg http://localhost:8530).
:type host: str
:param connector: Supports connection pooling.
:type connector: aiohttp.BaseConnector
:param timeout: Request timeout settings.
:type timeout: aiohttp.ClientTimeout
:param read_bufsize: Size of read buffer. 64 Kib by default.
:type read_bufsize: int
:param auth: HTTP Authorization.
:type auth: aiohttp.BasicAuth | None
"""
self._session = ClientSession(
base_url=host,
connector=connector,
timeout=timeout,
auth=auth,
read_bufsize=read_bufsize,
connector_owner=False,
auto_decompress=True,
)

async def request(self, request: Request) -> Response:
"""Send an HTTP request.
:param request: HTTP request.
:type request: arangoasync.request.Request
:returns: HTTP response.
:rtype: arangoasync.response.Response
"""
method = request.method
endpoint = request.endpoint
headers = request.headers
params = request.params
data = request.data

async with self._session.request(
method.name,
endpoint,
headers=headers,
params=params,
data=data,
) as response:
raw_body = await response.read()
return Response(
method=method,
url=str(response.real_url),
headers=response.headers,
status_code=response.status,
status_text=response.reason,
raw_body=raw_body,
)

async def close(self) -> None:
"""Close the session."""
await self._session.close()


# TODO implement DefaultHTTPClient
121 changes: 121 additions & 0 deletions arangoasync/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
__all__ = [
"Method",
"Request",
]

from enum import Enum, auto
from typing import Generic, Optional, TypeVar

from typings import Headers, Params
from version import __version__ as driver_version

T = TypeVar("T")


class Method(Enum):
"""HTTP methods."""

GET = auto()
POST = auto()
PUT = auto()
PATCH = auto()
DELETE = auto()
HEAD = auto()
OPTIONS = auto()


class Request(Generic[T]):
"""HTTP request.
:param method: HTTP method.
:type method: request.Method
:param endpoint: API endpoint.
:type endpoint: str
:param headers: Request headers.
:type headers: dict | None
:param params: URL (query) parameters.
:type params: dict | None
:param data: Request payload.
:type data: Any
:param deserialize: Whether the response body should be deserialized.
:type deserialize: bool
:ivar method: HTTP method.
:vartype method: request.Method
:ivar endpoint: API endpoint.
:vartype endpoint: str
:ivar headers: Request headers.
:vartype headers: dict | None
:ivar params: URL (query) parameters.
:vartype params: dict | None
:ivar data: Request payload.
:vartype data: Any
:ivar deserialize: Whether the response body should be deserialized.
:vartype deserialize: bool
"""

__slots__ = (
"method",
"endpoint",
"headers",
"params",
"data",
"deserialize",
)

def __init__(
self,
method: Method,
endpoint: str,
headers: Optional[Headers] = None,
params: Optional[Params] = None,
data: Optional[T] = None,
deserialize: bool = True,
) -> None:
self.method: Method = method
self.endpoint: str = endpoint
self.headers: Headers = self._normalize_headers(headers)
self.params: Params = self._normalize_params(params)
self.data: Optional[T] = data
self.deserialize: bool = deserialize

@staticmethod
def _normalize_headers(headers: Optional[Headers]) -> Headers:
"""Normalize request headers.
:param headers: Request headers.
:type headers: dict | None
:returns: Normalized request headers.
:rtype: dict
"""
driver_header = f"arangoasync/{driver_version}"
normalized_headers: Headers = {
"charset": "utf-8",
"content-type": "application/json",
"x-arango-driver": driver_header,
}

if headers is not None:
for key, value in headers.items():
normalized_headers[key.lower()] = value

return normalized_headers

@staticmethod
def _normalize_params(params: Optional[Params]) -> Params:
"""Normalize URL (query) parameters.
:param params: URL (query) parameters.
:type params: dict | None
:returns: Normalized URL (query) parameters.
:rtype: dict
"""
normalized_params: Params = {}

if params is not None:
for key, value in params.items():
if isinstance(value, bool):
value = int(value)
normalized_params[key] = str(value)

return normalized_params
80 changes: 80 additions & 0 deletions arangoasync/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import Generic, Optional, TypeVar

from request import Method
from typings import Headers

T = TypeVar("T")


class Response(Generic[T]):
"""HTTP response.
:param method: HTTP method.
:type method: request.Method
:param url: API URL.
:type url: str
:param headers: Response headers.
:type headers: dict | None
:param status_code: Response status code.
:type status_code: int
:param status_text: Response status text.
:type status_text: str
:param raw_body: Raw response body.
:type raw_body: str
:ivar method: HTTP method.
:vartype method: request.Method
:ivar url: API URL.
:vartype url: str
:ivar headers: Response headers.
:vartype headers: dict | None
:ivar status_code: Response status code.
:vartype status_code: int
:ivar status_text: Response status text.
:vartype status_text: str
:ivar raw_body: Raw response body.
:vartype raw_body: str
:ivar body: Response body after processing.
:vartype body: Any
:ivar error_code: Error code from ArangoDB server.
:vartype error_code: int
:ivar error_message: Error message from ArangoDB server.
:vartype error_message: str
:ivar is_success: True if response status code was 2XX.
:vartype is_success: bool
"""

__slots__ = (
"method",
"url",
"headers",
"status_code",
"status_text",
"body",
"raw_body",
"error_code",
"error_message",
"is_success",
)

def __init__(
self,
method: Method,
url: str,
headers: Headers,
status_code: int,
status_text: str,
raw_body: bytes,
) -> None:
self.method: Method = method
self.url: str = url
self.headers: Headers = headers
self.status_code: int = status_code
self.status_text: str = status_text
self.raw_body: bytes = raw_body

# Populated later
self.body: Optional[T] = None
self.error_code: Optional[int] = None
self.error_message: Optional[str] = None
self.is_success: Optional[bool] = None
14 changes: 14 additions & 0 deletions arangoasync/typings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
__all__ = [
"Headers",
"Params",
]

from typing import MutableMapping

from multidict import MultiDict

Headers = MutableMapping[str, str] | MultiDict[str]
Headers.__doc__ = """Type definition for HTTP headers"""

Params = MutableMapping[str, bool | int | str]
Params.__doc__ = """Type definition for URL (query) parameters"""

0 comments on commit ba3d617

Please sign in to comment.