├── .github
└── workflows
│ ├── build.yaml
│ └── pypi.yaml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.rst
├── LICENSE.txt
├── Makefile
├── README.md
├── docs
└── source
│ ├── changelog.rst
│ ├── conf.py
│ ├── index.rst
│ ├── swagger_zipkin.rst
│ └── zipkin_decorator.rst
├── mypy.ini
├── requirements-dev.txt
├── setup.py
├── swagger_zipkin
├── __init__.py
├── decorate_client.py
└── zipkin_decorator.py
├── tests
├── __init__.py
├── decorate_client_test.py
└── zipkin_decorator_test.py
└── tox.ini
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: build
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 |
7 | jobs:
8 | tox:
9 | runs-on: ubuntu-22.04
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | toxenv:
14 | - py37
15 | - py38
16 | steps:
17 | - uses: actions/checkout@v2
18 | - name: Set up Python
19 | uses: actions/setup-python@v2
20 | with:
21 | python-version: ${{ matrix.python-version }}
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install tox==3.26.0
26 | - name: Run tests
27 | run: tox -e py
28 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yaml:
--------------------------------------------------------------------------------
1 | name: Publish on PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-python@v4
15 | with:
16 | python-version: 3.8
17 |
18 | - name: Install Python dependencies
19 | run: pip install wheel
20 |
21 | - name: Create a Wheel file and source distribution
22 | run: python setup.py sdist bdist_wheel
23 |
24 | - name: Publish distribution package to PyPI
25 | uses: pypa/gh-action-pypi-publish@v1.5.1
26 | with:
27 | user: __token__
28 | password: ${{ secrets.PYPI_PASSWORD }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.iml
3 | *.log
4 | *.py[co]
5 | *.so
6 | *.sublime-*
7 | *.sw[nop]
8 | *~
9 | .#*
10 | .DS_Store
11 | ._*
12 | .coverage
13 | .cache
14 | .idea
15 | .project
16 | .pydevproject
17 | .ropeproject
18 | .tox
19 | \#*\#
20 | /.venv.touch
21 | /build
22 | /dist
23 | /virtualenv_run
24 | /coverage-html
25 | /venv
26 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.0.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-json
8 | files: \.(bowerrc|jshintrc|json)$
9 | - id: check-yaml
10 | - id: debug-statements
11 | - id: name-tests-test
12 | - id: flake8
13 | args: ['--max-line-length=131']
14 | - id: requirements-txt-fixer
15 | - repo: https://github.com/pre-commit/mirrors-autopep8
16 | rev: v1.4
17 | hooks:
18 | - id: autopep8
19 | - repo: https://github.com/asottile/reorder_python_imports.git
20 | rev: v1.3.2
21 | hooks:
22 | - id: reorder-python-imports
23 | language_version: python3.7
24 | - repo: https://github.com/asottile/pyupgrade
25 | rev: v3.1.0
26 | hooks:
27 | - id: pyupgrade
28 | args: [--py37-plus]
29 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | Unreleased
5 | ------------------
6 | - Add type annotations
7 | - Drop support for EOL Pythons
8 |
9 | 0.4.0 (2018-02-08)
10 | ------------------
11 | - Add support for passing in an explicit context object.
12 |
13 | 0.3.3 (2017-08-01)
14 | ------------------
15 | - Add REPL completion for resource.
16 |
17 | 0.3.2 (2017-05-08)
18 | ------------------
19 | - Rename 'client' attribute.
20 |
21 | 0.3.1 (2016-10-02)
22 | ------------------
23 | - Don't clobber AttributeError stack traces
24 |
25 | 0.3.0 (2016-09-21)
26 | ------------------
27 | - Make the wrapper a bit friendlier to the wrapped client, supporting `dir`
28 | properly.
29 |
30 | 0.2.0 (2016-09-20)
31 | ----------------------
32 | - Use py_zipkin instead of pyramid_zipkin.
33 |
34 | 0.1.0 (2015-12-10)
35 | ----------------------
36 | - Standalone zipkin decorator for swagger clients.
37 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2015 Yelp Inc
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBUILD_FLAG =
2 |
3 | .PHONY: help all clean clean-pyc clean-build lint test coverage docs
4 |
5 | help:
6 | @echo "clean-build - remove build artifacts"
7 | @echo "clean-pyc - remove Python file artifacts"
8 | @echo "test - run tests quickly with the default Python"
9 | @echo "coverage - check code coverage"
10 |
11 | all: test
12 |
13 | clean: clean-build clean-pyc
14 |
15 | clean-build:
16 | rm -fr build/
17 | rm -fr dist/
18 | rm -fr *.egg-info
19 |
20 | clean-pyc:
21 | find . -name '*.pyc' -exec rm -f {} +
22 | find . -name '*.pyo' -exec rm -f {} +
23 | find . -name '*~' -exec rm -f {} +
24 |
25 | test: .venv.touch
26 | tox $(REBUILD_FLAG)
27 |
28 | docs:
29 | tox -e docs
30 |
31 | coverage: test
32 |
33 | venv:
34 | tox -e venv
35 |
36 | .venv.touch: setup.py requirements-dev.txt
37 | $(eval REBUILD_FLAG := --recreate)
38 | touch .venv.touch
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/Yelp/swagger_zipkin/actions/workflows/build.yaml?query=branch:master)
2 | [](https://pypi.python.org/pypi/swagger_zipkin/)
3 | [](https://pypi.python.org/pypi/swagger_zipkin/)
4 |
5 | # swagger_zipkin
6 |
7 | `swagger_zipkin` contains zipkin integration tools for instrumenting downstream
8 | service calls made using [bravado](http://bravado.readthedocs.org/en/latest/) or
9 | [swagger-py](http://swagger-py.readthedocs.org/en/latest/) http clients.
10 |
11 | It is aimed to be a standalone package, with only dependency being
12 | [py_zipkin](https://github.com/Yelp/py_zipkin).
13 |
14 | Quick Start
15 | -----------
16 |
17 | The decorator can be applied to any swagger-py or bravado client. The example
18 | shows a sample usage on bravado. It assume the code is being called from within
19 | a `py_zipkin.zipkin.zipkin_span` context manager or decorator.
20 |
21 | ```py
22 | from bravado.client import SwaggerClient
23 | from swagger_zipkin.zipkin_decorator import ZipkinClientDecorator
24 |
25 | client = SwaggerClient.from_url("http://petstore.swagger.io/v2/swagger.json")
26 | zipkin_wrapped_client = ZipkinClientDecorator(client)
27 |
28 | pet = zipkin_wrapped_client.pet.getPetById(petId=42).result()
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/source/changelog.rst:
--------------------------------------------------------------------------------
1 | ../../CHANGELOG.rst
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | from swagger_zipkin import __version__ as version
2 |
3 |
4 | extensions = [
5 | 'sphinx.ext.autodoc',
6 | 'sphinx.ext.doctest',
7 | 'sphinx.ext.intersphinx',
8 | ]
9 |
10 | release = version
11 |
12 | # Add any paths that contain templates here, relative to this directory.
13 | templates_path = ['_templates']
14 |
15 | # The suffix of source filenames.
16 | source_suffix = '.rst'
17 |
18 | # The master toctree document.
19 | master_doc = 'index'
20 |
21 | # General information about the project.
22 | project = 'swagger_zipkin'
23 | copyright = '2016, Yelp, Inc'
24 |
25 | exclude_patterns = []
26 |
27 | pygments_style = 'sphinx'
28 |
29 |
30 | # -- Options for HTML output ---------------------------------------------
31 |
32 | html_theme = 'sphinxdoc'
33 |
34 | html_static_path = ['_static']
35 |
36 | htmlhelp_basename = 'swagger_zipkindoc'
37 |
38 |
39 | intersphinx_mapping = {
40 | 'http://docs.python.org/': None,
41 | }
42 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | swagger_zipkin |version|
2 | ============================
3 |
4 | :mod:`swagger_zipkin` contains zipkin integration tools for instrumenting downstream service calls made using `bravado `_ or `swagger-py `_ http clients.
5 |
6 | It is aimed to be a standalone package, with only dependency being `py_zipkin `_.
7 |
8 | Quick Start
9 | -----------
10 |
11 | The decorator can be applied to any swagger-py or bravado client. The example
12 | shows a sample usage on bravado. It assume the code is being called from within
13 | a `py_zipkin.zipkin.zipkin_span` context manager or decorator.
14 |
15 | .. code-block:: python
16 |
17 | from bravado.client import SwaggerClient
18 | from swagger_zipkin.zipkin_decorator import ZipkinClientDecorator
19 |
20 | client = SwaggerClient.from_url("http://petstore.swagger.io/v2/swagger.json")
21 | zipkin_wrapped_client = ZipkinClientDecorator(client)
22 |
23 | pet = zipkin_wrapped_client.pet.getPetById(petId=42).result()
24 |
25 | Contents
26 | --------
27 |
28 | .. toctree::
29 | :maxdepth: 1
30 |
31 | swagger_zipkin
32 | changelog
33 |
34 | Indices and tables
35 | ==================
36 |
37 | * :ref:`genindex`
38 | * :ref:`modindex`
39 | * :ref:`search`
40 |
--------------------------------------------------------------------------------
/docs/source/swagger_zipkin.rst:
--------------------------------------------------------------------------------
1 | Zipkin Decorator
2 | ================
3 |
4 | .. automodule:: swagger_zipkin.zipkin_decorator
5 | :members:
6 |
--------------------------------------------------------------------------------
/docs/source/zipkin_decorator.rst:
--------------------------------------------------------------------------------
1 | swagger_zipkin Package
2 | ======================
3 |
4 | :mod:`swagger_zipkin` Package
5 | -----------------------------
6 |
7 | :mod:`decorate_client` Module
8 | -----------------------------
9 |
10 | .. automodule:: swagger_zipkin.decorate_client
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | :mod:`zipkin_decorator` Module
16 | -----------------------------
17 |
18 | .. automodule:: swagger_zipkin.zipkin_decorator
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | python_version = 3.7
3 | disallow_untyped_defs = True
4 | disallow_incomplete_defs = True
5 | disallow_untyped_calls = True
6 | disallow_untyped_decorators = True
7 | disallow_any_generics = False
8 | warn_redundant_casts = True
9 | warn_unused_ignores = True
10 | warn_unreachable = True
11 | show_error_context = True
12 | show_column_numbers = True
13 | ignore_missing_imports = False
14 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | coverage
2 | flake8
3 | mypy
4 | pre-commit
5 | pytest
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from setuptools import find_packages
3 | from setuptools import setup
4 |
5 |
6 | setup(
7 | name='swagger_zipkin',
8 | version="0.5.1",
9 | provides=["swagger_zipkin"],
10 | author='Yelp, Inc.',
11 | author_email='opensource+swagger-zipkin@yelp.com',
12 | license='Copyright Yelp 2016',
13 | url="https://github.com/Yelp/swagger_zipkin",
14 | description='Zipkin decorators for swagger clients - swagger-py and bravado.',
15 | packages=find_packages(exclude=['tests*']),
16 | python_requires=">=3.7",
17 | install_requires=[
18 | 'py_zipkin>=0.10.1',
19 | ],
20 | keywords='zipkin',
21 | classifiers=[
22 | "Development Status :: 5 - Production/Stable",
23 | "Intended Audience :: Developers",
24 | "Topic :: Software Development :: Libraries :: Python Modules",
25 | "License :: OSI Approved :: Apache Software License",
26 | "Operating System :: OS Independent",
27 | "Programming Language :: Python :: 3",
28 | "Programming Language :: Python :: 3.7",
29 | "Programming Language :: Python :: 3.8",
30 | ],
31 | )
32 |
--------------------------------------------------------------------------------
/swagger_zipkin/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/swagger_zipkin/92e8715a696ae166ba8aaa1ec2d453b136ae40f2/swagger_zipkin/__init__.py
--------------------------------------------------------------------------------
/swagger_zipkin/decorate_client.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import functools
4 | from typing import Callable
5 | from typing import Generic
6 | from typing import TypeVar
7 |
8 | from typing_extensions import ParamSpec
9 | from typing_extensions import Protocol
10 |
11 |
12 | T = TypeVar('T', covariant=True)
13 | P = ParamSpec('P')
14 |
15 |
16 | class Operation(Protocol):
17 | ...
18 |
19 |
20 | class Resource(Protocol[P, T]):
21 | def __getattr__(self, name: str) -> Operation:
22 | ... # pragma: no cover
23 |
24 | def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
25 | ... # pragma: no cover
26 |
27 |
28 | class Client(Protocol):
29 | def __getattr__(self, name: str) -> Resource:
30 | ... # pragma: no cover
31 |
32 |
33 | class OperationDecorator(Generic[P, T]):
34 | """A helper to preserve attributes of :class:`swaggerpy.client.Operation`
35 | and :class:`bravado.client.CallableOperation` while decorating their
36 | __call__() methods
37 |
38 | :param operation: callable operation, e.g., attributes of
39 | :class:`swaggerpy.client.Resource` or
40 | :class:`bravado_core.resource.Resource`
41 | :type operation: :class:`swaggerpy.client.Operation` or
42 | :class:`bravado.client.CallableOperation`
43 | :param func: a callable which accepts `*args`, `**kwargs`
44 | :type func: callable
45 | """
46 |
47 | def __init__(self, operation: Resource, func: Callable[P, T]) -> None:
48 | self.operation = operation
49 | self.func = func
50 |
51 | def __getattr__(self, name: str) -> Operation:
52 | return getattr(self.operation, name)
53 |
54 | def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
55 | return self.func(*args, **kwargs)
56 |
57 |
58 | def decorate_client(
59 | api_client: Client,
60 | func: Callable[P, T],
61 | name: str,
62 | ) -> Resource[P, T]:
63 | """A helper for decorating :class:`bravado.client.SwaggerClient`.
64 | :class:`bravado.client.SwaggerClient` can be extended by creating a class
65 | which wraps all calls to it. This helper is used in a :func:`__getattr__`
66 | to check if the attr exists on the api_client. If the attr does not exist
67 | raise :class:`AttributeError`, if it exists and is not callable return it,
68 | and if it is callable return a partial function calling `func` with `name`.
69 |
70 | Example usage:
71 |
72 | .. code-block:: python
73 |
74 | class SomeClientDecorator(object):
75 |
76 | def __init__(self, api_client, ...):
77 | self.api_client = api_client
78 |
79 | # First arg should be suffiently unique to not conflict with any of
80 | # the kwargs
81 | def wrap_call(self, client_call_name, *args, **kwargs):
82 | ...
83 |
84 | def __getattr__(self, name):
85 | return decorate_client(self.api_client, self.wrap_call, name)
86 |
87 | :param api_client: the client which is being decorated
88 | :type api_client: :class:`bravado.client.SwaggerClient`
89 | :param func: a callable which accepts `name`, `*args`, `**kwargs`
90 | :type func: callable
91 | :param name: the attribute being accessed
92 | :type name: string
93 | :returns: the attribute from the `api_client` or a partial of `func`
94 | :raises: :class:`AttributeError`
95 | """
96 | client_attr = getattr(api_client, name)
97 | if not callable(client_attr):
98 | return client_attr
99 |
100 | return OperationDecorator(client_attr, functools.partial(func, name))
101 |
--------------------------------------------------------------------------------
/swagger_zipkin/zipkin_decorator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 | from typing import TypeVar
5 |
6 | from py_zipkin.storage import Stack
7 | from py_zipkin.zipkin import create_http_headers_for_new_span
8 | from typing_extensions import ParamSpec
9 |
10 | from swagger_zipkin.decorate_client import Client
11 | from swagger_zipkin.decorate_client import decorate_client
12 | from swagger_zipkin.decorate_client import Resource
13 |
14 | T = TypeVar('T', covariant=True)
15 | P = ParamSpec('P')
16 |
17 |
18 | class ZipkinResourceDecorator:
19 | """A wrapper to the swagger resource.
20 |
21 | :param resource: A resource object. eg. `client.pet`, `client.store`.
22 | :type resource: :class:`swaggerpy.client.Resource` or :class:`bravado_core.resource.Resource`
23 | """
24 |
25 | def __init__(self, resource: Client, context_stack: Stack | None = None) -> None:
26 | self.resource = resource
27 | self._context_stack = context_stack
28 |
29 | def __getattr__(self, name: str) -> Resource:
30 | return decorate_client(self.resource, self.with_headers, name)
31 |
32 | def with_headers(self, call_name: str, *args: Any, **kwargs: Any) -> Any:
33 | kwargs.setdefault('_request_options', {})
34 | request_options: dict = kwargs['_request_options']
35 | headers = request_options.setdefault('headers', {})
36 |
37 | headers.update(create_http_headers_for_new_span(
38 | context_stack=self._context_stack))
39 | return getattr(self.resource, call_name)(*args, **kwargs)
40 |
41 | def __dir__(self) -> list[str]:
42 | return dir(self.resource)
43 |
44 |
45 | class ZipkinClientDecorator:
46 | """A wrapper to swagger client (swagger-py or bravado) to pass on zipkin
47 | headers to the service call.
48 |
49 | Even though client is initialised once, all the calls made will have
50 | independent spans.
51 |
52 | :param client: Swagger Client
53 | :type client: :class:`swaggerpy.client.SwaggerClient` or :class:`bravado.client.SwaggerClient`.
54 | """
55 |
56 | def __init__(self, client: Client, context_stack: Stack | None = None):
57 | self._client = client
58 | self._context_stack = context_stack
59 |
60 | def __getattr__(self, name: str) -> Client:
61 | return ZipkinResourceDecorator(
62 | getattr(self._client, name),
63 | context_stack=self._context_stack,
64 | )
65 |
66 | def __dir__(self) -> list[str]:
67 | return dir(self._client) # pragma: no cover
68 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yelp/swagger_zipkin/92e8715a696ae166ba8aaa1ec2d453b136ae40f2/tests/__init__.py
--------------------------------------------------------------------------------
/tests/decorate_client_test.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | import pytest
4 |
5 | from swagger_zipkin.decorate_client import decorate_client
6 |
7 |
8 | def test_decorate_client_non_attr():
9 | client = object()
10 |
11 | with pytest.raises(AttributeError):
12 | decorate_client(client, mock.Mock(), 'attr')
13 |
14 |
15 | def test_decorate_client_non_callable():
16 | client = mock.Mock()
17 | client.attr = 1
18 |
19 | decorated = decorate_client(client, mock.Mock(), 'attr')
20 | assert client.attr == decorated
21 |
22 |
23 | def test_decorate_client_callable_being_invoked():
24 | def foo(a, b, c):
25 | pass
26 |
27 | client = mock.Mock()
28 | client.attr = foo
29 | decorated_foo = mock.Mock()
30 |
31 | decorated_callable = decorate_client(client, decorated_foo, 'attr')
32 | assert decorated_callable.operation == foo
33 |
34 | # Ensure that it's `decorated_foo` being called, not `foo`
35 | decorated_callable()
36 | decorated_foo.assert_called_once_with('attr')
37 |
38 |
39 | def test_decorate_client_callable_attribute_retrieved():
40 | class Foo:
41 | def __init__(self):
42 | self.bar = 'bar'
43 |
44 | def __call__(self, a, b, c):
45 | return a + b + c
46 |
47 | client = mock.Mock()
48 | client.attr = Foo()
49 | decorated_foo = mock.Mock(return_value=100)
50 | decorated_callable = decorate_client(client, decorated_foo, 'attr')
51 |
52 | # `decorated_foo` is called, not `Foo().__call__`
53 | assert decorated_callable(2, 3, 7) == 100
54 | # Foo().bar is accessible after it is decorated
55 | assert decorated_callable.bar == 'bar'
56 |
--------------------------------------------------------------------------------
/tests/zipkin_decorator_test.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 |
3 | from swagger_zipkin.zipkin_decorator import ZipkinClientDecorator
4 |
5 |
6 | def create_span(trace_id, span_id, parent_span_id):
7 | return {
8 | 'X-B3-TraceId': trace_id,
9 | 'X-B3-SpanId': span_id,
10 | 'X-B3-ParentSpanId': parent_span_id,
11 | 'X-B3-Flags': '0',
12 | 'X-B3-Sampled': 'true',
13 | }
14 |
15 |
16 | def create_request_options(trace_id, span_id, parent_span_id):
17 | return {
18 | 'headers': {
19 | 'X-B3-TraceId': trace_id,
20 | 'X-B3-SpanId': span_id,
21 | 'X-B3-ParentSpanId': parent_span_id,
22 | 'X-B3-Flags': '0',
23 | 'X-B3-Sampled': 'true',
24 | }
25 | }
26 |
27 |
28 | def test_client_request_option_decorator():
29 | trace_id = 'trace_id'
30 | span_id = 'span_id'
31 |
32 | client = mock.Mock()
33 | wrapped_client = ZipkinClientDecorator(client)
34 | param = mock.Mock()
35 |
36 | with mock.patch('swagger_zipkin.zipkin_decorator.create_http_headers_for_new_span', side_effect=[
37 | create_span(trace_id, 'span1', span_id),
38 | create_span(trace_id, 'span2', span_id),
39 | ]):
40 | # This line is actually very important. Every time `.resource` is called,
41 | # a new decorator is constructed, and we want to make sure we're testing
42 | # against the same decorator to prevent regressing on WEBCORE-1240.
43 | resource = wrapped_client.resource
44 |
45 | resource.operation(param)
46 | client.resource.operation.assert_called_with(
47 | param,
48 | _request_options=create_request_options(trace_id, 'span1', span_id)
49 | )
50 |
51 | resource.operation(param)
52 | client.resource.operation.assert_called_with(
53 | param,
54 | _request_options=create_request_options(trace_id, 'span2', span_id)
55 | )
56 |
57 |
58 | def test_client_explicit_context():
59 | client = mock.Mock()
60 | wrapped_client = ZipkinClientDecorator(
61 | client,
62 | context_stack=mock.sentinel.context_stack,
63 | )
64 |
65 | with mock.patch(
66 | 'swagger_zipkin.zipkin_decorator.create_http_headers_for_new_span',
67 | side_effect=[
68 | create_span('trace_id', 'span1', 'span_id'),
69 | create_span('trace_id', 'span2', 'span_id'),
70 | ],
71 | ) as create_http_headers_mock:
72 | resource = wrapped_client.resource
73 | resource.operation(mock.Mock())
74 | create_http_headers_mock.assert_called_with(
75 | context_stack=mock.sentinel.context_stack,
76 | )
77 |
78 |
79 | def test_client_dir():
80 | class V1Operation():
81 | def foo():
82 | return 'foo'
83 |
84 | class Client:
85 | def __init__(self):
86 | self.v1 = V1Operation()
87 |
88 | client = Client()
89 | wrapped_client = ZipkinClientDecorator(client)
90 |
91 | assert 'foo' in dir(wrapped_client.v1)
92 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = pre-commit, py37, py38, flake8
3 |
4 | [testenv]
5 | deps = -rrequirements-dev.txt
6 | commands =
7 | mypy swagger_zipkin
8 | coverage erase
9 | coverage run --source=swagger_zipkin/ -m pytest --strict {posargs}
10 | coverage report -m --show-missing --fail-under 100
11 |
12 | [testenv:venv]
13 | envdir = venv
14 | basepython = /usr/bin/python3.7
15 | commands =
16 |
17 | [testenv:docs]
18 | basepython = /usr/bin/python3.7
19 | deps =
20 | {[testenv]deps}
21 | sphinx
22 | changedir = docs
23 | commands = sphinx-build -b html -d build/doctrees source build/html
24 |
25 | [testenv:pre-commit]
26 | basepython = /usr/bin/python3.7
27 | deps = -rrequirements-dev.txt
28 | commands = pre-commit run --all-files
29 |
30 | [testenv:flake8]
31 | basepython = /usr/bin/python3.7
32 | deps = flake8
33 | commands =
34 | flake8 swagger_zipkin tests
35 |
36 | [flake8]
37 | ignore =
38 | exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,docs,virtualenv_run
39 | max-line-length = 131
40 |
41 | [pytest]
42 | norecursedirs = .* _darcs CVS docs virtualenv_run
43 |
44 | [report]
45 | exclude_lines =
46 | pragma: no cover
47 | raise NotImplementedError
48 |
--------------------------------------------------------------------------------