py2http 0.1.60__tar.gz → 0.1.61__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py2http-0.1.60 → py2http-0.1.61}/PKG-INFO +16 -4
- {py2http-0.1.60 → py2http-0.1.61}/py2http/__init__.py +6 -6
- {py2http-0.1.60 → py2http-0.1.61}/py2http/bottle_plugins.py +18 -19
- {py2http-0.1.60 → py2http-0.1.61}/py2http/config.py +20 -19
- {py2http-0.1.60 → py2http-0.1.61}/py2http/constants.py +6 -5
- {py2http-0.1.60 → py2http-0.1.61}/py2http/decorators.py +69 -67
- {py2http-0.1.60 → py2http-0.1.61}/py2http/default_configs.py +38 -38
- {py2http-0.1.60 → py2http-0.1.61}/py2http/diagnosis.py +1 -2
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/example_service.py +2 -2
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/example_service_bottle.py +5 -5
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/example_service_flask.py +3 -3
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/object_dispatch.py +13 -10
- {py2http-0.1.60 → py2http-0.1.61}/py2http/middleware.py +21 -20
- {py2http-0.1.60 → py2http-0.1.61}/py2http/openapi_utils.py +82 -82
- {py2http-0.1.60 → py2http-0.1.61}/py2http/route_utils.py +11 -10
- {py2http-0.1.60 → py2http-0.1.61}/py2http/schema_tools.py +96 -54
- {py2http-0.1.60 → py2http-0.1.61}/py2http/service.py +112 -95
- py2http-0.1.61/py2http/tests/example_service_test.py +33 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/simple_run_process_test.py +9 -6
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/test_decorators.py +9 -9
- py2http-0.1.61/py2http/tests/test_not_set_defaults.py +73 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/test_p2h2p.py +14 -15
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/utils_for_testing.py +4 -4
- {py2http-0.1.60 → py2http-0.1.61}/py2http/types.py +6 -4
- {py2http-0.1.60 → py2http-0.1.61}/py2http/util.py +45 -39
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/PKG-INFO +16 -4
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/SOURCES.txt +1 -0
- {py2http-0.1.60 → py2http-0.1.61}/setup.cfg +1 -2
- py2http-0.1.60/py2http/tests/example_service_test.py +0 -33
- {py2http-0.1.60 → py2http-0.1.61}/LICENSE +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/README.md +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/__init__.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/custom_input_mapper.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/examples/custom_input_mappers.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/__init__.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/objects_for_testing.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http/tests/test_flask.py +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/dependency_links.txt +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/not-zip-safe +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/requires.txt +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/py2http.egg-info/top_level.txt +0 -0
- {py2http-0.1.60 → py2http-0.1.61}/setup.py +0 -0
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: py2http
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.61
|
|
4
4
|
Summary: Declarative HTTP service entry point.
|
|
5
5
|
Home-page: https://github.com/i2mint/py2http
|
|
6
|
-
Author: Otosense
|
|
7
6
|
License: MIT
|
|
8
7
|
Keywords: http service,web service
|
|
9
8
|
Platform: any
|
|
10
9
|
Description-Content-Type: text/markdown
|
|
11
|
-
Provides-Extra: testing
|
|
12
10
|
License-File: LICENSE
|
|
11
|
+
Requires-Dist: bottle
|
|
12
|
+
Requires-Dist: PyJWT[crypto]
|
|
13
|
+
Requires-Dist: aiohttp
|
|
14
|
+
Requires-Dist: i2
|
|
15
|
+
Requires-Dist: glom
|
|
16
|
+
Requires-Dist: gunicorn
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: strand
|
|
19
|
+
Requires-Dist: swagger-ui-py
|
|
20
|
+
Requires-Dist: pycparser
|
|
21
|
+
Requires-Dist: http2py
|
|
22
|
+
Provides-Extra: testing
|
|
23
|
+
Requires-Dist: http2py; extra == "testing"
|
|
24
|
+
Dynamic: license-file
|
|
13
25
|
|
|
14
26
|
[Documentation here](https://i2mint.github.io/py2http/)
|
|
15
27
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
"""This module provides functionality to easily create HTTP services from Python
|
|
2
|
-
functions and class methods. It includes features such as method transformation,
|
|
3
|
-
input mapping, output mapping, error handling, and client generation.
|
|
4
|
-
The module also allows for customization through various configuration options
|
|
5
|
-
and provides the ability to generate OpenAPI specifications for the created HTTP
|
|
6
|
-
services. Additionally, it includes middleware, plugins, and decorators to enhance
|
|
1
|
+
"""This module provides functionality to easily create HTTP services from Python
|
|
2
|
+
functions and class methods. It includes features such as method transformation,
|
|
3
|
+
input mapping, output mapping, error handling, and client generation.
|
|
4
|
+
The module also allows for customization through various configuration options
|
|
5
|
+
and provides the ability to generate OpenAPI specifications for the created HTTP
|
|
6
|
+
services. Additionally, it includes middleware, plugins, and decorators to enhance
|
|
7
7
|
the functionality of the HTTP services."""
|
|
8
8
|
|
|
9
9
|
from .middleware import mk_jwt_middleware, mk_superadmin_middleware
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""Plugins for adding middleware functionality to Bottle apps.
|
|
2
|
-
"""
|
|
1
|
+
"""Plugins for adding middleware functionality to Bottle apps."""
|
|
3
2
|
|
|
4
3
|
from bottle import request, response, abort
|
|
5
4
|
from functools import wraps
|
|
@@ -9,7 +8,7 @@ from typing import Iterable
|
|
|
9
8
|
from warnings import warn
|
|
10
9
|
from py2http.constants import JSON_CONTENT_TYPE
|
|
11
10
|
|
|
12
|
-
OPTIONS =
|
|
11
|
+
OPTIONS = "OPTIONS"
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class JWTPlugin:
|
|
@@ -21,7 +20,7 @@ class JWTPlugin:
|
|
|
21
20
|
|
|
22
21
|
def __init__(
|
|
23
22
|
self,
|
|
24
|
-
secret: str =
|
|
23
|
+
secret: str = "",
|
|
25
24
|
verify: bool = True,
|
|
26
25
|
mapper: dict = None,
|
|
27
26
|
ignore_methods: Iterable[str] = None,
|
|
@@ -39,7 +38,7 @@ class JWTPlugin:
|
|
|
39
38
|
self._verify = verify
|
|
40
39
|
self._mapper = mapper if mapper else {}
|
|
41
40
|
self._ignore_methods = ignore_methods if ignore_methods else []
|
|
42
|
-
self._algorithms = algorithms if algorithms else [
|
|
41
|
+
self._algorithms = algorithms if algorithms else ["HS256"]
|
|
43
42
|
|
|
44
43
|
def __call__(self, handler):
|
|
45
44
|
if self._ignore_methods and handler.method_name in self._ignore_methods:
|
|
@@ -49,13 +48,13 @@ class JWTPlugin:
|
|
|
49
48
|
def wrapped_handler(*args, **kwargs):
|
|
50
49
|
if request.method == OPTIONS:
|
|
51
50
|
return handler(*args, *kwargs)
|
|
52
|
-
auth_header = request.headers.get(
|
|
51
|
+
auth_header = request.headers.get("Authorization", "")
|
|
53
52
|
token = auth_header[7:]
|
|
54
53
|
try:
|
|
55
54
|
decoded = jwt.decode(
|
|
56
55
|
token,
|
|
57
56
|
self._secret,
|
|
58
|
-
options={
|
|
57
|
+
options={"verify_signature": self._verify},
|
|
59
58
|
algorithms=self._algorithms,
|
|
60
59
|
)
|
|
61
60
|
for k, v in self._mapper.items():
|
|
@@ -69,10 +68,10 @@ class JWTPlugin:
|
|
|
69
68
|
response.content_type = JSON_CONTENT_TYPE
|
|
70
69
|
return json.dumps(
|
|
71
70
|
{
|
|
72
|
-
|
|
71
|
+
"error": f'Invalid authentication token "{token}", {str(error)}'
|
|
73
72
|
}
|
|
74
73
|
)
|
|
75
|
-
warn(f
|
|
74
|
+
warn(f"Invalid JWT: {token}")
|
|
76
75
|
return handler(*args, **kwargs)
|
|
77
76
|
|
|
78
77
|
return wrapped_handler
|
|
@@ -84,12 +83,12 @@ class ApiKeyAuthPlugin:
|
|
|
84
83
|
|
|
85
84
|
def __call__(self, handler):
|
|
86
85
|
def wrapped_handler(*args, **kwargs):
|
|
87
|
-
auth_header = request.headers.get(
|
|
86
|
+
auth_header = request.headers.get("Authorization", "")
|
|
88
87
|
if auth_header == self._api_key:
|
|
89
88
|
return handler(*args, **kwargs)
|
|
90
89
|
response.status = 401
|
|
91
90
|
response.content_type = JSON_CONTENT_TYPE
|
|
92
|
-
return json.dumps({
|
|
91
|
+
return json.dumps({"error": "invalid API key"})
|
|
93
92
|
|
|
94
93
|
return wrapped_handler
|
|
95
94
|
|
|
@@ -97,18 +96,18 @@ class ApiKeyAuthPlugin:
|
|
|
97
96
|
# from https://stackoverflow.com/questions/17262170/bottle-py-enabling-cors-for-jquery-ajax-requests
|
|
98
97
|
# TODO: accept lists of headers and methods as init args
|
|
99
98
|
class CorsPlugin:
|
|
100
|
-
def __init__(self, origins: str =
|
|
99
|
+
def __init__(self, origins: str = "*"):
|
|
101
100
|
self._origins = origins
|
|
102
101
|
|
|
103
102
|
def __call__(self, handler):
|
|
104
103
|
def wrapped_handler(*args, **kwargs):
|
|
105
|
-
response.headers[
|
|
106
|
-
response.headers[
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
response.headers[
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
response.headers["Access-Control-Allow-Origin"] = self._origins
|
|
105
|
+
response.headers["Access-Control-Allow-Methods"] = (
|
|
106
|
+
"GET, POST, PUT, DELETE, OPTIONS"
|
|
107
|
+
)
|
|
108
|
+
response.headers["Access-Control-Allow-Headers"] = (
|
|
109
|
+
"Origin, Accept, Content-Type, X-Requested-With, Authorization, X-api-key"
|
|
110
|
+
)
|
|
112
111
|
if request.method != OPTIONS:
|
|
113
112
|
return handler(*args, **kwargs)
|
|
114
113
|
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
"""The code module provides functions for retrieving configuration values for a
|
|
2
|
-
function based on a hierarchy of sources, including function properties,
|
|
3
|
-
configuration dictionaries, and default values. The `get_result` function is used to
|
|
4
|
-
allow nested configurations by function name, with an example provided in the docstring
|
|
5
|
-
for illustration.
|
|
1
|
+
"""The code module provides functions for retrieving configuration values for a
|
|
2
|
+
function based on a hierarchy of sources, including function properties,
|
|
3
|
+
configuration dictionaries, and default values. The `get_result` function is used to
|
|
4
|
+
allow nested configurations by function name, with an example provided in the docstring
|
|
5
|
+
for illustration.
|
|
6
6
|
|
|
7
|
-
The `mk_config` function is used to get a configuration value for a function by checking
|
|
8
|
-
the function properties, configuration dictionaries, and default values. It also allows
|
|
9
|
-
for specifying the expected type of the output, with options to provide additional
|
|
10
|
-
options such as the function name and expected type.
|
|
7
|
+
The `mk_config` function is used to get a configuration value for a function by checking
|
|
8
|
+
the function properties, configuration dictionaries, and default values. It also allows
|
|
9
|
+
for specifying the expected type of the output, with options to provide additional
|
|
10
|
+
options such as the function name and expected type.
|
|
11
11
|
|
|
12
|
-
Overall, the module helps manage and retrieve configuration values for functions,
|
|
12
|
+
Overall, the module helps manage and retrieve configuration values for functions,
|
|
13
13
|
ensuring they are appropriately set and validated based on the function's requirements.
|
|
14
14
|
"""
|
|
15
|
+
|
|
15
16
|
from typing import Callable
|
|
16
17
|
|
|
17
|
-
AIOHTTP =
|
|
18
|
-
BOTTLE =
|
|
19
|
-
FLASK =
|
|
18
|
+
AIOHTTP = "aiohttp"
|
|
19
|
+
BOTTLE = "bottle"
|
|
20
|
+
FLASK = "flask"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def get_result(configs, func, funcname, key, options):
|
|
@@ -41,9 +42,9 @@ def get_result(configs, func, funcname, key, options):
|
|
|
41
42
|
dict_value = result.get(funcname, None)
|
|
42
43
|
if dict_value:
|
|
43
44
|
result = dict_value
|
|
44
|
-
elif
|
|
45
|
-
result = result[
|
|
46
|
-
elif options.get(
|
|
45
|
+
elif "$else" in result:
|
|
46
|
+
result = result["$else"]
|
|
47
|
+
elif options.get("type", None) is not dict:
|
|
47
48
|
result = None
|
|
48
49
|
return result
|
|
49
50
|
|
|
@@ -66,12 +67,12 @@ def mk_config(key, func, configs, defaults, **options):
|
|
|
66
67
|
* *type*
|
|
67
68
|
The expected type of the output (use Callable for functions)
|
|
68
69
|
"""
|
|
69
|
-
funcname = options.get(
|
|
70
|
+
funcname = options.get("funcname", getattr(func, "__name__", None))
|
|
70
71
|
|
|
71
72
|
result = get_result(configs, func, funcname, key, options)
|
|
72
73
|
|
|
73
74
|
if result:
|
|
74
|
-
expected_type = options.get(
|
|
75
|
+
expected_type = options.get("type", None) # align names expected_type <-> type
|
|
75
76
|
if not expected_type:
|
|
76
77
|
assert key in defaults, f'Missing default value for key "{key}"'
|
|
77
78
|
default_value = defaults.get(key)
|
|
@@ -81,7 +82,7 @@ def mk_config(key, func, configs, defaults, **options):
|
|
|
81
82
|
expected_type = type(default_value)
|
|
82
83
|
assert expected_type == type(None) or isinstance(
|
|
83
84
|
result, expected_type
|
|
84
|
-
), f
|
|
85
|
+
), f"Config {key} does not match type {expected_type}."
|
|
85
86
|
else:
|
|
86
87
|
result = defaults.get(key, None)
|
|
87
88
|
return result
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""This module provides functionality for dispatching Python functions as HTTP services. It allows users to create a basic HTTP server from a list of functions or instance methods, customize input and output mapping, handle errors, generate client code from an OpenAPI specification, and configure various aspects of the server setup. The module includes features for transforming class methods, mapping input and output data, handling errors, and generating client code. It also supports configuration options for the HTTP framework, input and output mappers, error handlers, openAPI specifications, logging, middleware, plugins, CORS, SSL certificates, and more."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
|
|
3
|
+
JSON_CONTENT_TYPE = "application/json"
|
|
4
|
+
BINARY_CONTENT_TYPE = "application/octet-stream"
|
|
5
|
+
FORM_CONTENT_TYPE = "multipart/form-data"
|
|
6
|
+
RAW_CONTENT_TYPE = "text/plain"
|
|
7
|
+
HTML_CONTENT_TYPE = "text/html"
|
|
@@ -30,7 +30,11 @@ from i2.signatures import (
|
|
|
30
30
|
)
|
|
31
31
|
from i2.errors import ModuleNotFoundIgnore
|
|
32
32
|
|
|
33
|
-
from py2http.schema_tools import
|
|
33
|
+
from py2http.schema_tools import (
|
|
34
|
+
mk_input_schema_from_func,
|
|
35
|
+
param_default,
|
|
36
|
+
validate_input,
|
|
37
|
+
)
|
|
34
38
|
from py2http.config import AIOHTTP, BOTTLE
|
|
35
39
|
from py2http.constants import (
|
|
36
40
|
JSON_CONTENT_TYPE,
|
|
@@ -246,7 +250,7 @@ class DecoParam:
|
|
|
246
250
|
from typing import Optional
|
|
247
251
|
import re
|
|
248
252
|
|
|
249
|
-
token_p = re.compile(r
|
|
253
|
+
token_p = re.compile(r"\w+")
|
|
250
254
|
|
|
251
255
|
|
|
252
256
|
# TODO: Research how to keep the params in the order they were declared.
|
|
@@ -338,7 +342,7 @@ class ParamsSpecifier:
|
|
|
338
342
|
def __init__(
|
|
339
343
|
self,
|
|
340
344
|
_annotations: Optional[dict] = None,
|
|
341
|
-
_names: str =
|
|
345
|
+
_names: str = "",
|
|
342
346
|
_dflt_default=None,
|
|
343
347
|
_kind=Parameter.KEYWORD_ONLY,
|
|
344
348
|
**name_and_dflts,
|
|
@@ -352,35 +356,35 @@ class ParamsSpecifier:
|
|
|
352
356
|
self._dflt_default = _dflt_default
|
|
353
357
|
self._kind = _kind
|
|
354
358
|
|
|
355
|
-
if hasattr(self,
|
|
359
|
+
if hasattr(self, "__annotations__"):
|
|
356
360
|
self.__annotations__.update(_annotations)
|
|
357
361
|
else:
|
|
358
362
|
self.__annotations__ = _annotations
|
|
359
363
|
|
|
360
364
|
reserved = {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
"_annotations",
|
|
366
|
+
"_names",
|
|
367
|
+
"_dflt_default",
|
|
368
|
+
"from_func",
|
|
369
|
+
"_extract_params",
|
|
370
|
+
"_to_signature",
|
|
371
|
+
"_kind",
|
|
372
|
+
"to_parameter_obj_list",
|
|
369
373
|
}
|
|
370
374
|
_name_and_dflts = {
|
|
371
375
|
k: v
|
|
372
376
|
for k, v in self.__class__.__dict__.items()
|
|
373
|
-
if not k.startswith(
|
|
377
|
+
if not k.startswith("__") and k not in reserved
|
|
374
378
|
}
|
|
375
379
|
_name_and_dflts.update(name_and_dflts or {})
|
|
376
380
|
self._name_and_dflts = _name_and_dflts
|
|
377
381
|
|
|
378
|
-
annots = set(getattr(self,
|
|
379
|
-
annots |= set(getattr(self.__class__,
|
|
382
|
+
annots = set(getattr(self, "__annotations__", {}))
|
|
383
|
+
annots |= set(getattr(self.__class__, "__annotations__", {}))
|
|
380
384
|
|
|
381
385
|
_reserved = reserved.intersection(set(_name_and_dflts) | set(annots))
|
|
382
386
|
if _reserved:
|
|
383
|
-
raise ValueError(f
|
|
387
|
+
raise ValueError(f"Sorry, {_reserved} are reserved names")
|
|
384
388
|
|
|
385
389
|
@classmethod
|
|
386
390
|
def from_func(cls, func, _dflt_default=None):
|
|
@@ -388,23 +392,21 @@ class ParamsSpecifier:
|
|
|
388
392
|
_annotations = {
|
|
389
393
|
x.name: x.annotation for x in params if x.annotation is not Parameter.empty
|
|
390
394
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
dflt =
|
|
394
|
-
if dflt is Parameter.empty
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
_name_and_dflts = {
|
|
398
|
-
x.name: x.default if x.default is not Parameter.empty else _dflt_default
|
|
399
|
-
for x in params
|
|
400
|
-
}
|
|
395
|
+
|
|
396
|
+
def dflt_of(param):
|
|
397
|
+
dflt = param_default(param) # i2's NotSet sentinel counts as "no default"
|
|
398
|
+
return _dflt_default if dflt is Parameter.empty else dflt
|
|
399
|
+
|
|
400
|
+
_name_and_dflts = {x.name: dflt_of(x) for x in params}
|
|
401
401
|
return cls(
|
|
402
|
-
_annotations=_annotations,
|
|
402
|
+
_annotations=_annotations,
|
|
403
|
+
_dflt_default=_dflt_default,
|
|
404
|
+
**_name_and_dflts,
|
|
403
405
|
)
|
|
404
406
|
|
|
405
407
|
def _extract_params(self):
|
|
406
408
|
_name_and_dflts = self._name_and_dflts
|
|
407
|
-
annots = getattr(self,
|
|
409
|
+
annots = getattr(self, "__annotations__", {})
|
|
408
410
|
for name in set(annots) - set(_name_and_dflts): # annots_not_in_attrs
|
|
409
411
|
yield dict(
|
|
410
412
|
name=name,
|
|
@@ -428,7 +430,7 @@ class ParamsSpecifier:
|
|
|
428
430
|
return list(self._extract_params())
|
|
429
431
|
|
|
430
432
|
def __repr__(self):
|
|
431
|
-
return f
|
|
433
|
+
return f"{self()}"
|
|
432
434
|
|
|
433
435
|
|
|
434
436
|
class Decora(Decorator):
|
|
@@ -539,7 +541,7 @@ class Decora(Decorator):
|
|
|
539
541
|
def __init_subclass__(cls, **kwargs):
|
|
540
542
|
super().__init_subclass__(**kwargs)
|
|
541
543
|
if (
|
|
542
|
-
|
|
544
|
+
"__new__" not in cls.__dict__
|
|
543
545
|
): # if __new__ hasn't been defined in the subclass...
|
|
544
546
|
params = []
|
|
545
547
|
# cls_annots = getattr(cls, '__annotations__', {})
|
|
@@ -554,8 +556,8 @@ class Decora(Decorator):
|
|
|
554
556
|
for p in params:
|
|
555
557
|
setattr(cls, p.name, p.default)
|
|
556
558
|
params = [
|
|
557
|
-
Parameter(
|
|
558
|
-
Parameter(
|
|
559
|
+
Parameter("self", PK),
|
|
560
|
+
Parameter("func", PK, default=None),
|
|
559
561
|
] + params
|
|
560
562
|
cls._injected_deco_params = [p.name for p in params]
|
|
561
563
|
|
|
@@ -564,8 +566,8 @@ class Decora(Decorator):
|
|
|
564
566
|
cls._injected_deco_params
|
|
565
567
|
):
|
|
566
568
|
raise TypeError(
|
|
567
|
-
|
|
568
|
-
f
|
|
569
|
+
"TypeError: __new__() got unexpected keyword arguments: "
|
|
570
|
+
f"{kwargs.keys() - cls._injected_deco_params}"
|
|
569
571
|
)
|
|
570
572
|
if func is None:
|
|
571
573
|
return partial(cls, **kwargs)
|
|
@@ -624,7 +626,8 @@ def replace_with_params(target=None, /, *, source=None, inplace=False):
|
|
|
624
626
|
target
|
|
625
627
|
) # make a copy of the function so we don't
|
|
626
628
|
target.__signature__ = Signature(
|
|
627
|
-
new_params,
|
|
629
|
+
new_params,
|
|
630
|
+
return_annotation=signature(target).return_annotation,
|
|
628
631
|
)
|
|
629
632
|
return target
|
|
630
633
|
else:
|
|
@@ -694,7 +697,7 @@ def methodizer(func=None, *, instance_params=()):
|
|
|
694
697
|
kwargs.update(kwargs_from_self)
|
|
695
698
|
return func(**kwargs)
|
|
696
699
|
|
|
697
|
-
set_signature_of_func(method, [
|
|
700
|
+
set_signature_of_func(method, ["self"] + list(method_argnames))
|
|
698
701
|
|
|
699
702
|
method.__name__ = func.__name__
|
|
700
703
|
|
|
@@ -706,14 +709,14 @@ from warnings import warn
|
|
|
706
709
|
|
|
707
710
|
def _handle_exisisting_method_name(cls, method, if_method_exists):
|
|
708
711
|
if hasattr(cls, method.__name__):
|
|
709
|
-
msg = f
|
|
710
|
-
if if_method_exists ==
|
|
712
|
+
msg = f"{cls} already has a method named {method.__name__}"
|
|
713
|
+
if if_method_exists == "raise":
|
|
711
714
|
raise ValueError(msg)
|
|
712
|
-
elif if_method_exists ==
|
|
713
|
-
warn(msg +
|
|
714
|
-
elif if_method_exists !=
|
|
715
|
+
elif if_method_exists == "warn":
|
|
716
|
+
warn(msg + " ... Will overwrite anyway.")
|
|
717
|
+
elif if_method_exists != "ignore":
|
|
715
718
|
raise ValueError(
|
|
716
|
-
f
|
|
719
|
+
f"if_method_exists value not recognized: {if_method_exists}"
|
|
717
720
|
)
|
|
718
721
|
|
|
719
722
|
|
|
@@ -721,7 +724,7 @@ def _handle_exisisting_method_name(cls, method, if_method_exists):
|
|
|
721
724
|
# - signatures have different orders every time (need to use ordered containers)
|
|
722
725
|
# - Values not computed correctly
|
|
723
726
|
def inject_methodized_funcs(
|
|
724
|
-
cls=None, *, funcs=(), instance_params=None, if_method_exists=
|
|
727
|
+
cls=None, *, funcs=(), instance_params=None, if_method_exists="raise"
|
|
725
728
|
):
|
|
726
729
|
"""
|
|
727
730
|
|
|
@@ -762,7 +765,7 @@ def inject_methodized_funcs(
|
|
|
762
765
|
# C.g(y, x)
|
|
763
766
|
# C.h(kwargs, c, x)
|
|
764
767
|
"""
|
|
765
|
-
raise NotImplementedError(
|
|
768
|
+
raise NotImplementedError("Not working yet: Come back to it!")
|
|
766
769
|
if cls is None:
|
|
767
770
|
return partial(
|
|
768
771
|
inject_methodized_funcs,
|
|
@@ -787,7 +790,7 @@ def flatten_callables(*callables, func_name=None):
|
|
|
787
790
|
"""
|
|
788
791
|
Flatten a pipeline of calls into one function.
|
|
789
792
|
"""
|
|
790
|
-
raise NotImplementedError(
|
|
793
|
+
raise NotImplementedError("Meant to be a generalization of mk_flat")
|
|
791
794
|
for call in callables:
|
|
792
795
|
pass
|
|
793
796
|
|
|
@@ -813,7 +816,7 @@ def _instantiate(cls, *, cache_id, **kwargs):
|
|
|
813
816
|
|
|
814
817
|
# TODO: signature of flat function doesn't reflect actual call restrictions
|
|
815
818
|
# TODO: Change default func_name to be dynamically, taking method as default
|
|
816
|
-
def mk_flat(cls, method, *, func_name: str =
|
|
819
|
+
def mk_flat(cls, method, *, func_name: str = "flat_func", cls_cache_key: str = None):
|
|
817
820
|
"""
|
|
818
821
|
Flatten a simple cls->instance->method call pipeline into one function.
|
|
819
822
|
|
|
@@ -889,7 +892,7 @@ def mk_flat(cls, method, *, func_name: str = 'flat_func', cls_cache_key: str = N
|
|
|
889
892
|
if cls_cache_key:
|
|
890
893
|
param = dict(name=cls_cache_key, kind=KO, default=None)
|
|
891
894
|
sig_flat = sig_flat.add_params([param])
|
|
892
|
-
sig_flat = sig_flat.remove_names([
|
|
895
|
+
sig_flat = sig_flat.remove_names(["self"])
|
|
893
896
|
sig_flat = sig_flat.replace(return_annotation=sig_method.return_annotation)
|
|
894
897
|
|
|
895
898
|
def flat_func(**kwargs):
|
|
@@ -950,7 +953,7 @@ def flatten_methods(methods: dict, decorator=None, validate_name_unicity=True):
|
|
|
950
953
|
if validate_name_unicity:
|
|
951
954
|
nb_function_names = len({x.__name__: x for x in functions})
|
|
952
955
|
if nb_function_names != len(functions):
|
|
953
|
-
raise ValueError(f
|
|
956
|
+
raise ValueError(f"Some function names are duplicated in {methods}")
|
|
954
957
|
return functions
|
|
955
958
|
|
|
956
959
|
|
|
@@ -987,10 +990,10 @@ def add_attrs(**attrs):
|
|
|
987
990
|
return add_attrs_to_func
|
|
988
991
|
|
|
989
992
|
|
|
990
|
-
http_get = add_attrs(http_method=
|
|
991
|
-
http_post = add_attrs(http_method=
|
|
992
|
-
http_put = add_attrs(http_method=
|
|
993
|
-
http_delete = add_attrs(http_method=
|
|
993
|
+
http_get = add_attrs(http_method="get")
|
|
994
|
+
http_post = add_attrs(http_method="post")
|
|
995
|
+
http_put = add_attrs(http_method="put")
|
|
996
|
+
http_delete = add_attrs(http_method="delete")
|
|
994
997
|
|
|
995
998
|
|
|
996
999
|
def route(route_name):
|
|
@@ -998,7 +1001,7 @@ def route(route_name):
|
|
|
998
1001
|
|
|
999
1002
|
|
|
1000
1003
|
def _validate_and_invoke_mapper(func, inputs):
|
|
1001
|
-
request_schema = getattr(func,
|
|
1004
|
+
request_schema = getattr(func, "request_schema", None)
|
|
1002
1005
|
if request_schema:
|
|
1003
1006
|
validate_input(inputs, request_schema)
|
|
1004
1007
|
return func(**inputs)
|
|
@@ -1012,10 +1015,8 @@ def _handle_req(func, content_type):
|
|
|
1012
1015
|
@wraps(func)
|
|
1013
1016
|
def input_mapper(req):
|
|
1014
1017
|
if content_type not in req.content_type:
|
|
1015
|
-
raise RuntimeError(
|
|
1016
|
-
|
|
1017
|
-
{req.content_type}, when {content_type} is expected."
|
|
1018
|
-
)
|
|
1018
|
+
raise RuntimeError(f"The incoming request's content is of type \
|
|
1019
|
+
{req.content_type}, when {content_type} is expected.")
|
|
1019
1020
|
inputs = _get_inputs_from_request(req, content_type)
|
|
1020
1021
|
return _validate_and_invoke_mapper(func, inputs)
|
|
1021
1022
|
|
|
@@ -1082,7 +1083,8 @@ class ProposalJsonRespEncoder(JSONEncoder):
|
|
|
1082
1083
|
# Note: Subclass and replace _pre_process_obj to get different preprocessing
|
|
1083
1084
|
# Note: To get control from init, definte init to set _pre_process_obj
|
|
1084
1085
|
_pre_process_obj = partial(
|
|
1085
|
-
_json_reponse_preproc,
|
|
1086
|
+
_json_reponse_preproc,
|
|
1087
|
+
serializer_for_type=_mk_default_serializer_for_type(),
|
|
1086
1088
|
)
|
|
1087
1089
|
|
|
1088
1090
|
def default(self, o):
|
|
@@ -1091,7 +1093,7 @@ class ProposalJsonRespEncoder(JSONEncoder):
|
|
|
1091
1093
|
|
|
1092
1094
|
|
|
1093
1095
|
def send_json_resp(func):
|
|
1094
|
-
framework = os.getenv(
|
|
1096
|
+
framework = os.getenv("PY2HTTP_FRAMEWORK", BOTTLE)
|
|
1095
1097
|
if framework == AIOHTTP:
|
|
1096
1098
|
|
|
1097
1099
|
async def output_mapper(output, **input_kwargs):
|
|
@@ -1118,7 +1120,7 @@ def send_binary_resp(func):
|
|
|
1118
1120
|
return pickle.dumps(mapped_output)
|
|
1119
1121
|
|
|
1120
1122
|
output_mapper.content_type = BINARY_CONTENT_TYPE
|
|
1121
|
-
output_mapper.response_schema = {
|
|
1123
|
+
output_mapper.response_schema = {"type": "binary"}
|
|
1122
1124
|
return output_mapper
|
|
1123
1125
|
|
|
1124
1126
|
|
|
@@ -1169,25 +1171,25 @@ def mk_input_mapper(input_map):
|
|
|
1169
1171
|
|
|
1170
1172
|
|
|
1171
1173
|
def _get_inputs_from_request(request, content_type):
|
|
1172
|
-
defaults = getattr(request,
|
|
1173
|
-
if request.method ==
|
|
1174
|
+
defaults = getattr(request, "defaults", {})
|
|
1175
|
+
if request.method == "POST":
|
|
1174
1176
|
if content_type == JSON_CONTENT_TYPE:
|
|
1175
1177
|
inputs = request.json
|
|
1176
1178
|
elif content_type == RAW_CONTENT_TYPE:
|
|
1177
|
-
data = request.body.read().decode(
|
|
1179
|
+
data = request.body.read().decode("utf-8")
|
|
1178
1180
|
inputs = json.loads(data)
|
|
1179
1181
|
elif content_type == BINARY_CONTENT_TYPE:
|
|
1180
1182
|
data = request.body.read()
|
|
1181
1183
|
inputs = pickle.loads(data)
|
|
1182
1184
|
elif content_type == FORM_CONTENT_TYPE:
|
|
1183
1185
|
fields = json.loads(
|
|
1184
|
-
request.files.pop(
|
|
1186
|
+
request.files.pop("__fields").file.read().decode("utf-8")
|
|
1185
1187
|
)
|
|
1186
1188
|
binaries = {k: v.file.read() for k, v in request.files.items()}
|
|
1187
1189
|
inputs = dict(fields, **binaries)
|
|
1188
1190
|
return dict(defaults, **inputs)
|
|
1189
1191
|
else:
|
|
1190
|
-
raise NotImplementedError(
|
|
1192
|
+
raise NotImplementedError("Only POST is supported for now")
|
|
1191
1193
|
|
|
1192
1194
|
|
|
1193
1195
|
def mk_handlers(methods: Iterable, *, decorator=None, cls_cache_key=None):
|
|
@@ -1200,7 +1202,7 @@ def mk_handlers(methods: Iterable, *, decorator=None, cls_cache_key=None):
|
|
|
1200
1202
|
if inspect.isfunction(meth):
|
|
1201
1203
|
cls = getattr(
|
|
1202
1204
|
inspect.getmodule(meth),
|
|
1203
|
-
meth.__qualname__.split(
|
|
1205
|
+
meth.__qualname__.split(".<locals>", 1)[0].rsplit(".", 1)[0],
|
|
1204
1206
|
None,
|
|
1205
1207
|
)
|
|
1206
1208
|
if isinstance(cls, type):
|
|
@@ -1215,7 +1217,7 @@ def mk_handlers(methods: Iterable, *, decorator=None, cls_cache_key=None):
|
|
|
1215
1217
|
handlers = []
|
|
1216
1218
|
for item in methods:
|
|
1217
1219
|
has_mappers = isinstance(item, dict)
|
|
1218
|
-
method = item[
|
|
1220
|
+
method = item["endpoint"] if has_mappers else item
|
|
1219
1221
|
cls = get_class_that_defined_method(method)
|
|
1220
1222
|
endpoint = decorator(
|
|
1221
1223
|
mk_flat(cls, method, func_name=method.__name__, cls_cache_key=cls_cache_key)
|