Skip to content

Commit

Permalink
Merge pull request #12 from python-microservices/feature/rest_template
Browse files Browse the repository at this point in the history
Encapsulate common rest operations between business services propagating trace headers if configured.
  • Loading branch information
avara1986 committed Nov 20, 2018
2 parents 220546d + baef479 commit fab57f8
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 4 deletions.
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Alberto Vara <a.vara.1986@gmail.com>
Hugo Camino <hcamino@paradigmadigital.com>
Hugo Camino <hugo.camino.villacorta@gmail.com>
José Manuel <jmrivas86@gmail.com>
5 changes: 3 additions & 2 deletions pyms/flask/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def create_app(self):

application = app.app
application.config.from_object(get_conf(service=self.service))
application.tracer = None

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

tracer = FlaskTracer(init_jaeger_tracer(), True, application)
application.tracer = FlaskTracer(init_jaeger_tracer(), True, application)
formatter.add_service_name(application.config["APP_NAME"])
formatter.add_trace_span(tracer)
formatter.add_trace_span(application.tracer)
log_handler.setFormatter(formatter)
application.logger.addHandler(log_handler)
application.logger.setLevel(logging.INFO)
Expand Down
209 changes: 209 additions & 0 deletions pyms/rest_template/rest_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""Encapsulate common rest operations between business services propagating trace headers if configured.
"""
import opentracing
import requests
from flask import current_app

DATA = 'data'


class Request(object):

def __init__(self):
"""Initialization for trace headers propagation"""
self._tracer = current_app.tracer

def insert_trace_headers(self, headers):
"""Inject trace headers if enabled.
:param headers: dictionary of HTTP Headers to send.
:rtype: dict
"""
try:
# FLASK https://github.com/opentracing-contrib/python-flask
span = self._tracer.get_span()
self._tracer._tracer.inject(span, opentracing.Format.HTTP_HEADERS, headers)
except Exception as ex:
current_app.logger.warning("Tracer error {}".format(ex))
return headers

def _get_headers(self, headers):
"""If enabled appends trace headers to received ones.
:param headers: dictionary of HTTP Headers to send.
:rtype: dict
"""

if not headers:
headers = {}

if self._tracer:
headers = self.insert_trace_headers(headers)

return headers

@staticmethod
def _build_url(url, path_params=None):
"""Compose full url replacing placeholders with path_params values.
:param url: base url
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:return: :class:`string`
:rtype: string
"""

return url.format_map(path_params)

def get(self, url, path_params=None, params=None, headers=None, **kwargs):
"""Sends a GET request.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param params: (optional) Dictionary, list of tuples or bytes to send in the body of the :class:`Request` (as query
string parameters)
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

full_url = Request._build_url(url, path_params)
headers = self._get_headers(headers)
current_app.logger.info("Get with url {}, params {}, headers {}, kwargs {}".
format(full_url, params, headers, kwargs))
response = requests.get(full_url, params=params, headers=headers, **kwargs)
current_app.logger.info("Response {}".format(response))

return response

def get_for_object(self, url, path_params=None, params=None, headers=None, **kwargs):
"""Sends a GET request and returns the json representation found in response's content data node.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param params: (optional) Dictionary, list of tuples or bytes to send in the body of the :class:`Request` (as query
string parameters)
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

response = self.get(url, path_params=path_params, params=params, headers=headers, **kwargs)

try:
return response.json().get(DATA, {})
except ValueError:
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
return {}

def post(self, url, path_params=None, data=None, json=None, headers=None, **kwargs):
"""Sends a POST request.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the
:class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

full_url = Request._build_url(url, path_params)
headers = self._get_headers(headers)
current_app.logger.info("Post with url {}, data {}, json {}, headers {}, kwargs {}".format(full_url, data, json,
headers, kwargs))
response = requests.post(full_url, data=data, json=json, headers=headers, **kwargs)
current_app.logger.info("Response {}".format(response))

return response

def post_for_object(self, url, path_params=None, data=None, json=None, headers=None, **kwargs):
"""Sends a POST request and returns the json representation found in response's content data node.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param data: (optional) Dictionary, list of tuples, bytes, or file-like object to send in the body of the
:class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

response = self.post(url, path_params=path_params, data=data, json=json, headers=headers, **kwargs)

try:
return response.json().get(DATA, {})
except ValueError:
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
return {}

def put(self, url, path_params=None, data=None, headers=None, **kwargs):
"""Sends a PUT request.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

full_url = Request._build_url(url, path_params)
headers = self._get_headers(headers)
current_app.logger.info("Put with url {}, data {}, headers {}, kwargs {}".format(full_url, data, headers,
kwargs))
response = requests.put(full_url, data, headers=headers, **kwargs)
current_app.logger.info("Response {}".format(response))

return response

def put_for_object(self, url, path_params=None, data=None, headers=None, **kwargs):
"""Sends a PUT request and returns the json representation found in response's content data node.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param data: (optional) Dictionary, list of tuples, bytes, or file-like
object to send in the body of the :class:`Request`.
:param json: (optional) json data to send in the body of the :class:`Request`.
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

response = self.put(url, path_params=path_params, data=data, headers=headers, **kwargs)

try:
return response.json().get(DATA, {})
except ValueError:
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
return {}

def delete(self, url, path_params=None, headers=None, **kwargs):
"""Sends a DELETE request.
:param url: URL for the new :class:`Request` object. Could contain path parameters
:param path_params: (optional) Dictionary, list of tuples with path parameters values to compose url
:param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:return: :class:`Response <Response>` object
:rtype: requests.Response
"""

full_url = Request._build_url(url, path_params)
headers = self._get_headers(headers)
current_app.logger.info("Delete with url {}, headers {}, kwargs {}".format(full_url, headers, kwargs))
response = requests.delete(full_url, headers=headers, **kwargs)
current_app.logger.info("Response {}".format(response))

return response
3 changes: 2 additions & 1 deletion requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ coverage==4.5.2
mock==2.0.0
nose==1.3.7
pylint==2.1.1
tox==3.5.3
tox==3.5.3
requests_mock==1.5.2
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ jaeger-client==3.12.0
python-json-logger==0.1.10
PyYAML==3.13
anyconfig==0.9.7
requests==2.20.0
# Optionals:
connexion[swagger-ui]==2.0.2
# Necessary to deploy swagger ui files in projects that install this library,
Expand Down
Loading

0 comments on commit fab57f8

Please sign in to comment.