http2py 0.1.36__tar.gz → 0.1.38__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.36 → http2py-0.1.38}/PKG-INFO +1 -1
- {http2py-0.1.36 → http2py-0.1.38}/http2py/api_pkg_maker.py +8 -8
- {http2py-0.1.36 → http2py-0.1.38}/http2py/cli_maker.py +2 -3
- {http2py-0.1.36 → http2py-0.1.38}/http2py/client.py +56 -3
- {http2py-0.1.36 → http2py-0.1.38}/http2py/py2request.py +28 -1
- {http2py-0.1.36 → http2py-0.1.38}/pyproject.toml +1 -1
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_generated_clients.py +1 -0
- http2py-0.1.38/tests/test_route_namespace.py +164 -0
- {http2py-0.1.36 → http2py-0.1.38}/.gitattributes +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/.github/workflows/ci.yml +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/.gitignore +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/LICENSE +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/README.md +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/conftest.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/__init__.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/authentication.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/constants.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/decorators.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/default_configs.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/example_cli.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/global_state.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/http2py_cls.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/resources/module_maker/func_tpl.txt +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/testing_utils.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/tests/__init__.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/tests/api_pkg_maker_test.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/tests/conftest.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/tests/test_requirements.txt +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/http2py/util.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/cli_goldens/mk_cli.json +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/spec_fixture.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_binary_payloads.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_ci_collection_contract.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_cli_surface.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_error_statuses.py +0 -0
- {http2py-0.1.36 → http2py-0.1.38}/tests/test_smoke.py +0 -0
|
@@ -79,11 +79,14 @@ _RESERVED_ROUTE_NAMES = {"api", "funcs"} # funcs: the module's own name
|
|
|
79
79
|
|
|
80
80
|
FUNC_TPL = """
|
|
81
81
|
|
|
82
|
-
@_apply_sig(api.{func_name})
|
|
82
|
+
@_apply_sig(api.routes[{func_name!r}])
|
|
83
83
|
def {func_name}(*args, **kwargs):
|
|
84
|
-
return api.{func_name}(*args, **kwargs)
|
|
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}
|
|
@@ -158,11 +161,7 @@ def object_ref(obj: Union[str, Callable]) -> str:
|
|
|
158
161
|
def _route_names(api: HttpClient) -> list:
|
|
159
162
|
"""The method names ``api`` registered, each a plain identifier, since each is
|
|
160
163
|
written into generated source."""
|
|
161
|
-
names =
|
|
162
|
-
name
|
|
163
|
-
for name, method in vars(api).items()
|
|
164
|
-
if getattr(method, "method_spec", None)
|
|
165
|
-
]
|
|
164
|
+
names = list(api.routes)
|
|
166
165
|
for name in names:
|
|
167
166
|
if not _is_plain_identifier(name):
|
|
168
167
|
raise ValueError(
|
|
@@ -219,6 +218,7 @@ def api_pkg_files(
|
|
|
219
218
|
author=_cfg_value(pkg_author or "API Package Maker"),
|
|
220
219
|
license=_cfg_value(pkg_license or "Apache"),
|
|
221
220
|
spec_filename=SPEC_FILENAME,
|
|
221
|
+
min_http2py=MIN_HTTP2PY_VERSION,
|
|
222
222
|
)
|
|
223
223
|
funcs_code = FUNCS_FILE_TPL.format(
|
|
224
224
|
spec_filename=SPEC_FILENAME,
|
|
@@ -143,8 +143,7 @@ def mk_cli(
|
|
|
143
143
|
config_filename,
|
|
144
144
|
binary_loads=binary_loads,
|
|
145
145
|
)
|
|
146
|
-
for
|
|
147
|
-
if getattr(method, "method_spec", None)
|
|
146
|
+
for method in client_details.routes.values()
|
|
148
147
|
]
|
|
149
148
|
return cw.mk_parser(cli_methods, convention=CLI_CONVENTION)
|
|
150
149
|
|
|
@@ -208,7 +207,7 @@ def register_cli_method(
|
|
|
208
207
|
auth_kwargs, expected_auth_kwargs, config_filename, profile
|
|
209
208
|
)
|
|
210
209
|
http_client = HttpClient(openapi_spec, binary_loads=binary_loads, **auth_kwargs)
|
|
211
|
-
return
|
|
210
|
+
return http_client.routes[methodname](**kwargs)
|
|
212
211
|
|
|
213
212
|
method_sig.wrap(cli_method)
|
|
214
213
|
cli_method.__name__ = methodname
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
and exposes each declared route as a normal python method.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
7
9
|
from glom import glom
|
|
8
10
|
from requests import request, get, Session
|
|
9
11
|
from i2.errors import AuthorizationError
|
|
@@ -22,6 +24,15 @@ class HttpClient:
|
|
|
22
24
|
"""
|
|
23
25
|
A client class meant as an interface to an HTTP service with one or more routes
|
|
24
26
|
defined with an OpenAPI spec.
|
|
27
|
+
|
|
28
|
+
Every route is in ``client.routes`` (a ``{name: method}`` dict), and is also an
|
|
29
|
+
attribute of the client (``client.<name>``) as long as its name neither starts
|
|
30
|
+
with ``_`` nor is already an attribute the client works with: a spec, possibly
|
|
31
|
+
served by a remote, must not be able to replace the client's own attributes (such
|
|
32
|
+
as ``session`` or ``handle_request``). A route that cannot be an attribute is
|
|
33
|
+
still reachable as ``client.routes[name]``, and a warning says so. The purely
|
|
34
|
+
informational attributes (``title``, ``version``, ``base_url``), which the client
|
|
35
|
+
never reads after ``__init__``, can be taken by a route.
|
|
25
36
|
"""
|
|
26
37
|
|
|
27
38
|
auth_type = ""
|
|
@@ -31,6 +42,10 @@ class HttpClient:
|
|
|
31
42
|
refresh_url = ""
|
|
32
43
|
refresh_input_keys = []
|
|
33
44
|
session = None
|
|
45
|
+
login_args = None # set by init_security; declared so no route can take the name
|
|
46
|
+
login_response_keys = () # idem
|
|
47
|
+
# Set by __init__ but never read afterwards, so a route may take these names.
|
|
48
|
+
_route_shadowable_attrs = frozenset({"title", "version", "base_url"})
|
|
34
49
|
|
|
35
50
|
def __init__(
|
|
36
51
|
self,
|
|
@@ -96,10 +111,13 @@ class HttpClient:
|
|
|
96
111
|
self.refresh_inputs = session_state.get("refresh_inputs")
|
|
97
112
|
if self.refresh_inputs is None:
|
|
98
113
|
raise ValueError("No refresh inputs provided when instantiating HttpClient")
|
|
114
|
+
self.routes = {}
|
|
99
115
|
for pathname, path_spec in openapi_spec["paths"].items():
|
|
100
116
|
url_template = self.base_url + pathname
|
|
101
117
|
for http_method, openapi_method_spec in path_spec.items():
|
|
102
|
-
self.register_method(
|
|
118
|
+
self.register_method(
|
|
119
|
+
url_template, http_method, openapi_method_spec, pathname=pathname
|
|
120
|
+
)
|
|
103
121
|
|
|
104
122
|
def init_security(self, openapi_spec, **auth_kwargs):
|
|
105
123
|
security = openapi_spec["security"]
|
|
@@ -127,7 +145,9 @@ class HttpClient:
|
|
|
127
145
|
if jwt:
|
|
128
146
|
self.set_header({"Authorization": f"Bearer {jwt}"})
|
|
129
147
|
|
|
130
|
-
def register_method(
|
|
148
|
+
def register_method(
|
|
149
|
+
self, url_template, http_method, openapi_method_spec, *, pathname=None
|
|
150
|
+
):
|
|
131
151
|
content_type = None
|
|
132
152
|
if "requestBody" in openapi_method_spec:
|
|
133
153
|
content_type = next(
|
|
@@ -138,6 +158,7 @@ class HttpClient:
|
|
|
138
158
|
method=http_method,
|
|
139
159
|
url_template=url_template,
|
|
140
160
|
content_type=content_type,
|
|
161
|
+
pathname=pathname,
|
|
141
162
|
)
|
|
142
163
|
func = mk_request_function(
|
|
143
164
|
method_spec,
|
|
@@ -149,7 +170,39 @@ class HttpClient:
|
|
|
149
170
|
func.method_spec = method_spec
|
|
150
171
|
func.content_type = content_type
|
|
151
172
|
funcname = func.__name__
|
|
152
|
-
|
|
173
|
+
method = func.__get__(self)
|
|
174
|
+
if "routes" not in vars(self): # e.g. a subclass that skips HttpClient.__init__
|
|
175
|
+
self.routes = {}
|
|
176
|
+
if funcname in self.routes:
|
|
177
|
+
warnings.warn(
|
|
178
|
+
f"Two routes are named {funcname!r} ({url_template!r} replaces the "
|
|
179
|
+
"earlier one): give each its own x-method_name.",
|
|
180
|
+
stacklevel=3,
|
|
181
|
+
)
|
|
182
|
+
can_be_attribute = self._can_be_route_attribute(funcname)
|
|
183
|
+
self.routes[funcname] = method
|
|
184
|
+
if can_be_attribute:
|
|
185
|
+
setattr(self, funcname, method)
|
|
186
|
+
else:
|
|
187
|
+
warnings.warn(
|
|
188
|
+
f"Route {funcname!r} is not set as an attribute of the client: the name "
|
|
189
|
+
f"starts with '_' or is already an attribute of {type(self).__name__}. "
|
|
190
|
+
f"Call it as client.routes[{funcname!r}](...).",
|
|
191
|
+
stacklevel=3,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _can_be_route_attribute(self, name):
|
|
195
|
+
"""Whether a route called ``name`` may be set as an attribute of the client:
|
|
196
|
+
the name is public, and is not an attribute the client works with already
|
|
197
|
+
(an earlier route of the same name, or a ``_route_shadowable_attrs`` one, can
|
|
198
|
+
be replaced)."""
|
|
199
|
+
if name.startswith("_"):
|
|
200
|
+
return False
|
|
201
|
+
if name in self.routes and vars(self).get(name) is self.routes[name]:
|
|
202
|
+
return True
|
|
203
|
+
if name in self._route_shadowable_attrs:
|
|
204
|
+
return True
|
|
205
|
+
return not (hasattr(type(self), name) or name in vars(self))
|
|
153
206
|
|
|
154
207
|
def handle_request(self, method, url, **_request_kwargs):
|
|
155
208
|
self.ensure_login()
|
|
@@ -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
|
|
@@ -607,6 +609,30 @@ def raw_response_on_error(func):
|
|
|
607
609
|
return _func
|
|
608
610
|
|
|
609
611
|
|
|
612
|
+
def _identifier(text):
|
|
613
|
+
"""``text`` as a public Python identifier: runs of other characters (and of ``_``) become ``_``,
|
|
614
|
+
a leading digit gets an ``op_`` prefix and a keyword a trailing ``_``."""
|
|
615
|
+
name = re.sub(r"[\W_]+", "_", text, flags=re.ASCII).strip("_")
|
|
616
|
+
if name[:1].isdigit():
|
|
617
|
+
name = f"op_{name}"
|
|
618
|
+
return f"{name}_" if keyword.iskeyword(name) else name
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _route_name(openapi_method_spec, method, pathname, url_template):
|
|
622
|
+
"""The name of the function for a route: its ``x-method_name``, else its
|
|
623
|
+
``operationId``, else the http method and the path (``get /users/{id}`` gives
|
|
624
|
+
``get_users_id``), so that routes without ``x-method_name`` do not all share a name.
|
|
625
|
+
``pathname`` is the path as the spec gives it, so that a server's base url does not
|
|
626
|
+
change the names; it defaults to the path of ``url_template``.
|
|
627
|
+
"""
|
|
628
|
+
if openapi_method_spec.get("x-method_name"):
|
|
629
|
+
return openapi_method_spec["x-method_name"]
|
|
630
|
+
name = _identifier(openapi_method_spec.get("operationId") or "")
|
|
631
|
+
if pathname is None:
|
|
632
|
+
pathname = urlparse(url_template).path
|
|
633
|
+
return name or default_path_to_func_name(method, pathname)
|
|
634
|
+
|
|
635
|
+
|
|
610
636
|
def mk_method_spec_from_openapi_method_spec(
|
|
611
637
|
openapi_method_spec,
|
|
612
638
|
method="post",
|
|
@@ -614,6 +640,7 @@ def mk_method_spec_from_openapi_method_spec(
|
|
|
614
640
|
content_type=JSON_CONTENT_TYPE,
|
|
615
641
|
input_trans=None,
|
|
616
642
|
output_trans=None,
|
|
643
|
+
pathname=None,
|
|
617
644
|
):
|
|
618
645
|
path_arg_names = []
|
|
619
646
|
query_arg_names = []
|
|
@@ -671,7 +698,7 @@ def mk_method_spec_from_openapi_method_spec(
|
|
|
671
698
|
path_arg_names=path_arg_names,
|
|
672
699
|
query_arg_names=query_arg_names,
|
|
673
700
|
body_arg_names=body_arg_names,
|
|
674
|
-
method_name=openapi_method_spec
|
|
701
|
+
method_name=_route_name(openapi_method_spec, method, pathname, url_template),
|
|
675
702
|
docstring=openapi_method_spec.get("description", ""),
|
|
676
703
|
content_type=content_type,
|
|
677
704
|
response_type=next(iter(glom(openapi_method_spec, f"responses.200.content"))),
|
|
@@ -125,6 +125,7 @@ def test_dispatch_cli_passes_binary_loads_to_the_client(monkeypatch, tmp_path):
|
|
|
125
125
|
class FakeClient:
|
|
126
126
|
def __init__(self, openapi_spec, **kwargs):
|
|
127
127
|
seen.update(kwargs)
|
|
128
|
+
self.routes = {"plain": self.plain}
|
|
128
129
|
|
|
129
130
|
def plain(self, **kwargs):
|
|
130
131
|
return "ran"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""A route's name cannot replace an attribute of ``HttpClient`` (i2mint/http2py#23):
|
|
2
|
+
every route is in ``client.routes``, and only names that collide with nothing are
|
|
3
|
+
also set as attributes."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
14
|
+
|
|
15
|
+
from spec_fixture import SPEC # noqa: E402
|
|
16
|
+
|
|
17
|
+
from http2py import HttpClient # noqa: E402
|
|
18
|
+
from http2py.api_pkg_maker import api_pkg_files # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _spec_naming_plain(name):
|
|
22
|
+
spec = copy.deepcopy(SPEC)
|
|
23
|
+
spec["paths"]["/plain"]["get"]["x-method_name"] = name
|
|
24
|
+
return spec
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _client(spec):
|
|
28
|
+
return HttpClient(spec, session_state={"session": object(), "refresh_inputs": {}})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"name",
|
|
33
|
+
["openapi_spec", "session", "handle_request", "ensure_login", "binary_loads"]
|
|
34
|
+
+ ["routes", "login_args", "login_response_keys", "verify_cert", "_private"],
|
|
35
|
+
)
|
|
36
|
+
def test_colliding_route_name_keeps_the_client_attribute(name):
|
|
37
|
+
spec = _spec_naming_plain(name)
|
|
38
|
+
with pytest.warns(UserWarning, match=f"client.routes\\[{name!r}\\]"):
|
|
39
|
+
client = _client(spec)
|
|
40
|
+
route = client.routes[name]
|
|
41
|
+
assert getattr(route, "method_spec", None)
|
|
42
|
+
assert getattr(client, name, None) is not route
|
|
43
|
+
assert client.openapi_spec is spec
|
|
44
|
+
assert callable(client.handle_request) and callable(client.ensure_login)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_non_colliding_routes_are_attributes_too():
|
|
48
|
+
client = _client(SPEC)
|
|
49
|
+
assert set(client.routes) == {"get_thing", "make", "plain"}
|
|
50
|
+
for name, route in client.routes.items():
|
|
51
|
+
assert getattr(client, name) is route
|
|
52
|
+
assert client.is_request_method(name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_api_pkg_for_a_route_named_like_a_client_attribute():
|
|
56
|
+
# used to crash with "'function' object has no attribute 'get'"
|
|
57
|
+
with pytest.warns(UserWarning):
|
|
58
|
+
files = api_pkg_files(_spec_naming_plain("openapi_spec"), pkg_name="genpkg_c")
|
|
59
|
+
funcs = files["genpkg_c/funcs.py"]
|
|
60
|
+
assert "def openapi_spec(" in funcs
|
|
61
|
+
assert "api.routes['openapi_spec']" in funcs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("name", ["title", "version", "base_url"])
|
|
65
|
+
def test_informational_attributes_can_be_route_names(name):
|
|
66
|
+
import warnings
|
|
67
|
+
|
|
68
|
+
with warnings.catch_warnings():
|
|
69
|
+
warnings.simplefilter("error")
|
|
70
|
+
client = _client(_spec_naming_plain(name))
|
|
71
|
+
assert getattr(client, name) is client.routes[name]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_duplicate_route_names_warn():
|
|
75
|
+
with pytest.warns(UserWarning, match="Two routes are named 'get_thing'"):
|
|
76
|
+
client = _client(_spec_naming_plain("get_thing"))
|
|
77
|
+
assert set(client.routes) == {"get_thing", "make"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cli_calls_the_route_not_the_client_attribute(monkeypatch):
|
|
81
|
+
from http2py import cli_maker
|
|
82
|
+
|
|
83
|
+
spec = _spec_naming_plain("ensure_login")
|
|
84
|
+
with pytest.warns(UserWarning):
|
|
85
|
+
route = _client(spec).routes["ensure_login"]
|
|
86
|
+
class Sent(Exception):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
def handle_request(self, method, url, **kw):
|
|
90
|
+
raise Sent(url)
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(HttpClient, "handle_request", handle_request)
|
|
93
|
+
with pytest.warns(UserWarning), pytest.raises(Sent, match="/plain$"):
|
|
94
|
+
cli_method = cli_maker.register_cli_method(spec, route, [])
|
|
95
|
+
cli_method(config="no-such-config", api_key="k")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_subclass_skipping_init_can_register_routes():
|
|
99
|
+
class Bare(HttpClient):
|
|
100
|
+
def __init__(self):
|
|
101
|
+
self.verify_cert, self.binary_loads, self.binary_dumps = True, None, None
|
|
102
|
+
|
|
103
|
+
bare = Bare()
|
|
104
|
+
spec = SPEC["paths"]["/plain"]["get"]
|
|
105
|
+
bare.register_method("https://example.com/plain", "get", spec)
|
|
106
|
+
assert bare.routes["plain"] is bare.plain
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _spec_without_names():
|
|
110
|
+
"""A spec whose routes have no ``x-method_name`` (i2mint/http2py#26)."""
|
|
111
|
+
spec = copy.deepcopy(SPEC)
|
|
112
|
+
for path_item in spec["paths"].values():
|
|
113
|
+
for op in path_item.values():
|
|
114
|
+
op.pop("x-method_name", None)
|
|
115
|
+
return spec
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_routes_without_x_method_name_get_distinct_names_from_method_and_path():
|
|
119
|
+
import warnings
|
|
120
|
+
|
|
121
|
+
with warnings.catch_warnings():
|
|
122
|
+
warnings.simplefilter("error")
|
|
123
|
+
client = _client(_spec_without_names())
|
|
124
|
+
assert len(client.routes) == len(SPEC["paths"]) == 3
|
|
125
|
+
assert "get_plain" in client.routes
|
|
126
|
+
for name, route in client.routes.items():
|
|
127
|
+
assert route.__name__ == name != "request_func"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_operation_id_names_a_route_without_x_method_name():
|
|
131
|
+
spec = _spec_without_names()
|
|
132
|
+
spec["paths"]["/plain"]["get"]["operationId"] = "list-plain.things"
|
|
133
|
+
assert "list_plain_things" in _client(spec).routes
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_x_method_name_wins_over_operation_id():
|
|
137
|
+
spec = _spec_naming_plain("mine")
|
|
138
|
+
spec["paths"]["/plain"]["get"]["operationId"] = "theirs"
|
|
139
|
+
assert "mine" in _client(spec).routes
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_api_pkg_for_a_spec_without_x_method_names():
|
|
143
|
+
files = api_pkg_files(_spec_without_names(), pkg_name="genpkg_d")
|
|
144
|
+
assert "def get_plain(" in files["genpkg_d/funcs.py"]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_generated_package_pins_the_http2py_that_has_client_routes():
|
|
148
|
+
from http2py.api_pkg_maker import MIN_HTTP2PY_VERSION
|
|
149
|
+
|
|
150
|
+
files = api_pkg_files(SPEC, pkg_name="genpkg_e")
|
|
151
|
+
assert f"http2py>={MIN_HTTP2PY_VERSION}" in files["setup.cfg"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_derived_names_do_not_depend_on_the_server_url():
|
|
155
|
+
spec = _spec_without_names()
|
|
156
|
+
spec["servers"] = [{"url": "https://example.com/v1/api"}]
|
|
157
|
+
assert set(_client(spec).routes) == set(_client(_spec_without_names()).routes)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize("operation_id", ["-", "___", "日本"])
|
|
161
|
+
def test_unusable_operation_id_falls_back_to_method_and_path(operation_id):
|
|
162
|
+
spec = _spec_without_names()
|
|
163
|
+
spec["paths"]["/plain"]["get"]["operationId"] = operation_id
|
|
164
|
+
assert "get_plain" in _client(spec).routes
|
|
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
|