Skip to content

Commit f14f13c

Browse files
authoredAug 31, 2019
Added retries to requests wrapper (#56)
* Added retry system to requests wrapper * Fix pylint errors * Updated docs
1 parent 477ea42 commit f14f13c

File tree

14 files changed

+314
-116
lines changed

14 files changed

+314
-116
lines changed
 

‎CONTRIBUTING.md

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Contributing
22

3+
## Installation
4+
35
After cloning this repo, create a [virtualenv](https://virtualenv.pypa.io/en/stable/) and ensure dependencies are installed by running:
46

57
```sh
@@ -30,4 +32,66 @@ If you wish to run against a specific version defined in the `tox.ini` file:
3032
tox -e py36
3133
```
3234

33-
Tox can only use whatever versions of Python are installed on your system. When you create a pull request, Travis will also be running the same tests and report the results, so there is no need for potential contributors to try to install every single version of Python on their own system ahead of time. We appreciate opening issues and pull requests to make PyMS even more stable & useful!
35+
Tox can only use whatever versions of Python are installed on your system. When you create a pull request, Travis will also be running the same tests and report the results, so there is no need for potential contributors to try to install every single version of Python on their own system ahead of time.
36+
37+
## Pipenv
38+
39+
### Advantages over plain pip and requirements.txt
40+
[Pipenv](https://pipenv.readthedocs.io/en/latest/) generates two files: a `Pipfile`and a `Pipfile.lock`.
41+
* `Pipfile`: Is a high level declaration of the dependencies of your project. It can contain "dev" dependencies (usually test related stuff) and "standard" dependencies which are the ones you'll need for your project to function
42+
* `Pipfile.lock`: Is the "list" of all the dependencies your Pipfile has installed, along with their version and their hashes. This prevents two things: Conflicts between dependencies and installing a malicious module.
43+
44+
### How to...
45+
46+
Here the most 'common' `pipenv` commands, for a more in-depth explanation please refer to the [official documentation](https://pipenv.readthedocs.io/en/latest/).
47+
48+
#### Install pipenv
49+
```bash
50+
pip install pipenv
51+
```
52+
53+
#### Install dependencies defined in a Pipfile
54+
```bash
55+
pipenv install
56+
```
57+
58+
#### Install both dev and "standard" dependencies defined in a Pipfile
59+
```bash
60+
pipenv install --dev
61+
```
62+
63+
#### Install a new module
64+
```bash
65+
pipenv install django
66+
```
67+
68+
#### Install a new dev module (usually test related stuff)
69+
```bash
70+
pipenv install nose --dev
71+
```
72+
73+
#### Install dependencies in production
74+
```bash
75+
pipenv install --deploy
76+
```
77+
78+
#### Start a shell
79+
```bash
80+
pipenv shell
81+
```
82+
83+
## Documentation
84+
85+
This project use MkDocs
86+
87+
* `mkdocs new [dir-name]` - Create a new project.
88+
* `mkdocs serve` - Start the live-reloading docs server.
89+
* `mkdocs build` - Build the documentation site.
90+
* `mkdocs help` - Print this help message.
91+
92+
### Project layout
93+
94+
mkdocs.yml # The configuration file.
95+
docs/
96+
index.md # The documentation homepage.
97+
... # Other markdown pages, images and other files.

‎README.md

+32-69
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,30 @@
1010
PyMS, Python MicroService, is a collections of libraries, best practices and recommended ways to build
1111
microservices with Python.
1212

13+
## Documentation
14+
1315
To know how use, install or build a project see the docs: https://py-ms.readthedocs.io/en/latest/
1416

17+
## Motivation
18+
19+
When we started to create microservice with no idea, we were looking for tutorials, guides, best practices, but we found
20+
nothing to create professional projects. Most articles say:
21+
- "Install flask"
22+
- "Create routes"
23+
- (Sometimes) "Create a swagger specs"
24+
- "TA-DA! you have a microservice"
25+
26+
But... what happens with our configuration out of code like Kubernetes configmap? what happens with transactionality?
27+
If we have many microservices, what happens with traces?.
28+
29+
There are many problems around Python and microservices and we can`t find anyone to give a solution.
30+
31+
We start creating these projects to try to solve all the problems we have found in our professional lives about
32+
microservices architecture.
33+
34+
Nowadays, is not perfect and we have a looong roadmap, but we hope this library could help other felas and friends ;)
35+
36+
1537
## Installation
1638
```bash
1739
pip install py-ms
@@ -23,80 +45,21 @@ pip install py-ms
2345
Module to read yaml or json configuration from a dictionary or a path.
2446

2547
### pyms/flask/app
26-
With the funcion `create_app` initialize the Flask app, register [blueprints](http://flask.pocoo.org/docs/0.12/blueprints/)
27-
and intialize all libraries like Swagger, database, trace system, custom logger format, etc.
48+
With the function `create_app` initialize the Flask app, register [blueprints](http://flask.pocoo.org/docs/0.12/blueprints/)
49+
and initialize all libraries such as Swagger, database, trace system, custom logger format, etc.
50+
51+
### pyms/flask/services
52+
Integrations and wrappers over common libs like request, swagger, connexion
2853

2954
### pyms/flask/healthcheck
30-
This views is usually used by Kubernetes, Eureka and other systems to check if our application is up and running.
55+
This view is usually used by Kubernetes, Eureka and other systems to check if our application is running.
3156

3257
### pyms/logger
3358
Print logger in JSON format to send to server like Elasticsearch. Inject span traces in logger.
3459

35-
### pyms/rest_template
36-
Encapsulate common rest operations between business services propagating trace headers if configured.
37-
3860
### pyms/tracer
39-
Create an injector `flask_opentracing.FlaskTracer` to use in our projects
40-
41-
## Pipenv
42-
43-
### Advantages over plain pip and requirements.txt
44-
[Pipenv](https://pipenv.readthedocs.io/en/latest/) generates two files: a `Pipfile`and a `Pipfile.lock`.
45-
* `Pipfile`: Is a high level declaration of the dependencies of your project. It can contain "dev" dependencies (usually test related stuff) and "standard" dependencies which are the ones you'll need for your project to function
46-
* `Pipfile.lock`: Is the "list" of all the dependencies your Pipfile has installed, along with their version and their hashes. This prevents two things: Conflicts between dependencies and installing a malicious module.
47-
48-
### How to...
49-
50-
Here the most 'common' `pipenv` commands, for a more in-depth explanation please refer to the [official documentation](https://pipenv.readthedocs.io/en/latest/).
51-
52-
#### Install pipenv
53-
```bash
54-
pip install pipenv
55-
```
56-
57-
#### Install dependencies defined in a Pipfile
58-
```bash
59-
pipenv install
60-
```
61-
62-
#### Install both dev and "standard" dependencies defined in a Pipfile
63-
```bash
64-
pipenv install --dev
65-
```
66-
67-
#### Install a new module
68-
```bash
69-
pipenv install django
70-
```
71-
72-
#### Install a new dev module (usually test related stuff)
73-
```bash
74-
pipenv install nose --dev
75-
```
76-
77-
#### Install dependencies in production
78-
```bash
79-
pipenv install --deploy
80-
```
81-
82-
#### Start a shell
83-
```bash
84-
pipenv shell
85-
```
86-
87-
## Documentation
88-
89-
This project use MkDocs
90-
91-
* `mkdocs new [dir-name]` - Create a new project.
92-
* `mkdocs serve` - Start the live-reloading docs server.
93-
* `mkdocs build` - Build the documentation site.
94-
* `mkdocs help` - Print this help message.
95-
96-
### Project layout
97-
98-
mkdocs.yml # The configuration file.
99-
docs/
100-
index.md # The documentation homepage.
101-
... # Other markdown pages, images and other files.
61+
Create an injector `flask_opentracing.FlaskTracer` to use in our projects.
10262

63+
## How To Contrib
64+
We appreciate opening issues and pull requests to make PyMS even more stable & useful! See [This doc](COONTRIBUTING.md)
65+
for more details

‎docs/index.md

+20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
PyMS, Python MicroService, is a collection of libraries, best practices and recommended ways to build
44
microservices with Python.
55

6+
## Motivation
7+
8+
When we started to create microservice with no idea, we were looking for tutorials, guides, best practices, but we found
9+
nothing to create professional projects. Most articles say:
10+
- "Install flask"
11+
- "Create routes"
12+
- (Sometimes) "Create a swagger specs"
13+
- "TA-DA! you have a microservice"
14+
15+
But... what happens with our configuration out of code like Kubernetes configmap? what happens with transactionality?
16+
If we have many microservices, what happens with traces?.
17+
18+
There are many problems around Python and microservices and we can`t find anyone to give a solution.
19+
20+
We start creating these projects to try to solve all the problems we have found in our professional lives about
21+
microservices architecture.
22+
23+
Nowadays, is not perfect and we have a looong roadmap, but we hope this library could help other felas and friends ;)
24+
25+
626
## Index:
727
* [PyMS structure](structure.md)
828
* [Configuration](configuration.md)

‎docs/structure.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Module to read yaml or json configuration from a dictionary or a path.
77
With the function `create_app` initialize the Flask app, register [blueprints](http://flask.pocoo.org/docs/0.12/blueprints/)
88
and initialize all libraries such as Swagger, database, trace system, custom logger format, etc.
99

10+
### pyms/flask/services
11+
Integrations and wrappers over common libs like request, swagger, connexion
12+
1013
### pyms/flask/healthcheck
1114
This view is usually used by Kubernetes, Eureka and other systems to check if our application is running.
1215

‎pylintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ confidence=
5454
# --enable=similarities". If you want to run only the classes checker, but have
5555
# no Warning level messages displayed, use"--disable=all --enable=classes
5656
# --disable=W"
57-
disable=C0301
57+
disable=C0301,C0111,C0103,logging-format-interpolation
5858

5959
# Enable the message, report, category or checker with the given id(s). You can
6060
# either give multiple identifier separated by comma (,) or put this option

‎pyms/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
__email__ = "a.vara.1986@gmail.com"
44

5-
__version__ = "1.1.0"
5+
__version__ = "1.2.0"

‎pyms/flask/app/create_app.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pyms.config.conf import get_conf
99
from pyms.constants import LOGGER_NAME, SERVICE_ENVIRONMENT
1010
from pyms.flask.healthcheck import healthcheck_blueprint
11-
from pyms.flask.services.driver import ServicesManager
11+
from pyms.flask.services.driver import ServicesManager, DriverService
1212
from pyms.logger import CustomJsonFormatter
1313
from pyms.tracer.main import init_lightstep_tracer
1414

@@ -35,6 +35,8 @@ def __call__(cls, *args, **kwargs):
3535
class Microservice(metaclass=SingletonMeta):
3636
service = None
3737
application = None
38+
swagger = DriverService
39+
requests = DriverService
3840

3941
def __init__(self, *args, **kwargs):
4042
self.service = kwargs.get("service", os.environ.get(SERVICE_ENVIRONMENT, "ms"))
@@ -70,14 +72,15 @@ def init_logger(self):
7072
def init_app(self) -> Flask:
7173
if getattr(self, "swagger", False):
7274
app = connexion.App(__name__, specification_dir=os.path.join(self.path, self.swagger.path))
73-
app.add_api(self.swagger.file,
74-
arguments={'title': self.config.APP_NAME},
75-
base_path=self.config.APPLICATION_ROOT
76-
)
75+
app.add_api(
76+
self.swagger.file,
77+
arguments={'title': self.config.APP_NAME},
78+
base_path=self.config.APPLICATION_ROOT
79+
)
7780

7881
# Invert the objects, instead connexion with a Flask object, a Flask object with
7982
application = app.app
80-
application._connexion_app = app
83+
application.connexion_app = app
8184
else:
8285
application = Flask(__name__, static_folder=os.path.join(self.path, 'static'),
8386
template_folder=os.path.join(self.path, 'templates'))
@@ -121,4 +124,4 @@ def add_error_handler(self, code_or_exception, handler):
121124
:param code_or_exception: HTTP error code or exception
122125
:param handler: callback for error handler
123126
"""
124-
self.application._connexion_app.add_error_handler(code_or_exception, handler)
127+
self.application.connexion_app.add_error_handler(code_or_exception, handler)

‎pyms/flask/app/create_config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
def config():
55
ms = Microservice()
6-
return ms.config
6+
return ms.config

‎pyms/flask/healthcheck/__init__.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
"""Init file
2-
"""
3-
from __future__ import unicode_literals, print_function, absolute_import, division
1+
from pyms.flask.healthcheck.healthcheck import healthcheck_blueprint
42

5-
from flask import Blueprint
6-
7-
healthcheck_blueprint = Blueprint('healthcheck', __name__, static_url_path='/static')
8-
9-
from pyms.flask.healthcheck import healthcheck
3+
__all__ = ['healthcheck_blueprint']

‎pyms/flask/healthcheck/healthcheck.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
"""Healthcheck
2-
"""
3-
from pyms.flask.healthcheck import healthcheck_blueprint
1+
from __future__ import unicode_literals, print_function, absolute_import, division
2+
3+
from flask import Blueprint
4+
5+
healthcheck_blueprint = Blueprint('healthcheck', __name__, static_url_path='/static')
46

57

68
@healthcheck_blueprint.route('/healthcheck', methods=['GET'])

‎pyms/flask/services/requests.py

+88-21
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,37 @@
55
import opentracing
66
import requests
77
from flask import current_app, request
8+
from requests.adapters import HTTPAdapter
9+
from requests.packages.urllib3.util.retry import Retry
810

911
from pyms.constants import LOGGER_NAME
1012
from pyms.flask.services.driver import DriverService
1113

1214
logger = logging.getLogger(LOGGER_NAME)
1315

16+
DEFAULT_RETRIES = 3
17+
18+
DEFAULT_STATUS_RETRIES = (500, 502, 504)
19+
20+
21+
def retry(f):
22+
def wrapper(*args, **kwargs):
23+
response = False
24+
i = 0
25+
response_ok = False
26+
retries = args[0].retries
27+
status_retries = args[0].status_retries
28+
while i < retries and response_ok is False:
29+
response = f(*args, **kwargs)
30+
i += 1
31+
if response.status_code not in status_retries:
32+
response_ok = True
33+
logger.debug("Response {}".format(response))
34+
if not response_ok:
35+
logger.warning("Response ERROR: {}".format(response))
36+
return response
37+
return wrapper
38+
1439

1540
class Service(DriverService):
1641
service = "requests"
@@ -21,8 +46,32 @@ class Service(DriverService):
2146
def __init__(self, service, *args, **kwargs):
2247
"""Initialization for trace headers propagation"""
2348
super().__init__(service, *args, **kwargs)
49+
self.retries = self.config.retries or DEFAULT_RETRIES
50+
self.status_retries = self.config.status_retries or DEFAULT_STATUS_RETRIES
2451

25-
def insert_trace_headers(self, headers):
52+
def requests(self, session: requests.Session):
53+
"""
54+
A backoff factor to apply between attempts after the second try (most errors are resolved immediately by a
55+
second try without a delay). urllib3 will sleep for: {backoff factor} * (2 ^ ({number of total retries} - 1))
56+
seconds. If the backoff_factor is 0.1, then sleep() will sleep for [0.0s, 0.2s, 0.4s, ...] between retries.
57+
It will never be longer than Retry.BACKOFF_MAX. By default, backoff is disabled (set to 0).
58+
:param session:
59+
:return:
60+
"""
61+
session_r = session or requests.Session()
62+
retry = Retry(
63+
total=self.retries,
64+
read=self.retries,
65+
connect=self.retries,
66+
backoff_factor=0.3,
67+
status_forcelist=self.status_retries,
68+
)
69+
adapter = HTTPAdapter(max_retries=retry)
70+
session_r.mount('http://', adapter)
71+
session_r.mount('https://', adapter)
72+
return session_r
73+
74+
def insert_trace_headers(self, headers: dict) -> dict:
2675
"""Inject trace headers if enabled.
2776
2877
:param headers: dictionary of HTTP Headers to send.
@@ -38,7 +87,13 @@ def insert_trace_headers(self, headers):
3887
logger.debug("Tracer error {}".format(ex))
3988
return headers
4089

41-
def _get_headers(self, headers):
90+
def propagate_headers(self, headers: dict) -> dict:
91+
for k, v in request.headers:
92+
if not headers.get(k):
93+
headers.update({k: v})
94+
return headers
95+
96+
def _get_headers(self, headers, propagate_headers=False):
4297
"""If enabled appends trace headers to received ones.
4398
4499
:param headers: dictionary of HTTP Headers to send.
@@ -52,7 +107,8 @@ def _get_headers(self, headers):
52107
self._tracer = current_app.tracer
53108
if self._tracer:
54109
headers = self.insert_trace_headers(headers)
55-
110+
if self.config.propagate_headers or propagate_headers:
111+
headers = self.propagate_headers(headers)
56112
return headers
57113

58114
@staticmethod
@@ -81,10 +137,11 @@ def parse_response(self, response):
81137
data = data.get(self.config.data, {})
82138
return data
83139
except ValueError:
84-
current_app.logger.warning("Response.content is not a valid json {}".format(response.content))
140+
logger.warning("Response.content is not a valid json {}".format(response.content))
85141
return {}
86142

87-
def get(self, url, path_params=None, params=None, headers=None, **kwargs):
143+
@retry
144+
def get(self, url, path_params=None, params=None, headers=None, propagate_headers=False, **kwargs):
88145
"""Sends a GET request.
89146
90147
:param url: URL for the new :class:`Request` object. Could contain path parameters
@@ -98,11 +155,12 @@ def get(self, url, path_params=None, params=None, headers=None, **kwargs):
98155
"""
99156

100157
full_url = self._build_url(url, path_params)
101-
headers = self._get_headers(headers)
102-
current_app.logger.info("Get with url {}, params {}, headers {}, kwargs {}".
103-
format(full_url, params, headers, kwargs))
104-
response = requests.get(full_url, params=params, headers=headers, **kwargs)
105-
current_app.logger.info("Response {}".format(response))
158+
headers = self._get_headers(headers=headers, propagate_headers=propagate_headers)
159+
logger.info("Get with url {}, params {}, headers {}, kwargs {}".
160+
format(full_url, params, headers, kwargs))
161+
162+
session = requests.Session()
163+
response = self.requests(session=session).get(full_url, params=params, headers=headers, **kwargs)
106164

107165
return response
108166

@@ -122,6 +180,7 @@ def get_for_object(self, url, path_params=None, params=None, headers=None, **kwa
122180
response = self.get(url, path_params=path_params, params=params, headers=headers, **kwargs)
123181
return self.parse_response(response)
124182

183+
@retry
125184
def post(self, url, path_params=None, data=None, json=None, headers=None, **kwargs):
126185
"""Sends a POST request.
127186
@@ -138,10 +197,12 @@ def post(self, url, path_params=None, data=None, json=None, headers=None, **kwar
138197

139198
full_url = self._build_url(url, path_params)
140199
headers = self._get_headers(headers)
141-
current_app.logger.info("Post with url {}, data {}, json {}, headers {}, kwargs {}".format(full_url, data, json,
142-
headers, kwargs))
143-
response = requests.post(full_url, data=data, json=json, headers=headers, **kwargs)
144-
current_app.logger.info("Response {}".format(response))
200+
logger.info("Post with url {}, data {}, json {}, headers {}, kwargs {}".format(full_url, data, json,
201+
headers, kwargs))
202+
203+
session = requests.Session()
204+
response = self.requests(session=session).post(full_url, data=data, json=json, headers=headers, **kwargs)
205+
logger.info("Response {}".format(response))
145206

146207
return response
147208

@@ -162,6 +223,7 @@ def post_for_object(self, url, path_params=None, data=None, json=None, headers=N
162223
response = self.post(url, path_params=path_params, data=data, json=json, headers=headers, **kwargs)
163224
return self.parse_response(response)
164225

226+
@retry
165227
def put(self, url, path_params=None, data=None, headers=None, **kwargs):
166228
"""Sends a PUT request.
167229
@@ -178,10 +240,12 @@ def put(self, url, path_params=None, data=None, headers=None, **kwargs):
178240

179241
full_url = self._build_url(url, path_params)
180242
headers = self._get_headers(headers)
181-
current_app.logger.info("Put with url {}, data {}, headers {}, kwargs {}".format(full_url, data, headers,
182-
kwargs))
183-
response = requests.put(full_url, data, headers=headers, **kwargs)
184-
current_app.logger.info("Response {}".format(response))
243+
logger.info("Put with url {}, data {}, headers {}, kwargs {}".format(full_url, data, headers,
244+
kwargs))
245+
246+
session = requests.Session()
247+
response = self.requests(session=session).put(full_url, data, headers=headers, **kwargs)
248+
logger.info("Response {}".format(response))
185249

186250
return response
187251

@@ -202,6 +266,7 @@ def put_for_object(self, url, path_params=None, data=None, headers=None, **kwarg
202266
response = self.put(url, path_params=path_params, data=data, headers=headers, **kwargs)
203267
return self.parse_response(response)
204268

269+
@retry
205270
def delete(self, url, path_params=None, headers=None, **kwargs):
206271
"""Sends a DELETE request.
207272
@@ -215,8 +280,10 @@ def delete(self, url, path_params=None, headers=None, **kwargs):
215280

216281
full_url = self._build_url(url, path_params)
217282
headers = self._get_headers(headers)
218-
current_app.logger.info("Delete with url {}, headers {}, kwargs {}".format(full_url, headers, kwargs))
219-
response = requests.delete(full_url, headers=headers, **kwargs)
220-
current_app.logger.info("Response {}".format(response))
283+
logger.info("Delete with url {}, headers {}, kwargs {}".format(full_url, headers, kwargs))
284+
285+
session = requests.Session()
286+
response = self.requests(session=session).delete(full_url, headers=headers, **kwargs)
287+
logger.info("Response {}".format(response))
221288

222289
return response

‎pyms/logger/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
"""Init file
22
"""
3-
from pyms.logger.logger import CustomJsonFormatter
3+
from pyms.logger.logger import CustomJsonFormatter
4+
5+
__all__ = ['CustomJsonFormatter', ]

‎pyms/tracer/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Init file
2-
"""
2+
"""

‎tests/test_requests.py

+82-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
"""
33
import json
44
import os
5+
import time
56
import unittest
67

78
import requests_mock
89

910
from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT
1011
from pyms.flask.app import Microservice
12+
from pyms.flask.services.requests import DEFAULT_RETRIES
1113

1214

1315
class RequestServiceTests(unittest.TestCase):
@@ -20,8 +22,7 @@ def setUp(self):
2022
os.environ[CONFIGMAP_FILE_ENVIRONMENT] = os.path.join(self.BASE_DIR, "config-tests.yml")
2123
ms = Microservice(service="my-ms", path=__file__)
2224
self.app = ms.create_app()
23-
with self.app.app_context():
24-
self.request = ms.requests
25+
self.request = ms.requests
2526

2627
@requests_mock.Mocker()
2728
def test_get(self, mock_request):
@@ -207,3 +208,82 @@ def test_delete(self, mock_request):
207208

208209
self.assertEqual(204, response.status_code)
209210
self.assertEqual('', response.text)
211+
212+
def test_propagate_headers_empty(self, ):
213+
input_headers = {
214+
215+
}
216+
expected_headers = {
217+
'Content-Length': '12',
218+
'Content-Type': 'application/x-www-form-urlencoded',
219+
'Host': 'localhost'
220+
}
221+
with self.app.test_request_context(
222+
'/tests/', data={'format': 'short'}):
223+
headers = self.request.propagate_headers(input_headers)
224+
225+
self.assertEqual(expected_headers, headers)
226+
227+
def test_propagate_headers_no_override(self):
228+
input_headers = {
229+
'Host': 'my-server'
230+
}
231+
expected_headers = {
232+
'Host': 'my-server'
233+
}
234+
with self.app.test_request_context(
235+
'/tests/'):
236+
headers = self.request.propagate_headers(input_headers)
237+
238+
self.assertEqual(expected_headers, headers)
239+
240+
def test_propagate_headers_propagate(self):
241+
input_headers = {
242+
}
243+
expected_headers = {
244+
'Content-Length': '12',
245+
'Content-Type': 'application/x-www-form-urlencoded',
246+
'Host': 'localhost',
247+
'A': 'b',
248+
}
249+
with self.app.test_request_context(
250+
'/tests/', data={'format': 'short'}, headers={'a': 'b'}):
251+
headers = self.request.propagate_headers(input_headers)
252+
253+
self.assertEqual(expected_headers, headers)
254+
255+
def test_propagate_headers_propagate_no_override(self):
256+
input_headers = {
257+
'Host': 'my-server',
258+
'Span': '1234',
259+
}
260+
expected_headers = {
261+
'Host': 'my-server',
262+
'A': 'b',
263+
'Span': '1234',
264+
}
265+
with self.app.test_request_context(
266+
'/tests/', headers={'a': 'b', 'span': '5678'}):
267+
headers = self.request.propagate_headers(input_headers)
268+
269+
self.assertEqual(expected_headers, headers)
270+
271+
@requests_mock.Mocker()
272+
def test_retries_with_500(self, mock_request):
273+
url = 'http://localhost:9999'
274+
with self.app.app_context():
275+
mock_request.get(url, text="", status_code=500)
276+
response = self.request.get(url)
277+
278+
self.assertEqual(DEFAULT_RETRIES, mock_request.call_count)
279+
self.assertEqual(500, response.status_code)
280+
281+
@requests_mock.Mocker()
282+
def test_retries_with_200(self, mock_request):
283+
url = 'http://localhost:9999'
284+
with self.app.app_context():
285+
mock_request.get(url, text="", status_code=200)
286+
response = self.request.get(url)
287+
288+
self.assertEqual(1, mock_request.call_count)
289+
self.assertEqual(200, response.status_code)

0 commit comments

Comments
 (0)
Please sign in to comment.