http2py 0.1.29__tar.gz → 0.1.31__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.31/PKG-INFO +88 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/api_pkg_maker.py +30 -30
- {http2py-0.1.29 → http2py-0.1.31}/http2py/authentication.py +7 -7
- {http2py-0.1.29 → http2py-0.1.31}/http2py/cli_maker.py +23 -22
- {http2py-0.1.29 → http2py-0.1.31}/http2py/client.py +44 -43
- http2py-0.1.31/http2py/constants.py +4 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/decorators.py +4 -4
- {http2py-0.1.29 → http2py-0.1.31}/http2py/example_cli.py +4 -4
- {http2py-0.1.29 → http2py-0.1.31}/http2py/py2request.py +205 -133
- {http2py-0.1.29 → http2py-0.1.31}/http2py/testing_utils.py +6 -6
- http2py-0.1.31/http2py/tests/api_pkg_maker_test.py +161 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/tests/conftest.py +5 -5
- {http2py-0.1.29 → http2py-0.1.31}/http2py/util.py +5 -9
- http2py-0.1.31/http2py.egg-info/PKG-INFO +88 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/entry_points.txt +0 -1
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/requires.txt +1 -0
- {http2py-0.1.29 → http2py-0.1.31}/setup.cfg +2 -2
- http2py-0.1.29/PKG-INFO +0 -80
- http2py-0.1.29/http2py/constants.py +0 -4
- http2py-0.1.29/http2py/tests/api_pkg_maker_test.py +0 -153
- http2py-0.1.29/http2py.egg-info/PKG-INFO +0 -80
- {http2py-0.1.29 → http2py-0.1.31}/LICENSE +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/README.md +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/__init__.py +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/default_configs.py +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/global_state.py +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/http2py_cls.py +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py/tests/__init__.py +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/SOURCES.txt +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/dependency_links.txt +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/not-zip-safe +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/http2py.egg-info/top_level.txt +0 -0
- {http2py-0.1.29 → http2py-0.1.31}/setup.py +0 -0
http2py-0.1.31/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: http2py
|
|
3
|
+
Version: 0.1.31
|
|
4
|
+
Summary: Tools to create python binders to http web services.
|
|
5
|
+
Home-page: https://github.com/i2mint/http2py
|
|
6
|
+
License: Apache
|
|
7
|
+
Keywords: webservice,http,requests,API
|
|
8
|
+
Platform: any
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: glom
|
|
12
|
+
Requires-Dist: i2
|
|
13
|
+
Requires-Dist: ju
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: argh
|
|
16
|
+
Requires-Dist: PyYAML
|
|
17
|
+
Requires-Dist: importlib_resources
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
[Documentation here](https://i2mint.github.io/http2py/)
|
|
21
|
+
|
|
22
|
+
`the_one_where_we_have_an_http_service_that_we_want_call_through_normal_looking_python_functions`
|
|
23
|
+
|
|
24
|
+
# (Http) Requests for humans
|
|
25
|
+
|
|
26
|
+
Tools to create python binders to http web services.
|
|
27
|
+
Here, we develop python data access to web content through http request such as web services or web pages.
|
|
28
|
+
|
|
29
|
+
The story is as follows: You found something on the web that you'd like to interact with.
|
|
30
|
+
|
|
31
|
+
* This could be data you'd like to have access to,
|
|
32
|
+
* or some actions you'd like to perform -- such as search, posting an article, buying some bitcoin, etc..
|
|
33
|
+
* If you're really lucky, someone's already written a python layer to do what you want to do.
|
|
34
|
+
* If you're just lucky, the resource comes with a clear interface definition that makes it
|
|
35
|
+
easy for you to write your own python for.
|
|
36
|
+
* If you're less lucky, you'll have to decipher some bad documentation of the API,
|
|
37
|
+
or even write your own bots, crawlers and parsers to get what you want.
|
|
38
|
+
|
|
39
|
+
We want to make that all easier for you.
|
|
40
|
+
|
|
41
|
+
What we'll focus on for now is the fundamental common aspect of making a python layer to the http requests.
|
|
42
|
+
|
|
43
|
+
"But... the excellent [`requests` library](https://requests.readthedocs.io/en/master/)
|
|
44
|
+
already provides the python wrapper to http!" you say. Yes, and we thank the `requests` developers for that.
|
|
45
|
+
It is indeed an excellent package that we won't have to write now!
|
|
46
|
+
|
|
47
|
+
We'd like to build from there, cover more annoying boilerplate, and get you closer, faster,
|
|
48
|
+
to the functionality you actually need:
|
|
49
|
+
Functions with the python types you're using in your code,
|
|
50
|
+
that responds with python objects you can directly use in your code.
|
|
51
|
+
|
|
52
|
+
The development of `requests`
|
|
53
|
+
(including the [recommended add-ons](https://requests.readthedocs.io/en/master/community/recommended/))
|
|
54
|
+
could be said to be facing the metal (raw http mechanics) and building interfaces with humans in mind.
|
|
55
|
+
By contrast, we want to face the human and build adapters to communicate with the metal.
|
|
56
|
+
That is, in our case, the fact that http is under the hood shouldn't even be visible (unless it needs to).
|
|
57
|
+
|
|
58
|
+
It's in honor of the inspiration of "designing for humans" that we extend requests' tag-line,
|
|
59
|
+
to "requests for humans".
|
|
60
|
+
|
|
61
|
+
# The approach
|
|
62
|
+
|
|
63
|
+
As always, we favor expressiveness, boilerplate minimization, separation of concerns, and a layered approach that
|
|
64
|
+
incrementally transforms and enhance to get us from where we are to where we want to be.
|
|
65
|
+
|
|
66
|
+
So for py2request, where are we and where do we want to be?
|
|
67
|
+
|
|
68
|
+
**What we have**: We have some http accessible resources. For instance, a perfectly well structured API
|
|
69
|
+
specification of a REST webservice, a very messy website, or anything in between.
|
|
70
|
+
|
|
71
|
+
**What we'd like**: A Python interface to these web stuff.
|
|
72
|
+
That is, we want to talk python to the resources, and have it talk python back to us.
|
|
73
|
+
Further, we want this conversation to be to the point; we don't want to have to say more than the exact necessary,
|
|
74
|
+
and don't want responses containing anything but exactly what we want and how we want it.
|
|
75
|
+
Finally, we'd like to get this with minimum effort -- letting default choices be made for us, but being able to make
|
|
76
|
+
our own choices if we want.
|
|
77
|
+
|
|
78
|
+
Is that too much to ask?
|
|
79
|
+
|
|
80
|
+
In the layered approach we'll go from function specifications, to functions returning python
|
|
81
|
+
Request objects, which we can then wrap further to create functions actually executing the request,
|
|
82
|
+
getting back a Response object, and wrap further to get the final python output type.
|
|
83
|
+
|
|
84
|
+
So the objects in this pipeline are:
|
|
85
|
+
* Specification
|
|
86
|
+
* Request
|
|
87
|
+
* Response
|
|
88
|
+
* Final python object
|
|
@@ -6,12 +6,12 @@ from http2py.client import HttpClient
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from setuptools import sandbox
|
|
8
8
|
|
|
9
|
-
OUTPUT_DIR = os.path.join(os.environ[
|
|
9
|
+
OUTPUT_DIR = os.path.join(os.environ["HOME"], "http2py", "api_pkgs")
|
|
10
10
|
|
|
11
|
-
INIT_FILE_TPL =
|
|
12
|
-
|
|
11
|
+
INIT_FILE_TPL = """from .funcs import {funcs}
|
|
12
|
+
"""
|
|
13
13
|
|
|
14
|
-
FUNCS_FILE_TPL =
|
|
14
|
+
FUNCS_FILE_TPL = """from i2 import Sig
|
|
15
15
|
from http2py.client import HttpClient
|
|
16
16
|
|
|
17
17
|
api = HttpClient(openapi_spec={openapi_spec}, url={openapi_url})
|
|
@@ -22,21 +22,21 @@ def apply_sig(meth):
|
|
|
22
22
|
sig = Sig(meth) - 'self'
|
|
23
23
|
return sig(func)
|
|
24
24
|
return wrapper
|
|
25
|
-
{funcs}
|
|
25
|
+
{funcs}"""
|
|
26
26
|
|
|
27
|
-
FUNC_TPL =
|
|
27
|
+
FUNC_TPL = """
|
|
28
28
|
|
|
29
29
|
@apply_sig(api.{func_name})
|
|
30
30
|
def {func_name}(*args, **kwargs):
|
|
31
31
|
return api.{func_name}(*args, **kwargs)
|
|
32
|
-
|
|
32
|
+
"""
|
|
33
33
|
|
|
34
|
-
SETUP_PY =
|
|
34
|
+
SETUP_PY = """from setuptools import setup
|
|
35
35
|
|
|
36
36
|
setup()
|
|
37
|
-
|
|
37
|
+
"""
|
|
38
38
|
|
|
39
|
-
SETUP_CFG_TPL =
|
|
39
|
+
SETUP_CFG_TPL = """[metadata]
|
|
40
40
|
name = {name}
|
|
41
41
|
version = {version}
|
|
42
42
|
platforms = any
|
|
@@ -51,7 +51,7 @@ zip_safe = False
|
|
|
51
51
|
install_requires =
|
|
52
52
|
i2
|
|
53
53
|
http2py
|
|
54
|
-
|
|
54
|
+
"""
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def mk_api_pkg(
|
|
@@ -94,55 +94,55 @@ def mk_api_pkg(
|
|
|
94
94
|
dt = datetime.now(timezone.utc)
|
|
95
95
|
utc_time = dt.replace(tzinfo=timezone.utc)
|
|
96
96
|
utc_timestamp = int(utc_time.timestamp())
|
|
97
|
-
return f
|
|
97
|
+
return f"apipkg{utc_timestamp}"
|
|
98
98
|
|
|
99
99
|
def create_file(filepath, content):
|
|
100
|
-
with open(filepath,
|
|
100
|
+
with open(filepath, "w") as file:
|
|
101
101
|
print(content, file=file)
|
|
102
102
|
|
|
103
103
|
tempdir = tempfile.mkdtemp()
|
|
104
104
|
try:
|
|
105
105
|
api = HttpClient(openapi_spec=openapi_spec, url=openapi_url)
|
|
106
|
-
paths_spec = api.openapi_spec.get(
|
|
106
|
+
paths_spec = api.openapi_spec.get("paths")
|
|
107
107
|
if not paths_spec:
|
|
108
|
-
raise RuntimeError(
|
|
108
|
+
raise RuntimeError("The API is empty")
|
|
109
109
|
func_names = [path[1:] for path in paths_spec]
|
|
110
110
|
funcs_code = [FUNC_TPL.format(func_name=func_name) for func_name in func_names]
|
|
111
111
|
funcs_file_code = FUNCS_FILE_TPL.format(
|
|
112
112
|
openapi_spec=mk_str_element(openapi_spec),
|
|
113
113
|
openapi_url=mk_str_element(openapi_url),
|
|
114
|
-
funcs=
|
|
114
|
+
funcs="".join(funcs_code),
|
|
115
115
|
)
|
|
116
|
-
init_file_code = INIT_FILE_TPL.format(funcs=
|
|
116
|
+
init_file_code = INIT_FILE_TPL.format(funcs=", ".join(func_names))
|
|
117
117
|
pkg_name = pkg_name or mk_pkg_name()
|
|
118
118
|
module_dir = os.path.join(tempdir, pkg_name)
|
|
119
119
|
os.makedirs(module_dir)
|
|
120
120
|
create_file(
|
|
121
|
-
filepath=os.path.join(module_dir, f
|
|
121
|
+
filepath=os.path.join(module_dir, f"__init__.py"), content=init_file_code
|
|
122
122
|
)
|
|
123
123
|
create_file(
|
|
124
|
-
filepath=os.path.join(module_dir, f
|
|
124
|
+
filepath=os.path.join(module_dir, f"funcs.py"), content=funcs_file_code
|
|
125
125
|
)
|
|
126
|
-
server_url = api.openapi_spec[
|
|
127
|
-
pkg_version = pkg_version or
|
|
126
|
+
server_url = api.openapi_spec["servers"][0]["url"]
|
|
127
|
+
pkg_version = pkg_version or "1.0.0"
|
|
128
128
|
setup_cfg_content = SETUP_CFG_TPL.format(
|
|
129
129
|
name=pkg_name,
|
|
130
130
|
version=pkg_version,
|
|
131
131
|
description=pkg_description
|
|
132
|
-
or f
|
|
133
|
-
author=pkg_author or
|
|
134
|
-
license=pkg_license or
|
|
132
|
+
or f"A client API to consume the webservices exposed at {server_url}",
|
|
133
|
+
author=pkg_author or "API Package Maker",
|
|
134
|
+
license=pkg_license or "Apache",
|
|
135
135
|
)
|
|
136
136
|
create_file(
|
|
137
|
-
filepath=os.path.join(tempdir, f
|
|
137
|
+
filepath=os.path.join(tempdir, f"setup.cfg"), content=setup_cfg_content
|
|
138
138
|
)
|
|
139
|
-
create_file(filepath=os.path.join(tempdir, f
|
|
139
|
+
create_file(filepath=os.path.join(tempdir, f"setup.py"), content=SETUP_PY)
|
|
140
140
|
os.chdir(tempdir)
|
|
141
|
-
sandbox.run_setup(
|
|
142
|
-
pkg_filename = f
|
|
141
|
+
sandbox.run_setup("setup.py", ["sdist"])
|
|
142
|
+
pkg_filename = f"{pkg_name}-{pkg_version}.tar.gz"
|
|
143
143
|
if not os.path.exists(OUTPUT_DIR):
|
|
144
144
|
os.makedirs(OUTPUT_DIR)
|
|
145
|
-
shutil.copy(src=os.path.join(tempdir,
|
|
145
|
+
shutil.copy(src=os.path.join(tempdir, "dist", pkg_filename), dst=OUTPUT_DIR)
|
|
146
146
|
|
|
147
147
|
return os.path.join(OUTPUT_DIR, pkg_filename)
|
|
148
148
|
|
|
@@ -154,5 +154,5 @@ def main():
|
|
|
154
154
|
argh.dispatch_command(mk_api_pkg)
|
|
155
155
|
|
|
156
156
|
|
|
157
|
-
if __name__ ==
|
|
157
|
+
if __name__ == "__main__":
|
|
158
158
|
main()
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Iterable
|
|
5
5
|
|
|
6
|
-
DFLT_CONFIG_FILENAME = os.path.join(Path.home(),
|
|
6
|
+
DFLT_CONFIG_FILENAME = os.path.join(Path.home(), ".http2py", "credentials.json")
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def mk_auth(
|
|
10
10
|
auth_kwargs: dict,
|
|
11
11
|
expected_auth_kwargs: Iterable[str],
|
|
12
12
|
config_filename=DFLT_CONFIG_FILENAME,
|
|
13
|
-
profile: str =
|
|
13
|
+
profile: str = "",
|
|
14
14
|
):
|
|
15
15
|
output = {}
|
|
16
16
|
stored_auth = {}
|
|
17
17
|
for kwarg in expected_auth_kwargs:
|
|
18
|
-
if not auth_kwargs.get(kwarg,
|
|
18
|
+
if not auth_kwargs.get(kwarg, ""):
|
|
19
19
|
if not stored_auth:
|
|
20
20
|
with open(config_filename) as fp:
|
|
21
21
|
stored_config = json.load(fp)
|
|
22
22
|
if profile and profile in stored_config:
|
|
23
23
|
stored_auth = stored_config[profile]
|
|
24
|
-
elif
|
|
25
|
-
stored_auth = stored_config[
|
|
24
|
+
elif "default" in stored_config:
|
|
25
|
+
stored_auth = stored_config["default"]
|
|
26
26
|
else:
|
|
27
27
|
stored_auth = stored_config
|
|
28
|
-
output[kwarg] = stored_auth.get(kwarg,
|
|
28
|
+
output[kwarg] = stored_auth.get(kwarg, "")
|
|
29
29
|
return dict(auth_kwargs, **output)
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import requests
|
|
9
|
-
from
|
|
9
|
+
from collections.abc import Callable, Iterable
|
|
10
10
|
import yaml
|
|
11
11
|
|
|
12
12
|
from i2.io_trans import JSONAnnotAndDfltIoTrans
|
|
@@ -19,16 +19,17 @@ def mk_sig_argparse_friendly(sig):
|
|
|
19
19
|
"""Modifies a signature to change all leading underscores in param names
|
|
20
20
|
to trailing underscores, to ensure compatibility with argparse"""
|
|
21
21
|
for argname, argspec in sig.parameters.items():
|
|
22
|
-
if argname.startswith(
|
|
23
|
-
argname = argname[1:] +
|
|
22
|
+
if argname.startswith("_"):
|
|
23
|
+
argname = argname[1:] + "_"
|
|
24
24
|
argspec = argspec.replace(name=argname)
|
|
25
|
-
assert argname not in sig.names, f
|
|
25
|
+
assert argname not in sig.names, f"{argname} was already in {sig.names}"
|
|
26
26
|
yield argname, argspec
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def mk_argparse_friendly(func):
|
|
30
30
|
"""Wraps a function to expose a signature that is compatible with argparse by stripping leading
|
|
31
|
-
underscores from all keyword arguments, but does not mutate the signature of the original function
|
|
31
|
+
underscores from all keyword arguments, but does not mutate the signature of the original function
|
|
32
|
+
"""
|
|
32
33
|
orig_sig = Sig(func)
|
|
33
34
|
new_params = dict(mk_sig_argparse_friendly(orig_sig))
|
|
34
35
|
new_sig = orig_sig.replace(
|
|
@@ -40,7 +41,7 @@ def mk_argparse_friendly(func):
|
|
|
40
41
|
mapped_kwargs = {}
|
|
41
42
|
for argname, argvalue in kwargs.items():
|
|
42
43
|
if argname not in [param.name for param in orig_sig.params]:
|
|
43
|
-
mapped_argname =
|
|
44
|
+
mapped_argname = "_" + argname[:-1]
|
|
44
45
|
else:
|
|
45
46
|
mapped_argname = argname
|
|
46
47
|
mapped_kwargs[mapped_argname] = argvalue
|
|
@@ -52,9 +53,9 @@ def mk_argparse_friendly(func):
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
def mk_cli(
|
|
55
|
-
openapi_spec: dict =
|
|
56
|
-
url: str =
|
|
57
|
-
filename: str =
|
|
56
|
+
openapi_spec: dict = "",
|
|
57
|
+
url: str = "",
|
|
58
|
+
filename: str = "",
|
|
58
59
|
parse_yaml: bool = False,
|
|
59
60
|
config_filename: str = DFLT_CONFIG_FILENAME,
|
|
60
61
|
):
|
|
@@ -75,32 +76,32 @@ def mk_cli(
|
|
|
75
76
|
with open(filename) as fp:
|
|
76
77
|
raw_spec = fp.read()
|
|
77
78
|
else:
|
|
78
|
-
raise ValueError(
|
|
79
|
+
raise ValueError("You must provide an OpenAPI spec dict, url, or filename.")
|
|
79
80
|
if raw_spec:
|
|
80
81
|
if parse_yaml:
|
|
81
82
|
openapi_spec = yaml.safe_load(raw_spec)
|
|
82
83
|
else:
|
|
83
84
|
openapi_spec = json.loads(raw_spec)
|
|
84
85
|
else:
|
|
85
|
-
raise ValueError(
|
|
86
|
-
security = openapi_spec.get(
|
|
86
|
+
raise ValueError("No valid OpenAPI spec found.")
|
|
87
|
+
security = openapi_spec.get("security", None)
|
|
87
88
|
expected_auth_kwargs = []
|
|
88
89
|
if security:
|
|
89
90
|
auth_type = list(security.keys())[0]
|
|
90
|
-
if auth_type ==
|
|
91
|
-
expected_auth_kwargs = [
|
|
92
|
-
elif auth_type ==
|
|
91
|
+
if auth_type == "apiKey":
|
|
92
|
+
expected_auth_kwargs = ["api_key"]
|
|
93
|
+
elif auth_type == "bearerAuth":
|
|
93
94
|
login_details = glom(
|
|
94
95
|
openapi_spec,
|
|
95
|
-
|
|
96
|
+
"components.securitySchemes.bearerAuth.x-login",
|
|
96
97
|
default={},
|
|
97
98
|
)
|
|
98
|
-
expected_auth_kwargs = login_details.get(
|
|
99
|
+
expected_auth_kwargs = login_details.get("login_inputs", [])
|
|
99
100
|
client_details = HttpClient(openapi_spec)
|
|
100
101
|
cli_methods = [
|
|
101
102
|
register_cli_method(openapi_spec, method, expected_auth_kwargs)
|
|
102
103
|
for methodname, method in client_details.__dict__.items()
|
|
103
|
-
if getattr(method,
|
|
104
|
+
if getattr(method, "method_spec", None)
|
|
104
105
|
]
|
|
105
106
|
parser = argh.ArghParser()
|
|
106
107
|
parser.add_commands(cli_methods)
|
|
@@ -121,7 +122,7 @@ def register_cli_method(
|
|
|
121
122
|
client_method: Callable,
|
|
122
123
|
expected_auth_kwargs: Iterable[str] = None,
|
|
123
124
|
config_filename: str = DFLT_CONFIG_FILENAME,
|
|
124
|
-
profile: str =
|
|
125
|
+
profile: str = "",
|
|
125
126
|
):
|
|
126
127
|
"""Creates a CLI-friendly function to instantiate an HttpClient with appropriate authentication
|
|
127
128
|
arguments and call a particular method of the client instance
|
|
@@ -141,15 +142,15 @@ def register_cli_method(
|
|
|
141
142
|
method_sig = method_sig.merge_with_sig(
|
|
142
143
|
[
|
|
143
144
|
*[
|
|
144
|
-
{
|
|
145
|
+
{"name": kwarg, "kind": KO, "default": ""}
|
|
145
146
|
for kwarg in expected_auth_kwargs
|
|
146
147
|
],
|
|
147
|
-
{
|
|
148
|
+
{"name": "config", "kind": KO, "default": config_filename},
|
|
148
149
|
]
|
|
149
150
|
)
|
|
150
151
|
|
|
151
152
|
def cli_method(*args, **kwargs):
|
|
152
|
-
config_filename = kwargs.pop(
|
|
153
|
+
config_filename = kwargs.pop("config")
|
|
153
154
|
auth_kwargs = {key: kwargs.pop(key) for key in expected_auth_kwargs}
|
|
154
155
|
auth_kwargs = mk_auth(
|
|
155
156
|
auth_kwargs, expected_auth_kwargs, config_filename, profile
|
|
@@ -16,11 +16,11 @@ class HttpClient:
|
|
|
16
16
|
defined with an OpenAPI spec.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
auth_type =
|
|
19
|
+
auth_type = ""
|
|
20
20
|
login_input_keys = []
|
|
21
|
-
login_url =
|
|
21
|
+
login_url = ""
|
|
22
22
|
openapi_spec = {}
|
|
23
|
-
refresh_url =
|
|
23
|
+
refresh_url = ""
|
|
24
24
|
refresh_input_keys = []
|
|
25
25
|
session = None
|
|
26
26
|
|
|
@@ -53,63 +53,64 @@ class HttpClient:
|
|
|
53
53
|
if url and not openapi_spec:
|
|
54
54
|
openapi_spec = get(url, verify=self.verify_cert).json()
|
|
55
55
|
self.openapi_spec = openapi_spec
|
|
56
|
-
server_info = openapi_spec[
|
|
57
|
-
self.title = server_info[
|
|
58
|
-
self.version = server_info[
|
|
56
|
+
server_info = openapi_spec["info"]
|
|
57
|
+
self.title = server_info["title"]
|
|
58
|
+
self.version = server_info["version"]
|
|
59
59
|
if base_url:
|
|
60
60
|
self.base_url = base_url
|
|
61
61
|
else:
|
|
62
|
-
self.base_url = glom(openapi_spec,
|
|
63
|
-
security = openapi_spec.get(
|
|
62
|
+
self.base_url = glom(openapi_spec, "servers.0.url")
|
|
63
|
+
security = openapi_spec.get("security", None)
|
|
64
64
|
self.session = Session()
|
|
65
65
|
if security:
|
|
66
66
|
self.init_security(openapi_spec, **auth_kwargs)
|
|
67
67
|
if not session_state:
|
|
68
68
|
session_state = get_global_state(
|
|
69
|
-
|
|
69
|
+
"session_state",
|
|
70
|
+
{"session": self.session, "refresh_inputs": {}},
|
|
70
71
|
)
|
|
71
|
-
self.session = session_state.get(
|
|
72
|
+
self.session = session_state.get("session")
|
|
72
73
|
if not self.session:
|
|
73
|
-
raise ValueError(
|
|
74
|
-
self.refresh_inputs = session_state.get(
|
|
74
|
+
raise ValueError("No session provided when instantiating HttpClient")
|
|
75
|
+
self.refresh_inputs = session_state.get("refresh_inputs")
|
|
75
76
|
if self.refresh_inputs is None:
|
|
76
|
-
raise ValueError(
|
|
77
|
-
for pathname, path_spec in openapi_spec[
|
|
77
|
+
raise ValueError("No refresh inputs provided when instantiating HttpClient")
|
|
78
|
+
for pathname, path_spec in openapi_spec["paths"].items():
|
|
78
79
|
url_template = self.base_url + pathname
|
|
79
80
|
for http_method, openapi_method_spec in path_spec.items():
|
|
80
81
|
self.register_method(url_template, http_method, openapi_method_spec)
|
|
81
82
|
|
|
82
83
|
def init_security(self, openapi_spec, **auth_kwargs):
|
|
83
|
-
security = openapi_spec[
|
|
84
|
+
security = openapi_spec["security"]
|
|
84
85
|
auth_type = list(security.keys())[0]
|
|
85
|
-
if auth_type ==
|
|
86
|
-
self.auth_type =
|
|
87
|
-
api_key = auth_kwargs.get(
|
|
88
|
-
self.set_header({
|
|
89
|
-
elif auth_type ==
|
|
90
|
-
self.auth_type =
|
|
86
|
+
if auth_type == "apiKey":
|
|
87
|
+
self.auth_type = "api_key"
|
|
88
|
+
api_key = auth_kwargs.get("api_key", None)
|
|
89
|
+
self.set_header({"Authorization": api_key})
|
|
90
|
+
elif auth_type == "bearerAuth":
|
|
91
|
+
self.auth_type = "login"
|
|
91
92
|
login_details = glom(
|
|
92
93
|
openapi_spec,
|
|
93
|
-
|
|
94
|
+
"components.securitySchemes.bearerAuth.x-login",
|
|
94
95
|
default={},
|
|
95
96
|
)
|
|
96
|
-
self.login_url = login_details.get(
|
|
97
|
-
self.login_input_keys = login_details.get(
|
|
97
|
+
self.login_url = login_details.get("login_url", None)
|
|
98
|
+
self.login_input_keys = login_details.get("login_inputs", [])
|
|
98
99
|
self.login_args = {}
|
|
99
100
|
for key in auth_kwargs:
|
|
100
101
|
if key in self.login_input_keys:
|
|
101
102
|
self.login_args[key] = auth_kwargs[key] or False
|
|
102
|
-
self.refresh_input_keys = login_details.get(
|
|
103
|
-
self.login_response_keys = login_details.get(
|
|
104
|
-
jwt = auth_kwargs.get(
|
|
103
|
+
self.refresh_input_keys = login_details.get("refresh_inputs", [])
|
|
104
|
+
self.login_response_keys = login_details.get("outputs", [])
|
|
105
|
+
jwt = auth_kwargs.get("jwt", None)
|
|
105
106
|
if jwt:
|
|
106
|
-
self.set_header({
|
|
107
|
+
self.set_header({"Authorization": f"Bearer {jwt}"})
|
|
107
108
|
|
|
108
109
|
def register_method(self, url_template, http_method, openapi_method_spec):
|
|
109
110
|
content_type = None
|
|
110
|
-
if
|
|
111
|
+
if "requestBody" in openapi_method_spec:
|
|
111
112
|
content_type = next(
|
|
112
|
-
iter(glom(openapi_method_spec,
|
|
113
|
+
iter(glom(openapi_method_spec, "requestBody.content").keys())
|
|
113
114
|
)
|
|
114
115
|
method_spec = mk_method_spec_from_openapi_method_spec(
|
|
115
116
|
openapi_method_spec,
|
|
@@ -133,42 +134,42 @@ class HttpClient:
|
|
|
133
134
|
self.session.headers.update(header)
|
|
134
135
|
|
|
135
136
|
def ensure_login(self):
|
|
136
|
-
if self.auth_type !=
|
|
137
|
+
if self.auth_type != "login":
|
|
137
138
|
return True
|
|
138
|
-
if not self.session.headers.get(
|
|
139
|
+
if not self.session.headers.get("Authorization", None):
|
|
139
140
|
return self._login()
|
|
140
141
|
return True
|
|
141
142
|
|
|
142
143
|
def _login(self):
|
|
143
144
|
if not self.login_url:
|
|
144
145
|
raise ValueError(
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
"Login was called without a login url. "
|
|
147
|
+
"Check your initialization arguments for HttpClient."
|
|
147
148
|
)
|
|
148
149
|
if not self.login_args:
|
|
149
150
|
raise ValueError(
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
"Login was called without any login inputs. "
|
|
152
|
+
"Check your initialization arguments for HttpClient."
|
|
152
153
|
)
|
|
153
|
-
login_result = request(
|
|
154
|
+
login_result = request("post", self.login_url, json=self.login_args).json()
|
|
154
155
|
return self.receive_login(login_result)
|
|
155
156
|
|
|
156
157
|
def refresh_login(self):
|
|
157
158
|
if not self.refresh_url or not self.refresh_inputs:
|
|
158
159
|
return self._login()
|
|
159
160
|
refresh_result = request(
|
|
160
|
-
|
|
161
|
+
"post", self.refresh_url, json=self.refresh_inputs
|
|
161
162
|
).json()
|
|
162
163
|
return self.receive_login(refresh_result)
|
|
163
164
|
|
|
164
165
|
def receive_login(self, login_result):
|
|
165
|
-
error = login_result.get(
|
|
166
|
+
error = login_result.get("error", None)
|
|
166
167
|
if error:
|
|
167
168
|
raise AuthorizationError(error)
|
|
168
169
|
for key in self.login_response_keys:
|
|
169
170
|
value = login_result.get(key, None)
|
|
170
|
-
if key ==
|
|
171
|
-
auth_header = {
|
|
171
|
+
if key == "jwt":
|
|
172
|
+
auth_header = {"Authorization": f"Bearer {value}"}
|
|
172
173
|
self.set_header(auth_header)
|
|
173
174
|
elif key in self.refresh_input_keys:
|
|
174
175
|
self.refresh_inputs[key] = value
|
|
@@ -177,7 +178,7 @@ class HttpClient:
|
|
|
177
178
|
new_auth = mk_auth({}, self.login_input_keys, config, profile)
|
|
178
179
|
if not new_auth:
|
|
179
180
|
raise KeyError(
|
|
180
|
-
f
|
|
181
|
+
f"Could not find authentication credentials for profile {profile} in file {config}"
|
|
181
182
|
)
|
|
182
183
|
for key in new_auth:
|
|
183
184
|
if key in self.login_input_keys:
|
|
@@ -186,4 +187,4 @@ class HttpClient:
|
|
|
186
187
|
|
|
187
188
|
def is_request_method(self, methodname):
|
|
188
189
|
method = getattr(self, methodname)
|
|
189
|
-
return method and hasattr(method,
|
|
190
|
+
return method and hasattr(method, "method_spec")
|
|
@@ -13,11 +13,11 @@ from http2py.constants import BINARY_CONTENT_TYPE, JSON_CONTENT_TYPE, RAW_CONTEN
|
|
|
13
13
|
|
|
14
14
|
def _handle_error(resp):
|
|
15
15
|
if resp.status_code == 400:
|
|
16
|
-
if resp.reason ==
|
|
16
|
+
if resp.reason == "AuthorizationError":
|
|
17
17
|
raise AuthorizationError(resp.text)
|
|
18
|
-
if resp.reason ==
|
|
18
|
+
if resp.reason == "InputError":
|
|
19
19
|
raise InputError(resp.text)
|
|
20
|
-
if resp.reason ==
|
|
20
|
+
if resp.reason == "DuplicateRecordError":
|
|
21
21
|
raise DuplicateRecordError(resp.text)
|
|
22
22
|
if resp.status_code == 403:
|
|
23
23
|
raise ForbiddenError(resp.text)
|
|
@@ -37,7 +37,7 @@ def _handle_resp(func, content_type):
|
|
|
37
37
|
output = resp.text
|
|
38
38
|
else:
|
|
39
39
|
raise NotImplementedError(
|
|
40
|
-
f
|
|
40
|
+
f"Response of type {content_type} is not supported yet."
|
|
41
41
|
)
|
|
42
42
|
return func(output)
|
|
43
43
|
else:
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import argh
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Iterable
|
|
3
3
|
|
|
4
4
|
from http2py.cli_maker import mk_argparse_friendly
|
|
5
5
|
from i2.io_trans import AnnotAndDfltIoTrans
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def myfun(a: int, b: Iterable[int]):
|
|
9
|
-
print(f
|
|
10
|
-
print(f
|
|
9
|
+
print(f"a: {a} {type(a)}")
|
|
10
|
+
print(f"b: {b} {type(b)}")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
if __name__ ==
|
|
13
|
+
if __name__ == "__main__":
|
|
14
14
|
parser = argh.ArghParser()
|
|
15
15
|
parser.add_commands([mk_argparse_friendly(AnnotAndDfltIoTrans()(myfun))])
|
|
16
16
|
parser.dispatch()
|