Skip to content

Commit b137749

Browse files
committed
finished docs + tests
1 parent 6da0503 commit b137749

29 files changed

+429
-453
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ pip install arrest
2323

2424
```python
2525

26-
from arrest.resource import Resource
27-
from arrest.service import Service
26+
from arrest import Resource, Service
2827
from arrest.exceptions import ArrestHTTPException
2928

3029

arrest/defaults.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
from arrest.http import ContentType
2-
3-
HEADER_DEFAULTS = {"Content-Type": ContentType.APPLICATION_JSON}
41
TIMEOUT_DEFAULT = 60 # sec

arrest/exceptions.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ class BaseException(Exception):
55
class ArrestError(BaseException):
66
"""used in error situations"""
77

8+
def __init__(self, *args: object) -> None:
9+
super().__init__(*args)
10+
811

912
class ArrestHTTPException(ArrestError):
1013
def __init__(self, status_code: int, data: dict | str) -> None:
@@ -17,9 +20,16 @@ def __init__(self, message: str):
1720
self.message = message
1821

1922

20-
class HandlerNotFound(ArrestError):
21-
pass
23+
class HandlerNotFound(NotFoundException):
24+
def __init__(self, message: str):
25+
super().__init__(message)
26+
27+
28+
class ResourceNotFound(NotFoundException):
29+
def __init__(self, message: str):
30+
super().__init__(message)
2231

2332

2433
class ConversionError(ArrestError):
25-
pass
34+
def __init__(self, *args: object) -> None:
35+
super().__init__(*args)

arrest/handler.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ class ResourceHandler(BaseModel):
4242
path_regex: Pattern | None = None
4343
param_types: dict[str, type] | None = None
4444

45-
def parse_path(
46-
self, method: Methods, path: str | AnyUrl, **kwargs
47-
) -> str | AnyUrl | None:
45+
def parse_path(self, method: Methods, path: str | AnyUrl, **kwargs) -> str | AnyUrl | None:
4846
if method != self.method:
4947
return None
5048

@@ -57,9 +55,7 @@ def __parse_exact_path(self, path: str | AnyUrl) -> str | AnyUrl | None:
5755
if self.path_regex.fullmatch(path):
5856
return path
5957

60-
def __resolve_path_param(
61-
self, path: str | AnyUrl, **kwargs
62-
) -> str | AnyUrl | None:
58+
def __resolve_path_param(self, path: str | AnyUrl, **kwargs) -> str | AnyUrl | None:
6359
params = self.__extract_path_params(path)
6460
if params is None:
6561
return None

arrest/http.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,3 @@ class Methods(StrEnum):
1717
DELETE = "DELETE"
1818
HEAD = "HEAD"
1919
OPTIONS = "OPTIONS"
20-
21-
22-
class ContentType(StrEnum):
23-
APPLICATION_JSON = "application/json"

arrest/params.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ class ParamTypes(Enum):
1616
class Param(FieldInfo):
1717
_param_type: ParamTypes
1818

19-
def __init__(
20-
self, default: Any = PydanticUndefined, **kwargs: Unpack[_FieldInfoInputs]
21-
) -> None:
19+
def __init__(self, default: Any = PydanticUndefined, **kwargs: Unpack[_FieldInfoInputs]) -> None:
2220
kwargs = dict(default=default, **kwargs)
2321
super().__init__(**kwargs)
2422

@@ -36,9 +34,7 @@ class Body(Param):
3634

3735

3836
class Params:
39-
def __init__(
40-
self, *, header: httpx.Headers, query: httpx.QueryParams, body: Any
41-
) -> None:
37+
def __init__(self, *, header: httpx.Headers, query: httpx.QueryParams, body: Any) -> None:
4238
self.header = header
4339
self.query = query
4440
self.body = body

arrest/resource.py

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from pydantic.version import VERSION as PYDANTIC_VERSION
1010

1111
from arrest.converters import compile_path
12-
from arrest.defaults import HEADER_DEFAULTS, TIMEOUT_DEFAULT
13-
from arrest.exceptions import ArrestError, ArrestHTTPException, HandlerNotFound
12+
from arrest.defaults import TIMEOUT_DEFAULT
13+
from arrest.exceptions import ArrestHTTPException, HandlerNotFound
1414
from arrest.handler import HandlerKey, ResourceHandler
1515
from arrest.http import Methods
1616
from arrest.logging import logger
1717
from arrest.params import Param, Params, ParamTypes
18-
from arrest.utils import extract_model_field, join_url, jsonify
18+
from arrest.utils import extract_model_field, join_url, jsonify, validate_request_model
1919

2020

2121
class Resource:
@@ -43,7 +43,7 @@ def __init__(
4343
name: Optional[str] = None,
4444
*,
4545
route: Optional[str],
46-
headers: Optional[dict] = HEADER_DEFAULTS,
46+
headers: Optional[dict] = None,
4747
timeout: Optional[int] = TIMEOUT_DEFAULT,
4848
response_model: Optional[Type[BaseModel]] = None,
4949
handlers: Union[
@@ -133,11 +133,9 @@ async def request(
133133

134134
params: dict = {}
135135

136-
if not (
137-
match := self.get_matching_handler(method=method, path=path, **kwargs)
138-
):
136+
if not (match := self.get_matching_handler(method=method, path=path, **kwargs)):
139137
logger.warning("no matching handler found for request")
140-
raise HandlerNotFound("no matching handler found for request")
138+
raise HandlerNotFound(message="no matching handler found for request")
141139

142140
handler, url = match
143141

@@ -161,9 +159,9 @@ async def request(
161159
callback_response = await handler.callback(response)
162160
else:
163161
callback_response = handler.callback(response)
164-
except Exception as exc:
162+
except Exception:
165163
logger.warning("something went wrong during callback", exc_info=True)
166-
raise ArrestError(str(exc)) from exc
164+
raise
167165
return callback_response
168166

169167
return response
@@ -344,27 +342,24 @@ def extract_request_params(
344342
"""
345343

346344
header_params = headers or {}
347-
header_params |= self.headers
345+
if self.headers:
346+
header_params |= self.headers
348347
query_params = query or {}
349348
body_params = {}
350349

351350
if request_type:
352351
# perform type validation on `request_data`
353-
request_data = request_type.model_validate(request_data)
352+
request_data = validate_request_model(type_=request_type, obj=request_data)
354353

355354
if isinstance(request_data, BaseModel):
356355
# extract pydantic fields into `Query`, `Body` and `Header`
357356
model_fields: dict = (
358-
request_data.__fields__
359-
if PYDANTIC_VERSION.startswith("2.")
360-
else request_data.model_fields
357+
request_data.__fields__ if PYDANTIC_VERSION.startswith("2.") else request_data.model_fields
361358
)
362359

363360
for field, field_info in model_fields.items():
364361
field_info = cast(Param, field_info)
365-
if not hasattr(field_info, "_param_type") and isinstance(
366-
field_info, FieldInfo
367-
):
362+
if not hasattr(field_info, "_param_type") and isinstance(field_info, FieldInfo):
368363
body_params |= extract_model_field(request_data, field)
369364
elif field_info._param_type == ParamTypes.query:
370365
query_params |= extract_model_field(request_data, field)
@@ -416,9 +411,7 @@ async def __make_request(
416411
params.body,
417412
)
418413
try:
419-
async with httpx.AsyncClient(
420-
timeout=self.timeout, headers=headers
421-
) as client:
414+
async with httpx.AsyncClient(timeout=self.timeout, headers=headers) as client:
422415
match method:
423416
case Methods.GET:
424417
response = await client.get(url=url, params=query_params)
@@ -452,19 +445,15 @@ async def __make_request(
452445
response = await client.options(url=url, params=query_params)
453446

454447
status_code = response.status_code
455-
logger.debug(
456-
f"{method!s} {url} returned with status code {status_code!s}"
457-
)
448+
logger.debug(f"{method!s} {url} returned with status code {status_code!s}")
458449
response.raise_for_status()
459450
response_body = response.json()
460451

461452
# parse response to pydantic model
462453
parsed_response = response_body
463454
if response_type:
464455
if isinstance(response_body, list):
465-
parsed_response = [
466-
response_type(**item) for item in response_body
467-
]
456+
parsed_response = [response_type(**item) for item in response_body]
468457
elif isinstance(response_body, dict):
469458
parsed_response = response_type(**response_body)
470459
else:
@@ -477,9 +466,7 @@ async def __make_request(
477466
# exception handling
478467
except httpx.HTTPStatusError as exc:
479468
err_response_body = exc.response.json()
480-
raise ArrestHTTPException(
481-
status_code=exc.response.status_code, data=err_response_body
482-
) from exc
469+
raise ArrestHTTPException(status_code=exc.response.status_code, data=err_response_body) from exc
483470

484471
except httpx.TimeoutException:
485472
raise ArrestHTTPException(
@@ -502,9 +489,7 @@ def get_matching_handler(
502489
url = join_url(self.base_url, self.route, parsed_path)
503490
return handler, url
504491

505-
def _bind_handler(
506-
self, base_url: str | None = None, *, handler: ResourceHandler
507-
) -> None:
492+
def _bind_handler(self, base_url: str | None = None, *, handler: ResourceHandler) -> None:
508493
"""
509494
compose a fully-qualified url by joining base service url, resource url
510495
and handler url,
@@ -514,18 +499,14 @@ def _bind_handler(
514499
"""
515500

516501
base_url = base_url or self.base_url
517-
handler.path_regex, handler.path_format, handler.param_types = compile_path(
518-
handler.route
519-
)
502+
handler.path_regex, handler.path_format, handler.param_types = compile_path(handler.route)
520503

521504
self.routes[HandlerKey(*(handler.method, handler.path_format))] = handler
522505

523506
def initialize_handlers(
524507
self,
525508
base_url: str | None = None,
526-
handlers: list[ResourceHandler]
527-
| list[Mapping[str, Any]]
528-
| list[tuple[Any, ...]] = None,
509+
handlers: list[ResourceHandler] | list[Mapping[str, Any]] | list[tuple[Any, ...]] = None,
529510
) -> None:
530511
"""
531512
specifically used to inject `base_url` from a Service class to
@@ -541,18 +522,12 @@ def initialize_handlers(
541522
for _handler in handlers:
542523
try:
543524
if isinstance(_handler, dict):
544-
self._bind_handler(
545-
base_url=base_url, handler=ResourceHandler(**_handler)
546-
)
525+
self._bind_handler(base_url=base_url, handler=ResourceHandler(**_handler))
547526
elif isinstance(_handler, tuple):
548527
if len(_handler) < 2:
549-
raise ValueError(
550-
"Too few arguments to unpack. Expected atleast 2"
551-
)
528+
raise ValueError("Too few arguments to unpack. Expected atleast 2")
552529
if len(_handler) > 5:
553-
raise ValueError(
554-
f"Too many arguments to unpack. Expected 5, got {len(_handler)}"
555-
)
530+
raise ValueError(f"Too many arguments to unpack. Expected 5, got {len(_handler)}")
556531

557532
method, route, rest = (
558533
_handler[0],

arrest/service.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from functools import partial
33
from typing import Any, Optional
44

5-
from arrest.exceptions import NotFoundException
5+
from arrest.exceptions import ResourceNotFound
66
from arrest.http import Methods
77
from arrest.resource import Resource
88

@@ -69,10 +69,8 @@ async def request(self, path: str, method: Methods, **kwargs):
6969
resource, suffix = parts[0], "/".join(parts[1:])
7070

7171
if resource not in self.resources:
72-
raise NotFoundException(message=f"resource {resource} not found")
73-
return await self.resources[resource].request(
74-
path=f"/{suffix}", method=method, **kwargs
75-
)
72+
raise ResourceNotFound(message=f"resource {resource} not found")
73+
return await self.resources[resource].request(path=f"/{suffix}", method=method, **kwargs)
7674

7775
def __getattr__(self, key: str) -> Resource | Any: # pragma: no cover
7876
if hasattr(self, key):

arrest/utils.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import posixpath
2-
import typing
2+
from typing import Any, Type
33
from urllib.parse import urljoin
44

55
import orjson
@@ -31,5 +31,11 @@ def extract_model_field(model: BaseModel, field: str) -> dict:
3131
return value
3232

3333

34-
def jsonify(obj: typing.Any) -> typing.Any:
34+
def jsonify(obj: Any) -> Any:
3535
return orjson.loads(orjson.dumps(obj))
36+
37+
38+
def validate_request_model(type_: Type[BaseModel], obj: Any) -> BaseModel:
39+
if PYDANTIC_VERSION.startswith("2."):
40+
return type_.model_validate(obj)
41+
return type_.parse_obj(obj)

docs/api.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,37 @@
2727
## `ResourceHandler`
2828

2929
::: arrest.handler.ResourceHandler
30+
31+
## Exceptions
32+
33+
### ArrestError
34+
::: arrest.exceptions.ArrestError
35+
base class for all Exception. Used in situations that are not one of the following
36+
37+
### ArrestHTTPException
38+
::: arrest.exceptions.ArrestHTTPException
39+
used for exceptions during HTTP calls
40+
41+
* `.status_code` - **str** status code of the exception, 500 for internal server error
42+
* `.data` - **str** json response for the exception
43+
44+
### NotFoundException
45+
::: arrest.exceptions.NotFoundException
46+
base class for all NotFound-type exceptions
47+
48+
### HandlerNotFound
49+
::: arrest.exceptions.HandlerNotFound
50+
raised when no matching handler is found for the requested path
51+
52+
* `.message` - **str**
53+
54+
### ResourceNotFound
55+
::: arrest.exceptions.ResourceNotFound
56+
raised when no matching resource is found for the service
57+
58+
* `.message` - **str**
59+
60+
61+
### ConversionError
62+
::: arrest.exceptions.ConversionError
63+
raised when Arrest cannot convert path-parameter type using any of the existing converters

0 commit comments

Comments
 (0)