http2py 0.1.37__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.
Files changed (36) hide show
  1. {http2py-0.1.37 → http2py-0.1.38}/PKG-INFO +1 -1
  2. {http2py-0.1.37 → http2py-0.1.38}/http2py/api_pkg_maker.py +5 -1
  3. {http2py-0.1.37 → http2py-0.1.38}/http2py/client.py +7 -2
  4. {http2py-0.1.37 → http2py-0.1.38}/http2py/py2request.py +28 -1
  5. {http2py-0.1.37 → http2py-0.1.38}/pyproject.toml +1 -1
  6. {http2py-0.1.37 → http2py-0.1.38}/tests/test_route_namespace.py +58 -0
  7. {http2py-0.1.37 → http2py-0.1.38}/.gitattributes +0 -0
  8. {http2py-0.1.37 → http2py-0.1.38}/.github/workflows/ci.yml +0 -0
  9. {http2py-0.1.37 → http2py-0.1.38}/.gitignore +0 -0
  10. {http2py-0.1.37 → http2py-0.1.38}/LICENSE +0 -0
  11. {http2py-0.1.37 → http2py-0.1.38}/README.md +0 -0
  12. {http2py-0.1.37 → http2py-0.1.38}/conftest.py +0 -0
  13. {http2py-0.1.37 → http2py-0.1.38}/http2py/__init__.py +0 -0
  14. {http2py-0.1.37 → http2py-0.1.38}/http2py/authentication.py +0 -0
  15. {http2py-0.1.37 → http2py-0.1.38}/http2py/cli_maker.py +0 -0
  16. {http2py-0.1.37 → http2py-0.1.38}/http2py/constants.py +0 -0
  17. {http2py-0.1.37 → http2py-0.1.38}/http2py/decorators.py +0 -0
  18. {http2py-0.1.37 → http2py-0.1.38}/http2py/default_configs.py +0 -0
  19. {http2py-0.1.37 → http2py-0.1.38}/http2py/example_cli.py +0 -0
  20. {http2py-0.1.37 → http2py-0.1.38}/http2py/global_state.py +0 -0
  21. {http2py-0.1.37 → http2py-0.1.38}/http2py/http2py_cls.py +0 -0
  22. {http2py-0.1.37 → http2py-0.1.38}/http2py/resources/module_maker/func_tpl.txt +0 -0
  23. {http2py-0.1.37 → http2py-0.1.38}/http2py/testing_utils.py +0 -0
  24. {http2py-0.1.37 → http2py-0.1.38}/http2py/tests/__init__.py +0 -0
  25. {http2py-0.1.37 → http2py-0.1.38}/http2py/tests/api_pkg_maker_test.py +0 -0
  26. {http2py-0.1.37 → http2py-0.1.38}/http2py/tests/conftest.py +0 -0
  27. {http2py-0.1.37 → http2py-0.1.38}/http2py/tests/test_requirements.txt +0 -0
  28. {http2py-0.1.37 → http2py-0.1.38}/http2py/util.py +0 -0
  29. {http2py-0.1.37 → http2py-0.1.38}/tests/cli_goldens/mk_cli.json +0 -0
  30. {http2py-0.1.37 → http2py-0.1.38}/tests/spec_fixture.py +0 -0
  31. {http2py-0.1.37 → http2py-0.1.38}/tests/test_binary_payloads.py +0 -0
  32. {http2py-0.1.37 → http2py-0.1.38}/tests/test_ci_collection_contract.py +0 -0
  33. {http2py-0.1.37 → http2py-0.1.38}/tests/test_cli_surface.py +0 -0
  34. {http2py-0.1.37 → http2py-0.1.38}/tests/test_error_statuses.py +0 -0
  35. {http2py-0.1.37 → http2py-0.1.38}/tests/test_generated_clients.py +0 -0
  36. {http2py-0.1.37 → http2py-0.1.38}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: http2py
3
- Version: 0.1.37
3
+ Version: 0.1.38
4
4
  Summary: Tools to create python binders to http web services.
5
5
  Project-URL: Homepage, https://github.com/i2mint/http2py
6
6
  Project-URL: Repository, https://github.com/i2mint/http2py
@@ -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(url_template, http_method, openapi_method_spec)
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(self, url_template, http_method, openapi_method_spec):
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
@@ -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.get("x-method_name", ""),
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"))),
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "http2py"
9
- version = "0.1.37"
9
+ version = "0.1.38"
10
10
  description = "Tools to create python binders to http web services."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -104,3 +104,61 @@ def test_subclass_skipping_init_can_register_routes():
104
104
  spec = SPEC["paths"]["/plain"]["get"]
105
105
  bare.register_method("https://example.com/plain", "get", spec)
106
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