Skip to content

Commit ba3d617

Browse files
committed
Drafting first http tools
1 parent 322390a commit ba3d617

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed

arangoasync/http.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# TODO __all__ = []
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Optional
5+
6+
from aiohttp import BaseConnector, BasicAuth, ClientSession, ClientTimeout
7+
from request import Request
8+
from response import Response
9+
10+
11+
class Session(ABC): # pragma: no cover
12+
"""Abstract base class for HTTP sessions."""
13+
14+
@abstractmethod
15+
async def request(self, request: Request) -> Response:
16+
"""Send an HTTP request.
17+
18+
This method must be overridden by the user.
19+
20+
:param request: HTTP request.
21+
:type request: arangoasync.request.Request
22+
:returns: HTTP response.
23+
:rtype: arangoasync.response.Response
24+
"""
25+
raise NotImplementedError
26+
27+
@abstractmethod
28+
async def close(self) -> None:
29+
"""Close the session.
30+
31+
This method must be overridden by the user.
32+
"""
33+
raise NotImplementedError
34+
35+
36+
class HTTPClient(ABC): # pragma: no cover
37+
"""Abstract base class for HTTP clients."""
38+
39+
@abstractmethod
40+
def create_session(self, host: str) -> Session:
41+
"""Return a new requests session given the host URL.
42+
43+
This method must be overridden by the user.
44+
45+
:param host: ArangoDB host URL.
46+
:type host: str
47+
:returns: Requests session object.
48+
:rtype: arangoasync.http.Session
49+
"""
50+
raise NotImplementedError
51+
52+
@abstractmethod
53+
async def send_request(
54+
self,
55+
session: Session,
56+
url: str,
57+
request: Request,
58+
) -> Response:
59+
"""Send an HTTP request.
60+
61+
This method must be overridden by the user.
62+
63+
:param session: Session object.
64+
:type session: arangoasync.http.Session
65+
:param url: Request URL.
66+
:type url: str
67+
:param request: HTTP request.
68+
:type request: arangoasync.request.Request
69+
:returns: HTTP response.
70+
:rtype: arango.response.Response
71+
"""
72+
raise NotImplementedError
73+
74+
75+
class DefaultSession(Session):
76+
"""Wrapper on top of an aiohttp.ClientSession."""
77+
78+
def __init__(
79+
self,
80+
host: str,
81+
connector: BaseConnector,
82+
timeout: ClientTimeout,
83+
read_bufsize: int = 2**16,
84+
auth: Optional[BasicAuth] = None,
85+
) -> None:
86+
"""Initialize the session.
87+
88+
:param host: ArangoDB coordinator URL (eg http://localhost:8530).
89+
:type host: str
90+
:param connector: Supports connection pooling.
91+
:type connector: aiohttp.BaseConnector
92+
:param timeout: Request timeout settings.
93+
:type timeout: aiohttp.ClientTimeout
94+
:param read_bufsize: Size of read buffer. 64 Kib by default.
95+
:type read_bufsize: int
96+
:param auth: HTTP Authorization.
97+
:type auth: aiohttp.BasicAuth | None
98+
"""
99+
self._session = ClientSession(
100+
base_url=host,
101+
connector=connector,
102+
timeout=timeout,
103+
auth=auth,
104+
read_bufsize=read_bufsize,
105+
connector_owner=False,
106+
auto_decompress=True,
107+
)
108+
109+
async def request(self, request: Request) -> Response:
110+
"""Send an HTTP request.
111+
112+
:param request: HTTP request.
113+
:type request: arangoasync.request.Request
114+
:returns: HTTP response.
115+
:rtype: arangoasync.response.Response
116+
"""
117+
method = request.method
118+
endpoint = request.endpoint
119+
headers = request.headers
120+
params = request.params
121+
data = request.data
122+
123+
async with self._session.request(
124+
method.name,
125+
endpoint,
126+
headers=headers,
127+
params=params,
128+
data=data,
129+
) as response:
130+
raw_body = await response.read()
131+
return Response(
132+
method=method,
133+
url=str(response.real_url),
134+
headers=response.headers,
135+
status_code=response.status,
136+
status_text=response.reason,
137+
raw_body=raw_body,
138+
)
139+
140+
async def close(self) -> None:
141+
"""Close the session."""
142+
await self._session.close()
143+
144+
145+
# TODO implement DefaultHTTPClient

