Skip to content

Commit fab57f8

Browse files
authored
Merge pull request #12 from python-microservices/feature/rest_template
Encapsulate common rest operations between business services propagating trace headers if configured.
2 parents 220546d + baef479 commit fab57f8

File tree

6 files changed

+426
-4
lines changed

6 files changed

+426
-4
lines changed

AUTHORS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Alberto Vara <a.vara.1986@gmail.com>
2-
Hugo Camino <hcamino@paradigmadigital.com>
2+
Hugo Camino <hugo.camino.villacorta@gmail.com>
33
José Manuel <jmrivas86@gmail.com>

pyms/flask/app/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def create_app(self):
3737

3838
application = app.app
3939
application.config.from_object(get_conf(service=self.service))
40+
application.tracer = None
4041

4142
# Initialize Blueprints
4243
application.register_blueprint(healthcheck_blueprint)
@@ -46,9 +47,9 @@ def create_app(self):
4647
if not application.config["TESTING"]:
4748
log_handler = logging.StreamHandler()
4849

49-
tracer = FlaskTracer(init_jaeger_tracer(), True, application)
50+
application.tracer = FlaskTracer(init_jaeger_tracer(), True, application)
5051
formatter.add_service_name(application.config["APP_NAME"])
51-
formatter.add_trace_span(tracer)
52+
formatter.add_trace_span(application.tracer)
5253
log_handler.setFormatter(formatter)
5354
application.logger.addHandler(log_handler)
5455
application.logger.setLevel(logging.INFO)
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""Encapsulate common rest operations between business services propagating trace headers if configured.
2+
"""
3+
import opentracing
4+
import requests
5+
from flask import current_app
6+
7+
DATA = 'data'
8+
9+
10+
class Request(object):
11+
12+
def __init__(self):
13+
"""Initialization for trace headers propagation"""
14+
self._tracer = current_app.tracer
15+
16+
def insert_trace_headers(self, headers):
17+
"""Inject trace headers if enabled.
18+
19+
:param headers: dictionary of HTTP Headers to send.
20+
21+
:rtype: dict
22+
"""
23+
try:
24+
# FLASK https://github.com/opentracing-contrib/python-flask
25+
span = self._tracer.get_span()
26+
self._tracer._tracer.inject(span, opentracing.Format.HTTP_HEADERS, headers)
27+
except Exception as ex:
28+
current_app.logger.warning("Tracer error {}".format(ex))
29+
return headers
30+
31+
def _get_headers(self, headers):
32+
"""If enabled appends trace headers to received ones.
33+
34+
:param headers: dictionary of HTTP Headers to send.
35+
36+
:rtype: dict
37+
"""
38+
39+
if not headers:
40+
headers = {}
41+
42+
if self._tracer:
43+
headers = self.insert_trace_headers(headers)
44+
45+
return headers
46+
47+
@staticmethod
48+
def _build_url(url, path_params=None):
49+
"""Compose full url replacing placeholders with path_params values.
50+
51+
:param url: base url
52+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
53+
:return: :class:`string`
54+
:rtype: string
55+
"""
56+
57+
return url.format_map(path_params)
58+
59+
def get(self, url, path_params=None, params=None, headers=None, **kwargs):
60+
"""Sends a GET request.
61+
62+
:param url: URL for the new :class:`Request` object. Could contain path parameters
63+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
64+
:param params: (optional) Dictionary, list of tuples or bytes to send in the body of the :class:`Request` (as query
65+
string parameters)
66+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
67+
:param \*\*kwargs: Optional arguments that ``request`` takes.
68+
:return: :class:`Response <Response>` object
69+
:rtype: requests.Response
70+
"""
71+
72+
full_url = Request._build_url(url, path_params)
73+
headers = self._get_headers(headers)
74+
current_app.logger.info("Get with url {}, params {}, headers {}, kwargs {}".
75+
format(full_url, params, headers, kwargs))
76+
response = requests.get(full_url, params=params, headers=headers, **kwargs)
77+
current_app.logger.info("Response {}".format(response))
78+
79+
return response
80+
81+
def get_for_object(self, url, path_params=None, params=None, headers=None, **kwargs):
82+
"""Sends a GET request and returns the json representation found in response's content data node.
83+
84+
:param url: URL for the new :class:`Request` object. Could contain path parameters
85+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
86+
:param params: (optional) Dictionary, list of tuples or bytes to send in the body of the :class:`Request` (as query
87+
string parameters)
88+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
89+
:param \*\*kwargs: Optional arguments that ``request`` takes.
90+
:return: :class:`Response <Response>` object
91+
:rtype: requests.Response
92+
"""
93+
94+
response = self.get(url, path_params=path_params, params=params, headers=headers, **kwargs)
95+
96+
try:
97+
return response.json().get(DATA, {})
98+
except ValueError:
99+
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
100+
return {}
101+
102+
def post(self, url, path_params=None, data=None, json=None, headers=None, **kwargs):
103+
"""Sends a POST request.
104+
105+
:param url: URL for the new :class:`Request` object. Could contain path parameters
106+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
107+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the
108+
:class:`Request`.
109+
:param json: (optional) json data to send in the body of the :class:`Request`.
110+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
111+
:param \*\*kwargs: Optional arguments that ``request`` takes.
112+
:return: :class:`Response <Response>` object
113+
:rtype: requests.Response
114+
"""
115+
116+
full_url = Request._build_url(url, path_params)
117+
headers = self._get_headers(headers)
118+
current_app.logger.info("Post with url {}, data {}, json {}, headers {}, kwargs {}".format(full_url, data, json,
119+
headers, kwargs))
120+
response = requests.post(full_url, data=data, json=json, headers=headers, **kwargs)
121+
current_app.logger.info("Response {}".format(response))
122+
123+
return response
124+
125+
def post_for_object(self, url, path_params=None, data=None, json=None, headers=None, **kwargs):
126+
"""Sends a POST request and returns the json representation found in response's content data node.
127+
128+
:param url: URL for the new :class:`Request` object. Could contain path parameters
129+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
130+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the
131+
:class:`Request`.
132+
:param json: (optional) json data to send in the body of the :class:`Request`.
133+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
134+
:param \*\*kwargs: Optional arguments that ``request`` takes.
135+
:return: :class:`Response <Response>` object
136+
:rtype: requests.Response
137+
"""
138+
139+
response = self.post(url, path_params=path_params, data=data, json=json, headers=headers, **kwargs)
140+
141+
try:
142+
return response.json().get(DATA, {})
143+
except ValueError:
144+
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
145+
return {}
146+
147+
def put(self, url, path_params=None, data=None, headers=None, **kwargs):
148+
"""Sends a PUT request.
149+
150+
:param url: URL for the new :class:`Request` object. Could contain path parameters
151+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
152+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
153+
object to send in the body of the :class:`Request`.
154+
:param json: (optional) json data to send in the body of the :class:`Request`.
155+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
156+
:param \*\*kwargs: Optional arguments that ``request`` takes.
157+
:return: :class:`Response <Response>` object
158+
:rtype: requests.Response
159+
"""
160+
161+
full_url = Request._build_url(url, path_params)
162+
headers = self._get_headers(headers)
163+
current_app.logger.info("Put with url {}, data {}, headers {}, kwargs {}".format(full_url, data, headers,
164+
kwargs))
165+
response = requests.put(full_url, data, headers=headers, **kwargs)
166+
current_app.logger.info("Response {}".format(response))
167+
168+
return response
169+
170+
def put_for_object(self, url, path_params=None, data=None, headers=None, **kwargs):
171+
"""Sends a PUT request and returns the json representation found in response's content data node.
172+
173+
:param url: URL for the new :class:`Request` object. Could contain path parameters
174+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
175+
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
176+
object to send in the body of the :class:`Request`.
177+
:param json: (optional) json data to send in the body of the :class:`Request`.
178+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
179+
:param \*\*kwargs: Optional arguments that ``request`` takes.
180+
:return: :class:`Response <Response>` object
181+
:rtype: requests.Response
182+
"""
183+
184+
response = self.put(url, path_params=path_params, data=data, headers=headers, **kwargs)
185+
186+
try:
187+
return response.json().get(DATA, {})
188+
except ValueError:
189+
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
190+
return {}
191+
192+
def delete(self, url, path_params=None, headers=None, **kwargs):
193+
"""Sends a DELETE request.
194+
195+
:param url: URL for the new :class:`Request` object. Could contain path parameters
196+
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
197+
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
198+
:param \*\*kwargs: Optional arguments that ``request`` takes.
199+
:return: :class:`Response <Response>` object
200+
:rtype: requests.Response
201+
"""
202+
203+
full_url = Request._build_url(url, path_params)
204+
headers = self._get_headers(headers)
205+
current_app.logger.info("Delete with url {}, headers {}, kwargs {}".format(full_url, headers, kwargs))
206+
response = requests.delete(full_url, headers=headers, **kwargs)
207+
current_app.logger.info("Response {}".format(response))
208+
209+
return response

requirements-tests.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ coverage==4.5.2
33
mock==2.0.0
44
nose==1.3.7
55
pylint==2.1.1
6-
tox==3.5.3
6+
tox==3.5.3
7+
requests_mock==1.5.2

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ jaeger-client==3.12.0
22
python-json-logger==0.1.10
33
PyYAML==3.13
44
anyconfig==0.9.7
5+
requests==2.20.0
56
# Optionals:
67
connexion[swagger-ui]==2.0.2
78
# Necessary to deploy swagger ui files in projects that install this library,

0 commit comments

Comments
 (0)