Skip to content

Commit

Permalink
Merge pull request #30 from lubaskinc0de/master
Browse files Browse the repository at this point in the history
implement #29
  • Loading branch information
Tishka17 authored Jul 29, 2024
2 parents 92c7203 + c9b6654 commit b3510bb
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 82 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,32 @@ class RealClient(RequestsClient):

@post("todos")
def create_todo(self, body: Todo) -> Todo:
"""Создаем Todo"""
pass
```

You can use Callable ```(...) -> str``` as the url source,
all parameters passed to the client method can be obtained inside the Callable

```python
from requests import Session
from dataclass_rest import get
from dataclass_rest.http.requests import RequestsClient

def url_generator(todo_id: int) -> str:
return f"/todos/{todo_id}/"


class RealClient(RequestsClient):
def __init__(self):
super().__init__("https://dummyjson.com/", Session())

@get(url_generator)
def todo(self, todo_id: int) -> Todo:
pass


client = RealClient()
client.todo(5)
```

## Asyncio
Expand Down
6 changes: 5 additions & 1 deletion src/dataclass_rest/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
__all__ = [
"File",
"rest",
"get", "put", "post", "patch", "delete",
"get",
"put",
"post",
"patch",
"delete",
]

from .http_request import File
Expand Down
38 changes: 24 additions & 14 deletions src/dataclass_rest/boundmethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@

class BoundMethod(ClientMethodProtocol, ABC):
def __init__(
self,
name: str,
method_spec: MethodSpec,
client: ClientProtocol,
on_error: Optional[Callable[[Any], Any]],
self,
name: str,
method_spec: MethodSpec,
client: ClientProtocol,
on_error: Optional[Callable[[Any], Any]],
):
self.name = name
self.method_spec = method_spec
Expand All @@ -26,21 +26,31 @@ def __init__(

def _apply_args(self, *args, **kwargs) -> Dict:
return getcallargs(
self.method_spec.func, self.client, *args, **kwargs,
self.method_spec.func,
self.client,
*args,
**kwargs,
)

def _get_url(self, args) -> str:
return self.method_spec.url_template.format(**args)
args = {
arg: value
for arg, value in args.items()
if arg in self.method_spec.url_params
}
return self.method_spec.url_template(**args)

def _get_body(self, args) -> Any:
python_body = args.get(self.method_spec.body_param_name)
return self.client.request_body_factory.dump(
python_body, self.method_spec.body_type,
python_body,
self.method_spec.body_type,
)

def _get_query_params(self, args) -> Any:
return self.client.request_args_factory.dump(
args, self.method_spec.query_params_type,
args,
self.method_spec.query_params_type,
)

def _get_files(self, args) -> Dict[str, File]:
Expand All @@ -51,11 +61,11 @@ def _get_files(self, args) -> Dict[str, File]:
}

def _create_request(
self,
url: str,
query_params: Any,
files: Dict[str, File],
data: Any,
self,
url: str,
query_params: Any,
files: Dict[str, File],
data: Any,
) -> HttpRequest:
return HttpRequest(
method=self.method_spec.http_method,
Expand Down
7 changes: 5 additions & 2 deletions src/dataclass_rest/client_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def load(self, data: Any, class_: Type[TypeT]) -> TypeT:
raise NotImplementedError

def dump(
self, data: TypeT, class_: Optional[Type[TypeT]] = None,
self,
data: TypeT,
class_: Optional[Type[TypeT]] = None,
) -> Any:
raise NotImplementedError

Expand All @@ -37,6 +39,7 @@ class ClientProtocol(Protocol):
method_class: Optional[Callable]

def do_request(
self, request: HttpRequest,
self,
request: HttpRequest,
) -> Any:
raise NotImplementedError
9 changes: 5 additions & 4 deletions src/dataclass_rest/http/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ class AiohttpClient(BaseClient):
method_class = AiohttpMethod

def __init__(
self,
base_url: str,
session: Optional[ClientSession] = None,
self,
base_url: str,
session: Optional[ClientSession] = None,
):
super().__init__()
self.session = session or ClientSession()
Expand All @@ -68,7 +68,8 @@ async def do_request(self, request: HttpRequest) -> Any:
for name, file in request.files.items():
data.add_field(
name,
filename=file.filename, content_type=file.content_type,
filename=file.filename,
content_type=file.content_type,
value=file.contents,
)
try:
Expand Down
7 changes: 3 additions & 4 deletions src/dataclass_rest/http/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@


class RequestsMethod(SyncMethod):

def _on_error_default(self, response: Response) -> Any:
if 400 <= response.status_code < 500:
raise ClientError(response.status_code)
Expand All @@ -39,9 +38,9 @@ class RequestsClient(BaseClient):
method_class = RequestsMethod

def __init__(
self,
base_url: str,
session: Optional[Session] = None,
self,
base_url: str,
session: Optional[Session] = None,
):
super().__init__()
self.session = session or Session()
Expand Down
10 changes: 6 additions & 4 deletions src/dataclass_rest/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

class Method:
def __init__(
self,
method_spec: MethodSpec,
method_class: Optional[Callable[..., BoundMethod]] = None,
self,
method_spec: MethodSpec,
method_class: Optional[Callable[..., BoundMethod]] = None,
):
self.name = method_spec.func.__name__
self.method_spec = method_spec
Expand All @@ -29,7 +29,9 @@ def __set_name__(self, owner, name):
)

def __get__(
self, instance: Optional[ClientProtocol], objtype=None,
self,
instance: Optional[ClientProtocol],
objtype=None,
) -> BoundMethod:
return self.method_class(
name=self.name,
Expand Down
25 changes: 13 additions & 12 deletions src/dataclass_rest/methodspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@

class MethodSpec:
def __init__(
self,
func: Callable,
*,
url_template: str,
http_method: str,
response_type: Type,
body_param_name: str,
body_type: Type,
is_json_request: bool,
query_params_type: Type,
file_param_names: List[str],
additional_params: Dict[str, Any],
self,
func: Callable,
url_template: Callable[..., str],
url_params: List[str],
http_method: str,
response_type: Type,
body_param_name: str,
body_type: Type,
is_json_request: bool, # noqa: FBT001
query_params_type: Type,
file_param_names: List[str],
additional_params: Dict[str, Any],
):
self.func = func
self.url_template = url_template
self.url_params = url_params
self.http_method = http_method
self.response_type = response_type
self.body_param_name = body_param_name
Expand Down
66 changes: 48 additions & 18 deletions src/dataclass_rest/parse_func.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import string
from inspect import FullArgSpec, getfullargspec, isclass
from typing import Any, Callable, Dict, List, Sequence, Type, TypedDict
from typing import (
Any,
Callable,
Dict,
List,
Sequence,
Type,
TypeAlias,
TypedDict,
Union,
)

from .http_request import File
from .methodspec import MethodSpec

DEFAULT_BODY_PARAM = "body"
UrlTemplate: TypeAlias = Union[str, Callable[..., str]]


def get_url_params(url_template: str) -> List[str]:
def get_url_params_from_string(url_template: str) -> List[str]:
parsed_format = string.Formatter().parse(url_template)
return [x[1] for x in parsed_format]
return [x[1] for x in parsed_format if x[1]]


def create_query_params_type(
spec: FullArgSpec,
func: Callable,
skipped: Sequence[str],
spec: FullArgSpec,
func: Callable,
skipped: Sequence[str],
) -> Type:
fields = {}
self_processed = False
Expand All @@ -31,14 +42,14 @@ def create_query_params_type(


def create_body_type(
spec: FullArgSpec,
body_param_name: str,
spec: FullArgSpec,
body_param_name: str,
) -> Type:
return spec.annotations.get(body_param_name, Any)


def create_response_type(
spec: FullArgSpec,
spec: FullArgSpec,
) -> Type:
return spec.annotations.get("return", Any)

Expand All @@ -51,23 +62,42 @@ def get_file_params(spec):
]


def get_url_params_from_callable(
url_template: Callable[..., str],
) -> List[str]:
url_template_func_arg_spec = getfullargspec(url_template)
return url_template_func_arg_spec.args


def parse_func(
func: Callable,
*,
method: str,
url_template: str,
additional_params: Dict[str, Any],
is_json_request: bool,
body_param_name: str,
func: Callable,
method: str,
url_template: UrlTemplate,
additional_params: Dict[str, Any],
is_json_request: bool, # noqa: FBT001
body_param_name: str,
) -> MethodSpec:
spec = getfullargspec(func)
url_params = get_url_params(url_template)
file_params = get_file_params(spec)

is_string_url_template = isinstance(url_template, str)
url_template_callable = (
url_template.format if is_string_url_template else url_template
)

url_params = (
get_url_params_from_string(url_template)
if is_string_url_template
else get_url_params_from_callable(url_template)
)

skipped_params = url_params + file_params + [body_param_name]

return MethodSpec(
func=func,
http_method=method,
url_template=url_template,
url_template=url_template_callable,
url_params=url_params,
query_params_type=create_query_params_type(spec, func, skipped_params),
body_type=create_body_type(spec, body_param_name),
response_type=create_response_type(spec),
Expand Down
4 changes: 2 additions & 2 deletions src/dataclass_rest/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

from .boundmethod import BoundMethod
from .method import Method
from .parse_func import DEFAULT_BODY_PARAM, parse_func
from .parse_func import DEFAULT_BODY_PARAM, UrlTemplate, parse_func

_Func = TypeVar("_Func", bound=Callable[..., Any])


def rest(
url_template: str,
url_template: UrlTemplate,
*,
method: str,
body_name: str = DEFAULT_BODY_PARAM,
Expand Down
3 changes: 2 additions & 1 deletion tests/requests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def session():
@pytest.fixture
def mocker(session):
with requests_mock.Mocker(
session=session, case_sensitive=True,
session=session,
case_sensitive=True,
) as session_mock:
yield session_mock
Loading

0 comments on commit b3510bb

Please sign in to comment.