arangoasync/request.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
__all__ = [
2+
"Method",
3+
"Request",
4+
]
5+
6+
from enum import Enum, auto
7+
from typing import Generic, Optional, TypeVar
8+
9+
from typings import Headers, Params
10+
from version import __version__ as driver_version
11+
12+
T = TypeVar("T")
13+
14+
15+
class Method(Enum):
16+
"""HTTP methods."""
17+
18+
GET = auto()
19+
POST = auto()
20+
PUT = auto()
21+
PATCH = auto()
22+
DELETE = auto()
23+
HEAD = auto()
24+
OPTIONS = auto()
25+
26+
27+
class Request(Generic[T]):
28+
"""HTTP request.
29+
30+
:param method: HTTP method.
31+
:type method: request.Method
32+
:param endpoint: API endpoint.
33+
:type endpoint: str
34+
:param headers: Request headers.
35+
:type headers: dict | None
36+
:param params: URL (query) parameters.
37+
:type params: dict | None
38+
:param data: Request payload.
39+
:type data: Any
40+
:param deserialize: Whether the response body should be deserialized.
41+
:type deserialize: bool
42+
43+
:ivar method: HTTP method.
44+
:vartype method: request.Method
45+
:ivar endpoint: API endpoint.
46+
:vartype endpoint: str
47+
:ivar headers: Request headers.
48+
:vartype headers: dict | None
49+
:ivar params: URL (query) parameters.
50+
:vartype params: dict | None
51+
:ivar data: Request payload.
52+
:vartype data: Any
53+
:ivar deserialize: Whether the response body should be deserialized.
54+
:vartype deserialize: bool
55+
"""
56+
57+
__slots__ = (
58+
"method",
59+
"endpoint",
60+
"headers",
61+
"params",
62+
"data",
63+
"deserialize",
64+
)
65+
66+
def __init__(
67+
self,
68+
method: Method,
69+
endpoint: str,
70+
headers: Optional[Headers] = None,
71+
params: Optional[Params] = None,
72+
data: Optional[T] = None,
73+
deserialize: bool = True,
74+
) -> None:
75+
self.method: Method = method
76+
self.endpoint: str = endpoint
77+
self.headers: Headers = self._normalize_headers(headers)
78+
self.params: Params = self._normalize_params(params)
79+
self.data: Optional[T] = data
80+
self.deserialize: bool = deserialize
81+
82+
@staticmethod
83+
def _normalize_headers(headers: Optional[Headers]) -> Headers:
84+
"""Normalize request headers.
85+
86+
:param headers: Request headers.
87+
:type headers: dict | None
88+
:returns: Normalized request headers.
89+
:rtype: dict
90+
"""
91+
driver_header = f"arangoasync/{driver_version}"
92+
normalized_headers: Headers = {
93+
"charset": "utf-8",
94+
"content-type": "application/json",
95+
"x-arango-driver": driver_header,
96+
}
97+
98+
if headers is not None:
99+
for key, value in headers.items():
100+
normalized_headers[key.lower()] = value
101+
102+
return normalized_headers
103+
104+
@staticmethod
105+
def _normalize_params(params: Optional[Params]) -> Params:
106+
"""Normalize URL (query) parameters.
107+
108+
:param params: URL (query) parameters.
109+
:type params: dict | None
110+
:returns: Normalized URL (query) parameters.
111+
:rtype: dict
112+
"""
113+
normalized_params: Params = {}
114+
115+
if params is not None:
116+
for key, value in params.items():
117+
if isinstance(value, bool):
118+
value = int(value)
119+
normalized_params[key] = str(value)
120+
121+
return normalized_params

arangoasync/response.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from typing import Generic, Optional, TypeVar
2+
3+
from request import Method
4+
from typings import Headers
5+
6+
T = TypeVar("T")
7+
8+
9+
class Response(Generic[T]):
10+
"""HTTP response.
11+
12+
:param method: HTTP method.
13+
:type method: request.Method
14+
:param url: API URL.
15+
:type url: str
16+
:param headers: Response headers.
17+
:type headers: dict | None
18+
:param status_code: Response status code.
19+
:type status_code: int
20+
:param status_text: Response status text.
21+
:type status_text: str
22+
:param raw_body: Raw response body.
23+
:type raw_body: str
24+
25+
:ivar method: HTTP method.
26+
:vartype method: request.Method
27+
:ivar url: API URL.
28+
:vartype url: str
29+
:ivar headers: Response headers.
30+
:vartype headers: dict | None
31+
:ivar status_code: Response status code.
32+
:vartype status_code: int
33+
:ivar status_text: Response status text.
34+
:vartype status_text: str
35+
:ivar raw_body: Raw response body.
36+
:vartype raw_body: str
37+
:ivar body: Response body after processing.
38+
:vartype body: Any
39+
:ivar error_code: Error code from ArangoDB server.
40+
:vartype error_code: int
41+
:ivar error_message: Error message from ArangoDB server.
42+
:vartype error_message: str
43+
:ivar is_success: True if response status code was 2XX.
44+
:vartype is_success: bool
45+
"""
46+
47+
__slots__ = (
48+
"method",
49+
"url",
50+
"headers",
51+
"status_code",
52+
"status_text",
53+
"body",
54+
"raw_body",
55+
"error_code",
56+
"error_message",
57+
"is_success",
58+
)
59+
60+
def __init__(
61+
self,
62+
method: Method,
63+
url: str,
64+
headers: Headers,
65+
status_code: int,
66+
status_text: str,
67+
raw_body: bytes,
68+
) -> None:
69+
self.method: Method = method
70+
self.url: str = url
71+
self.headers: Headers = headers
72+
self.status_code: int = status_code
73+
self.status_text: str = status_text
74+
self.raw_body: bytes = raw_body
75+
76+
# Populated later
77+
self.body: Optional[T] = None
78+
self.error_code: Optional[int] = None
79+
self.error_message: Optional[str] = None
80+
self.is_success: Optional[bool] = None

arangoasync/typings.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__all__ = [
2+
"Headers",
3+
"Params",
4+
]
5+
6+
from typing import MutableMapping
7+
8+
from multidict import MultiDict
9+
10+
Headers = MutableMapping[str, str] | MultiDict[str]
11+
Headers.__doc__ = """Type definition for HTTP headers"""
12+
13+
Params = MutableMapping[str, bool | int | str]
14+
Params.__doc__ = """Type definition for URL (query) parameters"""

0 commit comments

Comments
 (0)