http2py 0.1.37__tar.gz → 0.1.39__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.
- {http2py-0.1.37 → http2py-0.1.39}/PKG-INFO +1 -1
- {http2py-0.1.37 → http2py-0.1.39}/http2py/api_pkg_maker.py +5 -1
- {http2py-0.1.37 → http2py-0.1.39}/http2py/client.py +7 -2
- {http2py-0.1.37 → http2py-0.1.39}/http2py/py2request.py +72 -2
- {http2py-0.1.37 → http2py-0.1.39}/pyproject.toml +1 -1
- http2py-0.1.39/tests/test_response_decoding.py +166 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_route_namespace.py +70 -0
- {http2py-0.1.37 → http2py-0.1.39}/.gitattributes +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/.github/workflows/ci.yml +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/.gitignore +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/LICENSE +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/README.md +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/conftest.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/__init__.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/authentication.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/cli_maker.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/constants.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/decorators.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/default_configs.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/example_cli.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/global_state.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/http2py_cls.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/resources/module_maker/func_tpl.txt +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/testing_utils.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/tests/__init__.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/tests/api_pkg_maker_test.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/tests/conftest.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/tests/test_requirements.txt +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/http2py/util.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/cli_goldens/mk_cli.json +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/spec_fixture.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_binary_payloads.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_ci_collection_contract.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_cli_surface.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_error_statuses.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_generated_clients.py +0 -0
- {http2py-0.1.37 → http2py-0.1.39}/tests/test_smoke.py +0 -0
|
@@ -84,6 +84,9 @@ def {func_name}(*args, **kwargs):
|
|
|
84
84
|
return api.routes[{func_name!r}](*args, **kwargs)
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
|
+
# The first http2py release with ``HttpClient.routes``, which generated packages call.
|
|
88
|
+
MIN_HTTP2PY_VERSION = "0.1.37"
|
|
89
|
+
|
|
87
90
|
SETUP_PY = """from setuptools import setup
|
|
88
91
|
|
|
89
92
|
setup()
|
|
@@ -103,7 +106,7 @@ include_package_data = True
|
|
|
103
106
|
zip_safe = False
|
|
104
107
|
install_requires =
|
|
105
108
|
i2
|
|
106
|
-
http2py
|
|
109
|
+
http2py>={min_http2py}
|
|
107
110
|
|
|
108
111
|
[options.package_data]
|
|
109
112
|
* = {spec_filename}
|
|
@@ -215,6 +218,7 @@ def api_pkg_files(
|
|
|
215
218
|
author=_cfg_value(pkg_author or "API Package Maker"),
|
|
216
219
|
license=_cfg_value(pkg_license or "Apache"),
|
|
217
220
|
spec_filename=SPEC_FILENAME,
|
|
221
|
+
min_http2py=MIN_HTTP2PY_VERSION,
|
|
218
222
|
)
|
|
219
223
|
funcs_code = FUNCS_FILE_TPL.format(
|
|
220
224
|
spec_filename=SPEC_FILENAME,
|
|
@@ -115,7 +115,9 @@ class HttpClient:
|
|
|
115
115
|
for pathname, path_spec in openapi_spec["paths"].items():
|
|
116
116
|
url_template = self.base_url + pathname
|
|
117
117
|
for http_method, openapi_method_spec in path_spec.items():
|
|
118
|
-
self.register_method(
|
|
118
|
+
self.register_method(
|
|
119
|
+
url_template, http_method, openapi_method_spec, pathname=pathname
|
|
120
|
+
)
|
|
119
121
|
|
|
120
122
|
def init_security(self, openapi_spec, **auth_kwargs):
|
|
121
123
|
security = openapi_spec["security"]
|
|
@@ -143,7 +145,9 @@ class HttpClient:
|
|
|
143
145
|
if jwt:
|
|
144
146
|
self.set_header({"Authorization": f"Bearer {jwt}"})
|
|
145
147
|
|
|
146
|
-
def register_method(
|
|
148
|
+
def register_method(
|
|
149
|
+
self, url_template, http_method, openapi_method_spec, *, pathname=None
|
|
150
|
+
):
|
|
147
151
|
content_type = None
|
|
148
152
|
if "requestBody" in openapi_method_spec:
|
|
149
153
|
content_type = next(
|
|
@@ -154,6 +158,7 @@ class HttpClient:
|
|
|
154
158
|
method=http_method,
|
|
155
159
|
url_template=url_template,
|
|
156
160
|
content_type=content_type,
|
|
161
|
+
pathname=pathname,
|
|
157
162
|
)
|
|
158
163
|
func = mk_request_function(
|
|
159
164
|
method_spec,
|
|
@@ -33,11 +33,13 @@ There must be a better way...
|
|
|
33
33
|
from functools import wraps
|
|
34
34
|
from inspect import signature, Parameter
|
|
35
35
|
import json
|
|
36
|
+
import keyword
|
|
36
37
|
import pickle
|
|
37
38
|
from glom import glom
|
|
38
39
|
from requests import request
|
|
39
40
|
import string
|
|
40
41
|
import urllib
|
|
42
|
+
from urllib.parse import urlparse
|
|
41
43
|
|
|
42
44
|
# import io
|
|
43
45
|
import re
|
|
@@ -198,6 +200,11 @@ def mk_request_function(
|
|
|
198
200
|
|
|
199
201
|
original_spec = method_spec
|
|
200
202
|
method_spec = method_spec.copy() # make a copy
|
|
203
|
+
if "url_template" in method_spec:
|
|
204
|
+
check_url_template(
|
|
205
|
+
method_spec["url_template"],
|
|
206
|
+
query_arg_names=_ensure_list(method_spec.get("query_arg_names", [])),
|
|
207
|
+
)
|
|
201
208
|
method_spec["input_trans"] = method_spec.get("input_trans", None) or {}
|
|
202
209
|
|
|
203
210
|
request_kwargs = method_spec.get("request_kwargs", {}).copy()
|
|
@@ -283,7 +290,7 @@ def mk_request_function(
|
|
|
283
290
|
_request_kwargs["data"] = json.dumps(json_kwargs)
|
|
284
291
|
elif content_type == BINARY_CONTENT_TYPE:
|
|
285
292
|
_request_kwargs["data"] = binary_dumps(kwargs)
|
|
286
|
-
|
|
293
|
+
if content_type == FORM_CONTENT_TYPE: # (it used to be sent with GET only)
|
|
287
294
|
fields = json.dumps(json_kwargs).encode("utf-8")
|
|
288
295
|
binaries = {k: (f"{k}.bin", v) for k, v in binary_kwargs.items()}
|
|
289
296
|
files_data = dict(binaries, __fields=fields)
|
|
@@ -500,6 +507,44 @@ def _difference_conserving_order(a, b):
|
|
|
500
507
|
return [x for x in a if x not in b]
|
|
501
508
|
|
|
502
509
|
|
|
510
|
+
def _reads_into_arguments(field_name: str) -> bool:
|
|
511
|
+
"""Whether a ``str.format`` field looks up an attribute or item of an argument;
|
|
512
|
+
any other name (``user-id``, ``0``...) only selects an argument."""
|
|
513
|
+
return "." in field_name or "[" in field_name
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def check_url_template(url_template: str, *, query_arg_names=()) -> str:
|
|
517
|
+
"""Return ``url_template`` if no placeholder in it, nor any of the
|
|
518
|
+
``query_arg_names`` (appended as ``?name={name}`` when calling), reads into an
|
|
519
|
+
argument.
|
|
520
|
+
|
|
521
|
+
A template is filled with ``str.format(**arguments)``, and it may come from a spec
|
|
522
|
+
fetched from a server. ``{a.__class__}`` or ``{a[0]}`` would read attributes or
|
|
523
|
+
items of the caller's arguments into the URL, so they are refused. Other names,
|
|
524
|
+
such as OpenAPI's hyphenated ``{user-id}``, are fine.
|
|
525
|
+
|
|
526
|
+
>>> check_url_template('http://h/u/{user-id}?q={q}')
|
|
527
|
+
'http://h/u/{user-id}?q={q}'
|
|
528
|
+
>>> check_url_template('http://h/{a.__init__.__globals__}')
|
|
529
|
+
Traceback (most recent call last):
|
|
530
|
+
...
|
|
531
|
+
ValueError: URL template placeholder '{a.__init__.__globals__}' reads into an argument
|
|
532
|
+
"""
|
|
533
|
+
fields = [
|
|
534
|
+
(field_name, format_spec)
|
|
535
|
+
for _, field_name, format_spec, _ in str_formatter.parse(url_template)
|
|
536
|
+
if field_name is not None
|
|
537
|
+
]
|
|
538
|
+
fields += [(name, "") for name in query_arg_names]
|
|
539
|
+
for field_name, format_spec in fields:
|
|
540
|
+
if _reads_into_arguments(field_name) or "{" in (format_spec or ""):
|
|
541
|
+
raise ValueError(
|
|
542
|
+
f"URL template placeholder {'{' + field_name + '}'!r} reads into an "
|
|
543
|
+
"argument"
|
|
544
|
+
)
|
|
545
|
+
return url_template
|
|
546
|
+
|
|
547
|
+
|
|
503
548
|
def extract_format_names(string_template):
|
|
504
549
|
"""
|
|
505
550
|
Extracts the names of the fields in a string template, returning a list of
|
|
@@ -607,6 +652,30 @@ def raw_response_on_error(func):
|
|
|
607
652
|
return _func
|
|
608
653
|
|
|
609
654
|
|
|
655
|
+
def _identifier(text):
|
|
656
|
+
"""``text`` as a public Python identifier: runs of other characters (and of ``_``) become ``_``,
|
|
657
|
+
a leading digit gets an ``op_`` prefix and a keyword a trailing ``_``."""
|
|
658
|
+
name = re.sub(r"[\W_]+", "_", text, flags=re.ASCII).strip("_")
|
|
659
|
+
if name[:1].isdigit():
|
|
660
|
+
name = f"op_{name}"
|
|
661
|
+
return f"{name}_" if keyword.iskeyword(name) else name
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _route_name(openapi_method_spec, method, pathname, url_template):
|
|
665
|
+
"""The name of the function for a route: its ``x-method_name``, else its
|
|
666
|
+
``operationId``, else the http method and the path (``get /users/{id}`` gives
|
|
667
|
+
``get_users_id``), so that routes without ``x-method_name`` do not all share a name.
|
|
668
|
+
``pathname`` is the path as the spec gives it, so that a server's base url does not
|
|
669
|
+
change the names; it defaults to the path of ``url_template``.
|
|
670
|
+
"""
|
|
671
|
+
if openapi_method_spec.get("x-method_name"):
|
|
672
|
+
return openapi_method_spec["x-method_name"]
|
|
673
|
+
name = _identifier(openapi_method_spec.get("operationId") or "")
|
|
674
|
+
if pathname is None:
|
|
675
|
+
pathname = urlparse(url_template).path
|
|
676
|
+
return name or default_path_to_func_name(method, pathname)
|
|
677
|
+
|
|
678
|
+
|
|
610
679
|
def mk_method_spec_from_openapi_method_spec(
|
|
611
680
|
openapi_method_spec,
|
|
612
681
|
method="post",
|
|
@@ -614,6 +683,7 @@ def mk_method_spec_from_openapi_method_spec(
|
|
|
614
683
|
content_type=JSON_CONTENT_TYPE,
|
|
615
684
|
input_trans=None,
|
|
616
685
|
output_trans=None,
|
|
686
|
+
pathname=None,
|
|
617
687
|
):
|
|
618
688
|
path_arg_names = []
|
|
619
689
|
query_arg_names = []
|
|
@@ -671,7 +741,7 @@ def mk_method_spec_from_openapi_method_spec(
|
|
|
671
741
|
path_arg_names=path_arg_names,
|
|
672
742
|
query_arg_names=query_arg_names,
|
|
673
743
|
body_arg_names=body_arg_names,
|
|
674
|
-
method_name=openapi_method_spec
|
|
744
|
+
method_name=_route_name(openapi_method_spec, method, pathname, url_template),
|
|
675
745
|
docstring=openapi_method_spec.get("description", ""),
|
|
676
746
|
content_type=content_type,
|
|
677
747
|
response_type=next(iter(glom(openapi_method_spec, f"responses.200.content"))),
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""How responses are decoded (``http2py.decorators``) and how a client's requests are
|
|
2
|
+
built from a spec it may have fetched from a server (``mk_request_function``)."""
|
|
3
|
+
|
|
4
|
+
import pickle
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from http2py.decorators import (
|
|
10
|
+
BinaryResponseNotDecoded,
|
|
11
|
+
check_loads,
|
|
12
|
+
handle_binary_resp,
|
|
13
|
+
unsafe_pickle_loads,
|
|
14
|
+
)
|
|
15
|
+
from http2py.py2request import mk_request_function
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Resp:
|
|
19
|
+
def __init__(self, content=b"", *, status_code=200, text="", reason="OK"):
|
|
20
|
+
self.content, self.status_code, self.text, self.reason = (
|
|
21
|
+
content,
|
|
22
|
+
status_code,
|
|
23
|
+
text,
|
|
24
|
+
reason,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _identity(x):
|
|
29
|
+
return x
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_binary_response_needs_an_explicit_decoder():
|
|
33
|
+
with pytest.raises(BinaryResponseNotDecoded):
|
|
34
|
+
handle_binary_resp(_identity)(Resp(pickle.dumps(1)))
|
|
35
|
+
assert handle_binary_resp(_identity, loads=bytes)(Resp(b"raw")) == b"raw"
|
|
36
|
+
trusted = handle_binary_resp(_identity, loads=unsafe_pickle_loads)
|
|
37
|
+
assert trusted(Resp(pickle.dumps({"a": 1}))) == {"a": 1}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_check_loads():
|
|
41
|
+
check_loads(None)
|
|
42
|
+
check_loads(bytes)
|
|
43
|
+
with pytest.raises(TypeError):
|
|
44
|
+
check_loads("pickle.loads")
|
|
45
|
+
with pytest.warns(UserWarning, match="unsafe_pickle_loads"):
|
|
46
|
+
check_loads(pickle.loads)
|
|
47
|
+
with warnings.catch_warnings():
|
|
48
|
+
warnings.simplefilter("error")
|
|
49
|
+
check_loads(unsafe_pickle_loads) # the explicit opt-in does not warn
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_non_200_binary_response_raises_the_mapped_error():
|
|
53
|
+
from i2.errors import NotFoundError
|
|
54
|
+
|
|
55
|
+
with pytest.raises(NotFoundError):
|
|
56
|
+
handle_binary_resp(_identity, loads=bytes)(Resp(status_code=404, text="no"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _captured_request(method_spec, **call_kwargs):
|
|
60
|
+
"""The (method, url, request kwargs) a request function would dispatch."""
|
|
61
|
+
calls = []
|
|
62
|
+
|
|
63
|
+
def dispatch(method, url, **kwargs):
|
|
64
|
+
calls.append((method, url, kwargs))
|
|
65
|
+
return Resp(b"{}")
|
|
66
|
+
|
|
67
|
+
func = mk_request_function(method_spec, dispatch=dispatch)
|
|
68
|
+
func(None, **call_kwargs)
|
|
69
|
+
return calls[0]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_binary_request_bodies_use_binary_dumps():
|
|
73
|
+
spec = {
|
|
74
|
+
"method": "POST",
|
|
75
|
+
"url_template": "http://h/f",
|
|
76
|
+
"body_arg_names": ["x"],
|
|
77
|
+
"content_type": "application/octet-stream",
|
|
78
|
+
"output_trans": _identity,
|
|
79
|
+
}
|
|
80
|
+
_, _, kwargs = _captured_request(spec, x=1)
|
|
81
|
+
assert pickle.loads(kwargs["data"]) == {"x": 1}
|
|
82
|
+
calls = []
|
|
83
|
+
func = mk_request_function(
|
|
84
|
+
spec,
|
|
85
|
+
dispatch=lambda m, u, **kw: calls.append(kw) or Resp(),
|
|
86
|
+
binary_dumps=lambda d: b"custom",
|
|
87
|
+
)
|
|
88
|
+
func(None, x=1)
|
|
89
|
+
assert calls[0]["data"] == b"custom"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_path_args_are_url_encoded():
|
|
93
|
+
spec = {
|
|
94
|
+
"method": "GET",
|
|
95
|
+
"url_template": "http://h/u/{uid}",
|
|
96
|
+
"path_arg_names": ["uid"],
|
|
97
|
+
"output_trans": _identity,
|
|
98
|
+
}
|
|
99
|
+
_, url, _ = _captured_request(spec, uid="a/b c")
|
|
100
|
+
assert url == "http://h/u/a%2Fb%20c"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.parametrize(
|
|
104
|
+
"template",
|
|
105
|
+
[
|
|
106
|
+
"http://h/{x.__class__}",
|
|
107
|
+
"http://h/{x[0]}",
|
|
108
|
+
"http://h/{x:{x.__class__}}",
|
|
109
|
+
"http://h/{x!r:{y}}",
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
def test_url_templates_may_only_name_arguments(template):
|
|
113
|
+
"""A spec served by a remote must not read attributes of the caller's
|
|
114
|
+
arguments into the URL."""
|
|
115
|
+
spec = {"method": "GET", "url_template": template, "path_arg_names": ["x"]}
|
|
116
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
117
|
+
mk_request_function(spec)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_query_arg_names_may_only_name_arguments():
|
|
121
|
+
spec = {
|
|
122
|
+
"method": "GET",
|
|
123
|
+
"url_template": "http://h/x",
|
|
124
|
+
"query_arg_names": ["obj", "obj.token"],
|
|
125
|
+
}
|
|
126
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
127
|
+
mk_request_function(spec)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_hyphenated_placeholders_still_work():
|
|
131
|
+
spec = {"method": "GET", "url_template": "http://h/u/{user-id}"}
|
|
132
|
+
calls = []
|
|
133
|
+
func = mk_request_function(
|
|
134
|
+
spec, dispatch=lambda m, u, **kw: calls.append(u) or Resp(b"")
|
|
135
|
+
)
|
|
136
|
+
func(None, **{"user-id": "42"})
|
|
137
|
+
assert calls == ["http://h/u/42"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_spec_with_attribute_placeholder_is_refused_by_the_client():
|
|
141
|
+
import copy
|
|
142
|
+
import sys
|
|
143
|
+
from pathlib import Path
|
|
144
|
+
|
|
145
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
146
|
+
from spec_fixture import SPEC
|
|
147
|
+
|
|
148
|
+
from http2py import HttpClient
|
|
149
|
+
|
|
150
|
+
spec = copy.deepcopy(SPEC)
|
|
151
|
+
spec["paths"]["/x/{uid.__class__.__mro__}"] = spec["paths"].pop("/u/{uid}/p/{pid}")
|
|
152
|
+
with pytest.raises(ValueError, match="reads into an argument"):
|
|
153
|
+
HttpClient(spec, session_state={"session": object(), "refresh_inputs": {}})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize("method", ["GET", "POST"])
|
|
157
|
+
def test_form_requests_send_their_fields_whatever_the_method(method):
|
|
158
|
+
spec = {
|
|
159
|
+
"method": method,
|
|
160
|
+
"url_template": "http://h/f",
|
|
161
|
+
"body_arg_names": ["x", "blob"],
|
|
162
|
+
"content_type": "multipart/form-data",
|
|
163
|
+
"output_trans": _identity,
|
|
164
|
+
}
|
|
165
|
+
_, _, kwargs = _captured_request(spec, x=1, blob=b"\x00")
|
|
166
|
+
assert set(kwargs["files"]) == {"blob", "__fields"}
|
|
@@ -83,6 +83,7 @@ def test_cli_calls_the_route_not_the_client_attribute(monkeypatch):
|
|
|
83
83
|
spec = _spec_naming_plain("ensure_login")
|
|
84
84
|
with pytest.warns(UserWarning):
|
|
85
85
|
route = _client(spec).routes["ensure_login"]
|
|
86
|
+
|
|
86
87
|
class Sent(Exception):
|
|
87
88
|
pass
|
|
88
89
|
|
|
@@ -104,3 +105,72 @@ def test_subclass_skipping_init_can_register_routes():
|
|
|
104
105
|
spec = SPEC["paths"]["/plain"]["get"]
|
|
105
106
|
bare.register_method("https://example.com/plain", "get", spec)
|
|
106
107
|
assert bare.routes["plain"] is bare.plain
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _spec_without_names():
|
|
111
|
+
"""A spec whose routes have no ``x-method_name`` (i2mint/http2py#26)."""
|
|
112
|
+
spec = copy.deepcopy(SPEC)
|
|
113
|
+
for path_item in spec["paths"].values():
|
|
114
|
+
for op in path_item.values():
|
|
115
|
+
op.pop("x-method_name", None)
|
|
116
|
+
return spec
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_routes_without_x_method_name_get_distinct_names_from_method_and_path():
|
|
120
|
+
import warnings
|
|
121
|
+
|
|
122
|
+
with warnings.catch_warnings():
|
|
123
|
+
warnings.simplefilter("error")
|
|
124
|
+
client = _client(_spec_without_names())
|
|
125
|
+
assert len(client.routes) == len(SPEC["paths"]) == 3
|
|
126
|
+
assert "get_plain" in client.routes
|
|
127
|
+
for name, route in client.routes.items():
|
|
128
|
+
assert route.__name__ == name != "request_func"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_operation_id_names_a_route_without_x_method_name():
|
|
132
|
+
spec = _spec_without_names()
|
|
133
|
+
spec["paths"]["/plain"]["get"]["operationId"] = "list-plain.things"
|
|
134
|
+
assert "list_plain_things" in _client(spec).routes
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_x_method_name_wins_over_operation_id():
|
|
138
|
+
spec = _spec_naming_plain("mine")
|
|
139
|
+
spec["paths"]["/plain"]["get"]["operationId"] = "theirs"
|
|
140
|
+
assert "mine" in _client(spec).routes
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_api_pkg_for_a_spec_without_x_method_names():
|
|
144
|
+
files = api_pkg_files(_spec_without_names(), pkg_name="genpkg_d")
|
|
145
|
+
assert "def get_plain(" in files["genpkg_d/funcs.py"]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_generated_package_pins_the_http2py_that_has_client_routes():
|
|
149
|
+
from http2py.api_pkg_maker import MIN_HTTP2PY_VERSION
|
|
150
|
+
|
|
151
|
+
files = api_pkg_files(SPEC, pkg_name="genpkg_e")
|
|
152
|
+
assert f"http2py>={MIN_HTTP2PY_VERSION}" in files["setup.cfg"]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_derived_names_do_not_depend_on_the_server_url():
|
|
156
|
+
spec = _spec_without_names()
|
|
157
|
+
spec["servers"] = [{"url": "https://example.com/v1/api"}]
|
|
158
|
+
assert set(_client(spec).routes) == set(_client(_spec_without_names()).routes)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize("operation_id", ["-", "___", "日本"])
|
|
162
|
+
def test_unusable_operation_id_falls_back_to_method_and_path(operation_id):
|
|
163
|
+
spec = _spec_without_names()
|
|
164
|
+
spec["paths"]["/plain"]["get"]["operationId"] = operation_id
|
|
165
|
+
assert "get_plain" in _client(spec).routes
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_a_second_route_of_the_same_name_replaces_the_first_with_a_warning():
|
|
169
|
+
spec = copy.deepcopy(SPEC)
|
|
170
|
+
other = copy.deepcopy(spec["paths"]["/plain"])
|
|
171
|
+
spec["paths"]["/plain2"] = other
|
|
172
|
+
with pytest.warns(UserWarning, match="Two routes are named"):
|
|
173
|
+
client = _client(spec)
|
|
174
|
+
name = other["get"]["x-method_name"]
|
|
175
|
+
assert client.routes[name].method_spec["url_template"].endswith("/plain2")
|
|
176
|
+
assert getattr(client, name) is client.routes[name]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|