-
Notifications
You must be signed in to change notification settings - Fork 0
/
restclient.py
134 lines (112 loc) · 4.43 KB
/
restclient.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
from json import dumps
from typing import Dict, List, Union, Any
from requests import Session
from requests.exceptions import BaseHTTPError
import logging
JSONValue = Union[str, int, float, bool, None, Dict[str, Any], List[Any]]
JSONType = Union[Dict[str, JSONValue], List[JSONValue]]
log = logging.getLogger(__name__)
"""
Defines very simple classes to create a callable interface to a REST api
from a discovery REST description document.
Intended as a super simple replacement for google-api-python-client, using
requests instead of httplib2
giles 2018
"""
# a dummy decorator to suppress unresolved references on this dynamic class
def dynamic_attrs(cls):
return cls
@dynamic_attrs
class RestClient:
""" To create a callable client to a REST API, instantiate this class.
For details of the discovery API see:
https://developers.google.com/discovery/v1/using
"""
def __init__(self, api_url: str, auth_session: Session):
""" """
self.auth_session: Session = auth_session
service_document = self.auth_session.get(api_url).json()
self.json: JSONType = service_document
self.base_url: str = str(service_document["baseUrl"])
for c_name, collection in service_document["resources"].items():
new_collection = Collection(c_name)
setattr(self, c_name, new_collection)
for m_name, method in collection["methods"].items():
new_method = Method(self, **method)
setattr(new_collection, m_name, new_method)
# pylint: disable=no-member
class Method:
""" Represents a method in the REST API. To be called using its execute
method, the execute method takes a single parameter for body and then
named parameters for Http Request parameters.
e.g.
api = RestClient(https://photoslibrary.googleapis.com/$discovery' \
'/rest?version=v1', authenticated_session)
api.albums.list.execute(pageSize=50)
"""
def __init__(self, service: RestClient, **k_args: Dict[str, str]):
self.path: str = None
self.httpMethod: str = None
self.service: RestClient = service
self.__dict__.update(k_args)
self.path_args: List[str] = []
self.query_args: List[str] = []
if hasattr(self, "parameters"):
for key, value in self.parameters.items():
if value["location"] == "path":
self.path_args.append(key)
else:
self.query_args.append(key)
def execute(self, body: str = None, **k_args: Dict[str, str]):
""" executes the remote REST call for this Method"""
path_args: Dict[str, str] = {
k: k_args[k] for k in self.path_args if k in k_args
}
query_args: Dict[str, str] = {
k: k_args[k] for k in self.query_args if k in k_args
}
path: str = self.service.base_url + self.make_path(path_args)
if body:
body = dumps(body)
log.trace(
"\nREQUEST: %s to %s params=%s\n%s",
self.httpMethod,
path,
query_args,
body,
)
result = self.service.auth_session.request(
self.httpMethod, data=body, url=path, timeout=10, params=query_args
)
log.trace("\nRESPONSE: %s\n%s", result.status_code, str(result.content))
try:
result.raise_for_status()
except BaseHTTPError:
log.error(
"Request failed with status {}: {}".format(
result.status_code, result.content
)
)
raise
return result
def make_path(self, path_args: Dict[str, str]) -> str:
""" Extracts the arguments from path_args and inserts them into
the URL template defined in self.path
Returns:
The URL with inserted parameters
"""
result = str(self.path)
path_params = []
for key, value in path_args.items():
path_param = "{{+{}}}".format(key)
if path_param in result:
result = result.replace("{{+{}}}".format(key), value)
path_params.append(key)
for key in path_params:
path_args.pop(key)
return result
class Collection:
""" Used to represent a collection of methods
e.g. Google Photos API - mediaItems """
def __init__(self, name: str):
self.collection_name = name