├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── renovate.json ├── requests_opentracing ├── __init__.py └── tracing.py ├── requirements.txt ├── setup.py ├── tests ├── integration │ ├── __init__.py │ └── test_requests.py └── unit │ ├── __init__.py │ └── test_tracing.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | # Edit at https://www.gitignore.io/?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # End of https://www.gitignore.io/api/python 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 SignalFx, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include requirements.txt 4 | 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | #################### 2 | Requests OpenTracing 3 | #################### 4 | 5 | This package enables tracing http requests in a `Requests`_ ``Session`` via `The OpenTracing Project`_. 6 | Once a production system contends with real concurrency or splits into many services, crucial (and 7 | formerly easy) tasks become difficult: user-facing latency optimization, root-cause analysis of backend 8 | errors, communication about distinct pieces of a now-distributed system, etc. Distributed tracing 9 | follows a request on its journey from inception to completion from mobile/browser all the way to the 10 | microservices. 11 | 12 | As core services and libraries adopt OpenTracing, the application builder is no longer burdened with 13 | the task of adding basic tracing instrumentation to their own code. In this way, developers can build 14 | their applications with the tools they prefer and benefit from built-in tracing instrumentation. 15 | OpenTracing implementations exist for major distributed tracing systems and can be bound or swapped 16 | with a one-line configuration change. 17 | 18 | If you want to learn more about the underlying Python API, visit the Python `source code`_. 19 | 20 | .. _Requests: http://docs.python-requests.org/en/master/ 21 | .. _The OpenTracing Project: http://opentracing.io/ 22 | .. _source code: https://github.com/signalfx/python-requests/ 23 | 24 | Installation 25 | ============ 26 | 27 | Run the following command: 28 | 29 | .. code-block:: 30 | 31 | $ pip install requests-opentracing 32 | 33 | Usage 34 | ===== 35 | 36 | The provided ``requests.Session`` subclass allows the tracing of http methods using the OpenTracing API. 37 | All that it requires is for a ``SessionTracing`` instance to be initialized using an instance 38 | of an OpenTracing tracer and treated as a standard Requests session. 39 | 40 | Initialize 41 | ---------- 42 | 43 | ``SessionTracing`` takes the ``Tracer`` instance that is supported by OpenTracing and an optional 44 | dictionary of desired tags for each created span. You can also specify whether you'd like your 45 | current trace context to be propagated via http headers with your client request. To create a 46 | ``SessionTracing`` object, you can either pass in a tracer object directly or default to the 47 | ``opentracing.tracer`` global tracer that's set elsewhere in your application: 48 | 49 | .. code-block:: python 50 | 51 | from requests_opentracing import SessionTracing 52 | 53 | opentracing_tracer = # some OpenTracing tracer implementation 54 | traced_session = SessionTracing(opentracing_tracer, propagate=False, # propagation allows distributed tracing in 55 | span_tags=dict(my_helpful='tag')) # upstream services you control (True by default). 56 | resp = traced_session.get(my_url) 57 | 58 | or 59 | 60 | .. code-block:: python 61 | 62 | from requests_opentracing import SessionTracing 63 | import opentracing 64 | import requests 65 | 66 | opentracing.tracer = # some OpenTracing tracer implementation 67 | traced_session = SessionTracing() # default to opentracing.tracer 68 | 69 | You can now monkeypatch the ``requests.Session`` and ``requests.sessions.Session`` objects to point to the 70 | ``SessionTracing`` subclass for easier initialization: 71 | 72 | .. code-block:: python 73 | 74 | from requests_opentracing import monkeypatch_requests 75 | 76 | monkeypatch_requests() 77 | 78 | 79 | from requests import Session 80 | 81 | opentracing_tracer = # some OpenTracing tracer implementation 82 | traced_session = Session(opentracing_tracer, propagate=False, # Same arguments as provided to SessionTracing 83 | span_tags=dict(my_helpful='tag')) 84 | resp = traced_session.get(my_url) 85 | 86 | Further Information 87 | =================== 88 | 89 | If you're interested in learning more about the OpenTracing standard, please visit 90 | `opentracing.io`_ or `join the mailing list`_. If you would like to implement OpenTracing 91 | in your project and need help, feel free to send us a note at `community@opentracing.io`_. 92 | 93 | .. _opentracing.io: http://opentracing.io/ 94 | .. _join the mailing list: http://opentracing.us13.list-manage.com/subscribe?u=180afe03860541dae59e84153&id=19117aa6cd 95 | .. _community@opentracing.io: community@opentracing.io 96 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>opentracing-contrib/common", 5 | "schedule:weekly" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /requests_opentracing/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018-2019 SignalFx, Inc. All rights reserved. 2 | from .tracing import monkeypatch_requests, SessionTracing # noqa 3 | -------------------------------------------------------------------------------- /requests_opentracing/tracing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018-2019 SignalFx, Inc. All rights reserved. 2 | from traceback import format_exc 3 | 4 | from opentracing.propagation import Format 5 | from opentracing.ext import tags 6 | import requests.sessions 7 | import opentracing 8 | 9 | 10 | class SessionTracing(requests.sessions.Session): 11 | 12 | def __init__(self, tracer=None, propagate=True, span_tags=None): 13 | self._tracer = tracer 14 | self._propagate = propagate 15 | self._span_tags = span_tags or {} 16 | super(SessionTracing, self).__init__() 17 | 18 | def _get_tracer(self): 19 | return self._tracer or opentracing.tracer 20 | 21 | def request(self, method, url, *args, **kwargs): 22 | lower_method = method.lower() 23 | with self._get_tracer().start_active_span('requests.{}'.format(lower_method)) as scope: 24 | span = scope.span 25 | span.set_tag(tags.SPAN_KIND, tags.SPAN_KIND_RPC_CLIENT) 26 | span.set_tag(tags.COMPONENT, 'requests') 27 | span.set_tag(tags.HTTP_METHOD, lower_method) 28 | span.set_tag(tags.HTTP_URL, url) 29 | for name, value in self._span_tags.items(): 30 | span.set_tag(name, value) 31 | 32 | if self._propagate: 33 | headers = kwargs.setdefault('headers', {}) 34 | try: 35 | self._get_tracer().inject(span.context, Format.HTTP_HEADERS, headers) 36 | except opentracing.UnsupportedFormatException: 37 | pass 38 | try: 39 | resp = super(SessionTracing, self).request(method, url, *args, **kwargs) 40 | span.set_tag(tags.HTTP_STATUS_CODE, resp.status_code) 41 | except Exception: 42 | span.set_tag(tags.ERROR, True) 43 | span.set_tag('error.object', format_exc()) 44 | raise 45 | 46 | return resp 47 | 48 | 49 | def monkeypatch_requests(): 50 | requests.Session = SessionTracing 51 | requests.sessions.Session = SessionTracing 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opentracing>=2.0 2 | requests>=2.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools.command.test import test as TestCommand 3 | from setuptools import setup, find_packages 4 | import sys 5 | import os 6 | 7 | 8 | class PyTest(TestCommand): 9 | 10 | user_options = [] 11 | 12 | def initialize_options(self): 13 | TestCommand.initialize_options(self) 14 | 15 | def run_tests(self): 16 | import pytest 17 | sys.exit(pytest.main('tests')) 18 | 19 | 20 | cwd = os.path.abspath(os.path.dirname(__file__)) 21 | 22 | with open(os.path.join(cwd, 'requirements.txt')) as requirements_file: 23 | requirements = requirements_file.read().splitlines() 24 | 25 | with open(os.path.join(cwd, 'README.rst')) as readme_file: 26 | long_description = readme_file.read() 27 | 28 | # Keep these separated for tox extras 29 | test_requirements = ['mock', 'pytest'] 30 | integration_test_requirements = ['docker'] 31 | 32 | setup( 33 | name='Requests-OpenTracing', 34 | version='0.3.0', 35 | url='http://github.com/opentracing-contrib/python-requests', 36 | download_url='http://github.com/opentracing-contrib/python-requests/tarball/master', 37 | author='SignalFx, Inc.', 38 | author_email='signalfx-oss@splunk.com', 39 | description='OpenTracing support for Requests', 40 | long_description=long_description, 41 | packages=find_packages(), 42 | platforms='any', 43 | license='Apache Software License v2', 44 | classifiers=[ 45 | 'Development Status :: 4 - Beta', 46 | 'Intended Audience :: Developers', 47 | 'Natural Language :: English', 48 | 'License :: OSI Approved :: Apache Software License', 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 2', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.4', 54 | 'Programming Language :: Python :: 3.5', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Programming Language :: Python :: 3.7', 57 | ], 58 | install_requires=requirements, 59 | tests_require=test_requirements + integration_test_requirements, 60 | extras_require=dict( 61 | unit_tests=test_requirements, 62 | integration_tests=test_requirements + integration_test_requirements 63 | ), 64 | cmdclass=dict(test=PyTest) 65 | ) 66 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 SignalFx, Inc. All rights reserved. 2 | -------------------------------------------------------------------------------- /tests/integration/test_requests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 SignalFx, Inc. All rights reserved. 2 | from random import randint 3 | import sys 4 | 5 | from opentracing.mocktracer import MockTracer 6 | from opentracing.ext import tags as ext_tags 7 | import docker 8 | import pytest 9 | 10 | from requests_opentracing import SessionTracing 11 | import requests 12 | 13 | 14 | server_port = 5678 15 | server = 'http://localhost:{}'.format(server_port) 16 | 17 | 18 | @pytest.fixture(scope='session') 19 | def echo_container(): 20 | session = docker.from_env() 21 | echo = session.containers.run('hashicorp/http-echo:latest', '-text="hello world"', 22 | ports={'5678/tcp': server_port}, detach=True) 23 | try: 24 | yield echo 25 | finally: 26 | echo.remove(force=True, v=True) 27 | 28 | 29 | class TestSessionTracing(object): 30 | 31 | @pytest.fixture 32 | def session_tracing(self, echo_container): 33 | tracer = MockTracer() 34 | session = SessionTracing(tracer, propagate=True, span_tags=dict(custom='tag')) 35 | return tracer, session 36 | 37 | @pytest.fixture 38 | def tracer(self, session_tracing): 39 | return session_tracing[0] 40 | 41 | @pytest.fixture 42 | def session(self, session_tracing): 43 | return session_tracing[1] 44 | 45 | @pytest.mark.parametrize('method', ('get', 'post', 'put', 'patch', 46 | 'head', 'delete', 'options')) 47 | def test_successful_requests(self, tracer, session, method): 48 | trace_id = randint(0, sys.maxsize) 49 | with tracer.start_active_span('root') as root_scope: 50 | root_scope.span.context.trace_id = trace_id 51 | response = getattr(session, method)(server) 52 | request = response.request 53 | spans = tracer.finished_spans() 54 | assert len(spans) == 2 55 | req_span, root_span = spans 56 | assert req_span.operation_name == 'requests.{}'.format(method) 57 | 58 | tags = req_span.tags 59 | assert tags['custom'] == 'tag' 60 | assert tags[ext_tags.COMPONENT] == 'requests' 61 | assert tags[ext_tags.SPAN_KIND] == ext_tags.SPAN_KIND_RPC_CLIENT 62 | assert tags[ext_tags.HTTP_STATUS_CODE] == 200 63 | assert tags[ext_tags.HTTP_METHOD] == method 64 | assert tags[ext_tags.HTTP_URL] == server 65 | assert ext_tags.ERROR not in tags 66 | 67 | assert request.headers['ot-tracer-spanid'] == '{0:x}'.format(req_span.context.span_id) 68 | assert request.headers['ot-tracer-traceid'] == '{0:x}'.format(trace_id) 69 | 70 | @pytest.mark.parametrize('method', ('get', 'post', 'put', 'patch', 71 | 'head', 'delete', 'options')) 72 | def test_unsuccessful_requests(self, tracer, session, method): 73 | invalid_server = 'https://localhost:123456789' 74 | with tracer.start_active_span('root'): 75 | with pytest.raises(requests.RequestException) as re: 76 | getattr(session, method)(invalid_server) 77 | spans = tracer.finished_spans() 78 | assert len(spans) == 2 79 | req_span, root_span = spans 80 | assert req_span.operation_name == 'requests.{}'.format(method) 81 | 82 | tags = req_span.tags 83 | assert tags['custom'] == 'tag' 84 | assert tags[ext_tags.COMPONENT] == 'requests' 85 | assert tags[ext_tags.SPAN_KIND] == ext_tags.SPAN_KIND_RPC_CLIENT 86 | assert ext_tags.HTTP_STATUS_CODE not in tags 87 | assert tags[ext_tags.HTTP_METHOD] == method 88 | assert tags[ext_tags.HTTP_URL] == invalid_server 89 | assert tags[ext_tags.ERROR] is True 90 | assert str(re.value) in tags['error.object'] 91 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018 SignalFx, Inc. All rights reserved. 2 | -------------------------------------------------------------------------------- /tests/unit/test_tracing.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2018-2019 SignalFx, Inc. All rights reserved. 2 | from collections import namedtuple 3 | 4 | from opentracing.mocktracer import MockTracer 5 | from opentracing.ext import tags as ext_tags 6 | import requests.sessions 7 | import opentracing 8 | import pytest 9 | import mock 10 | 11 | from requests_opentracing.tracing import SessionTracing, monkeypatch_requests 12 | 13 | 14 | def stubbed_request(_, method, url, *args, **kwargs): 15 | response = namedtuple('response', 'method url status_code headers') 16 | return response(method, url, 200, kwargs.get('headers', {})) 17 | 18 | 19 | @pytest.fixture 20 | def original_session(): 21 | yield requests.sessions.Session 22 | 23 | 24 | @pytest.fixture(params=(True, False)) 25 | def session_cls(request): 26 | if request.param: 27 | original_session = requests.Session 28 | try: 29 | monkeypatch_requests() 30 | yield requests.Session 31 | finally: 32 | requests.Session = original_session 33 | requests.sessions.Session = original_session 34 | else: 35 | yield SessionTracing 36 | 37 | 38 | class TestSessionTracing(object): 39 | 40 | def test_sources_tracer(self, session_cls): 41 | tracer = MockTracer() 42 | assert session_cls(tracer)._tracer is tracer 43 | assert session_cls(tracer)._get_tracer() is tracer 44 | 45 | def test_sources_global_tracer_by_default(self, session_cls): 46 | assert session_cls()._tracer is None 47 | assert session_cls()._get_tracer() is opentracing.tracer 48 | 49 | def test_sources_propagate(self, session_cls): 50 | assert session_cls()._propagate is True 51 | assert session_cls(propagate=True)._propagate is True 52 | assert session_cls(propagate=False)._propagate is False 53 | 54 | def test_sources_span_tags(self, session_cls): 55 | assert session_cls()._span_tags == {} 56 | desired_tags = dict(one=123) 57 | assert session_cls(span_tags=desired_tags)._span_tags is desired_tags 58 | 59 | @pytest.mark.parametrize('method', ('get', 'post', 'put', 'patch', 60 | 'head', 'delete', 'options')) 61 | def test_request_without_propagate(self, method, original_session, session_cls): 62 | tracer = MockTracer() 63 | tracing = session_cls(tracer, False, span_tags=dict(one=123)) 64 | with mock.patch.object(original_session, 'request', stubbed_request): 65 | response = getattr(tracing, method)('my_url') 66 | 67 | assert len(tracer.finished_spans()) == 1 68 | span = tracer.finished_spans()[0] 69 | assert span.operation_name == 'requests.{}'.format(method) 70 | tags = span.tags 71 | assert tags['one'] == 123 72 | assert tags[ext_tags.COMPONENT] == 'requests' 73 | assert tags[ext_tags.SPAN_KIND] == ext_tags.SPAN_KIND_RPC_CLIENT 74 | assert tags[ext_tags.HTTP_STATUS_CODE] == 200 75 | assert tags[ext_tags.HTTP_METHOD] == method 76 | assert tags[ext_tags.HTTP_URL] == 'my_url' 77 | 78 | assert 'ot-tracer-spanid' not in response.headers 79 | assert 'ot-tracer-traceid' not in response.headers 80 | 81 | @pytest.mark.parametrize('method', ('get', 'post', 'put', 'patch', 82 | 'head', 'delete', 'options')) 83 | def test_request_with_propagate(self, method, original_session, session_cls): 84 | tracer = MockTracer() 85 | tracing = session_cls(tracer, True, span_tags=dict(one=123)) 86 | with mock.patch.object(original_session, 'request', stubbed_request): 87 | response = getattr(tracing, method)('my_url') 88 | 89 | assert len(tracer.finished_spans()) == 1 90 | span = tracer.finished_spans()[0] 91 | assert span.operation_name == 'requests.{}'.format(method) 92 | tags = span.tags 93 | assert tags['one'] == 123 94 | assert tags[ext_tags.COMPONENT] == 'requests' 95 | assert tags[ext_tags.SPAN_KIND] == ext_tags.SPAN_KIND_RPC_CLIENT 96 | assert tags[ext_tags.HTTP_STATUS_CODE] == 200 97 | assert tags[ext_tags.HTTP_METHOD] == method 98 | assert tags[ext_tags.HTTP_URL] == 'my_url' 99 | 100 | assert response.headers['ot-tracer-spanid'] == '{0:x}'.format(span.context.span_id) 101 | assert response.headers['ot-tracer-traceid'] == '{0:x}'.format(span.context.trace_id) 102 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | flake8 4 | py{27,34,35,36,37}-requests{20,21,22,23,24,25,26,27,28,29,210,211,212,213,214,215,216,217,218,219,220,221} 5 | py{27,35,36,37}-requests{222} 6 | 7 | [testenv] 8 | basepython = 9 | flake8: python2.7 10 | py27: python2.7 11 | py34: python3.4 12 | py35: python3.5 13 | py36: python3.6 14 | py37: python3.7 15 | passenv = PYTHONPATH 16 | setenv = PYTHONPATH = {toxinidir}:{env:PYTHONPATH:} 17 | deps = 18 | flake8: flake8 19 | requests20: requests>=2.0,<2.1 20 | requests21: requests>=2.1,<2.2 21 | requests22: requests>=2.2,<2.3 22 | requests23: requests>=2.3,<2.4 23 | requests24: requests>=2.4,<2.5 24 | requests25: requests>=2.5,<2.6 25 | requests26: requests>=2.6,<2.7 26 | requests27: requests>=2.7,<2.8 27 | requests28: requests>=2.8,<2.9 28 | requests29: requests>=2.9,<2.10 29 | requests210: requests>=2.10,<2.11 30 | requests211: requests>=2.11,<2.12 31 | requests212: requests>=2.12,<2.13 32 | requests213: requests>=2.13,<2.14 33 | requests214: requests>=2.14,<2.15 34 | requests215: requests>=2.15,<2.16 35 | requests216: requests>=2.16,<2.17 36 | requests217: requests>=2.17,<2.18 37 | requests218: requests>=2.18,<2.19 38 | requests219: requests>=2.19,<2.20 39 | requests220: requests>=2.20,<2.21 40 | requests221: requests>=2.21,<2.22 41 | requests222: requests>=2.22,<2.23 42 | extras = 43 | requests{20,21,22,23,24,25,26,27,28,29,210,211,212,213,214,215,216,217,218,219,220,221,222}: integration_tests 44 | commands = 45 | flake8: flake8 setup.py requests_opentracing tests 46 | requests{20,21,22,23,24,25,26,27,28,29,210,211,212,213,214,215,216,217,218,219,220,221,222}: pytest tests/unit 47 | requests{20,21,22,23,24,25,26,27,28,29,210,211,212,213,214,215,216,217,218,219,220,221,222}: pytest tests/integration 48 | 49 | [flake8] 50 | max-line-length = 120 51 | --------------------------------------------------------------------------------