From d3224f07a03bb3f720877a3ed86fb9083b45a3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Thu, 21 Dec 2023 23:21:38 +0300 Subject: [PATCH 01/13] implement #29 --- dataclass_rest/boundmethod.py | 18 +++++++++++++++--- dataclass_rest/methodspec.py | 4 ++-- dataclass_rest/parse_func.py | 17 ++++++++++++----- dataclass_rest/rest.py | 4 ++-- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/dataclass_rest/boundmethod.py b/dataclass_rest/boundmethod.py index f69021a..e4e62bd 100644 --- a/dataclass_rest/boundmethod.py +++ b/dataclass_rest/boundmethod.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from inspect import getcallargs +from inspect import getcallargs, getfullargspec from logging import getLogger from typing import Dict, Any, Callable, Optional, NoReturn, Type @@ -7,6 +7,7 @@ from .exceptions import MalformedResponse from .http_request import HttpRequest, File from .methodspec import MethodSpec +from .parse_func import get_url_params, create_query_params_type logger = getLogger(__name__) @@ -30,7 +31,11 @@ def _apply_args(self, *args, **kwargs) -> Dict: ) def _get_url(self, args) -> str: - return self.method_spec.url_template.format(**args) + if isinstance(self.method_spec.url_template, str): + return self.method_spec.url_template.format(**args) + + args.pop("self") + return self.method_spec.url_template(**args) def _get_body(self, args) -> Any: python_body = args.get(self.method_spec.body_param_name) @@ -39,8 +44,15 @@ def _get_body(self, args) -> Any: ) def _get_query_params(self, args) -> Any: + query_params_type = self.method_spec.query_params_type + + if not isinstance(self.method_spec.url_template, str): + url_params = get_url_params(self.method_spec.url_template, args) + skipped_params = url_params + self.method_spec.file_param_names + [self.method_spec.body_param_name] + query_params_type = create_query_params_type(getfullargspec(self.method_spec.func), self.method_spec.func, skipped_params) + return self.client.request_args_factory.dump( - args, self.method_spec.query_params_type, + args, query_params_type, ) def _get_files(self, args) -> Dict[str, File]: diff --git a/dataclass_rest/methodspec.py b/dataclass_rest/methodspec.py index bf42c61..032f0c0 100644 --- a/dataclass_rest/methodspec.py +++ b/dataclass_rest/methodspec.py @@ -1,11 +1,11 @@ -from typing import Any, Dict, Type, Callable, List +from typing import Any, Dict, Type, Callable, List, Union class MethodSpec: def __init__( self, func: Callable, - url_template: str, + url_template: Union[str | Callable[..., str]], http_method: str, response_type: Type, body_param_name: str, diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index 3c02e8f..1377a53 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -1,6 +1,6 @@ import string -from inspect import getfullargspec, FullArgSpec, isclass -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict +from inspect import getfullargspec, FullArgSpec, isclass, getcallargs +from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union, Optional from .http_request import File from .methodspec import MethodSpec @@ -8,7 +8,14 @@ DEFAULT_BODY_PARAM = "body" -def get_url_params(url_template: str) -> List[str]: +def get_url_params(url_template: Union[str | Callable[..., str]], callback_kwargs: Optional[dict[str, Any]] = None) -> List[str]: + if not isinstance(url_template, str) and not callback_kwargs: + return [] + + if not isinstance(url_template, str) and callback_kwargs: + parsed_format = string.Formatter().parse(url_template(callback_kwargs)) + return [x[1] for x in parsed_format] + parsed_format = string.Formatter().parse(url_template) return [x[1] for x in parsed_format] @@ -54,7 +61,7 @@ def get_file_params(spec): def parse_func( func: Callable, method: str, - url_template: str, + url_template: Union[str | Callable[..., str]], additional_params: Dict[str, Any], is_json_request: bool, body_param_name: str, @@ -62,7 +69,7 @@ def parse_func( spec = getfullargspec(func) url_params = get_url_params(url_template) file_params = get_file_params(spec) - skipped_params = url_params + file_params + [body_param_name] + skipped_params = url_params + file_params + [body_param_name] if url_params else [] return MethodSpec( func=func, http_method=method, diff --git a/dataclass_rest/rest.py b/dataclass_rest/rest.py index 9394811..686ebcc 100644 --- a/dataclass_rest/rest.py +++ b/dataclass_rest/rest.py @@ -1,5 +1,5 @@ from functools import partial -from typing import Any, Dict, Optional, Callable +from typing import Any, Dict, Optional, Callable, Union from .boundmethod import BoundMethod from .method import Method @@ -7,7 +7,7 @@ def rest( - url_template: str, + url_template: Union[str | Callable[..., str]], *, method: str, body_name: str = DEFAULT_BODY_PARAM, From 0f9ad40a209f9cdf0e64629ac5bb126313d21db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 00:44:37 +0300 Subject: [PATCH 02/13] fixes --- dataclass_rest/parse_func.py | 21 +++++++-------------- dataclass_rest/rest.py | 4 +++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index 1377a53..f690af4 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -1,6 +1,6 @@ import string -from inspect import getfullargspec, FullArgSpec, isclass, getcallargs -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union, Optional +from inspect import getfullargspec, FullArgSpec, isclass +from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict from .http_request import File from .methodspec import MethodSpec @@ -8,15 +8,8 @@ DEFAULT_BODY_PARAM = "body" -def get_url_params(url_template: Union[str | Callable[..., str]], callback_kwargs: Optional[dict[str, Any]] = None) -> List[str]: - if not isinstance(url_template, str) and not callback_kwargs: - return [] - - if not isinstance(url_template, str) and callback_kwargs: - parsed_format = string.Formatter().parse(url_template(callback_kwargs)) - return [x[1] for x in parsed_format] - - parsed_format = string.Formatter().parse(url_template) +def get_url_params(url_template: Callable[..., str], callback_kwargs: dict[str, Any]) -> List[str]: + parsed_format = string.Formatter().parse(url_template(callback_kwargs)) return [x[1] for x in parsed_format] @@ -61,15 +54,15 @@ def get_file_params(spec): def parse_func( func: Callable, method: str, - url_template: Union[str | Callable[..., str]], + url_template: Callable[..., str], additional_params: Dict[str, Any], is_json_request: bool, body_param_name: str, ) -> MethodSpec: spec = getfullargspec(func) - url_params = get_url_params(url_template) file_params = get_file_params(spec) - skipped_params = url_params + file_params + [body_param_name] if url_params else [] + skipped_params = [] + return MethodSpec( func=func, http_method=method, diff --git a/dataclass_rest/rest.py b/dataclass_rest/rest.py index 686ebcc..243c65f 100644 --- a/dataclass_rest/rest.py +++ b/dataclass_rest/rest.py @@ -19,10 +19,12 @@ def rest( additional_params = {} def dec(func: Callable) -> Method: + new_url_template = (lambda *args, **kwargs: url_template) if isinstance(url_template, str) else url_template + method_spec = parse_func( func=func, body_param_name=body_name, - url_template=url_template, + url_template=new_url_template, method=method, additional_params=additional_params, is_json_request=send_json, From a4bc4a4b497e0eb6177c088eecfb40177df99d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 00:47:36 +0300 Subject: [PATCH 03/13] remove isinstance str checking --- dataclass_rest/boundmethod.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dataclass_rest/boundmethod.py b/dataclass_rest/boundmethod.py index e4e62bd..824d206 100644 --- a/dataclass_rest/boundmethod.py +++ b/dataclass_rest/boundmethod.py @@ -31,9 +31,6 @@ def _apply_args(self, *args, **kwargs) -> Dict: ) def _get_url(self, args) -> str: - if isinstance(self.method_spec.url_template, str): - return self.method_spec.url_template.format(**args) - args.pop("self") return self.method_spec.url_template(**args) From a8c097d9b9d9ed77aa9bd126eee1cbba0cc6f466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 00:49:12 +0300 Subject: [PATCH 04/13] remove isinstance str checking again --- dataclass_rest/boundmethod.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dataclass_rest/boundmethod.py b/dataclass_rest/boundmethod.py index 824d206..199fcf4 100644 --- a/dataclass_rest/boundmethod.py +++ b/dataclass_rest/boundmethod.py @@ -41,12 +41,9 @@ def _get_body(self, args) -> Any: ) def _get_query_params(self, args) -> Any: - query_params_type = self.method_spec.query_params_type - - if not isinstance(self.method_spec.url_template, str): - url_params = get_url_params(self.method_spec.url_template, args) - skipped_params = url_params + self.method_spec.file_param_names + [self.method_spec.body_param_name] - query_params_type = create_query_params_type(getfullargspec(self.method_spec.func), self.method_spec.func, skipped_params) + url_params = get_url_params(self.method_spec.url_template, args) + skipped_params = url_params + self.method_spec.file_param_names + [self.method_spec.body_param_name] + query_params_type = create_query_params_type(getfullargspec(self.method_spec.func), self.method_spec.func, skipped_params) return self.client.request_args_factory.dump( args, query_params_type, From b589ed135b0ef21424c45cfa2b746331824a83bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:28:34 +0300 Subject: [PATCH 05/13] split parsing and execution logic, add support for kwonly args, fix support for older versions of python --- dataclass_rest/boundmethod.py | 22 +++++++++++-------- dataclass_rest/methodspec.py | 13 ++++++++++-- dataclass_rest/parse_func.py | 40 +++++++++++++++++++++++++++++------ dataclass_rest/rest.py | 6 ++---- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/dataclass_rest/boundmethod.py b/dataclass_rest/boundmethod.py index 199fcf4..c27acf8 100644 --- a/dataclass_rest/boundmethod.py +++ b/dataclass_rest/boundmethod.py @@ -1,5 +1,7 @@ +import copy + from abc import ABC, abstractmethod -from inspect import getcallargs, getfullargspec +from inspect import getcallargs from logging import getLogger from typing import Dict, Any, Callable, Optional, NoReturn, Type @@ -7,7 +9,6 @@ from .exceptions import MalformedResponse from .http_request import HttpRequest, File from .methodspec import MethodSpec -from .parse_func import get_url_params, create_query_params_type logger = getLogger(__name__) @@ -31,8 +32,15 @@ def _apply_args(self, *args, **kwargs) -> Dict: ) def _get_url(self, args) -> str: - args.pop("self") - return self.method_spec.url_template(**args) + args = copy.copy(args) + + if not self.method_spec.url_template_func_arg_spec: + return self.method_spec.url_template_func(**args) + + for arg in self.method_spec.url_template_func_pop_args: + args.pop(arg) + + return self.method_spec.url_template_func(**args) def _get_body(self, args) -> Any: python_body = args.get(self.method_spec.body_param_name) @@ -41,12 +49,8 @@ def _get_body(self, args) -> Any: ) def _get_query_params(self, args) -> Any: - url_params = get_url_params(self.method_spec.url_template, args) - skipped_params = url_params + self.method_spec.file_param_names + [self.method_spec.body_param_name] - query_params_type = create_query_params_type(getfullargspec(self.method_spec.func), self.method_spec.func, skipped_params) - return self.client.request_args_factory.dump( - args, query_params_type, + args, self.method_spec.query_params_type, ) def _get_files(self, args) -> Dict[str, File]: diff --git a/dataclass_rest/methodspec.py b/dataclass_rest/methodspec.py index 032f0c0..593d938 100644 --- a/dataclass_rest/methodspec.py +++ b/dataclass_rest/methodspec.py @@ -1,11 +1,16 @@ -from typing import Any, Dict, Type, Callable, List, Union +from inspect import FullArgSpec +from typing import Any, Dict, Type, Callable, List, Optional class MethodSpec: def __init__( self, func: Callable, - url_template: Union[str | Callable[..., str]], + func_arg_spec: FullArgSpec, + url_template: Optional[str], + url_template_func: Optional[Callable[..., str]], + url_template_func_arg_spec: Optional[FullArgSpec], + url_template_func_pop_args: Optional[List[str]], http_method: str, response_type: Type, body_param_name: str, @@ -17,6 +22,8 @@ def __init__( ): self.func = func self.url_template = url_template + self.url_template_func = url_template_func + self.url_template_func_pop_args = url_template_func_pop_args self.http_method = http_method self.response_type = response_type self.body_param_name = body_param_name @@ -25,3 +32,5 @@ def __init__( self.additional_params = additional_params self.is_json_request = is_json_request self.file_param_names = file_param_names + self.func_arg_spec = func_arg_spec + self.url_template_func_arg_spec = url_template_func_arg_spec diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index f690af4..4529e9e 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -1,6 +1,6 @@ import string from inspect import getfullargspec, FullArgSpec, isclass -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict +from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union from .http_request import File from .methodspec import MethodSpec @@ -8,9 +8,14 @@ DEFAULT_BODY_PARAM = "body" -def get_url_params(url_template: Callable[..., str], callback_kwargs: dict[str, Any]) -> List[str]: - parsed_format = string.Formatter().parse(url_template(callback_kwargs)) - return [x[1] for x in parsed_format] +def get_url_params(url_template: Union[str, Callable[..., str]]) -> List[str]: + is_string = isinstance(url_template, str) + + if is_string: + parsed_format = string.Formatter().parse(url_template) + return [x[1] for x in parsed_format] + else: + return getfullargspec(url_template).args def create_query_params_type( @@ -54,19 +59,40 @@ def get_file_params(spec): def parse_func( func: Callable, method: str, - url_template: Callable[..., str], + url_template: Union[str, Callable[..., str]], additional_params: Dict[str, Any], is_json_request: bool, body_param_name: str, ) -> MethodSpec: spec = getfullargspec(func) file_params = get_file_params(spec) - skipped_params = [] + + is_string_url_template = isinstance(url_template, str) + url_template_func = url_template.format if is_string_url_template else url_template + + try: + url_template_func_arg_spec = getfullargspec(url_template_func) + + url_template_func_args = set(url_template_func_arg_spec.args) + diff_kwargs = set(spec.kwonlyargs).difference(url_template_func_args) + diff_args = set(spec.args).difference(url_template_func_args) + + url_template_func_pop_args = diff_args.union(diff_kwargs) + except TypeError as _exc: + url_template_func_arg_spec = None + url_template_func_pop_args = None + + url_params = get_url_params(url_template if is_string_url_template else url_template_func) + skipped_params = url_params + file_params + [body_param_name] return MethodSpec( func=func, + func_arg_spec=spec, http_method=method, - url_template=url_template, + url_template=url_template if is_string_url_template else None, + url_template_func=url_template_func, + url_template_func_arg_spec=url_template_func_arg_spec, + url_template_func_pop_args=url_template_func_pop_args, 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), diff --git a/dataclass_rest/rest.py b/dataclass_rest/rest.py index 243c65f..663bf6d 100644 --- a/dataclass_rest/rest.py +++ b/dataclass_rest/rest.py @@ -7,7 +7,7 @@ def rest( - url_template: Union[str | Callable[..., str]], + url_template: Union[str, Callable[..., str]], *, method: str, body_name: str = DEFAULT_BODY_PARAM, @@ -19,12 +19,10 @@ def rest( additional_params = {} def dec(func: Callable) -> Method: - new_url_template = (lambda *args, **kwargs: url_template) if isinstance(url_template, str) else url_template - method_spec = parse_func( func=func, body_param_name=body_name, - url_template=new_url_template, + url_template=url_template, method=method, additional_params=additional_params, is_json_request=send_json, From cf8822697213929ab814a735bde96d2bb08cc2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:34:31 +0300 Subject: [PATCH 06/13] remove unused getfullargspec call --- dataclass_rest/parse_func.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index 4529e9e..8caf27e 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -1,6 +1,6 @@ import string from inspect import getfullargspec, FullArgSpec, isclass -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union +from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union, Optional from .http_request import File from .methodspec import MethodSpec @@ -8,14 +8,14 @@ DEFAULT_BODY_PARAM = "body" -def get_url_params(url_template: Union[str, Callable[..., str]]) -> List[str]: +def get_url_params(url_template: Union[str, Callable[..., str]], arg_spec: Optional[FullArgSpec] = None) -> List[str]: is_string = isinstance(url_template, str) if is_string: parsed_format = string.Formatter().parse(url_template) return [x[1] for x in parsed_format] else: - return getfullargspec(url_template).args + return arg_spec.args def create_query_params_type( @@ -82,7 +82,11 @@ def parse_func( url_template_func_arg_spec = None url_template_func_pop_args = None - url_params = get_url_params(url_template if is_string_url_template else url_template_func) + if is_string_url_template: + url_params = get_url_params(url_template) + else: + url_params = get_url_params(url_template_func, url_template_func_arg_spec) + skipped_params = url_params + file_params + [body_param_name] return MethodSpec( From 6c68a08a890e0d8812e01da6348a88509ee11af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:02:29 +0300 Subject: [PATCH 07/13] remove unused method spec attrs --- dataclass_rest/boundmethod.py | 2 +- dataclass_rest/methodspec.py | 5 ----- dataclass_rest/parse_func.py | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/dataclass_rest/boundmethod.py b/dataclass_rest/boundmethod.py index c27acf8..367f0aa 100644 --- a/dataclass_rest/boundmethod.py +++ b/dataclass_rest/boundmethod.py @@ -34,7 +34,7 @@ def _apply_args(self, *args, **kwargs) -> Dict: def _get_url(self, args) -> str: args = copy.copy(args) - if not self.method_spec.url_template_func_arg_spec: + if not self.method_spec.url_template_func_pop_args: return self.method_spec.url_template_func(**args) for arg in self.method_spec.url_template_func_pop_args: diff --git a/dataclass_rest/methodspec.py b/dataclass_rest/methodspec.py index 593d938..0101213 100644 --- a/dataclass_rest/methodspec.py +++ b/dataclass_rest/methodspec.py @@ -1,4 +1,3 @@ -from inspect import FullArgSpec from typing import Any, Dict, Type, Callable, List, Optional @@ -6,10 +5,8 @@ class MethodSpec: def __init__( self, func: Callable, - func_arg_spec: FullArgSpec, url_template: Optional[str], url_template_func: Optional[Callable[..., str]], - url_template_func_arg_spec: Optional[FullArgSpec], url_template_func_pop_args: Optional[List[str]], http_method: str, response_type: Type, @@ -32,5 +29,3 @@ def __init__( self.additional_params = additional_params self.is_json_request = is_json_request self.file_param_names = file_param_names - self.func_arg_spec = func_arg_spec - self.url_template_func_arg_spec = url_template_func_arg_spec diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index 8caf27e..f114ed3 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -91,11 +91,9 @@ def parse_func( return MethodSpec( func=func, - func_arg_spec=spec, http_method=method, url_template=url_template if is_string_url_template else None, url_template_func=url_template_func, - url_template_func_arg_spec=url_template_func_arg_spec, url_template_func_pop_args=url_template_func_pop_args, query_params_type=create_query_params_type(spec, func, skipped_params), body_type=create_body_type(spec, body_param_name), From 6f2b4b3df20dda0c4b1514fcd3b2e92c807c01e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:24:24 +0300 Subject: [PATCH 08/13] remove unused get_url_params func, add get_url_params_from_string function, remove unused try except --- dataclass_rest/parse_func.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index f114ed3..0fbc483 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -1,6 +1,6 @@ import string from inspect import getfullargspec, FullArgSpec, isclass -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union, Optional +from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union from .http_request import File from .methodspec import MethodSpec @@ -8,14 +8,9 @@ DEFAULT_BODY_PARAM = "body" -def get_url_params(url_template: Union[str, Callable[..., str]], arg_spec: Optional[FullArgSpec] = None) -> List[str]: - is_string = isinstance(url_template, str) - - if is_string: - parsed_format = string.Formatter().parse(url_template) - return [x[1] for x in parsed_format] - else: - return arg_spec.args +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] def create_query_params_type( @@ -70,7 +65,9 @@ def parse_func( is_string_url_template = isinstance(url_template, str) url_template_func = url_template.format if is_string_url_template else url_template - try: + url_template_func_pop_args = None + + if not is_string_url_template: url_template_func_arg_spec = getfullargspec(url_template_func) url_template_func_args = set(url_template_func_arg_spec.args) @@ -78,14 +75,9 @@ def parse_func( diff_args = set(spec.args).difference(url_template_func_args) url_template_func_pop_args = diff_args.union(diff_kwargs) - except TypeError as _exc: - url_template_func_arg_spec = None - url_template_func_pop_args = None - - if is_string_url_template: - url_params = get_url_params(url_template) + url_params = url_template_func_arg_spec.args else: - url_params = get_url_params(url_template_func, url_template_func_arg_spec) + url_params = get_url_params_from_string(url_template) skipped_params = url_params + file_params + [body_param_name] From 5f4fcd728568809ff86ccaee6cf0ef2565bca861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=9B=D1=8E=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9?= <100635212+lubaskinc0de@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:27:58 +0300 Subject: [PATCH 09/13] some refactoring --- dataclass_rest/parse_func.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dataclass_rest/parse_func.py b/dataclass_rest/parse_func.py index 0fbc483..fa5c826 100644 --- a/dataclass_rest/parse_func.py +++ b/dataclass_rest/parse_func.py @@ -69,13 +69,14 @@ def parse_func( if not is_string_url_template: url_template_func_arg_spec = getfullargspec(url_template_func) + url_template_func_args = url_template_func_arg_spec.args - url_template_func_args = set(url_template_func_arg_spec.args) - diff_kwargs = set(spec.kwonlyargs).difference(url_template_func_args) - diff_args = set(spec.args).difference(url_template_func_args) + url_template_func_args_set = set(url_template_func_args) + diff_kwargs = set(spec.kwonlyargs).difference(url_template_func_args_set) + diff_args = set(spec.args).difference(url_template_func_args_set) url_template_func_pop_args = diff_args.union(diff_kwargs) - url_params = url_template_func_arg_spec.args + url_params = url_template_func_args else: url_params = get_url_params_from_string(url_template) From 98b254025effb58790c519e0fbd5ba216230ad07 Mon Sep 17 00:00:00 2001 From: lubaskincode Date: Sun, 28 Jul 2024 20:25:20 +0300 Subject: [PATCH 10/13] move to src --- {dataclass_rest => src/dataclass_rest}/__init__.py | 0 {dataclass_rest => src/dataclass_rest}/base_client.py | 0 {dataclass_rest => src/dataclass_rest}/boundmethod.py | 0 {dataclass_rest => src/dataclass_rest}/client_protocol.py | 0 {dataclass_rest => src/dataclass_rest}/exceptions.py | 0 {dataclass_rest => src/dataclass_rest}/http/__init__.py | 0 {dataclass_rest => src/dataclass_rest}/http/aiohttp.py | 0 {dataclass_rest => src/dataclass_rest}/http/requests.py | 0 {dataclass_rest => src/dataclass_rest}/http_request.py | 0 {dataclass_rest => src/dataclass_rest}/method.py | 0 {dataclass_rest => src/dataclass_rest}/methodspec.py | 0 {dataclass_rest => src/dataclass_rest}/parse_func.py | 0 {dataclass_rest => src/dataclass_rest}/rest.py | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename {dataclass_rest => src/dataclass_rest}/__init__.py (100%) rename {dataclass_rest => src/dataclass_rest}/base_client.py (100%) rename {dataclass_rest => src/dataclass_rest}/boundmethod.py (100%) rename {dataclass_rest => src/dataclass_rest}/client_protocol.py (100%) rename {dataclass_rest => src/dataclass_rest}/exceptions.py (100%) rename {dataclass_rest => src/dataclass_rest}/http/__init__.py (100%) rename {dataclass_rest => src/dataclass_rest}/http/aiohttp.py (100%) rename {dataclass_rest => src/dataclass_rest}/http/requests.py (100%) rename {dataclass_rest => src/dataclass_rest}/http_request.py (100%) rename {dataclass_rest => src/dataclass_rest}/method.py (100%) rename {dataclass_rest => src/dataclass_rest}/methodspec.py (100%) rename {dataclass_rest => src/dataclass_rest}/parse_func.py (100%) rename {dataclass_rest => src/dataclass_rest}/rest.py (100%) diff --git a/dataclass_rest/__init__.py b/src/dataclass_rest/__init__.py similarity index 100% rename from dataclass_rest/__init__.py rename to src/dataclass_rest/__init__.py diff --git a/dataclass_rest/base_client.py b/src/dataclass_rest/base_client.py similarity index 100% rename from dataclass_rest/base_client.py rename to src/dataclass_rest/base_client.py diff --git a/dataclass_rest/boundmethod.py b/src/dataclass_rest/boundmethod.py similarity index 100% rename from dataclass_rest/boundmethod.py rename to src/dataclass_rest/boundmethod.py diff --git a/dataclass_rest/client_protocol.py b/src/dataclass_rest/client_protocol.py similarity index 100% rename from dataclass_rest/client_protocol.py rename to src/dataclass_rest/client_protocol.py diff --git a/dataclass_rest/exceptions.py b/src/dataclass_rest/exceptions.py similarity index 100% rename from dataclass_rest/exceptions.py rename to src/dataclass_rest/exceptions.py diff --git a/dataclass_rest/http/__init__.py b/src/dataclass_rest/http/__init__.py similarity index 100% rename from dataclass_rest/http/__init__.py rename to src/dataclass_rest/http/__init__.py diff --git a/dataclass_rest/http/aiohttp.py b/src/dataclass_rest/http/aiohttp.py similarity index 100% rename from dataclass_rest/http/aiohttp.py rename to src/dataclass_rest/http/aiohttp.py diff --git a/dataclass_rest/http/requests.py b/src/dataclass_rest/http/requests.py similarity index 100% rename from dataclass_rest/http/requests.py rename to src/dataclass_rest/http/requests.py diff --git a/dataclass_rest/http_request.py b/src/dataclass_rest/http_request.py similarity index 100% rename from dataclass_rest/http_request.py rename to src/dataclass_rest/http_request.py diff --git a/dataclass_rest/method.py b/src/dataclass_rest/method.py similarity index 100% rename from dataclass_rest/method.py rename to src/dataclass_rest/method.py diff --git a/dataclass_rest/methodspec.py b/src/dataclass_rest/methodspec.py similarity index 100% rename from dataclass_rest/methodspec.py rename to src/dataclass_rest/methodspec.py diff --git a/dataclass_rest/parse_func.py b/src/dataclass_rest/parse_func.py similarity index 100% rename from dataclass_rest/parse_func.py rename to src/dataclass_rest/parse_func.py diff --git a/dataclass_rest/rest.py b/src/dataclass_rest/rest.py similarity index 100% rename from dataclass_rest/rest.py rename to src/dataclass_rest/rest.py From d8ca305d4bc9802a50fe8c9885183c38fe4a350f Mon Sep 17 00:00:00 2001 From: lubaskincode Date: Mon, 29 Jul 2024 15:32:00 +0300 Subject: [PATCH 11/13] refactoring --- src/dataclass_rest/__init__.py | 6 ++- src/dataclass_rest/boundmethod.py | 47 ++++++++++---------- src/dataclass_rest/client_protocol.py | 7 ++- src/dataclass_rest/http/aiohttp.py | 9 ++-- src/dataclass_rest/http/requests.py | 7 ++- src/dataclass_rest/method.py | 10 +++-- src/dataclass_rest/methodspec.py | 30 ++++++------- src/dataclass_rest/parse_func.py | 63 ++++++++++++++------------- src/dataclass_rest/rest.py | 4 +- tests/requests/conftest.py | 3 +- tests/requests/test_factory.py | 24 ++++++---- tests/requests/test_params.py | 12 +++-- tests/test_init.py | 16 ++++--- 13 files changed, 132 insertions(+), 106 deletions(-) diff --git a/src/dataclass_rest/__init__.py b/src/dataclass_rest/__init__.py index 064320e..e472fa8 100644 --- a/src/dataclass_rest/__init__.py +++ b/src/dataclass_rest/__init__.py @@ -1,7 +1,11 @@ __all__ = [ "File", "rest", - "get", "put", "post", "patch", "delete", + "get", + "put", + "post", + "patch", + "delete", ] from .http_request import File diff --git a/src/dataclass_rest/boundmethod.py b/src/dataclass_rest/boundmethod.py index 3123577..e74c560 100644 --- a/src/dataclass_rest/boundmethod.py +++ b/src/dataclass_rest/boundmethod.py @@ -1,4 +1,3 @@ -import copy from abc import ABC, abstractmethod from inspect import getcallargs @@ -15,11 +14,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 @@ -28,29 +27,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: - args = copy.copy(args) - - if not self.method_spec.url_template_func_pop_args: - return self.method_spec.url_template_func(**args) - - for arg in self.method_spec.url_template_func_pop_args: - args.pop(arg) - - return self.method_spec.url_template_func(**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]: @@ -61,11 +62,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, diff --git a/src/dataclass_rest/client_protocol.py b/src/dataclass_rest/client_protocol.py index 1ed30d5..454b28f 100644 --- a/src/dataclass_rest/client_protocol.py +++ b/src/dataclass_rest/client_protocol.py @@ -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 @@ -37,6 +39,7 @@ class ClientProtocol(Protocol): method_class: Optional[Callable] def do_request( - self, request: HttpRequest, + self, + request: HttpRequest, ) -> Any: raise NotImplementedError diff --git a/src/dataclass_rest/http/aiohttp.py b/src/dataclass_rest/http/aiohttp.py index e8e400f..a8e2d60 100644 --- a/src/dataclass_rest/http/aiohttp.py +++ b/src/dataclass_rest/http/aiohttp.py @@ -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() @@ -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: diff --git a/src/dataclass_rest/http/requests.py b/src/dataclass_rest/http/requests.py index 6b17484..f8c58a8 100644 --- a/src/dataclass_rest/http/requests.py +++ b/src/dataclass_rest/http/requests.py @@ -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) @@ -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() diff --git a/src/dataclass_rest/method.py b/src/dataclass_rest/method.py index 23c11fa..b924995 100644 --- a/src/dataclass_rest/method.py +++ b/src/dataclass_rest/method.py @@ -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 @@ -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, diff --git a/src/dataclass_rest/methodspec.py b/src/dataclass_rest/methodspec.py index 0101213..e8032c4 100644 --- a/src/dataclass_rest/methodspec.py +++ b/src/dataclass_rest/methodspec.py @@ -1,26 +1,24 @@ -from typing import Any, Dict, Type, Callable, List, Optional +from typing import Any, Callable, Dict, List, Type class MethodSpec: def __init__( - self, - func: Callable, - url_template: Optional[str], - url_template_func: Optional[Callable[..., str]], - url_template_func_pop_args: Optional[List[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_template_func = url_template_func - self.url_template_func_pop_args = url_template_func_pop_args + self.url_params = url_params self.http_method = http_method self.response_type = response_type self.body_param_name = body_param_name diff --git a/src/dataclass_rest/parse_func.py b/src/dataclass_rest/parse_func.py index fa5c826..9714d94 100644 --- a/src/dataclass_rest/parse_func.py +++ b/src/dataclass_rest/parse_func.py @@ -1,22 +1,33 @@ import string -from inspect import getfullargspec, FullArgSpec, isclass -from typing import Callable, List, Sequence, Any, Type, TypedDict, Dict, Union +from inspect import FullArgSpec, getfullargspec, isclass +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_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 @@ -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) @@ -52,31 +63,24 @@ def get_file_params(spec): def parse_func( - func: Callable, - method: str, - url_template: Union[str, Callable[..., 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) file_params = get_file_params(spec) is_string_url_template = isinstance(url_template, str) - url_template_func = url_template.format if is_string_url_template else url_template - - url_template_func_pop_args = None + url_template_callable = ( + url_template.format if is_string_url_template else url_template + ) if not is_string_url_template: - url_template_func_arg_spec = getfullargspec(url_template_func) - url_template_func_args = url_template_func_arg_spec.args - - url_template_func_args_set = set(url_template_func_args) - diff_kwargs = set(spec.kwonlyargs).difference(url_template_func_args_set) - diff_args = set(spec.args).difference(url_template_func_args_set) - - url_template_func_pop_args = diff_args.union(diff_kwargs) - url_params = url_template_func_args + url_template_func_arg_spec = getfullargspec(url_template_callable) + url_params = url_template_func_arg_spec.args else: url_params = get_url_params_from_string(url_template) @@ -85,9 +89,8 @@ def parse_func( return MethodSpec( func=func, http_method=method, - url_template=url_template if is_string_url_template else None, - url_template_func=url_template_func, - url_template_func_pop_args=url_template_func_pop_args, + 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), diff --git a/src/dataclass_rest/rest.py b/src/dataclass_rest/rest.py index 21e03b0..61e29d1 100644 --- a/src/dataclass_rest/rest.py +++ b/src/dataclass_rest/rest.py @@ -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, diff --git a/tests/requests/conftest.py b/tests/requests/conftest.py index 849f945..2950ca8 100644 --- a/tests/requests/conftest.py +++ b/tests/requests/conftest.py @@ -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 diff --git a/tests/requests/test_factory.py b/tests/requests/test_factory.py index d6a5ebc..72ae49f 100644 --- a/tests/requests/test_factory.py +++ b/tests/requests/test_factory.py @@ -27,19 +27,25 @@ class ResponseBody: def test_body(session, mocker): class Api(RequestsClient): def _init_request_body_factory(self) -> Retort: - return Retort(recipe=[ - name_mapping(name_style=NameStyle.CAMEL), - ]) + return Retort( + recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ], + ) def _init_request_args_factory(self) -> Retort: - return Retort(recipe=[ - name_mapping(name_style=NameStyle.UPPER_DOT), - ]) + return Retort( + recipe=[ + name_mapping(name_style=NameStyle.UPPER_DOT), + ], + ) def _init_response_body_factory(self) -> Retort: - return Retort(recipe=[ - name_mapping(name_style=NameStyle.LOWER_KEBAB), - ]) + return Retort( + recipe=[ + name_mapping(name_style=NameStyle.LOWER_KEBAB), + ], + ) @patch("/post/") def post_x(self, long_param: str, body: RequestBody) -> ResponseBody: diff --git a/tests/requests/test_params.py b/tests/requests/test_params.py index bd8405b..d91fc56 100644 --- a/tests/requests/test_params.py +++ b/tests/requests/test_params.py @@ -46,15 +46,18 @@ def post_x(self, id: str, param: Optional[int]) -> List[int]: mocker.post( url="http://example.com/post/x?", - text="[0]", complete_qs=True, + text="[0]", + complete_qs=True, ) mocker.post( url="http://example.com/post/x?param=1", - text="[1]", complete_qs=True, + text="[1]", + complete_qs=True, ) mocker.post( url="http://example.com/post/x?param=2", - text="[1,2]", complete_qs=True, + text="[1,2]", + complete_qs=True, ) client = Api(base_url="http://example.com", session=session) assert client.post_x("x", None) == [0] @@ -76,7 +79,8 @@ def post_x(self, body: RequestBody) -> None: mocker.post( url="http://example.com/post/", - text="null", complete_qs=True, + text="null", + complete_qs=True, ) client = Api(base_url="http://example.com", session=session) assert client.post_x(RequestBody(x=1, y="test")) is None diff --git a/tests/test_init.py b/tests/test_init.py index e2226e8..431302f 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -23,9 +23,11 @@ def __init__(self): ) def _init_request_body_factory(self) -> Retort: - return Retort(recipe=[ - name_mapping(name_style=NameStyle.CAMEL), - ]) + return Retort( + recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ], + ) @get("todos/{id}") def get_todo(self, id: str) -> Todo: @@ -41,9 +43,11 @@ def __init__(self): super().__init__("https://jsonplaceholder.typicode.com/") def _init_request_body_factory(self) -> Retort: - return Retort(recipe=[ - name_mapping(name_style=NameStyle.CAMEL), - ]) + return Retort( + recipe=[ + name_mapping(name_style=NameStyle.CAMEL), + ], + ) @get("todos/{id}") async def get_todo(self, id: str) -> Todo: From 3e0be5fe20902586e3be693f671d4fabb28f649e Mon Sep 17 00:00:00 2001 From: lubaskincode Date: Mon, 29 Jul 2024 15:37:43 +0300 Subject: [PATCH 12/13] inline --- src/dataclass_rest/boundmethod.py | 1 - src/dataclass_rest/parse_func.py | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/dataclass_rest/boundmethod.py b/src/dataclass_rest/boundmethod.py index e74c560..c57a222 100644 --- a/src/dataclass_rest/boundmethod.py +++ b/src/dataclass_rest/boundmethod.py @@ -1,4 +1,3 @@ - from abc import ABC, abstractmethod from inspect import getcallargs from logging import getLogger diff --git a/src/dataclass_rest/parse_func.py b/src/dataclass_rest/parse_func.py index 9714d94..acbffab 100644 --- a/src/dataclass_rest/parse_func.py +++ b/src/dataclass_rest/parse_func.py @@ -62,6 +62,13 @@ 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, @@ -78,11 +85,11 @@ def parse_func( url_template.format if is_string_url_template else url_template ) - if not is_string_url_template: - url_template_func_arg_spec = getfullargspec(url_template_callable) - url_params = url_template_func_arg_spec.args - else: - url_params = get_url_params_from_string(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] From c9b6654e9d324184509ef1f2ed215c15f5fcb7ac Mon Sep 17 00:00:00 2001 From: lubaskincode Date: Mon, 29 Jul 2024 17:37:56 +0300 Subject: [PATCH 13/13] tests, docs --- README.md | 27 ++++++++- tests/requests/test_callable_url.py | 94 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tests/requests/test_callable_url.py diff --git a/README.md b/README.md index dc1e039..ad8b6d5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/requests/test_callable_url.py b/tests/requests/test_callable_url.py new file mode 100644 index 0000000..1fa4e54 --- /dev/null +++ b/tests/requests/test_callable_url.py @@ -0,0 +1,94 @@ +from typing import List, Optional + +import pytest +import requests +import requests_mock + +from dataclass_rest import get +from dataclass_rest.http.requests import RequestsClient + + +def static_url() -> str: + return "/get" + + +def param_url(entry_id: int) -> str: + return f"/get/{entry_id}" + + +def kwonly_param_url(entry_id: Optional[int] = None) -> str: + if entry_id: + return f"/get/{entry_id}" + return "/get/random" + + +def test_simple(session: requests.Session, mocker: requests_mock.Mocker): + class Api(RequestsClient): + @get(static_url) + def get_x(self) -> List[int]: + raise NotImplementedError + + mocker.get("http://example.com/get", text="[1,2]", complete_qs=True) + client = Api(base_url="http://example.com", session=session) + assert client.get_x() == [1, 2] + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ( + 1, + 1, + ), + ( + 2, + 2, + ), + ], +) +def test_with_param( + session: requests.Session, + mocker: requests_mock.Mocker, + value: int, + expected: int, +): + class Api(RequestsClient): + @get(param_url) + def get_entry(self, entry_id: int) -> int: + raise NotImplementedError + + url = f"http://example.com/get/{expected}" + mocker.get(url, text=str(expected), complete_qs=True) + + client = Api(base_url="http://example.com", session=session) + assert client.get_entry(value) == expected + + +def test_excess_param(session: requests.Session, mocker: requests_mock.Mocker): + class Api(RequestsClient): + @get(param_url) + def get_entry( + self, entry_id: int, some_param: Optional[int] = None, + ) -> int: + raise NotImplementedError + + mocker.get( + "http://example.com/get/1?some_param=2", text="1", complete_qs=True, + ) + + client = Api(base_url="http://example.com", session=session) + assert client.get_entry(1, 2) == 1 + + +def test_kwonly_param(session: requests.Session, mocker: requests_mock.Mocker): + class Api(RequestsClient): + @get(kwonly_param_url) + def get_entry(self, *, entry_id: Optional[int] = None) -> int: + raise NotImplementedError + + mocker.get("http://example.com/get/1", text="1", complete_qs=True) + mocker.get("http://example.com/get/random", text="2", complete_qs=True) + + client = Api(base_url="http://example.com", session=session) + assert client.get_entry(entry_id=1) == 1 + assert client.get_entry() == 2