├── .gitignore ├── .travis.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ ├── .gitkeep │ └── custom.css ├── conf.py ├── index.rst ├── make.bat └── requirements-docs.txt ├── google ├── __init__.py ├── gapic │ ├── __init__.py │ └── longrunning │ │ ├── __init__.py │ │ ├── operations_client.py │ │ └── operations_client_config.json └── gax │ ├── __init__.py │ ├── _grpc_google_auth.py │ ├── api_callable.py │ ├── bundling.py │ ├── config.py │ ├── errors.py │ ├── grpc.py │ ├── path_template.py │ ├── retry.py │ └── utils │ ├── __init__.py │ ├── messages.py │ ├── metrics.py │ ├── oneof.py │ └── protobuf.py ├── nox.py ├── pylint.conf.py ├── setup.cfg ├── setup.py ├── test-requirements.txt └── tests ├── __init__.py ├── fixtures ├── __init__.py ├── fixture.proto └── fixture_pb2.py ├── test__grpc_google_auth.py ├── test_api_callable.py ├── test_bundling.py ├── test_gax.py ├── test_grpc.py ├── test_path_template.py ├── test_retry.py ├── test_utils_messages.py ├── test_utils_metrics.py ├── test_utils_oneof.py └── test_utils_protobuf.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | 4 | # https://github.com/github/gitignore/blob/master/Python.gitignore 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | docs/generated 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # Eclipse files 34 | .project 35 | .pydevproject 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *,cover 57 | pylintrc 58 | pylintrc.test 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | _build 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # ctags 75 | tags 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | matrix: 4 | include: 5 | - python: 3.6 6 | env: NOX_SESSION=lint 7 | - python: 3.6 8 | env: NOX_SESSION=docs 9 | - python: 3.6 10 | env: NOX_SESSION=cover 11 | - python: 2.7 12 | env: NOX_SESSION="unit_tests(python='python2.7')" 13 | - python: 3.4 14 | env: NOX_SESSION="unit_tests(python='python3.4')" 15 | - python: 3.5 16 | env: NOX_SESSION="unit_tests(python='python3.5')" 17 | # Disabled until gRPC supports PyPy. 18 | # - python: pypy 19 | # env: NOX_SESSION=unit_tests(python='pypy') 20 | cache: 21 | directories: 22 | - ${HOME}/.cache 23 | install: 24 | - pip install --upgrade nox-automation 25 | script: nox -s $NOX_SESSION 26 | after_success: 27 | - test $NOX_SESSION = "cover" && codecov 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Here are some guidelines for hacking on `gax-python`_. 5 | 6 | - Please **sign** one of the `Contributor License Agreements`_ below. 7 | - `File an issue`_ to notify the maintainers about what you're working on. 8 | - `Fork the repo`_; develop and `test your code changes`_; add docs. 9 | - Make sure that your `commit messages`_ clearly describe the changes. 10 | - `Make the pull request`_. 11 | 12 | .. _`Fork the repo`: https://help.github.com/articles/fork-a-repo 13 | .. _`forking`: https://help.github.com/articles/fork-a-repo 14 | .. _`commit messages`: http://chris.beams.io/posts/git-commit/ 15 | 16 | .. _`File an issue`: 17 | 18 | Before writing code, file an issue 19 | ---------------------------------- 20 | 21 | Use the issue tracker to start the discussion. It is possible that someone else 22 | is already working on your idea, your approach is not quite right, or that the 23 | functionality exists already. The ticket you file in the issue tracker will be 24 | used to hash that all out. 25 | 26 | Fork `gax-python` 27 | ------------------- 28 | 29 | We will use GitHub's mechanism for `forking`_ repositories and making pull 30 | requests. Fork the repository, and make your changes in the forked repository. 31 | 32 | .. _`test your code changes`: 33 | 34 | Include tests 35 | ------------- 36 | 37 | Be sure to add relevant tests and run then them using :code:`tox` before making the pull request. 38 | Docs will be updated automatically when we merge to `master`, but 39 | you should also build the docs yourself via :code:`tox -e docs`, making sure that the docs build OK 40 | and that they are readable. 41 | 42 | .. _`tox`: https://tox.readthedocs.org/en/latest/ 43 | 44 | Make the pull request 45 | --------------------- 46 | 47 | Once you have made all your changes, tested, and updated the documentation, 48 | make a pull request to move everything back into the main `gax-python`_ 49 | repository. Be sure to reference the original issue in the pull request. 50 | Expect some back-and-forth with regards to style and compliance of these 51 | rules. 52 | 53 | Using a Development Checkout 54 | ---------------------------- 55 | 56 | You’ll have to create a development environment to hack on 57 | `gax-python`_, using a Git checkout: 58 | 59 | - While logged into your GitHub account, navigate to the `gax-python repo`_ on GitHub. 60 | - Fork and clone the `gax-python` repository to your GitHub account 61 | by clicking the "Fork" button. 62 | - Clone your fork of `gax-python` from your GitHub account to your 63 | local computer, substituting your account username and specifying 64 | the destination as `hack-on-gax-python`. For example: 65 | 66 | .. code:: bash 67 | 68 | cd ${HOME} 69 | git clone git@github.com:USERNAME/gax-python.git hack-on-gax-python 70 | cd hack-on-gax-python 71 | 72 | # Configure remotes such that you can pull changes from the gax-python 73 | # repository into your local repository. 74 | git remote add upstream https://github.com:google/gax-python 75 | 76 | # fetch and merge changes from upstream into master 77 | git fetch upstream 78 | git merge upstream/master 79 | 80 | 81 | Now your local repo is set up such that you will push changes to your 82 | GitHub repo, from which you can submit a pull request. 83 | 84 | - Create use tox to create development virtualenv in which `gax-python`_ is installed: 85 | 86 | .. code:: bash 87 | 88 | sudo pip install tox 89 | cd ~/hack-on-gax-python 90 | tox -e devenv 91 | 92 | - This is creates a tox virtualenv named `development` that has gax-python installed. 93 | Activate it to use gax-python locally, e.g, from the python prompt. 94 | 95 | .. code:: bash 96 | 97 | cd ~/hack-on-gax-python 98 | . ./tox/develop/bin/activate 99 | 100 | .. _`gax-python`: https://github.com/googleapis/gax-python 101 | .. _`gax-python repo`: https://github.com/googleapis/gax-python 102 | 103 | 104 | Running Tests 105 | ------------- 106 | 107 | - To run the full set of `gax-python` tests on all platforms, install 108 | `tox`_ into a system Python. The :code:`tox` console script will be 109 | installed into the scripts location for that Python. While in the 110 | `gax-python` checkout root directory (it contains :code:`tox.ini`), 111 | invoke the `tox` console script. This will read the :code:`tox.ini` file and 112 | execute the tests on multiple Python versions and platforms; while it runs, 113 | it creates a virtualenv for each version/platform combination. Note that 114 | your `BREW_HOME` must be set to the brew directory where protoc is installed 115 | in order for tests to work. For example: 116 | 117 | .. code:: bash 118 | 119 | export BREW_HOME=~/.linuxbrew 120 | sudo pip install tox 121 | cd ~/hack-on-gax-python 122 | tox 123 | 124 | Contributor License Agreements 125 | ------------------------------ 126 | 127 | Before we can accept your pull requests you'll need to sign a Contributor 128 | License Agreement (CLA): 129 | 130 | - **If you are an individual writing original source code** and **you own 131 | the intellectual property**, then you'll need to sign an 132 | `individual CLA`_. 133 | - **If you work for a company that wants to allow you to contribute your 134 | work**, then you'll need to sign a `corporate CLA`_. 135 | 136 | You can sign these electronically (just scroll to the bottom). After that, 137 | we'll be able to accept your pull requests. 138 | 139 | .. _`individual CLA`: https://developers.google.com/open-source/cla/individual 140 | .. _`corporate CLA`: https://developers.google.com/open-source/cla/corporate 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, Google Inc. 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following disclaimer 10 | in the documentation and/or other materials provided with the 11 | distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst 2 | recursive-include google *.json 3 | recursive-include tests * 4 | global-exclude __pycache__ *.pyc 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | This project has been deprecated and will no longer be developed. It has 5 | been wholly replaced by `google-api-core`_. 6 | 7 | .. _`google-api-core`: https://pypi.org/project/google-api-core/ 8 | 9 | Google API Extensions for Python 10 | ================================ 11 | 12 | .. image:: https://img.shields.io/travis/googleapis/gax-python.svg 13 | :target: https://travis-ci.org/googleapis/gax-python 14 | 15 | .. image:: https://img.shields.io/pypi/dw/google-gax.svg 16 | :target: https://pypi.python.org/pypi/google-gax 17 | 18 | .. image:: https://readthedocs.org/projects/gax-python/badge/?version=latest 19 | :target: http://gax-python.readthedocs.org/ 20 | 21 | .. image:: https://img.shields.io/codecov/c/github/googleapis/gax-python.svg 22 | :target: https://codecov.io/github/googleapis/gax-python 23 | 24 | 25 | Google API Extensions for Python (gax-python) is a set of modules which aids the 26 | development of APIs for clients and servers based on `gRPC`_ and Google API 27 | conventions. 28 | 29 | Application code will rarely need to use most of the classes within this library 30 | directly, but code generated automatically from the API definition files in 31 | `Google APIs`_ can use services such as page streaming and request bundling to 32 | provide a more convenient and idiomatic API surface to callers. 33 | 34 | .. _`gRPC`: http://grpc.io 35 | .. _`Google APIs`: https://github.com/googleapis/googleapis/ 36 | 37 | 38 | Python Versions 39 | --------------- 40 | 41 | gax-python is currently tested with Python 2.7, 3.4, 3.5, and 3.6. 42 | 43 | 44 | Contributing 45 | ------------ 46 | 47 | Contributions to this library are always welcome and highly encouraged. 48 | 49 | See `CONTRIBUTING.rst`_ for more information on how to get started. 50 | 51 | .. _CONTRIBUTING.rst: https://github.com/googleapi/gax-python/blob/master/CONTRIBUTING.rst 52 | 53 | Versioning 54 | ---------- 55 | 56 | This library follows `Semantic Versioning`_ 57 | 58 | It is currently in major version zero (``0.y.z``), which means that anything 59 | may change at any time and the public API should not be considered 60 | stable. 61 | 62 | .. _`Semantic Versioning`: http://semver.org/ 63 | 64 | 65 | Details 66 | ------- 67 | 68 | For detailed documentation of the modules in gax-python, please watch `DOCUMENTATION`_. 69 | 70 | .. _`DOCUMENTATION`: https://gax-python.readthedocs.org/ 71 | 72 | 73 | License 74 | ------- 75 | 76 | BSD - See `the LICENSE`_ for more information. 77 | 78 | .. _`the LICENSE`: https://github.com/googleapis/gax-python/blob/master/LICENSE 79 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nurpc-hyper.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nurpc-hyper.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/nurpc-hyper" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/nurpc-hyper" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/gax-python/309aedfcfd48e4c8fa22dd60e9c84c3cc71bb20e/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono'); 2 | 3 | @media screen and (min-width: 1080px) { 4 | div.document { 5 | width: 1040px; 6 | } 7 | } 8 | 9 | code.descname { 10 | color: #4885ed; 11 | } 12 | 13 | th.field-name { 14 | min-width: 100px; 15 | color: #3cba54; 16 | } 17 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright 2015, Google Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are 8 | # met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following disclaimer 14 | # in the documentation and/or other materials provided with the 15 | # distribution. 16 | # * Neither the name of Google Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | # 33 | # google-gax documentation build configuration file, created by 34 | # sphinx-quickstart on Sun Nov 29 14:22:46 2015. 35 | # 36 | # This file is execfile()d with the current directory set to its 37 | # containing dir. 38 | # 39 | # Note that not all possible configuration values are present in this 40 | # autogenerated file. 41 | # 42 | # All configuration values have a default; values that are commented out 43 | # serve to show the default. 44 | 45 | import pkg_resources 46 | 47 | # If extensions (or modules to document with autodoc) are in another directory, 48 | # add these directories to sys.path here. If the directory is relative to the 49 | # documentation root, use os.path.abspath to make it absolute, like shown here. 50 | # sys.path.insert(0, os.path.abspath('..')) 51 | 52 | # -- General configuration ------------------------------------------------ 53 | 54 | # If your documentation needs a minimal Sphinx version, state it here. 55 | # 56 | # needs_sphinx = '1.0' 57 | 58 | # Add any Sphinx extension module names here, as strings. They can be 59 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 60 | # ones. 61 | extensions = [ 62 | 'sphinx.ext.autodoc', 63 | 'sphinx.ext.autosummary', 64 | 'sphinx.ext.intersphinx', 65 | 'sphinx.ext.coverage', 66 | 'sphinx.ext.napoleon', 67 | 'sphinx.ext.viewcode', 68 | 'sphinx_docstring_typing' 69 | ] 70 | 71 | # Add any paths that contain templates here, relative to this directory. 72 | templates_path = ['_templates'] 73 | 74 | # The suffix(es) of source filenames. 75 | # You can specify multiple suffix as a list of string: 76 | # source_suffix = ['.rst', '.md'] 77 | source_suffix = '.rst' 78 | 79 | # The encoding of source files. 80 | # 81 | # source_encoding = 'utf-8-sig' 82 | 83 | # The master toctree document. 84 | master_doc = 'index' 85 | 86 | # General information about the project. 87 | project = u'google-gax' 88 | copyright = u'2015, Google' 89 | author = u'Google Inc' 90 | 91 | # The version info for the project you're documenting, acts as replacement for 92 | # |version| and |release|, also used in various other places throughout the 93 | # built documents. 94 | # 95 | # The short X.Y version. 96 | version = pkg_resources.get_distribution('google-gax').version 97 | # The full version, including alpha/beta/rc tags. 98 | release = version 99 | 100 | # The language for content autogenerated by Sphinx. Refer to documentation 101 | # for a list of supported languages. 102 | # 103 | # This is also used if you do content translation via gettext catalogs. 104 | # Usually you set "language" from the command line for these cases. 105 | language = None 106 | 107 | # There are two options for replacing |today|: either, you set today to some 108 | # non-false value, then it is used: 109 | # 110 | # today = '' 111 | # 112 | # Else, today_fmt is used as the format for a strftime call. 113 | # 114 | # today_fmt = '%B %d, %Y' 115 | 116 | # List of patterns, relative to source directory, that match files and 117 | # directories to ignore when looking for source files. 118 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 119 | 120 | # The reST default role (used for this markup: `text`) to use for all 121 | # documents. 122 | # 123 | # default_role = None 124 | 125 | # If true, '()' will be appended to :func: etc. cross-reference text. 126 | # 127 | # add_function_parentheses = True 128 | 129 | # If true, the current module name will be prepended to all description 130 | # unit titles (such as .. function::). 131 | add_module_names = False 132 | 133 | # If true, sectionauthor and moduleauthor directives will be shown in the 134 | # output. They are ignored by default. 135 | # 136 | # show_authors = False 137 | 138 | # The name of the Pygments (syntax highlighting) style to use. 139 | pygments_style = 'sphinx' 140 | 141 | # A list of ignored prefixes for module index sorting. 142 | # 143 | # modindex_common_prefix = [] 144 | 145 | # If true, keep warnings as "system message" paragraphs in the built documents. 146 | # 147 | # keep_warnings = False 148 | 149 | # If true, `todo` and `todoList` produce output, else they produce nothing. 150 | todo_include_todos = True 151 | 152 | 153 | # -- Options for HTML output ---------------------------------------------- 154 | 155 | # The theme to use for HTML and HTML Help pages. See the documentation for 156 | # a list of builtin themes. 157 | html_theme = 'alabaster' 158 | 159 | # Theme options are theme-specific and customize the look and feel of a theme 160 | # further. For a list of options available for each theme, see the 161 | # documentation. 162 | html_theme_options = { 163 | 'description': 'Google API Extensions for Python', 164 | 'github_user': 'googleapis', 165 | 'github_repo': 'gax-python', 166 | 'github_banner': True, 167 | 'travis_button': True, 168 | 'font_family': "'Roboto', Georgia, sans", 169 | 'head_font_family': "'Roboto', Georgia, serif", 170 | 'code_font_family': "'Roboto Mono', 'Consolas', monospace", 171 | } 172 | 173 | # Add any paths that contain custom themes here, relative to this directory. 174 | # html_theme_path = [] 175 | 176 | # The name for this set of Sphinx documents. 177 | # " v documentation" by default. 178 | # 179 | # html_title = 'google-auth v0.0.1a' 180 | 181 | # A shorter title for the navigation bar. Default is the same as html_title. 182 | # 183 | # html_short_title = None 184 | 185 | # The name of an image file (relative to this directory) to place at the top 186 | # of the sidebar. 187 | # 188 | # html_logo = None 189 | 190 | # The name of an image file (relative to this directory) to use as a favicon of 191 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 192 | # pixels large. 193 | # 194 | # html_favicon = None 195 | 196 | # Add any paths that contain custom static files (such as style sheets) here, 197 | # relative to this directory. They are copied after the builtin static files, 198 | # so a file named "default.css" will overwrite the builtin "default.css". 199 | html_static_path = ['_static'] 200 | 201 | # Add any extra paths that contain custom files (such as robots.txt or 202 | # .htaccess) here, relative to this directory. These files are copied 203 | # directly to the root of the documentation. 204 | # 205 | # html_extra_path = [] 206 | 207 | # If not None, a 'Last updated on:' timestamp is inserted at every page 208 | # bottom, using the given strftime format. 209 | # The empty string is equivalent to '%b %d, %Y'. 210 | # 211 | # html_last_updated_fmt = None 212 | 213 | # If true, SmartyPants will be used to convert quotes and dashes to 214 | # typographically correct entities. 215 | # 216 | # html_use_smartypants = True 217 | 218 | # Custom sidebar templates, maps document names to template names. 219 | # 220 | 221 | html_sidebars = { 222 | '**': [ 223 | 'about.html', 224 | 'navigation.html', 225 | 'relations.html', 226 | 'searchbox.html', 227 | ] 228 | } 229 | 230 | # Additional templates that should be rendered to pages, maps page names to 231 | # template names. 232 | # 233 | # html_additional_pages = {} 234 | 235 | # If false, no module index is generated. 236 | # 237 | # html_domain_indices = True 238 | 239 | # If false, no index is generated. 240 | # 241 | # html_use_index = True 242 | 243 | # If true, the index is split into individual pages for each letter. 244 | # 245 | # html_split_index = False 246 | 247 | # If true, links to the reST sources are added to the pages. 248 | # 249 | # html_show_sourcelink = True 250 | 251 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 252 | # 253 | # html_show_sphinx = True 254 | 255 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 256 | # 257 | # html_show_copyright = True 258 | 259 | # If true, an OpenSearch description file will be output, and all pages will 260 | # contain a tag referring to it. The value of this option must be the 261 | # base URL from which the finished HTML is served. 262 | # 263 | # html_use_opensearch = '' 264 | 265 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 266 | # html_file_suffix = None 267 | 268 | # Language to be used for generating the HTML full-text search index. 269 | # Sphinx supports the following languages: 270 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 271 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 272 | # 273 | # html_search_language = 'en' 274 | 275 | # A dictionary with options for the search language support, empty by default. 276 | # 'ja' uses this config value. 277 | # 'zh' user can custom change `jieba` dictionary path. 278 | # 279 | # html_search_options = {'type': 'default'} 280 | 281 | # The name of a javascript file (relative to the configuration directory) that 282 | # implements a search results scorer. If empty, the default will be used. 283 | # 284 | # html_search_scorer = 'scorer.js' 285 | 286 | # Output file base name for HTML help builder. 287 | htmlhelp_basename = 'google-gaxdoc' 288 | 289 | # -- Options for LaTeX output --------------------------------------------- 290 | 291 | latex_elements = { 292 | # The paper size ('letterpaper' or 'a4paper'). 293 | # 294 | # 'papersize': 'letterpaper', 295 | 296 | # The font size ('10pt', '11pt' or '12pt'). 297 | # 298 | # 'pointsize': '10pt', 299 | 300 | # Additional stuff for the LaTeX preamble. 301 | # 302 | # 'preamble': '', 303 | 304 | # Latex figure (float) alignment 305 | # 306 | # 'figure_align': 'htbp', 307 | } 308 | 309 | # Grouping the document tree into LaTeX files. List of tuples 310 | # (source start file, target name, title, 311 | # author, documentclass [howto, manual, or own class]). 312 | latex_documents = [ 313 | (master_doc, 'google-gax.tex', u'Google API eXtension Documentation', 314 | u'Tim Emiola', 'manual'), 315 | ] 316 | 317 | # The name of an image file (relative to this directory) to place at the top of 318 | # the title page. 319 | # 320 | # latex_logo = None 321 | 322 | # For "manual" documents, if this is true, then toplevel headings are parts, 323 | # not chapters. 324 | # 325 | # latex_use_parts = False 326 | 327 | # If true, show page references after internal links. 328 | # 329 | # latex_show_pagerefs = False 330 | 331 | # If true, show URL addresses after external links. 332 | # 333 | # latex_show_urls = False 334 | 335 | # Documents to append as an appendix to all manuals. 336 | # 337 | # latex_appendices = [] 338 | 339 | # It false, will not define \strong, \code, itleref, \crossref ... but only 340 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 341 | # packages. 342 | # 343 | # latex_keep_old_macro_names = True 344 | 345 | # If false, no module index is generated. 346 | # 347 | # latex_domain_indices = True 348 | 349 | 350 | # -- Options for manual page output --------------------------------------- 351 | 352 | # One entry per manual page. List of tuples 353 | # (source start file, name, description, authors, manual section). 354 | man_pages = [ 355 | (master_doc, 'google-gax', u'Google API eXtension Documentation', 356 | [author], 1) 357 | ] 358 | 359 | # If true, show URL addresses after external links. 360 | # 361 | # man_show_urls = False 362 | 363 | 364 | # -- Options for Texinfo output ------------------------------------------- 365 | 366 | # Grouping the document tree into Texinfo files. List of tuples 367 | # (source start file, target name, title, author, 368 | # dir menu entry, description, category) 369 | texinfo_documents = [ 370 | (master_doc, 'google-gax', u'Google API eXtension Documentation', 371 | author, 'google-gax', 'Extends Google APIs', 372 | 'Miscellaneous'), 373 | ] 374 | 375 | # Documents to append as an appendix to all manuals. 376 | # 377 | # texinfo_appendices = [] 378 | 379 | # If false, no module index is generated. 380 | # 381 | # texinfo_domain_indices = True 382 | 383 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 384 | # 385 | # texinfo_show_urls = 'footnote' 386 | 387 | # If true, do not generate a @detailmenu in the "Top" node's menu. 388 | # 389 | # texinfo_no_detailmenu = False 390 | 391 | 392 | # Example configuration for intersphinx: refer to the Python standard library. 393 | intersphinx_mapping = { 394 | 'python': ('http://python.readthedocs.org/en/latest/', None), 395 | } 396 | 397 | # autodoc/autosummary flags 398 | autoclass_content = 'both' 399 | autodoc_default_flags = ['members'] 400 | autosummary_generate = True 401 | 402 | # Napoleon settings 403 | napoleon_google_docstring = True 404 | napoleon_numpy_docstring = True 405 | napoleon_include_private_with_doc = False 406 | napoleon_include_special_with_doc = True 407 | napoleon_use_admonition_for_examples = False 408 | napoleon_use_admonition_for_notes = False 409 | napoleon_use_admonition_for_references = False 410 | napoleon_use_ivar = False 411 | napoleon_use_param = True 412 | napoleon_use_rtype = True 413 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. google-gax documentation master file, created by 2 | sphinx-quickstart on Sun Feb 04 16:22:46 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | :caption: Google APIs Extensions 10 | 11 | DEPRECATED 12 | ========== 13 | 14 | This project has been deprecated and will no longer be developed. It has 15 | been wholly replaced by `google-api-core`_. 16 | 17 | .. _`google-api-core`: https://pypi.org/project/google-api-core/ 18 | 19 | 20 | Google API Extensions for Python 21 | ================================ 22 | 23 | This is the API documentation for Google API Extensions for Python (gax-python), 24 | a set of libraries which aids the development of APIs for clients and servers 25 | based on `GRPC`_ and `Google APIs`_ conventions. 26 | 27 | 28 | .. autosummary:: 29 | :toctree: generated 30 | 31 | google.gax 32 | google.gax.api_callable 33 | google.gax.bundling 34 | google.gax.config 35 | google.gax.errors 36 | google.gax.grpc 37 | google.gax.path_template 38 | 39 | 40 | Indices and tables 41 | ================== 42 | 43 | * :ref:`genindex` 44 | * :ref:`modindex` 45 | * :ref:`search` 46 | 47 | 48 | .. _`GRPC`: http://grpc.io 49 | .. _`Google APIs`: https://github.com/googleapis/googleapis/ 50 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\nurpc-hyper.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\nurpc-hyper.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-docstring-typing 3 | -------------------------------------------------------------------------------- /google/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring 31 | from __future__ import absolute_import 32 | try: 33 | import pkg_resources 34 | pkg_resources.declare_namespace(__name__) 35 | except ImportError: 36 | import pkgutil 37 | __path__ = pkgutil.extend_path(__path__, __name__) 38 | -------------------------------------------------------------------------------- /google/gapic/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /google/gapic/longrunning/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) 2 | -------------------------------------------------------------------------------- /google/gapic/longrunning/operations_client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. All rights reserved. 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are 5 | # met: 6 | # 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above 10 | # copyright notice, this list of conditions and the following disclaimer 11 | # in the documentation and/or other materials provided with the 12 | # distribution. 13 | # * Neither the name of Google Inc. nor the names of its 14 | # contributors may be used to endorse or promote products derived from 15 | # this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | # 29 | # EDITING INSTRUCTIONS 30 | # This file was generated from the file 31 | # https://github.com/google/googleapis/blob/master/google/longrunning/operations.proto, 32 | # and updates to that file get reflected here through a refresh process. 33 | # For the short term, the refresh process will only be runnable by Google engineers. 34 | # 35 | # The only allowed edits are to method and file documentation. A 3-way 36 | # merge preserves those additions if the generated source changes. 37 | """Accesses the google.longrunning Operations API.""" 38 | 39 | import collections 40 | import json 41 | import os 42 | import pkg_resources 43 | import platform 44 | 45 | from google.gax import api_callable 46 | from google.gax import config 47 | from google.gax import path_template 48 | import google.gax 49 | 50 | from google.longrunning import operations_pb2 51 | 52 | _PageDesc = google.gax.PageDescriptor 53 | 54 | 55 | class OperationsClient(object): 56 | """ 57 | Manages long-running operations with an API service. 58 | 59 | When an API method normally takes long time to complete, it can be designed 60 | to return ``Operation`` to the client, and the client can use this 61 | interface to receive the real response asynchronously by polling the 62 | operation resource, or pass the operation resource to another API (such as 63 | Google Cloud Pub/Sub API) to receive the response. Any API service that 64 | returns long-running operations should implement the ``Operations`` interface 65 | so developers can have a consistent client experience. 66 | """ 67 | 68 | SERVICE_ADDRESS = 'longrunning.googleapis.com' 69 | """The default address of the service.""" 70 | 71 | DEFAULT_SERVICE_PORT = 443 72 | """The default port of the service.""" 73 | 74 | _PAGE_DESCRIPTORS = { 75 | 'list_operations': _PageDesc('page_token', 'next_page_token', 76 | 'operations') 77 | } 78 | 79 | # The scopes needed to make gRPC calls to all of the methods defined in 80 | # this service 81 | _ALL_SCOPES = () 82 | 83 | def __init__(self, 84 | service_path=SERVICE_ADDRESS, 85 | port=DEFAULT_SERVICE_PORT, 86 | channel=None, 87 | credentials=None, 88 | ssl_credentials=None, 89 | scopes=None, 90 | client_config=None, 91 | app_name=None, 92 | app_version='', 93 | lib_name=None, 94 | lib_version='', 95 | metrics_headers=()): 96 | """Constructor. 97 | 98 | Args: 99 | service_path (string): The domain name of the API remote host. 100 | port (int): The port on which to connect to the remote host. 101 | channel (:class:`grpc.Channel`): A ``Channel`` instance through 102 | which to make calls. 103 | credentials (object): The authorization credentials to attach to 104 | requests. These credentials identify this application to the 105 | service. 106 | ssl_credentials (:class:`grpc.ChannelCredentials`): A 107 | ``ChannelCredentials`` instance for use with an SSL-enabled 108 | channel. 109 | scopes (list[string]): A list of OAuth2 scopes to attach to requests. 110 | client_config (dict): 111 | A dictionary for call options for each method. See 112 | :func:`google.gax.construct_settings` for the structure of 113 | this data. Falls back to the default config if not specified 114 | or the specified config is missing data points. 115 | app_name (string): The name of the application calling 116 | the service. Recommended for analytics purposes. 117 | app_version (string): The version of the application calling 118 | the service. Recommended for analytics purposes. 119 | lib_name (string): The API library software used for calling 120 | the service. (Unless you are writing an API client itself, 121 | leave this as default.) 122 | lib_version (string): The API library software version used 123 | for calling the service. (Unless you are writing an API client 124 | itself, leave this as default.) 125 | metrics_headers (dict): A dictionary of values for tracking 126 | client library metrics. Ultimately serializes to a string 127 | (e.g. 'foo/1.2.3 bar/3.14.1'). This argument should be 128 | considered private. 129 | 130 | Returns: 131 | A OperationsClient object. 132 | """ 133 | # Unless the calling application specifically requested 134 | # OAuth scopes, request everything. 135 | if scopes is None: 136 | scopes = self._ALL_SCOPES 137 | 138 | # Initialize an empty client config, if none is set. 139 | if client_config is None: 140 | client_config = {} 141 | 142 | # Initialize metrics_headers as an ordered dictionary 143 | # (cuts down on cardinality of the resulting string slightly). 144 | metrics_headers = collections.OrderedDict(metrics_headers) 145 | metrics_headers['gl-python'] = platform.python_version() 146 | 147 | # The library may or may not be set, depending on what is 148 | # calling this client. Newer client libraries set the library name 149 | # and version. 150 | if lib_name: 151 | metrics_headers[lib_name] = lib_version 152 | 153 | # Load the configuration defaults. 154 | default_client_config = json.loads( 155 | pkg_resources.resource_string( 156 | __name__, 'operations_client_config.json').decode()) 157 | defaults = api_callable.construct_settings( 158 | 'google.longrunning.Operations', 159 | default_client_config, 160 | client_config, 161 | config.STATUS_CODE_NAMES, 162 | metrics_headers=metrics_headers, 163 | page_descriptors=self._PAGE_DESCRIPTORS, ) 164 | self.operations_stub = config.create_stub( 165 | operations_pb2.OperationsStub, 166 | channel=channel, 167 | service_path=service_path, 168 | service_port=port, 169 | credentials=credentials, 170 | scopes=scopes, 171 | ssl_credentials=ssl_credentials) 172 | 173 | self._get_operation = api_callable.create_api_call( 174 | self.operations_stub.GetOperation, 175 | settings=defaults['get_operation']) 176 | self._list_operations = api_callable.create_api_call( 177 | self.operations_stub.ListOperations, 178 | settings=defaults['list_operations']) 179 | self._cancel_operation = api_callable.create_api_call( 180 | self.operations_stub.CancelOperation, 181 | settings=defaults['cancel_operation']) 182 | self._delete_operation = api_callable.create_api_call( 183 | self.operations_stub.DeleteOperation, 184 | settings=defaults['delete_operation']) 185 | 186 | # Service calls 187 | def get_operation(self, name, options=None): 188 | """ 189 | Gets the latest state of a long-running operation. Clients can use this 190 | method to poll the operation result at intervals as recommended by the API 191 | service. 192 | 193 | Example: 194 | >>> from google.gapic.longrunning import operations_client 195 | >>> api = operations_client.OperationsClient() 196 | >>> name = '' 197 | >>> response = api.get_operation(name) 198 | 199 | Args: 200 | name (string): The name of the operation resource. 201 | options (:class:`google.gax.CallOptions`): Overrides the default 202 | settings for this call, e.g, timeout, retries etc. 203 | 204 | Returns: 205 | A :class:`google.longrunning.operations_pb2.Operation` instance. 206 | 207 | Raises: 208 | :exc:`google.gax.errors.GaxError` if the RPC is aborted. 209 | :exc:`ValueError` if the parameters are invalid. 210 | """ 211 | # Create the request object. 212 | request = operations_pb2.GetOperationRequest(name=name) 213 | return self._get_operation(request, options) 214 | 215 | def list_operations(self, name, filter_, page_size=0, options=None): 216 | """ 217 | Lists operations that match the specified filter in the request. If the 218 | server doesn't support this method, it returns ``UNIMPLEMENTED``. 219 | NOTE: the ``name`` binding below allows API services to override the binding 220 | to use different resource name schemes, such as ``users/*/operations``. 221 | 222 | Example: 223 | >>> from google.gapic.longrunning import operations_client 224 | >>> from google.gax import CallOptions, INITIAL_PAGE 225 | >>> api = operations_client.OperationsClient() 226 | >>> name = '' 227 | >>> filter_ = '' 228 | >>> 229 | >>> # Iterate over all results 230 | >>> for element in api.list_operations(name, filter_): 231 | >>> # process element 232 | >>> pass 233 | >>> 234 | >>> # Or iterate over results one page at a time 235 | >>> for page in api.list_operations(name, filter_, options=CallOptions(page_token=INITIAL_PAGE)): 236 | >>> for element in page: 237 | >>> # process element 238 | >>> pass 239 | 240 | Args: 241 | name (string): The name of the operation collection. 242 | filter_ (string): The standard list filter. 243 | page_size (int): The maximum number of resources contained in the 244 | underlying API response. If page streaming is performed per- 245 | resource, this parameter does not affect the return value. If page 246 | streaming is performed per-page, this determines the maximum number 247 | of resources in a page. 248 | options (:class:`google.gax.CallOptions`): Overrides the default 249 | settings for this call, e.g, timeout, retries etc. 250 | 251 | Returns: 252 | A :class:`google.gax.PageIterator` instance. By default, this 253 | is an iterable of :class:`google.longrunning.operations_pb2.Operation` instances. 254 | This object can also be configured to iterate over the pages 255 | of the response through the `CallOptions` parameter. 256 | 257 | Raises: 258 | :exc:`google.gax.errors.GaxError` if the RPC is aborted. 259 | :exc:`ValueError` if the parameters are invalid. 260 | """ 261 | # Create the request object. 262 | request = operations_pb2.ListOperationsRequest( 263 | name=name, filter=filter_, page_size=page_size) 264 | return self._list_operations(request, options) 265 | 266 | def cancel_operation(self, name, options=None): 267 | """ 268 | Starts asynchronous cancellation on a long-running operation. The server 269 | makes a best effort to cancel the operation, but success is not 270 | guaranteed. If the server doesn't support this method, it returns 271 | ``google.rpc.Code.UNIMPLEMENTED``. Clients can use 272 | ``Operations.GetOperation`` or 273 | other methods to check whether the cancellation succeeded or whether the 274 | operation completed despite cancellation. On successful cancellation, 275 | the operation is not deleted; instead, it becomes an operation with 276 | an ``Operation.error`` value with a ``google.rpc.Status.code`` of 1, 277 | corresponding to ``Code.CANCELLED``. 278 | 279 | Example: 280 | >>> from google.gapic.longrunning import operations_client 281 | >>> api = operations_client.OperationsClient() 282 | >>> name = '' 283 | >>> api.cancel_operation(name) 284 | 285 | Args: 286 | name (string): The name of the operation resource to be cancelled. 287 | options (:class:`google.gax.CallOptions`): Overrides the default 288 | settings for this call, e.g, timeout, retries etc. 289 | 290 | Raises: 291 | :exc:`google.gax.errors.GaxError` if the RPC is aborted. 292 | :exc:`ValueError` if the parameters are invalid. 293 | """ 294 | # Create the request object. 295 | request = operations_pb2.CancelOperationRequest(name=name) 296 | self._cancel_operation(request, options) 297 | 298 | def delete_operation(self, name, options=None): 299 | """ 300 | Deletes a long-running operation. This method indicates that the client is 301 | no longer interested in the operation result. It does not cancel the 302 | operation. If the server doesn't support this method, it returns 303 | ``google.rpc.Code.UNIMPLEMENTED``. 304 | 305 | Example: 306 | >>> from google.gapic.longrunning import operations_client 307 | >>> api = operations_client.OperationsClient() 308 | >>> name = '' 309 | >>> api.delete_operation(name) 310 | 311 | Args: 312 | name (string): The name of the operation resource to be deleted. 313 | options (:class:`google.gax.CallOptions`): Overrides the default 314 | settings for this call, e.g, timeout, retries etc. 315 | 316 | Raises: 317 | :exc:`google.gax.errors.GaxError` if the RPC is aborted. 318 | :exc:`ValueError` if the parameters are invalid. 319 | """ 320 | # Create the request object. 321 | request = operations_pb2.DeleteOperationRequest(name=name) 322 | self._delete_operation(request, options) 323 | -------------------------------------------------------------------------------- /google/gapic/longrunning/operations_client_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "interfaces": { 3 | "google.longrunning.Operations": { 4 | "retry_codes": { 5 | "idempotent": [ 6 | "DEADLINE_EXCEEDED", 7 | "UNAVAILABLE" 8 | ], 9 | "non_idempotent": [] 10 | }, 11 | "retry_params": { 12 | "default": { 13 | "initial_retry_delay_millis": 100, 14 | "retry_delay_multiplier": 1.3, 15 | "max_retry_delay_millis": 60000, 16 | "initial_rpc_timeout_millis": 20000, 17 | "rpc_timeout_multiplier": 1.0, 18 | "max_rpc_timeout_millis": null, 19 | "total_timeout_millis": 600000 20 | } 21 | }, 22 | "methods": { 23 | "GetOperation": { 24 | "timeout_millis": 60000, 25 | "retry_codes_name": "idempotent", 26 | "retry_params_name": "default" 27 | }, 28 | "ListOperations": { 29 | "timeout_millis": 60000, 30 | "retry_codes_name": "idempotent", 31 | "retry_params_name": "default" 32 | }, 33 | "CancelOperation": { 34 | "timeout_millis": 60000, 35 | "retry_codes_name": "idempotent", 36 | "retry_params_name": "default" 37 | }, 38 | "DeleteOperation": { 39 | "timeout_millis": 60000, 40 | "retry_codes_name": "idempotent", 41 | "retry_params_name": "default" 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /google/gax/_grpc_google_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Provides gRPC authentication support using google.auth.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | import google.auth 35 | import google.auth.credentials 36 | import google.auth.transport.grpc 37 | 38 | try: 39 | import google.auth.transport.requests 40 | # pylint: disable=invalid-name 41 | # Pylint recognizes this as a class, but we're aliasing it as a factory 42 | # function. 43 | _request_factory = google.auth.transport.requests.Request 44 | # pylint: enable=invalid-name 45 | except ImportError: 46 | try: 47 | import httplib2 48 | import google_auth_httplib2 49 | 50 | def _request_factory(): 51 | http = httplib2.Http() 52 | return google_auth_httplib2.Request(http) 53 | 54 | except ImportError: 55 | raise ImportError( 56 | 'No HTTP transport is available. Please install requests or ' 57 | 'httplib2 and google_auth_httplib2.') 58 | 59 | 60 | def get_default_credentials(scopes): 61 | """Gets the Application Default Credentials.""" 62 | credentials, _ = google.auth.default(scopes=scopes) 63 | return credentials 64 | 65 | 66 | def secure_authorized_channel( 67 | credentials, target, ssl_credentials=None): 68 | """Creates a secure authorized gRPC channel.""" 69 | http_request = _request_factory() 70 | return google.auth.transport.grpc.secure_authorized_channel( 71 | credentials, http_request, target, 72 | ssl_credentials=ssl_credentials) 73 | -------------------------------------------------------------------------------- /google/gax/api_callable.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Provides function wrappers that implement page streaming and retrying.""" 31 | 32 | from __future__ import absolute_import, division, unicode_literals 33 | 34 | from future import utils 35 | 36 | from google import gax 37 | from google.gax import bundling 38 | from google.gax.utils import metrics 39 | 40 | _MILLIS_PER_SECOND = 1000 41 | 42 | 43 | def _bundleable(desc): 44 | """Creates a function that transforms an API call into a bundling call. 45 | 46 | It transform a_func from an API call that receives the requests and returns 47 | the response into a callable that receives the same request, and 48 | returns a :class:`bundling.Event`. 49 | 50 | The returned Event object can be used to obtain the eventual result of the 51 | bundled call. 52 | 53 | Args: 54 | desc (gax.BundleDescriptor): describes the bundling that a_func 55 | supports. 56 | 57 | Returns: 58 | Callable: takes the API call's request and keyword args and returns a 59 | bundling.Event object. 60 | 61 | """ 62 | def inner(a_func, settings, request, **kwargs): 63 | """Schedules execution of a bundling task.""" 64 | if not settings.bundler: 65 | return a_func(request, **kwargs) 66 | 67 | the_id = bundling.compute_bundle_id( 68 | request, desc.request_discriminator_fields) 69 | return settings.bundler.schedule(a_func, the_id, desc, request, kwargs) 70 | 71 | return inner 72 | 73 | 74 | def _page_streamable(page_descriptor): 75 | """Creates a function that yields an iterable to performs page-streaming. 76 | 77 | Args: 78 | page_descriptor (:class:`PageDescriptor`): indicates the structure 79 | of page streaming to be performed. 80 | 81 | Returns: 82 | Callable: A function that returns an iterator. 83 | """ 84 | 85 | def inner(a_func, settings, request, **kwargs): 86 | """Actual page-streaming based on the settings.""" 87 | page_iterator = gax.PageIterator( 88 | a_func, page_descriptor, settings.page_token, request, **kwargs) 89 | if settings.flatten_pages: 90 | return gax.ResourceIterator(page_iterator) 91 | else: 92 | return page_iterator 93 | 94 | return inner 95 | 96 | 97 | def _construct_bundling(bundle_config, bundle_descriptor): 98 | """Helper for ``construct_settings()``. 99 | 100 | Args: 101 | bundle_config (dict): A dictionary specifying a bundle parameters, the 102 | value for 'bundling' field in a method config (See 103 | ``construct_settings()`` for information on this config.) 104 | bundle_descriptor (BundleDescriptor): A BundleDescriptor object 105 | describing the structure of bundling for this method. If not set, 106 | this method will not bundle. 107 | 108 | Returns: 109 | Tuple[bundling.Executor, BundleDescriptor]: A tuple that configures 110 | bundling. The bundling.Executor may be None if this method should not 111 | bundle. 112 | """ 113 | if bundle_config and bundle_descriptor: 114 | bundler = bundling.Executor(gax.BundleOptions( 115 | element_count_threshold=bundle_config.get( 116 | 'element_count_threshold', 0), 117 | element_count_limit=bundle_config.get('element_count_limit', 0), 118 | request_byte_threshold=bundle_config.get( 119 | 'request_byte_threshold', 0), 120 | request_byte_limit=bundle_config.get('request_byte_limit', 0), 121 | delay_threshold=bundle_config.get('delay_threshold_millis', 0))) 122 | else: 123 | bundler = None 124 | 125 | return bundler 126 | 127 | 128 | def _construct_retry(method_config, retry_codes, retry_params, retry_names): 129 | """Helper for ``construct_settings()``. 130 | 131 | Args: 132 | method_config (dict): A dictionary representing a single ``methods`` 133 | entry of the standard API client config file. (See 134 | ``construct_settings()`` for information on this yaml.) 135 | retry_codes (dict): A dictionary parsed from the ``retry_codes`` entry 136 | of the standard API client config file. (See ``construct_settings()`` 137 | for information on this yaml.) 138 | retry_params (dict): A dictionary parsed from the ``retry_params`` entry 139 | of the standard API client config file. (See ``construct_settings()`` 140 | for information on this yaml.) 141 | retry_names (dict): A dictionary mapping the string names used in the 142 | standard API client config file to API response status codes. 143 | 144 | Returns: 145 | Optional[RetryOptions]: The retry options, if applicable. 146 | """ 147 | if method_config is None: 148 | return None 149 | 150 | codes = None 151 | if retry_codes and 'retry_codes_name' in method_config: 152 | codes_name = method_config['retry_codes_name'] 153 | if codes_name in retry_codes and retry_codes[codes_name]: 154 | codes = [retry_names[name] for name in retry_codes[codes_name]] 155 | else: 156 | codes = [] 157 | 158 | backoff_settings = None 159 | if retry_params and 'retry_params_name' in method_config: 160 | params_name = method_config['retry_params_name'] 161 | if params_name and params_name in retry_params: 162 | backoff_settings = gax.BackoffSettings(**retry_params[params_name]) 163 | 164 | return gax.RetryOptions( 165 | backoff_settings=backoff_settings, 166 | retry_codes=codes, 167 | ) 168 | 169 | 170 | def _merge_retry_options(retry_options, overrides): 171 | """Helper for ``construct_settings()``. 172 | 173 | Takes two retry options, and merges them into a single RetryOption instance. 174 | 175 | Args: 176 | retry_options (RetryOptions): The base RetryOptions. 177 | overrides (RetryOptions): The RetryOptions used for overriding ``retry``. 178 | Use the values if it is not None. If entire ``overrides`` is None, 179 | ignore the base retry and return None. 180 | 181 | Returns: 182 | RetryOptions: The merged options, or None if it will be canceled. 183 | """ 184 | if overrides is None: 185 | return None 186 | 187 | if overrides.retry_codes is None and overrides.backoff_settings is None: 188 | return retry_options 189 | 190 | codes = retry_options.retry_codes 191 | if overrides.retry_codes is not None: 192 | codes = overrides.retry_codes 193 | backoff_settings = retry_options.backoff_settings 194 | if overrides.backoff_settings is not None: 195 | backoff_settings = overrides.backoff_settings 196 | 197 | return gax.RetryOptions( 198 | backoff_settings=backoff_settings, 199 | retry_codes=codes, 200 | ) 201 | 202 | 203 | def _upper_camel_to_lower_under(string): 204 | if not string: 205 | return '' 206 | out = '' 207 | out += string[0].lower() 208 | for char in string[1:]: 209 | if char.isupper(): 210 | out += '_' + char.lower() 211 | else: 212 | out += char 213 | return out 214 | 215 | 216 | def construct_settings( 217 | service_name, client_config, config_override, 218 | retry_names, bundle_descriptors=None, page_descriptors=None, 219 | metrics_headers=(), kwargs=None): 220 | """Constructs a dictionary mapping method names to _CallSettings. 221 | 222 | The ``client_config`` parameter is parsed from a client configuration JSON 223 | file of the form: 224 | 225 | .. code-block:: json 226 | 227 | { 228 | "interfaces": { 229 | "google.fake.v1.ServiceName": { 230 | "retry_codes": { 231 | "idempotent": ["UNAVAILABLE", "DEADLINE_EXCEEDED"], 232 | "non_idempotent": [] 233 | }, 234 | "retry_params": { 235 | "default": { 236 | "initial_retry_delay_millis": 100, 237 | "retry_delay_multiplier": 1.2, 238 | "max_retry_delay_millis": 1000, 239 | "initial_rpc_timeout_millis": 2000, 240 | "rpc_timeout_multiplier": 1.5, 241 | "max_rpc_timeout_millis": 30000, 242 | "total_timeout_millis": 45000 243 | } 244 | }, 245 | "methods": { 246 | "CreateFoo": { 247 | "retry_codes_name": "idempotent", 248 | "retry_params_name": "default", 249 | "timeout_millis": 30000 250 | }, 251 | "Publish": { 252 | "retry_codes_name": "non_idempotent", 253 | "retry_params_name": "default", 254 | "bundling": { 255 | "element_count_threshold": 40, 256 | "element_count_limit": 200, 257 | "request_byte_threshold": 90000, 258 | "request_byte_limit": 100000, 259 | "delay_threshold_millis": 100 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | Args: 268 | service_name (str): The fully-qualified name of this service, used as a 269 | key into the client config file (in the example above, this value 270 | would be ``google.fake.v1.ServiceName``). 271 | client_config (dict): A dictionary parsed from the standard API client 272 | config file. 273 | bundle_descriptors (Mapping[str, BundleDescriptor]): A dictionary of 274 | method names to BundleDescriptor objects for methods that are 275 | bundling-enabled. 276 | page_descriptors (Mapping[str, PageDescriptor]): A dictionary of method 277 | names to PageDescriptor objects for methods that are page 278 | streaming-enabled. 279 | config_override (str): A dictionary in the same structure of 280 | client_config to override the settings. Usually client_config is 281 | supplied from the default config and config_override will be 282 | specified by users. 283 | retry_names (Mapping[str, object]): A dictionary mapping the strings 284 | referring to response status codes to the Python objects representing 285 | those codes. 286 | metrics_headers (Mapping[str, str]): Dictionary of headers to be passed 287 | for analytics. Sent as a dictionary; eventually becomes a 288 | space-separated string (e.g. 'foo/1.0.0 bar/3.14.1'). 289 | kwargs (dict): The keyword arguments to be passed to the API calls. 290 | 291 | Returns: 292 | dict: A dictionary mapping method names to _CallSettings. 293 | 294 | Raises: 295 | KeyError: If the configuration for the service in question cannot be 296 | located in the provided ``client_config``. 297 | """ 298 | # pylint: disable=too-many-locals 299 | # pylint: disable=protected-access 300 | defaults = {} 301 | bundle_descriptors = bundle_descriptors or {} 302 | page_descriptors = page_descriptors or {} 303 | kwargs = kwargs or {} 304 | 305 | # Sanity check: It is possible that we got this far but some headers 306 | # were specified with an older library, which sends them as... 307 | # kwargs={'metadata': [('x-goog-api-client', 'foo/1.0 bar/3.0')]} 308 | # 309 | # Note: This is the final format we will send down to GRPC shortly. 310 | # 311 | # Remove any x-goog-api-client header that may have been present 312 | # in the metadata list. 313 | if 'metadata' in kwargs: 314 | kwargs['metadata'] = [value for value in kwargs['metadata'] 315 | if value[0].lower() != 'x-goog-api-client'] 316 | 317 | # Fill out the metrics headers with GAX and GRPC info, and convert 318 | # to a string in the format that the GRPC layer expects. 319 | kwargs.setdefault('metadata', []) 320 | kwargs['metadata'].append( 321 | ('x-goog-api-client', metrics.stringify(metrics.fill(metrics_headers))) 322 | ) 323 | 324 | try: 325 | service_config = client_config['interfaces'][service_name] 326 | except KeyError: 327 | raise KeyError('Client configuration not found for service: {}' 328 | .format(service_name)) 329 | 330 | overrides = config_override.get('interfaces', {}).get(service_name, {}) 331 | 332 | for method in service_config.get('methods'): 333 | method_config = service_config['methods'][method] 334 | overriding_method = overrides.get('methods', {}).get(method, {}) 335 | snake_name = _upper_camel_to_lower_under(method) 336 | 337 | if overriding_method and overriding_method.get('timeout_millis'): 338 | timeout = overriding_method['timeout_millis'] 339 | else: 340 | timeout = method_config['timeout_millis'] 341 | timeout /= _MILLIS_PER_SECOND 342 | 343 | bundle_descriptor = bundle_descriptors.get(snake_name) 344 | bundling_config = method_config.get('bundling', None) 345 | if overriding_method and 'bundling' in overriding_method: 346 | bundling_config = overriding_method['bundling'] 347 | bundler = _construct_bundling(bundling_config, bundle_descriptor) 348 | 349 | retry_options = _merge_retry_options( 350 | _construct_retry(method_config, service_config['retry_codes'], 351 | service_config['retry_params'], retry_names), 352 | _construct_retry(overriding_method, overrides.get('retry_codes'), 353 | overrides.get('retry_params'), retry_names)) 354 | 355 | defaults[snake_name] = gax._CallSettings( 356 | timeout=timeout, retry=retry_options, 357 | page_descriptor=page_descriptors.get(snake_name), 358 | bundler=bundler, bundle_descriptor=bundle_descriptor, 359 | kwargs=kwargs) 360 | return defaults 361 | 362 | 363 | def _catch_errors(a_func, to_catch): 364 | """Updates a_func to wrap exceptions with GaxError 365 | 366 | Args: 367 | a_func (callable): A callable. 368 | to_catch (list[Exception]): Configures the exceptions to wrap. 369 | 370 | Returns: 371 | Callable: A function that will wrap certain exceptions with GaxError 372 | """ 373 | def inner(*args, **kwargs): 374 | """Wraps specified exceptions""" 375 | try: 376 | return a_func(*args, **kwargs) 377 | # pylint: disable=catching-non-exception 378 | except tuple(to_catch) as exception: 379 | utils.raise_with_traceback( 380 | gax.errors.create_error('RPC failed', cause=exception)) 381 | 382 | return inner 383 | 384 | 385 | def _merge_options_metadata(options, settings): 386 | """Merge metadata list (add all missing tuples)""" 387 | if not options: 388 | return options 389 | kwargs = options.kwargs 390 | if kwargs == gax.OPTION_INHERIT or 'metadata' not in kwargs: 391 | return options 392 | 393 | kwarg_meta_dict = {} 394 | merged_kwargs = options.kwargs.copy() 395 | for kwarg_meta in merged_kwargs['metadata']: 396 | kwarg_meta_dict[kwarg_meta[0].lower()] = kwarg_meta 397 | for kwarg_meta in settings.kwargs['metadata']: 398 | if kwarg_meta[0].lower() not in kwarg_meta_dict: 399 | merged_kwargs['metadata'].append(kwarg_meta) 400 | return gax.CallOptions( 401 | timeout=options.timeout, retry=options.retry, 402 | page_token=options.page_token, 403 | is_bundling=options.is_bundling, 404 | **merged_kwargs) 405 | 406 | 407 | def create_api_call(func, settings): 408 | """Converts an rpc call into an API call governed by the settings. 409 | 410 | In typical usage, ``func`` will be a callable used to make an rpc request. 411 | This will mostly likely be a bound method from a request stub used to make 412 | an rpc call. 413 | 414 | The result is created by applying a series of function decorators defined 415 | in this module to ``func``. ``settings`` is used to determine which 416 | function decorators to apply. 417 | 418 | The result is another callable which for most values of ``settings`` has 419 | has the same signature as the original. Only when ``settings`` configures 420 | bundling does the signature change. 421 | 422 | Args: 423 | func (Callable[Sequence[object], object]): is used to make a bare rpc 424 | call. 425 | settings (_CallSettings): provides the settings for this call 426 | 427 | Returns: 428 | Callable[Sequence[object], object]: a bound method on a request stub used 429 | to make an rpc call 430 | 431 | Raises: 432 | ValueError: if ``settings`` has incompatible values, e.g, if bundling 433 | and page_streaming are both configured 434 | 435 | """ 436 | def base_caller(api_call, _, *args): 437 | """Simply call api_call and ignore settings.""" 438 | return api_call(*args) 439 | 440 | def inner(request, options=None): 441 | """Invoke with the actual settings.""" 442 | this_options = _merge_options_metadata(options, settings) 443 | this_settings = settings.merge(this_options) 444 | 445 | if this_settings.retry and this_settings.retry.retry_codes: 446 | api_call = gax.retry.retryable( 447 | func, this_settings.retry, **this_settings.kwargs) 448 | else: 449 | api_call = gax.retry.add_timeout_arg( 450 | func, this_settings.timeout, **this_settings.kwargs) 451 | api_call = _catch_errors(api_call, gax.config.API_ERRORS) 452 | return api_caller(api_call, this_settings, request) 453 | 454 | if settings.page_descriptor: 455 | if settings.bundler and settings.bundle_descriptor: 456 | raise ValueError('The API call has incompatible settings: ' 457 | 'bundling and page streaming') 458 | api_caller = _page_streamable(settings.page_descriptor) 459 | elif settings.bundler and settings.bundle_descriptor: 460 | api_caller = _bundleable(settings.bundle_descriptor) 461 | else: 462 | api_caller = base_caller 463 | 464 | return inner 465 | -------------------------------------------------------------------------------- /google/gax/bundling.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Provides behavior that supports request bundling. 31 | 32 | :func:`compute_bundle_id` is used generate ids linking API requests to the 33 | appropriate bundles. 34 | 35 | :class:`Event` is the result of scheduling a bundled api call. It is a 36 | decorated :class:`threading.Event`; its ``wait`` and ``is_set`` methods 37 | are used wait for the bundle request to complete or determine if it has 38 | been completed respectively. 39 | 40 | :class:`Task` manages the sending of all the requests in a specific bundle. 41 | 42 | :class:`Executor` has a ``schedule`` method that is used add bundled api calls 43 | to a new or existing :class:`Task`. 44 | 45 | """ 46 | 47 | from __future__ import absolute_import 48 | 49 | import collections 50 | import copy 51 | import logging 52 | import threading 53 | 54 | _LOG = logging.getLogger(__name__) 55 | 56 | 57 | def _str_dotted_getattr(obj, name): 58 | """Expands extends getattr to allow dots in x to indicate nested objects. 59 | 60 | Args: 61 | obj (object): an object. 62 | name (str): a name for a field in the object. 63 | 64 | Returns: 65 | Any: the value of named attribute. 66 | 67 | Raises: 68 | AttributeError: if the named attribute does not exist. 69 | """ 70 | for part in name.split('.'): 71 | obj = getattr(obj, part) 72 | return str(obj) if obj else None 73 | 74 | 75 | def compute_bundle_id(obj, discriminator_fields): 76 | """Computes a bundle id from the discriminator fields of `obj`. 77 | 78 | discriminator_fields may include '.' as a separator, which is used to 79 | indicate object traversal. This is meant to allow fields in the 80 | computed bundle_id. 81 | 82 | the id is a tuple computed by going through the discriminator fields in 83 | order and obtaining the str(value) object field (or nested object field) 84 | 85 | if any discriminator field cannot be found, ValueError is raised. 86 | 87 | Args: 88 | obj (object): an object. 89 | discriminator_fields (Sequence[str]): a list of discriminator fields in 90 | the order to be to be used in the id. 91 | 92 | Returns: 93 | Tuple[str]: computed as described above. 94 | 95 | Raises: 96 | AttributeError: if any discriminator fields attribute does not exist. 97 | """ 98 | return tuple(_str_dotted_getattr(obj, x) for x in discriminator_fields) 99 | 100 | 101 | _WARN_DEMUX_MISMATCH = ('cannot demultiplex the bundled response, got' 102 | ' %d subresponses; want %d, each bundled request will' 103 | ' receive all responses') 104 | 105 | 106 | class Task(object): 107 | """Coordinates the execution of a single bundle.""" 108 | # pylint: disable=too-many-instance-attributes 109 | 110 | def __init__(self, api_call, bundle_id, bundled_field, bundling_request, 111 | kwargs, subresponse_field=None): 112 | """ 113 | Args: 114 | api_call (Callable[Sequence[object], object]): the func that is this 115 | tasks's API call. 116 | bundle_id (Tuple[str]): the id of this bundle. 117 | bundled_field (str): the field used to create the bundled request. 118 | bundling_request (object): the request to pass as the arg to 119 | api_call. 120 | kwargs (dict): keyword arguments passed to api_call. 121 | subresponse_field (str): optional field used to demultiplex 122 | responses. 123 | 124 | """ 125 | self._api_call = api_call 126 | self._bundling_request = bundling_request 127 | self._kwargs = kwargs 128 | self.bundle_id = bundle_id 129 | self.bundled_field = bundled_field 130 | self.subresponse_field = subresponse_field 131 | self.timer = None 132 | self._in_deque = collections.deque() 133 | self._event_deque = collections.deque() 134 | 135 | @property 136 | def element_count(self): 137 | """The number of bundled elements in the repeated field.""" 138 | return sum(len(elts) for elts in self._in_deque) 139 | 140 | @property 141 | def request_bytesize(self): 142 | """The size of in bytes of the bundled field elements.""" 143 | return sum(len(str(e)) for elts in self._in_deque for e in elts) 144 | 145 | def run(self): 146 | """Call the task's func. 147 | 148 | The task's func will be called with the bundling requests func 149 | """ 150 | if not self._in_deque: 151 | return 152 | req = self._bundling_request 153 | del getattr(req, self.bundled_field)[:] 154 | getattr(req, self.bundled_field).extend( 155 | [e for elts in self._in_deque for e in elts]) 156 | 157 | subresponse_field = self.subresponse_field 158 | if subresponse_field: 159 | self._run_with_subresponses(req, subresponse_field, self._kwargs) 160 | else: 161 | self._run_with_no_subresponse(req, self._kwargs) 162 | 163 | def _run_with_no_subresponse(self, req, kwargs): 164 | try: 165 | resp = self._api_call(req, **kwargs) 166 | for event in self._event_deque: 167 | event.result = resp 168 | event.set() 169 | except Exception as exc: # pylint: disable=broad-except 170 | for event in self._event_deque: 171 | event.result = exc 172 | event.set() 173 | finally: 174 | self._in_deque.clear() 175 | self._event_deque.clear() 176 | 177 | def _run_with_subresponses(self, req, subresponse_field, kwargs): 178 | try: 179 | resp = self._api_call(req, **kwargs) 180 | in_sizes = [len(elts) for elts in self._in_deque] 181 | all_subresponses = getattr(resp, subresponse_field) 182 | if len(all_subresponses) != sum(in_sizes): 183 | _LOG.warning(_WARN_DEMUX_MISMATCH, len(all_subresponses), 184 | sum(in_sizes)) 185 | for event in self._event_deque: 186 | event.result = resp 187 | event.set() 188 | else: 189 | start = 0 190 | for i, event in zip(in_sizes, self._event_deque): 191 | next_copy = copy.copy(resp) 192 | subresponses = all_subresponses[start:start + i] 193 | next_copy.ClearField(subresponse_field) 194 | getattr(next_copy, subresponse_field).extend(subresponses) 195 | start += i 196 | event.result = next_copy 197 | event.set() 198 | except Exception as exc: # pylint: disable=broad-except 199 | for event in self._event_deque: 200 | event.result = exc 201 | event.set() 202 | finally: 203 | self._in_deque.clear() 204 | self._event_deque.clear() 205 | 206 | def extend(self, elts): 207 | """Adds elts to the tasks. 208 | 209 | Args: 210 | elts (Sequence): a iterable of elements that can be appended to the 211 | task's bundle_field. 212 | 213 | Returns: 214 | Event: an event that can be used to wait on the response. 215 | """ 216 | # Use a copy, not a reference, as it is later necessary to mutate 217 | # the proto field from which elts are drawn in order to construct 218 | # the bundled request. 219 | elts = elts[:] 220 | self._in_deque.append(elts) 221 | event = self._event_for(elts) 222 | self._event_deque.append(event) 223 | return event 224 | 225 | def _event_for(self, elts): 226 | """Creates an Event that is set when the bundle with elts is sent.""" 227 | event = Event() 228 | event.canceller = self._canceller_for(elts, event) 229 | return event 230 | 231 | def _canceller_for(self, elts, event): 232 | """Obtains a cancellation function that removes elts. 233 | 234 | The returned cancellation function returns ``True`` if all elements 235 | was removed successfully from the _in_deque, and false if it was not. 236 | """ 237 | def canceller(): 238 | """Cancels submission of ``elts`` as part of this bundle. 239 | 240 | Returns: 241 | bool: ``False`` if any of elements had already been sent, 242 | otherwise ``True``. 243 | """ 244 | try: 245 | self._event_deque.remove(event) 246 | self._in_deque.remove(elts) 247 | return True 248 | except ValueError: 249 | return False 250 | 251 | return canceller 252 | 253 | 254 | TIMER_FACTORY = threading.Timer # pylint: disable=invalid-name 255 | """A class with an interface similar to threading.Timer. 256 | 257 | Defaults to threading.Timer. This makes it easy to plug-in alternate 258 | timer implementations.""" 259 | 260 | 261 | class Executor(object): 262 | """Organizes bundling for an api service that requires it.""" 263 | # pylint: disable=too-few-public-methods 264 | 265 | def __init__(self, options): 266 | """Constructor. 267 | 268 | Args: 269 | options (gax.BundleOptions): configures strategy this instance 270 | uses when executing bundled functions. 271 | 272 | """ 273 | self._options = options 274 | self._tasks = {} 275 | self._task_lock = threading.RLock() 276 | 277 | def schedule(self, api_call, bundle_id, bundle_desc, bundling_request, 278 | kwargs=None): 279 | """Schedules bundle_desc of bundling_request as part of bundle_id. 280 | 281 | The returned value an :class:`Event` that 282 | 283 | * has a ``result`` attribute that will eventually be set to the result 284 | the api call 285 | * will be used to wait for the response 286 | * holds the canceller function for canceling this part of the bundle 287 | 288 | Args: 289 | api_call (callable[[object], object]): the scheduled API call. 290 | bundle_id (str): identifies the bundle on which the API call should be 291 | made. 292 | bundle_desc (gax.BundleDescriptor): describes the structure of the 293 | bundled call. 294 | bundling_request (object): the request instance to use in the API 295 | call. 296 | kwargs (dict): optional, the keyword arguments passed to the API call. 297 | 298 | Returns: 299 | Event: the scheduled event. 300 | """ 301 | kwargs = kwargs or dict() 302 | bundle = self._bundle_for(api_call, bundle_id, bundle_desc, 303 | bundling_request, kwargs) 304 | elts = getattr(bundling_request, bundle_desc.bundled_field) 305 | event = bundle.extend(elts) 306 | 307 | # Run the bundle if the count threshold was reached. 308 | count_threshold = self._options.element_count_threshold 309 | if count_threshold > 0 and bundle.element_count >= count_threshold: 310 | self._run_now(bundle.bundle_id) 311 | 312 | # Run the bundle if the size threshold was reached. 313 | size_threshold = self._options.request_byte_threshold 314 | if size_threshold > 0 and bundle.request_bytesize >= size_threshold: 315 | self._run_now(bundle.bundle_id) 316 | 317 | return event 318 | 319 | def _bundle_for(self, api_call, bundle_id, bundle_desc, bundling_request, 320 | kwargs): 321 | with self._task_lock: 322 | bundle = self._tasks.get(bundle_id) 323 | if bundle is None: 324 | bundle = Task(api_call, bundle_id, bundle_desc.bundled_field, 325 | bundling_request, kwargs, 326 | subresponse_field=bundle_desc.subresponse_field) 327 | delay_threshold = self._options.delay_threshold 328 | if delay_threshold > 0: 329 | self._run_later(bundle, delay_threshold) 330 | self._tasks[bundle_id] = bundle 331 | return bundle 332 | 333 | def _run_later(self, bundle, delay_threshold): 334 | with self._task_lock: 335 | if bundle.timer is None: 336 | the_timer = TIMER_FACTORY( 337 | delay_threshold, 338 | self._run_now, 339 | args=[bundle.bundle_id]) 340 | the_timer.start() 341 | bundle.timer = the_timer 342 | 343 | def _run_now(self, bundle_id): 344 | with self._task_lock: 345 | if bundle_id in self._tasks: 346 | a_task = self._tasks.pop(bundle_id) 347 | a_task.run() 348 | 349 | 350 | class Event(object): 351 | """Wraps a threading.Event, adding, canceller and result attributes.""" 352 | 353 | def __init__(self): 354 | """Constructor. 355 | 356 | """ 357 | self._event = threading.Event() 358 | self.result = None 359 | self.canceller = None 360 | 361 | def is_set(self): 362 | """Calls ``is_set`` on the decorated :class:`threading.Event`.""" 363 | return self._event.is_set() 364 | 365 | def set(self): 366 | """Calls ``set`` on the decorated :class:`threading.Event`.""" 367 | return self._event.set() 368 | 369 | def clear(self): 370 | """Calls ``clear`` on the decorated :class:`threading.Event`. 371 | 372 | Also resets the result if one has been set. 373 | """ 374 | self.result = None 375 | return self._event.clear() 376 | 377 | def wait(self, timeout=None): 378 | """Calls ``wait`` on the decorated :class:`threading.Event`.""" 379 | return self._event.wait(timeout=timeout) 380 | 381 | def cancel(self): 382 | """Invokes the cancellation function provided on construction.""" 383 | if self.canceller: 384 | return self.canceller() 385 | else: 386 | return False 387 | -------------------------------------------------------------------------------- /google/gax/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Runtime configuration shared by gax modules.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | from google.gax import grpc 35 | 36 | 37 | exc_to_code = grpc.exc_to_code # pylint: disable=invalid-name 38 | """A function that takes an exception and returns a status code. 39 | 40 | May return None if the exception is not associated with a status code. 41 | """ 42 | 43 | 44 | STATUS_CODE_NAMES = grpc.STATUS_CODE_NAMES 45 | """Maps strings used in client config to the status codes they represent. 46 | 47 | This is necessary for google.gax.api_callable.construct_settings to translate 48 | the client constants configuration for retrying into the correct gRPC objects. 49 | """ 50 | 51 | 52 | NAME_STATUS_CODES = grpc.NAME_STATUS_CODES 53 | """Inverse map for STATUS_CODE_NAMES""" 54 | 55 | 56 | create_stub = grpc.create_stub # pylint: disable=invalid-name, 57 | """The function to use to create stubs.""" 58 | 59 | 60 | API_ERRORS = grpc.API_ERRORS 61 | """Errors that indicate that an RPC was aborted.""" 62 | -------------------------------------------------------------------------------- /google/gax/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Provides GAX exceptions.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | from google.gax import config 35 | 36 | 37 | class GaxError(Exception): 38 | """Common base class for exceptions raised by GAX. 39 | 40 | Attributes: 41 | msg (string): describes the error that occurred. 42 | cause (Exception, optional): the exception raised by a lower 43 | layer of the RPC stack (for example, gRPC) that caused this 44 | exception, or None if this exception originated in GAX. 45 | """ 46 | def __init__(self, msg, cause=None): 47 | super(GaxError, self).__init__(msg) 48 | self.cause = cause 49 | 50 | def __str__(self): 51 | msg = super(GaxError, self).__str__() 52 | if not self.cause: 53 | return msg 54 | 55 | return '{}({}, caused by {})'.format( 56 | self.__class__.__name__, msg, self.cause) 57 | 58 | 59 | class InvalidArgumentError(ValueError, GaxError): 60 | """GAX exception class for ``INVALID_ARGUMENT`` errors. 61 | 62 | Attributes: 63 | msg (string): describes the error that occurred. 64 | cause (Exception, optional): the exception raised by a lower 65 | layer of the RPC stack (for example, gRPC) that caused this 66 | exception, or None if this exception originated in GAX. 67 | """ 68 | 69 | def __init__(self, msg, cause=None): 70 | GaxError.__init__(self, msg, cause=cause) 71 | 72 | 73 | def create_error(msg, cause=None): 74 | """Creates a ``GaxError`` or subclass. 75 | 76 | Attributes: 77 | msg (string): describes the error that occurred. 78 | cause (Exception, optional): the exception raised by a lower 79 | layer of the RPC stack (for example, gRPC) that caused this 80 | exception, or None if this exception originated in GAX. 81 | 82 | Returns: 83 | .GaxError: The exception that wraps ``cause``. 84 | """ 85 | status_code = config.exc_to_code(cause) 86 | status_name = config.NAME_STATUS_CODES.get(status_code) 87 | if status_name == 'INVALID_ARGUMENT': 88 | return InvalidArgumentError(msg, cause=cause) 89 | else: 90 | return GaxError(msg, cause=cause) 91 | 92 | 93 | class RetryError(GaxError): 94 | """Indicates an error during automatic GAX retrying.""" 95 | pass 96 | -------------------------------------------------------------------------------- /google/gax/grpc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Adapts the grpc surface.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | from grpc import RpcError, StatusCode 35 | 36 | from google.gax import _grpc_google_auth 37 | 38 | 39 | API_ERRORS = (RpcError, ) 40 | """gRPC exceptions that indicate that an RPC was aborted.""" 41 | 42 | 43 | STATUS_CODE_NAMES = { 44 | 'ABORTED': StatusCode.ABORTED, 45 | 'CANCELLED': StatusCode.CANCELLED, 46 | 'DATA_LOSS': StatusCode.DATA_LOSS, 47 | 'DEADLINE_EXCEEDED': StatusCode.DEADLINE_EXCEEDED, 48 | 'FAILED_PRECONDITION': StatusCode.FAILED_PRECONDITION, 49 | 'INTERNAL': StatusCode.INTERNAL, 50 | 'INVALID_ARGUMENT': StatusCode.INVALID_ARGUMENT, 51 | 'NOT_FOUND': StatusCode.NOT_FOUND, 52 | 'OUT_OF_RANGE': StatusCode.OUT_OF_RANGE, 53 | 'PERMISSION_DENIED': StatusCode.PERMISSION_DENIED, 54 | 'RESOURCE_EXHAUSTED': StatusCode.RESOURCE_EXHAUSTED, 55 | 'UNAUTHENTICATED': StatusCode.UNAUTHENTICATED, 56 | 'UNAVAILABLE': StatusCode.UNAVAILABLE, 57 | 'UNIMPLEMENTED': StatusCode.UNIMPLEMENTED, 58 | 'UNKNOWN': StatusCode.UNKNOWN} 59 | """Maps strings used in client config to gRPC status codes.""" 60 | 61 | 62 | NAME_STATUS_CODES = dict([(v, k) for (k, v) in STATUS_CODE_NAMES.items()]) 63 | """Inverse map for STATUS_CODE_NAMES""" 64 | 65 | 66 | def exc_to_code(exc): 67 | """Retrieves the status code from an exception""" 68 | if not isinstance(exc, RpcError): 69 | return None 70 | else: 71 | try: 72 | return exc.code() 73 | except AttributeError: 74 | return None 75 | 76 | 77 | def create_stub(generated_create_stub, channel=None, service_path=None, 78 | service_port=None, credentials=None, scopes=None, 79 | ssl_credentials=None): 80 | """Creates a gRPC client stub. 81 | 82 | Args: 83 | generated_create_stub (Callable): The generated gRPC method to create a 84 | stub. 85 | channel (grpc.Channel): A Channel object through which to make calls. 86 | If None, a secure channel is constructed. If specified, all 87 | remaining arguments are ignored. 88 | service_path (str): The domain name of the API remote host. 89 | service_port (int): The port on which to connect to the remote host. 90 | credentials (google.auth.credentials.Credentials): The authorization 91 | credentials to attach to requests. These credentials identify your 92 | application to the service. 93 | scopes (Sequence[str]): The OAuth scopes for this service. This 94 | parameter is ignored if a credentials is specified. 95 | ssl_credentials (grpc.ChannelCredentials): gRPC channel credentials 96 | used to create a secure gRPC channel. If not specified, SSL 97 | credentials will be created using default certificates. 98 | 99 | Returns: 100 | grpc.Client: A gRPC client stub. 101 | """ 102 | if channel is None: 103 | target = '{}:{}'.format(service_path, service_port) 104 | 105 | if credentials is None: 106 | credentials = _grpc_google_auth.get_default_credentials(scopes) 107 | 108 | channel = _grpc_google_auth.secure_authorized_channel( 109 | credentials, target, ssl_credentials=ssl_credentials) 110 | 111 | return generated_create_stub(channel) 112 | -------------------------------------------------------------------------------- /google/gax/path_template.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Implements a utility for parsing and formatting path templates.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | from collections import namedtuple 35 | 36 | from ply import lex, yacc 37 | 38 | _BINDING = 1 39 | _END_BINDING = 2 40 | _TERMINAL = 3 41 | _Segment = namedtuple('_Segment', ['kind', 'literal']) 42 | 43 | 44 | def _format(segments): 45 | template = '' 46 | slash = True 47 | for segment in segments: 48 | if segment.kind == _TERMINAL: 49 | if slash: 50 | template += '/' 51 | template += segment.literal 52 | slash = True 53 | if segment.kind == _BINDING: 54 | template += '/{%s=' % segment.literal 55 | slash = False 56 | if segment.kind == _END_BINDING: 57 | template += '%s}' % segment.literal 58 | return template[1:] # Remove the leading / 59 | 60 | 61 | class ValidationException(Exception): 62 | """Represents a path template validation error.""" 63 | pass 64 | 65 | 66 | class PathTemplate(object): 67 | """Represents a path template.""" 68 | 69 | segments = None 70 | segment_count = 0 71 | 72 | def __init__(self, data): 73 | parser = _Parser() 74 | self.segments = parser.parse(data) 75 | self.segment_count = parser.segment_count 76 | 77 | def __len__(self): 78 | return self.segment_count 79 | 80 | def __repr__(self): 81 | return _format(self.segments) 82 | 83 | def render(self, bindings): 84 | """Renders a string from a path template using the provided bindings. 85 | 86 | Args: 87 | bindings (dict): A dictionary of var names to binding strings. 88 | 89 | Returns: 90 | str: The rendered instantiation of this path template. 91 | 92 | Raises: 93 | ValidationException: If a key isn't provided or if a sub-template 94 | can't be parsed. 95 | """ 96 | out = [] 97 | binding = False 98 | for segment in self.segments: 99 | if segment.kind == _BINDING: 100 | if segment.literal not in bindings: 101 | raise ValidationException( 102 | ('rendering error: value for key \'{}\' ' 103 | 'not provided').format(segment.literal)) 104 | out.extend(PathTemplate(bindings[segment.literal]).segments) 105 | binding = True 106 | elif segment.kind == _END_BINDING: 107 | binding = False 108 | else: 109 | if binding: 110 | continue 111 | out.append(segment) 112 | path = _format(out) 113 | self.match(path) 114 | return path 115 | 116 | def match(self, path): 117 | """Matches a fully qualified path template string. 118 | 119 | Args: 120 | path (str): A fully qualified path template string. 121 | 122 | Returns: 123 | dict: Var names to matched binding values. 124 | 125 | Raises: 126 | ValidationException: If path can't be matched to the template. 127 | """ 128 | this = self.segments 129 | that = path.split('/') 130 | current_var = None 131 | bindings = {} 132 | segment_count = self.segment_count 133 | j = 0 134 | for i in range(0, len(this)): 135 | if j >= len(that): 136 | break 137 | if this[i].kind == _TERMINAL: 138 | if this[i].literal == '*': 139 | bindings[current_var] = that[j] 140 | j += 1 141 | elif this[i].literal == '**': 142 | until = j + len(that) - segment_count + 1 143 | segment_count += len(that) - segment_count 144 | bindings[current_var] = '/'.join(that[j:until]) 145 | j = until 146 | elif this[i].literal != that[j]: 147 | raise ValidationException( 148 | 'mismatched literal: \'%s\' != \'%s\'' % ( 149 | this[i].literal, that[j])) 150 | else: 151 | j += 1 152 | elif this[i].kind == _BINDING: 153 | current_var = this[i].literal 154 | if j != len(that) or j != segment_count: 155 | raise ValidationException( 156 | 'match error: could not render from the path template: {}' 157 | .format(path)) 158 | return bindings 159 | 160 | 161 | # pylint: disable=C0103 162 | # pylint: disable=R0201 163 | class _Parser(object): 164 | tokens = ( 165 | 'FORWARD_SLASH', 166 | 'LEFT_BRACE', 167 | 'RIGHT_BRACE', 168 | 'EQUALS', 169 | 'WILDCARD', 170 | 'PATH_WILDCARD', 171 | 'LITERAL', 172 | ) 173 | 174 | t_FORWARD_SLASH = r'/' 175 | t_LEFT_BRACE = r'\{' 176 | t_RIGHT_BRACE = r'\}' 177 | t_EQUALS = r'=' 178 | t_WILDCARD = r'\*' 179 | t_PATH_WILDCARD = r'\*\*' 180 | t_LITERAL = r'[^*=}{\/]+' 181 | 182 | t_ignore = ' \t' 183 | 184 | binding_var_count = 0 185 | segment_count = 0 186 | 187 | def __init__(self): 188 | self.lexer = lex.lex(module=self) 189 | self.parser = yacc.yacc(module=self, debug=False, write_tables=False) 190 | 191 | def parse(self, data): 192 | """Returns a list of path template segments parsed from data. 193 | 194 | Args: 195 | data (str): A path template string. 196 | 197 | Returns: 198 | Sequence[_PathSegment]: The path template segments. 199 | 200 | Raises: 201 | ValidationException: If the path template is invalid. 202 | """ 203 | self.binding_var_count = 0 204 | self.segment_count = 0 205 | 206 | segments = self.parser.parse(data) 207 | # Validation step: checks that there are no nested bindings. 208 | path_wildcard = False 209 | for segment in segments: 210 | if segment.kind == _TERMINAL and segment.literal == '**': 211 | if path_wildcard: 212 | raise ValidationException( 213 | 'validation error: path template cannot contain more ' 214 | 'than one path wildcard') 215 | path_wildcard = True 216 | return segments 217 | 218 | def p_template(self, p): 219 | """template : FORWARD_SLASH bound_segments 220 | | bound_segments""" 221 | # ply fails on a negative index. 222 | p[0] = p[len(p) - 1] 223 | 224 | def p_bound_segments(self, p): 225 | """bound_segments : bound_segment FORWARD_SLASH bound_segments 226 | | bound_segment""" 227 | p[0] = p[1] 228 | if len(p) > 2: 229 | p[0].extend(p[3]) 230 | 231 | def p_unbound_segments(self, p): 232 | """unbound_segments : unbound_terminal FORWARD_SLASH unbound_segments 233 | | unbound_terminal""" 234 | p[0] = p[1] 235 | if len(p) > 2: 236 | p[0].extend(p[3]) 237 | 238 | def p_bound_segment(self, p): 239 | """bound_segment : bound_terminal 240 | | variable""" 241 | p[0] = p[1] 242 | 243 | def p_unbound_terminal(self, p): 244 | """unbound_terminal : WILDCARD 245 | | PATH_WILDCARD 246 | | LITERAL""" 247 | p[0] = [_Segment(_TERMINAL, p[1])] 248 | self.segment_count += 1 249 | 250 | def p_bound_terminal(self, p): 251 | """bound_terminal : unbound_terminal""" 252 | if p[1][0].literal in ['*', '**']: 253 | p[0] = [_Segment(_BINDING, '$%d' % self.binding_var_count), 254 | p[1][0], 255 | _Segment(_END_BINDING, '')] 256 | self.binding_var_count += 1 257 | else: 258 | p[0] = p[1] 259 | 260 | def p_variable(self, p): 261 | """variable : LEFT_BRACE LITERAL EQUALS unbound_segments RIGHT_BRACE 262 | | LEFT_BRACE LITERAL RIGHT_BRACE""" 263 | p[0] = [_Segment(_BINDING, p[2])] 264 | if len(p) > 4: 265 | p[0].extend(p[4]) 266 | else: 267 | p[0].append(_Segment(_TERMINAL, '*')) 268 | self.segment_count += 1 269 | p[0].append(_Segment(_END_BINDING, '')) 270 | 271 | def p_error(self, p): 272 | """Raises a parser error.""" 273 | if p: 274 | raise ValidationException( 275 | 'parser error: unexpected token \'%s\'' % p.type) 276 | else: 277 | raise ValidationException('parser error: unexpected EOF') 278 | 279 | def t_error(self, t): 280 | """Raises a lexer error.""" 281 | raise ValidationException( 282 | 'lexer error: illegal character \'%s\'' % t.value[0]) 283 | -------------------------------------------------------------------------------- /google/gax/retry.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Provides function wrappers that implement retrying.""" 31 | 32 | from __future__ import absolute_import, division 33 | 34 | import random 35 | import time 36 | 37 | from google.gax import config, errors 38 | 39 | _MILLIS_PER_SECOND = 1000 40 | 41 | 42 | def _has_timeout_settings(backoff_settings): 43 | return (backoff_settings.rpc_timeout_multiplier is not None and 44 | backoff_settings.max_rpc_timeout_millis is not None and 45 | backoff_settings.total_timeout_millis is not None and 46 | backoff_settings.initial_rpc_timeout_millis is not None) 47 | 48 | 49 | def add_timeout_arg(a_func, timeout, **kwargs): 50 | """Updates a_func so that it gets called with the timeout as its final arg. 51 | 52 | This converts a callable, a_func, into another callable with an additional 53 | positional arg. 54 | 55 | Args: 56 | a_func (callable): a callable to be updated 57 | timeout (int): to be added to the original callable as it final positional 58 | arg. 59 | kwargs: Addtional arguments passed through to the callable. 60 | 61 | Returns: 62 | callable: the original callable updated to the timeout arg 63 | """ 64 | 65 | def inner(*args): 66 | """Updates args with the timeout.""" 67 | updated_args = args + (timeout,) 68 | return a_func(*updated_args, **kwargs) 69 | 70 | return inner 71 | 72 | 73 | def retryable(a_func, retry_options, **kwargs): 74 | """Creates a function equivalent to a_func, but that retries on certain 75 | exceptions. 76 | 77 | Args: 78 | a_func (callable): A callable. 79 | retry_options (RetryOptions): Configures the exceptions upon which the 80 | callable should retry, and the parameters to the exponential backoff 81 | retry algorithm. 82 | kwargs: Addtional arguments passed through to the callable. 83 | 84 | Returns: 85 | Callable: A function that will retry on exception. 86 | """ 87 | delay_mult = retry_options.backoff_settings.retry_delay_multiplier 88 | max_delay_millis = retry_options.backoff_settings.max_retry_delay_millis 89 | has_timeout_settings = _has_timeout_settings(retry_options.backoff_settings) 90 | 91 | if has_timeout_settings: 92 | timeout_mult = retry_options.backoff_settings.rpc_timeout_multiplier 93 | max_timeout = (retry_options.backoff_settings.max_rpc_timeout_millis / 94 | _MILLIS_PER_SECOND) 95 | total_timeout = (retry_options.backoff_settings.total_timeout_millis / 96 | _MILLIS_PER_SECOND) 97 | 98 | def inner(*args): 99 | """Equivalent to ``a_func``, but retries upon transient failure. 100 | 101 | Retrying is done through an exponential backoff algorithm configured 102 | by the options in ``retry``. 103 | """ 104 | delay = retry_options.backoff_settings.initial_retry_delay_millis 105 | exc = errors.RetryError('Retry total timeout exceeded before any' 106 | 'response was received') 107 | if has_timeout_settings: 108 | timeout = ( 109 | retry_options.backoff_settings.initial_rpc_timeout_millis / 110 | _MILLIS_PER_SECOND) 111 | 112 | now = time.time() 113 | deadline = now + total_timeout 114 | else: 115 | timeout = None 116 | deadline = None 117 | 118 | while deadline is None or now < deadline: 119 | try: 120 | to_call = add_timeout_arg(a_func, timeout, **kwargs) 121 | return to_call(*args) 122 | except Exception as exception: # pylint: disable=broad-except 123 | code = config.exc_to_code(exception) 124 | if code not in retry_options.retry_codes: 125 | raise errors.RetryError( 126 | 'Exception occurred in retry method that was not' 127 | ' classified as transient', exception) 128 | 129 | exc = errors.RetryError( 130 | 'Retry total timeout exceeded with exception', exception) 131 | 132 | # Sleep a random number which will, on average, equal the 133 | # expected delay. 134 | to_sleep = random.uniform(0, delay * 2) 135 | time.sleep(to_sleep / _MILLIS_PER_SECOND) 136 | delay = min(delay * delay_mult, max_delay_millis) 137 | 138 | if has_timeout_settings: 139 | now = time.time() 140 | timeout = min( 141 | timeout * timeout_mult, max_timeout, deadline - now) 142 | 143 | raise exc 144 | 145 | return inner 146 | -------------------------------------------------------------------------------- /google/gax/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/gax-python/309aedfcfd48e4c8fa22dd60e9c84c3cc71bb20e/google/gax/utils/__init__.py -------------------------------------------------------------------------------- /google/gax/utils/messages.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Utility functions around identifying protobuf messages.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | import collections 35 | import inspect 36 | 37 | from google.protobuf.message import Message 38 | 39 | 40 | def get_messages(module): 41 | """Return a dictionary of message names and objects. 42 | 43 | Args: 44 | module (module): A Python module; dir() will be run against this 45 | module to find Message subclasses. 46 | 47 | Returns: 48 | dict[str, Message]: A dictionary with the Message class names as 49 | keys, and the Message subclasses themselves as values. 50 | """ 51 | answer = collections.OrderedDict() 52 | for name in dir(module): 53 | candidate = getattr(module, name) 54 | if inspect.isclass(candidate) and issubclass(candidate, Message): 55 | answer[name] = candidate 56 | return answer 57 | -------------------------------------------------------------------------------- /google/gax/utils/metrics.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Utility functions for manipulation of metrics headers.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | import collections 35 | import platform 36 | 37 | import pkg_resources 38 | 39 | from google import gax 40 | 41 | 42 | def fill(metrics_headers=()): 43 | """Add the metrics headers known to GAX. 44 | 45 | Return an OrderedDict with all of the metrics headers provided to 46 | this function, as well as the metrics known to GAX (such as its own 47 | version, the GRPC version, etc.). 48 | """ 49 | # Create an ordered dictionary with the Python version, which 50 | # should go first. 51 | answer = collections.OrderedDict(( 52 | ('gl-python', platform.python_version()), 53 | )) 54 | 55 | # Add anything that already appears in the passed metrics headers, 56 | # in order. 57 | for key, value in collections.OrderedDict(metrics_headers).items(): 58 | answer[key] = value 59 | 60 | # Add the GAX and GRPC headers to our metrics. 61 | # These come after what may have been passed in (generally the GAPIC 62 | # library). 63 | answer['gax'] = gax.__version__ 64 | # pylint: disable=no-member 65 | answer['grpc'] = pkg_resources.get_distribution('grpcio').version 66 | # pylint: enable=no-member 67 | 68 | return answer 69 | 70 | 71 | def stringify(metrics_headers=()): 72 | """Convert the provided metrics headers to a string. 73 | 74 | Iterate over the metrics headers (a dictionary, usually ordered) and 75 | return a properly-formatted space-separated string 76 | (e.g. foo/1.2.3 bar/3.14.159). 77 | """ 78 | metrics_headers = collections.OrderedDict(metrics_headers) 79 | return ' '.join(['%s/%s' % (k, v) for k, v in metrics_headers.items()]) 80 | -------------------------------------------------------------------------------- /google/gax/utils/oneof.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Utility function to error if multiple "one of" arguments are sent.""" 31 | 32 | from __future__ import absolute_import 33 | 34 | 35 | def check_oneof(**kwargs): 36 | """Raise ValueError if more than one keyword argument is not none. 37 | 38 | Args: 39 | kwargs (dict): The keyword arguments sent to the function. 40 | 41 | Returns: None 42 | 43 | Raises: 44 | ValueError: If more than one entry in kwargs is not none. 45 | """ 46 | # Sanity check: If no keyword arguments were sent, this is fine. 47 | if not kwargs: 48 | return None 49 | 50 | not_nones = [val for val in kwargs.values() if val is not None] 51 | if len(not_nones) > 1: 52 | raise ValueError('Only one of {fields} should be set.'.format( 53 | fields=', '.join(sorted(kwargs.keys())), 54 | )) 55 | -------------------------------------------------------------------------------- /google/gax/utils/protobuf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """Utility functions around reading and writing from protobufs.""" 31 | 32 | import collections 33 | 34 | from google.protobuf.message import Message 35 | 36 | __all__ = ('get', 'set', 'setdefault') 37 | 38 | 39 | _SENTINEL = object() 40 | 41 | 42 | def get(pb_or_dict, key, default=_SENTINEL): 43 | """Retrieve the given key off of the object. 44 | 45 | If a default is specified, return it if the key is not found, otherwise 46 | raise KeyError. 47 | 48 | Args: 49 | pb_or_dict (Union[~google.protobuf.message.Message, Mapping]): the 50 | object. 51 | key (str): The key to retrieve from the object in question. 52 | default (Any): If the key is not present on the object, and a default 53 | is set, returns that default instead. A type-appropriate falsy 54 | default is generally recommended, as protobuf messages almost 55 | always have default values for unset values and it is not always 56 | possible to tell the difference between a falsy value and an 57 | unset one. If no default is set, raises KeyError for not found 58 | values. 59 | 60 | Returns: 61 | Any: The return value from the underlying message or dict. 62 | 63 | Raises: 64 | KeyError: If the key is not found. Note that, for unset values, 65 | messages and dictionaries may not have consistent behavior. 66 | TypeError: If pb_or_dict is not a Message or Mapping. 67 | """ 68 | # We may need to get a nested key. Resolve this. 69 | key, subkey = _resolve_subkeys(key) 70 | 71 | # Attempt to get the value from the two types of objects we know baout. 72 | # If we get something else, complain. 73 | if isinstance(pb_or_dict, Message): 74 | answer = getattr(pb_or_dict, key, default) 75 | elif isinstance(pb_or_dict, collections.Mapping): 76 | answer = pb_or_dict.get(key, default) 77 | else: 78 | raise TypeError('Tried to fetch a key %s on an invalid object; ' 79 | 'expected a dict or protobuf message.') 80 | 81 | # If the object we got back is our sentinel, raise KeyError; this is 82 | # a "not found" case. 83 | if answer is _SENTINEL: 84 | raise KeyError(key) 85 | 86 | # If a subkey exists, call this method recursively against the answer. 87 | if subkey and answer is not default: 88 | return get(answer, subkey, default=default) 89 | 90 | # Return the value. 91 | return answer 92 | 93 | 94 | def set(pb_or_dict, key, value): 95 | """Set the given key on the object. 96 | 97 | Args: 98 | pb_or_dict (Union[~google.protobuf.message.Message, Mapping]): the 99 | object. 100 | key (str): The key on the object in question. 101 | value (Any): The value to set. 102 | 103 | Raises: 104 | TypeError: If pb_or_dict is not a Message or Mapping. 105 | """ 106 | # pylint: disable=redefined-builtin,too-many-branches 107 | # redefined-builtin: We want 'set' to be part of the public interface. 108 | # too-many-branches: This method is inherently complex. 109 | 110 | # Sanity check: Is our target object valid? 111 | if not isinstance(pb_or_dict, (collections.MutableMapping, Message)): 112 | raise TypeError('Tried to set a key %s on an invalid object; ' 113 | 'expected a dict or protobuf message.' % key) 114 | 115 | # We may be setting a nested key. Resolve this. 116 | key, subkey = _resolve_subkeys(key) 117 | 118 | # If a subkey exists, then get that object and call this method 119 | # recursively against it using the subkey. 120 | if subkey is not None: 121 | if isinstance(pb_or_dict, collections.MutableMapping): 122 | pb_or_dict.setdefault(key, {}) 123 | set(get(pb_or_dict, key), subkey, value) 124 | return 125 | 126 | # Attempt to set the value on the types of objects we know how to deal 127 | # with. 128 | if isinstance(pb_or_dict, collections.MutableMapping): 129 | pb_or_dict[key] = value 130 | elif isinstance(value, (collections.MutableSequence, tuple)): 131 | # Clear the existing repeated protobuf message of any elements 132 | # currently inside it. 133 | while getattr(pb_or_dict, key): 134 | getattr(pb_or_dict, key).pop() 135 | 136 | # Write our new elements to the repeated field. 137 | for item in value: 138 | if isinstance(item, collections.Mapping): 139 | getattr(pb_or_dict, key).add(**item) 140 | else: 141 | getattr(pb_or_dict, key).extend([item]) 142 | elif isinstance(value, collections.Mapping): 143 | # Assign the dictionary values to the protobuf message. 144 | for item_key, item_value in value.items(): 145 | set(getattr(pb_or_dict, key), item_key, item_value) 146 | elif isinstance(value, Message): 147 | # Assign the protobuf message values to the protobuf message. 148 | for item_key, item_value in value.ListFields(): 149 | set(getattr(pb_or_dict, key), item_key.name, item_value) 150 | else: 151 | setattr(pb_or_dict, key, value) 152 | 153 | 154 | def setdefault(pb_or_dict, key, value): 155 | """Set the key on the object to the value if the current value is falsy. 156 | 157 | Because protobuf Messages do not distinguish between unset values and 158 | falsy ones particularly well, this method treats any falsy value 159 | (e.g. 0, empty list) as a target to be overwritten, on both Messages 160 | and dictionaries. 161 | 162 | Args: 163 | pb_or_dict (Union[~google.protobuf.message.Message, Mapping]): the 164 | object. 165 | key (str): The key on the object in question. 166 | value (Any): The value to set. 167 | 168 | Raises: 169 | TypeError: If pb_or_dict is not a Message or Mapping. 170 | """ 171 | if not get(pb_or_dict, key, default=None): 172 | set(pb_or_dict, key, value) 173 | 174 | 175 | def _resolve_subkeys(key, separator='.'): 176 | """Given a key which may actually be a nested key, return the top level 177 | key and any nested subkeys as separate values. 178 | 179 | Args: 180 | key (str): A string that may or may not contain the separator. 181 | separator (str): The namespace separator. Defaults to `.`. 182 | 183 | Returns: 184 | Tuple[str, str]: The key and subkey(s). 185 | """ 186 | subkey = None 187 | if separator in key: 188 | index = key.index(separator) 189 | subkey = key[index + 1:] 190 | key = key[:index] 191 | return key, subkey 192 | -------------------------------------------------------------------------------- /nox.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import nox 31 | 32 | 33 | @nox.session 34 | def lint(session): 35 | session.interpreter = 'python3.6' 36 | session.install( 37 | 'flake8', 'flake8-import-order', 'pylint', 'docutils', 38 | 'gcp-devrel-py-tools>=0.0.3') 39 | session.install('.') 40 | session.run( 41 | 'python', 'setup.py', 'check', '--metadata', 42 | '--restructuredtext', '--strict') 43 | session.run( 44 | 'flake8', 45 | '--import-order-style=google', 46 | '--application-import-names=google,tests', 47 | '--ignore=E501,I202', 48 | '--exclude=gapic,fixtures', 49 | 'google', 'tests') 50 | session.run( 51 | 'gcp-devrel-py-tools', 'run-pylint', 52 | '--config', 'pylint.conf.py', 53 | '--library-filesets', 'google', 54 | '--test-filesets', 'tests', 55 | success_codes=range(0, 31)) 56 | 57 | 58 | @nox.session 59 | def docs(session): 60 | session.interpreter = 'python3.6' 61 | session.install('-r', 'docs/requirements-docs.txt') 62 | session.install('.') 63 | session.chdir('docs') 64 | session.run('make', 'html') 65 | 66 | 67 | @nox.session 68 | def generate_fixtures(session): 69 | session.interpreter = 'python2.7' 70 | session.install('-r', 'test-requirements.txt') 71 | session.install('.') 72 | session.run( 73 | 'python', '-m', 'grpc.tools.protoc', 74 | '--proto_path=tests', 75 | '--python_out=tests', 76 | 'tests/fixtures/fixture.proto') 77 | 78 | 79 | @nox.session 80 | def cover(session): 81 | session.interpreter = 'python3.6' 82 | session.install('-r', 'test-requirements.txt') 83 | session.install('.') 84 | session.run( 85 | 'py.test', '--cov=google.gax', '--cov=tests', '--cov-report=', 'tests') 86 | session.run( 87 | 'coverage', 'report', '--show-missing', '--fail-under=98') 88 | 89 | 90 | @nox.session 91 | @nox.parametrize( 92 | 'python', ['python2.7', 'python3.4', 'python3.5', 'python3.6']) 93 | def unit_tests(session, python): 94 | session.interpreter = python 95 | session.install('-r', 'test-requirements.txt') 96 | session.install('.') 97 | session.run( 98 | 'py.test', '--cov=google.gax', '--cov=tests', 'tests', 99 | *session.posargs) 100 | -------------------------------------------------------------------------------- /pylint.conf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | """This module is used to config gcp-devrel-py-tools run-pylint.""" 31 | 32 | import copy 33 | 34 | library_additions = { 35 | 'MESSAGES CONTROL': { 36 | 'disable': [ 37 | 'I', 38 | 'import-error', 39 | 'no-member', 40 | 'protected-access', 41 | 'redefined-variable-type', 42 | 'similarities', 43 | 'no-else-return', 44 | ], 45 | }, 46 | } 47 | 48 | library_replacements = { 49 | 'MASTER': { 50 | 'ignore': [ 51 | 'CVS', '.git', '.cache', '.tox', '.nox', 'gapic', 'fixtures'], 52 | 'load-plugins': 'pylint.extensions.check_docs', 53 | }, 54 | 'REPORTS': { 55 | 'reports': 'no', 56 | }, 57 | 'BASIC': { 58 | 'method-rgx': '[a-z_][a-z0-9_]{2,40}$', 59 | 'function-rgx': '[a-z_][a-z0-9_]{2,40}$', 60 | }, 61 | 'TYPECHECK': { 62 | 'ignored-modules': ['six', 'google.protobuf'], 63 | }, 64 | 'DESIGN': { 65 | 'min-public-methods': '0', 66 | 'max-args': '10', 67 | 'max-attributes': '15', 68 | }, 69 | } 70 | 71 | test_additions = copy.deepcopy(library_additions) 72 | test_additions['MESSAGES CONTROL']['disable'].extend([ 73 | 'missing-docstring', 74 | 'no-self-use', 75 | 'redefined-outer-name', 76 | 'unused-argument', 77 | 'no-name-in-module', 78 | ]) 79 | test_replacements = copy.deepcopy(library_replacements) 80 | test_replacements.setdefault('BASIC', {}) 81 | test_replacements['BASIC'].update({ 82 | 'good-names': ['i', 'j', 'k', 'ex', 'Run', '_', 'fh', 'pytestmark'], 83 | 'method-rgx': '[a-z_][a-z0-9_]{2,80}$', 84 | 'function-rgx': '[a-z_][a-z0-9_]{2,80}$', 85 | }) 86 | 87 | ignored_files = () 88 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2015, Google Inc. 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are 8 | # met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following disclaimer 14 | # in the documentation and/or other materials provided with the 15 | # distribution. 16 | # * Neither the name of Google Inc. nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | import io 33 | 34 | from setuptools import find_packages 35 | from setuptools import setup 36 | 37 | 38 | DEPENDENCIES = [ 39 | 'dill >= 0.2.5, < 0.3dev', 40 | 'future >= 0.16.0, < 0.17dev', 41 | 'googleapis-common-protos >= 1.5.2, < 2.0dev', 42 | 'grpcio >=1.0.2, <2.0dev', 43 | 'google-auth >= 1.0.0, <2.0dev', 44 | 'ply == 3.8', 45 | 'protobuf >= 3.0.0, < 4.0dev', 46 | 'requests >= 2.13.0, < 3.0dev' 47 | ] 48 | 49 | with io.open('README.rst', 'r') as readme: 50 | long_description = readme.read() 51 | 52 | setup( 53 | name='google-gax', 54 | version='0.16.0', 55 | description='Google API Extensions', 56 | long_description=long_description, 57 | author='Google API Authors', 58 | author_email='googleapis-packages@google.com', 59 | url='https://github.com/googleapis/gax-python', 60 | packages=find_packages(exclude=('tests*',)), 61 | namespace_packages=('google',), 62 | license='BSD-3-Clause', 63 | classifiers=[ 64 | 'Development Status :: 7 - Inactive', 65 | 'Intended Audience :: Developers', 66 | 'License :: OSI Approved :: BSD License', 67 | 'Programming Language :: Python', 68 | 'Programming Language :: Python :: 2', 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.4', 72 | 'Programming Language :: Python :: 3.5', 73 | 'Programming Language :: Python :: 3.6', 74 | 'Programming Language :: Python :: Implementation :: CPython', 75 | ], 76 | install_requires=DEPENDENCIES, 77 | include_package_data=True, 78 | ) 79 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | mock>=1.3.0 2 | pytest>=2.8.3 3 | pytest-cov>=1.8.1 4 | pytest-timeout>=1.0.0 5 | unittest2>=1.1.0 6 | grpcio-tools>=1.0.0 7 | google-auth>=0.2.0 8 | requests>=2.11.1 9 | httplib2>=0.9.2 10 | google-auth-httplib2>=0.0.1 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googleapis/gax-python/309aedfcfd48e4c8fa22dd60e9c84c3cc71bb20e/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/fixture.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google 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 | syntax = "proto3"; 15 | 16 | package google.protobuf; 17 | 18 | message Simple { 19 | string field1 = 1; 20 | string field2 = 2; 21 | } 22 | 23 | message Outer { 24 | Simple inner = 1; 25 | string field1 = 2; 26 | } 27 | 28 | message Bundled { 29 | repeated string field1 = 1; 30 | } 31 | -------------------------------------------------------------------------------- /tests/fixtures/fixture_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: fixtures/fixture.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='fixtures/fixture.proto', 20 | package='google.protobuf', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x16\x66ixtures/fixture.proto\x12\x0fgoogle.protobuf\"(\n\x06Simple\x12\x0e\n\x06\x66ield1\x18\x01 \x01(\t\x12\x0e\n\x06\x66ield2\x18\x02 \x01(\t\"?\n\x05Outer\x12&\n\x05inner\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Simple\x12\x0e\n\x06\x66ield1\x18\x02 \x01(\t\"\x19\n\x07\x42undled\x12\x0e\n\x06\x66ield1\x18\x01 \x03(\tb\x06proto3') 23 | ) 24 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 25 | 26 | 27 | 28 | 29 | _SIMPLE = _descriptor.Descriptor( 30 | name='Simple', 31 | full_name='google.protobuf.Simple', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='field1', full_name='google.protobuf.Simple.field1', index=0, 38 | number=1, type=9, cpp_type=9, label=1, 39 | has_default_value=False, default_value=_b("").decode('utf-8'), 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | options=None), 43 | _descriptor.FieldDescriptor( 44 | name='field2', full_name='google.protobuf.Simple.field2', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=_b("").decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | options=None), 50 | ], 51 | extensions=[ 52 | ], 53 | nested_types=[], 54 | enum_types=[ 55 | ], 56 | options=None, 57 | is_extendable=False, 58 | syntax='proto3', 59 | extension_ranges=[], 60 | oneofs=[ 61 | ], 62 | serialized_start=43, 63 | serialized_end=83, 64 | ) 65 | 66 | 67 | _OUTER = _descriptor.Descriptor( 68 | name='Outer', 69 | full_name='google.protobuf.Outer', 70 | filename=None, 71 | file=DESCRIPTOR, 72 | containing_type=None, 73 | fields=[ 74 | _descriptor.FieldDescriptor( 75 | name='inner', full_name='google.protobuf.Outer.inner', index=0, 76 | number=1, type=11, cpp_type=10, label=1, 77 | has_default_value=False, default_value=None, 78 | message_type=None, enum_type=None, containing_type=None, 79 | is_extension=False, extension_scope=None, 80 | options=None), 81 | _descriptor.FieldDescriptor( 82 | name='field1', full_name='google.protobuf.Outer.field1', index=1, 83 | number=2, type=9, cpp_type=9, label=1, 84 | has_default_value=False, default_value=_b("").decode('utf-8'), 85 | message_type=None, enum_type=None, containing_type=None, 86 | is_extension=False, extension_scope=None, 87 | options=None), 88 | ], 89 | extensions=[ 90 | ], 91 | nested_types=[], 92 | enum_types=[ 93 | ], 94 | options=None, 95 | is_extendable=False, 96 | syntax='proto3', 97 | extension_ranges=[], 98 | oneofs=[ 99 | ], 100 | serialized_start=85, 101 | serialized_end=148, 102 | ) 103 | 104 | 105 | _BUNDLED = _descriptor.Descriptor( 106 | name='Bundled', 107 | full_name='google.protobuf.Bundled', 108 | filename=None, 109 | file=DESCRIPTOR, 110 | containing_type=None, 111 | fields=[ 112 | _descriptor.FieldDescriptor( 113 | name='field1', full_name='google.protobuf.Bundled.field1', index=0, 114 | number=1, type=9, cpp_type=9, label=3, 115 | has_default_value=False, default_value=[], 116 | message_type=None, enum_type=None, containing_type=None, 117 | is_extension=False, extension_scope=None, 118 | options=None), 119 | ], 120 | extensions=[ 121 | ], 122 | nested_types=[], 123 | enum_types=[ 124 | ], 125 | options=None, 126 | is_extendable=False, 127 | syntax='proto3', 128 | extension_ranges=[], 129 | oneofs=[ 130 | ], 131 | serialized_start=150, 132 | serialized_end=175, 133 | ) 134 | 135 | _OUTER.fields_by_name['inner'].message_type = _SIMPLE 136 | DESCRIPTOR.message_types_by_name['Simple'] = _SIMPLE 137 | DESCRIPTOR.message_types_by_name['Outer'] = _OUTER 138 | DESCRIPTOR.message_types_by_name['Bundled'] = _BUNDLED 139 | 140 | Simple = _reflection.GeneratedProtocolMessageType('Simple', (_message.Message,), dict( 141 | DESCRIPTOR = _SIMPLE, 142 | __module__ = 'fixtures.fixture_pb2' 143 | # @@protoc_insertion_point(class_scope:google.protobuf.Simple) 144 | )) 145 | _sym_db.RegisterMessage(Simple) 146 | 147 | Outer = _reflection.GeneratedProtocolMessageType('Outer', (_message.Message,), dict( 148 | DESCRIPTOR = _OUTER, 149 | __module__ = 'fixtures.fixture_pb2' 150 | # @@protoc_insertion_point(class_scope:google.protobuf.Outer) 151 | )) 152 | _sym_db.RegisterMessage(Outer) 153 | 154 | Bundled = _reflection.GeneratedProtocolMessageType('Bundled', (_message.Message,), dict( 155 | DESCRIPTOR = _BUNDLED, 156 | __module__ = 'fixtures.fixture_pb2' 157 | # @@protoc_insertion_point(class_scope:google.protobuf.Bundled) 158 | )) 159 | _sym_db.RegisterMessage(Bundled) 160 | 161 | 162 | # @@protoc_insertion_point(module_scope) 163 | -------------------------------------------------------------------------------- /tests/test__grpc_google_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring,no-self-use,no-init,invalid-name 31 | # pylint: disable=protected-access 32 | """Unit tests for _grpc_google_auth.""" 33 | 34 | from __future__ import absolute_import 35 | 36 | import sys 37 | 38 | import google_auth_httplib2 39 | import mock 40 | from six.moves import reload_module 41 | import unittest2 42 | 43 | import google.auth.credentials 44 | import google.auth.transport.requests 45 | from google.gax import _grpc_google_auth 46 | 47 | 48 | class TestGetDefaultCredentials(unittest2.TestCase): 49 | @mock.patch('google.auth.default', autospec=True) 50 | def test(self, default_mock): 51 | default_mock.return_value = ( 52 | mock.sentinel.credentials, mock.sentinel.project) 53 | scopes = ['fake', 'scopes'] 54 | 55 | got = _grpc_google_auth.get_default_credentials(scopes) 56 | 57 | self.assertEqual(got, mock.sentinel.credentials) 58 | default_mock.assert_called_once_with(scopes) 59 | 60 | 61 | class TestSecureAuthorizedChannel(unittest2.TestCase): 62 | @mock.patch('google.gax._grpc_google_auth._request_factory', autospec=True) 63 | @mock.patch( 64 | 'google.auth.transport.grpc.secure_authorized_channel', autospec=True) 65 | def test(self, secure_authorized_channel_mock, request_factory_mock): 66 | 67 | got_channel = _grpc_google_auth.secure_authorized_channel( 68 | mock.sentinel.credentials, 69 | mock.sentinel.target) 70 | 71 | request_factory_mock.assert_called_once_with() 72 | secure_authorized_channel_mock.assert_called_once_with( 73 | mock.sentinel.credentials, 74 | request_factory_mock.return_value, 75 | mock.sentinel.target, 76 | ssl_credentials=None) 77 | 78 | self.assertEqual( 79 | got_channel, secure_authorized_channel_mock.return_value) 80 | 81 | 82 | class TestRequestsRequestFactory(unittest2.TestCase): 83 | def test(self): 84 | self.assertEqual( 85 | _grpc_google_auth._request_factory, 86 | google.auth.transport.requests.Request) 87 | 88 | 89 | class TestHttplib2RequestFactory(unittest2.TestCase): 90 | def setUp(self): 91 | # Block requests module during this test. 92 | self._requests_module = sys.modules['google.auth.transport.requests'] 93 | sys.modules['google.auth.transport.requests'] = None 94 | reload_module(_grpc_google_auth) 95 | 96 | def tearDown(self): 97 | sys.modules['google.auth.transport.requests'] = self._requests_module 98 | reload_module(_grpc_google_auth) 99 | 100 | def test(self): 101 | request = _grpc_google_auth._request_factory() 102 | self.assertIsInstance(request, google_auth_httplib2.Request) 103 | 104 | 105 | class TestNoHttpClient(unittest2.TestCase): 106 | def setUp(self): 107 | # Block all transport modules during this test. 108 | self._requests_module = sys.modules['google.auth.transport.requests'] 109 | self._httplib2_module = sys.modules['google_auth_httplib2'] 110 | sys.modules['google.auth.transport.requests'] = None 111 | sys.modules['google_auth_httplib2'] = None 112 | 113 | def tearDown(self): 114 | sys.modules['google.auth.transport.requests'] = self._requests_module 115 | sys.modules['google_auth_httplib2'] = self._httplib2_module 116 | reload_module(_grpc_google_auth) 117 | 118 | def test(self): 119 | with self.assertRaises(ImportError): 120 | reload_module(_grpc_google_auth) 121 | -------------------------------------------------------------------------------- /tests/test_gax.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring,no-self-use,no-init,invalid-name,protected-access,too-many-public-methods 31 | """Unit tests for gax package globals.""" 32 | 33 | from __future__ import absolute_import 34 | 35 | import logging 36 | import multiprocessing as mp 37 | 38 | import mock 39 | import unittest2 40 | 41 | from google.gax import ( 42 | _CallSettings, _LOG, _OperationFuture, BundleOptions, CallOptions, 43 | INITIAL_PAGE, OPTION_INHERIT, RetryOptions) 44 | from google.gax.errors import GaxError, RetryError 45 | from google.longrunning import operations_pb2 46 | from google.rpc import code_pb2, status_pb2 47 | 48 | from tests.fixtures.fixture_pb2 import Simple 49 | 50 | 51 | class TestBundleOptions(unittest2.TestCase): 52 | 53 | def test_cannot_construct_with_noarg_options(self): 54 | self.assertRaises(AssertionError, 55 | BundleOptions) 56 | 57 | def test_cannot_construct_with_bad_options(self): 58 | not_an_int = 'i am a string' 59 | self.assertRaises(AssertionError, 60 | BundleOptions, 61 | element_count_threshold=not_an_int) 62 | self.assertRaises(AssertionError, 63 | BundleOptions, 64 | request_byte_threshold=not_an_int) 65 | self.assertRaises(AssertionError, 66 | BundleOptions, 67 | delay_threshold=not_an_int) 68 | 69 | 70 | class TestCallSettings(unittest2.TestCase): 71 | 72 | def test_call_options_simple(self): 73 | options = CallOptions(timeout=23) 74 | self.assertEqual(options.timeout, 23) 75 | self.assertEqual(options.retry, OPTION_INHERIT) 76 | self.assertEqual(options.page_token, OPTION_INHERIT) 77 | 78 | def test_cannot_construct_bad_options(self): 79 | self.assertRaises( 80 | ValueError, CallOptions, timeout=47, retry=RetryOptions(None, None)) 81 | 82 | def test_settings_merge_options1(self): 83 | options = CallOptions(timeout=46) 84 | settings = _CallSettings(timeout=9, page_descriptor=None, retry=None) 85 | final = settings.merge(options) 86 | self.assertEqual(final.timeout, 46) 87 | self.assertIsNone(final.retry) 88 | self.assertIsNone(final.page_descriptor) 89 | 90 | def test_settings_merge_options2(self): 91 | retry = RetryOptions(None, None) 92 | options = CallOptions(retry=retry) 93 | settings = _CallSettings( 94 | timeout=9, page_descriptor=None, retry=RetryOptions(None, None)) 95 | final = settings.merge(options) 96 | self.assertEqual(final.timeout, 9) 97 | self.assertIsNone(final.page_descriptor) 98 | self.assertEqual(final.retry, retry) 99 | 100 | def test_settings_merge_options_page_streaming(self): 101 | retry = RetryOptions(None, None) 102 | page_descriptor = object() 103 | options = CallOptions(timeout=46, page_token=INITIAL_PAGE) 104 | settings = _CallSettings(timeout=9, retry=retry, 105 | page_descriptor=page_descriptor) 106 | final = settings.merge(options) 107 | self.assertEqual(final.timeout, 46) 108 | self.assertEqual(final.page_descriptor, page_descriptor) 109 | self.assertEqual(final.page_token, INITIAL_PAGE) 110 | self.assertFalse(final.flatten_pages) 111 | self.assertEqual(final.retry, retry) 112 | 113 | def test_settings_merge_none(self): 114 | settings = _CallSettings( 115 | timeout=23, page_descriptor=object(), bundler=object(), 116 | retry=object()) 117 | final = settings.merge(None) 118 | self.assertEqual(final.timeout, settings.timeout) 119 | self.assertEqual(final.retry, settings.retry) 120 | self.assertEqual(final.page_descriptor, settings.page_descriptor) 121 | self.assertEqual(final.bundler, settings.bundler) 122 | self.assertEqual(final.bundle_descriptor, settings.bundle_descriptor) 123 | 124 | 125 | def _task1(operation_future): 126 | operation_future.test_queue.put(operation_future.result().field1) 127 | 128 | 129 | def _task2(operation_future): 130 | operation_future.test_queue.put(operation_future.result().field2) 131 | 132 | 133 | class _FakeOperationsClient(object): 134 | def __init__(self, operations): 135 | self.operations = list(reversed(operations)) 136 | 137 | def get_operation(self, *_): 138 | return self.operations.pop() 139 | 140 | def cancel_operation(self, *_): 141 | pass 142 | 143 | 144 | class _FakeLoggingHandler(logging.Handler): 145 | def __init__(self, *args, **kwargs): 146 | self.queue = mp.Queue() 147 | super(_FakeLoggingHandler, self).__init__(*args, **kwargs) 148 | 149 | def emit(self, record): 150 | self.acquire() 151 | try: 152 | self.queue.put(record.getMessage()) 153 | finally: 154 | self.release() 155 | 156 | def reset(self): 157 | self.acquire() 158 | try: 159 | self.queue = mp.Queue() 160 | finally: 161 | self.release() 162 | 163 | 164 | class TestOperationFuture(unittest2.TestCase): 165 | 166 | OPERATION_NAME = 'operations/projects/foo/instances/bar/operations/123' 167 | 168 | @classmethod 169 | def setUpClass(cls): 170 | cls._log_handler = _FakeLoggingHandler(level='DEBUG') 171 | _LOG.addHandler(cls._log_handler) 172 | 173 | def setUp(self): 174 | self._log_handler.reset() 175 | 176 | def _make_operation(self, metadata=None, response=None, error=None, 177 | **kwargs): 178 | operation = operations_pb2.Operation(name=self.OPERATION_NAME, **kwargs) 179 | 180 | if metadata is not None: 181 | operation.metadata.Pack(metadata) 182 | 183 | if response is not None: 184 | operation.response.Pack(response) 185 | 186 | if error is not None: 187 | operation.error.CopyFrom(error) 188 | 189 | return operation 190 | 191 | def _make_operation_future(self, *operations): 192 | if not operations: 193 | operations = [self._make_operation()] 194 | 195 | fake_client = _FakeOperationsClient(operations) 196 | return _OperationFuture(operations[0], fake_client, Simple, Simple) 197 | 198 | def test_cancel_issues_call_when_not_done(self): 199 | operation = self._make_operation() 200 | 201 | fake_client = _FakeOperationsClient([operation]) 202 | fake_client.cancel_operation = mock.Mock() 203 | 204 | operation_future = _OperationFuture( 205 | operation, fake_client, Simple, Simple) 206 | 207 | self.assertTrue(operation_future.cancel()) 208 | fake_client.cancel_operation.assert_called_with(self.OPERATION_NAME) 209 | 210 | def test_cancel_does_nothing_when_already_done(self): 211 | operation = self._make_operation(done=True) 212 | 213 | fake_client = _FakeOperationsClient([operation]) 214 | fake_client.cancel_operation = mock.Mock() 215 | 216 | operation_future = _OperationFuture( 217 | operation, fake_client, Simple, Simple) 218 | 219 | self.assertFalse(operation_future.cancel()) 220 | fake_client.cancel_operation.assert_not_called() 221 | 222 | def test_cancelled_true(self): 223 | error = status_pb2.Status(code=code_pb2.CANCELLED) 224 | operation = self._make_operation(error=error) 225 | operation_future = self._make_operation_future(operation) 226 | 227 | self.assertTrue(operation_future.cancelled()) 228 | 229 | def test_cancelled_false(self): 230 | operation = self._make_operation(error=status_pb2.Status()) 231 | operation_future = self._make_operation_future(operation) 232 | self.assertFalse(operation_future.cancelled()) 233 | 234 | def test_done_true(self): 235 | operation = self._make_operation(done=True) 236 | operation_future = self._make_operation_future(operation) 237 | self.assertTrue(operation_future.done()) 238 | 239 | def test_done_false(self): 240 | operation_future = self._make_operation_future() 241 | self.assertFalse(operation_future.done()) 242 | 243 | def test_operation_name(self): 244 | operation_future = self._make_operation_future() 245 | self.assertEqual(self.OPERATION_NAME, operation_future.operation_name()) 246 | 247 | def test_metadata(self): 248 | metadata = Simple() 249 | operation = self._make_operation(metadata=metadata) 250 | operation_future = self._make_operation_future(operation) 251 | 252 | self.assertEqual(metadata, operation_future.metadata()) 253 | 254 | def test_metadata_none(self): 255 | operation_future = self._make_operation_future() 256 | self.assertIsNone(operation_future.metadata()) 257 | 258 | def test_last_operation_data(self): 259 | operation = self._make_operation() 260 | operation_future = self._make_operation_future(operation) 261 | self.assertEqual(operation, operation_future.last_operation_data()) 262 | 263 | def test_result_response(self): 264 | response = Simple() 265 | operation = self._make_operation(done=True, response=response) 266 | operation_future = self._make_operation_future(operation) 267 | 268 | self.assertEqual(response, operation_future.result()) 269 | 270 | def test_result_error(self): 271 | operation = self._make_operation(done=True, error=status_pb2.Status()) 272 | operation_future = self._make_operation_future(operation) 273 | self.assertRaises(GaxError, operation_future.result) 274 | 275 | def test_result_timeout(self): 276 | operation_future = self._make_operation_future() 277 | self.assertRaises(RetryError, operation_future.result, 0) 278 | 279 | def test_exception_error(self): 280 | error = status_pb2.Status() 281 | operation = self._make_operation(done=True, error=error) 282 | operation_future = self._make_operation_future(operation) 283 | 284 | self.assertEqual(error, operation_future.exception()) 285 | 286 | def test_exception_response(self): 287 | operation = self._make_operation(done=True, response=Simple()) 288 | operation_future = self._make_operation_future(operation) 289 | self.assertIsNone(operation_future.exception()) 290 | 291 | def test_exception_timeout(self): 292 | operation_future = self._make_operation_future() 293 | self.assertRaises(RetryError, operation_future.exception, 0) 294 | 295 | def test_add_done_callback(self): 296 | response = Simple(field1='foo', field2='bar') 297 | operation_future = self._make_operation_future( 298 | self._make_operation(), 299 | self._make_operation(done=True, response=response)) 300 | operation_future.test_queue = mp.Queue() 301 | 302 | operation_future.add_done_callback(_task1) 303 | operation_future.add_done_callback(_task2) 304 | 305 | self.assertEqual('foo', operation_future.test_queue.get()) 306 | self.assertEqual('bar', operation_future.test_queue.get()) 307 | 308 | def test_add_done_callback_when_already_done(self): 309 | response = Simple(field1='foo', field2='bar') 310 | operation_future = self._make_operation_future( 311 | self._make_operation(done=True, response=response)) 312 | operation_future.test_queue = mp.Queue() 313 | 314 | operation_future.add_done_callback(_task1) 315 | 316 | self.assertEqual('foo', operation_future.test_queue.get()) 317 | 318 | def test_add_done_callback_when_exception(self): 319 | def _raising_task(_): 320 | raise Exception('Test message') 321 | 322 | operation_future = self._make_operation_future( 323 | self._make_operation(), 324 | self._make_operation(done=True, response=Simple())) 325 | operation_future.add_done_callback(_raising_task) 326 | self.assertEqual('Test message', self._log_handler.queue.get()) 327 | -------------------------------------------------------------------------------- /tests/test_grpc.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring,no-self-use,no-init,invalid-name 31 | """Unit tests for grpc.""" 32 | 33 | from __future__ import absolute_import 34 | 35 | import mock 36 | import unittest2 37 | 38 | from google.gax import grpc 39 | 40 | 41 | def _fake_create_stub(channel): 42 | return channel 43 | 44 | 45 | class TestCreateStub(unittest2.TestCase): 46 | FAKE_SERVICE_PATH = 'service_path' 47 | FAKE_PORT = 10101 48 | 49 | @mock.patch('google.gax._grpc_google_auth.get_default_credentials') 50 | @mock.patch('google.gax._grpc_google_auth.secure_authorized_channel') 51 | def test_creates_a_stub_with_default_credentials( 52 | self, secure_authorized_channel, get_default_credentials): 53 | fake_scopes = ['one', 'two'] 54 | got_channel = grpc.create_stub( 55 | _fake_create_stub, service_path=self.FAKE_SERVICE_PATH, 56 | service_port=self.FAKE_PORT, scopes=fake_scopes) 57 | 58 | get_default_credentials.assert_called_once_with(fake_scopes) 59 | secure_authorized_channel.assert_called_once_with( 60 | get_default_credentials.return_value, 61 | '{}:{}'.format(self.FAKE_SERVICE_PATH, self.FAKE_PORT), 62 | ssl_credentials=None) 63 | 64 | self.assertEqual(got_channel, secure_authorized_channel.return_value) 65 | 66 | @mock.patch('google.gax._grpc_google_auth.get_default_credentials') 67 | @mock.patch('google.gax._grpc_google_auth.secure_authorized_channel') 68 | def test_creates_a_stub_with_explicit_credentials( 69 | self, secure_authorized_channel, get_default_credentials): 70 | credentials = mock.Mock() 71 | got_channel = grpc.create_stub( 72 | _fake_create_stub, service_path=self.FAKE_SERVICE_PATH, 73 | service_port=self.FAKE_PORT, credentials=credentials) 74 | 75 | self.assertFalse(get_default_credentials.called) 76 | secure_authorized_channel.assert_called_once_with( 77 | credentials, 78 | '{}:{}'.format(self.FAKE_SERVICE_PATH, self.FAKE_PORT), 79 | ssl_credentials=None) 80 | 81 | self.assertEqual(got_channel, secure_authorized_channel.return_value) 82 | 83 | @mock.patch('google.gax._grpc_google_auth.get_default_credentials') 84 | @mock.patch('google.gax._grpc_google_auth.secure_authorized_channel') 85 | def test_creates_a_stub_with_given_channel( 86 | self, secure_authorized_channel, get_default_credentials): 87 | fake_channel = mock.Mock() 88 | got_channel = grpc.create_stub( 89 | _fake_create_stub, channel=fake_channel) 90 | self.assertEqual(got_channel, fake_channel) 91 | self.assertFalse(secure_authorized_channel.called) 92 | self.assertFalse(get_default_credentials.called) 93 | 94 | 95 | class TestErrors(unittest2.TestCase): 96 | class MyError(grpc.RpcError): 97 | def code(self): 98 | return grpc.StatusCode.UNKNOWN 99 | 100 | def test_exc_to_code(self): 101 | code = grpc.exc_to_code(TestErrors.MyError()) 102 | self.assertEqual(code, grpc.StatusCode.UNKNOWN) 103 | self.assertEqual(code, grpc.STATUS_CODE_NAMES['UNKNOWN']) 104 | self.assertIsNone(grpc.exc_to_code(Exception)) 105 | self.assertIsNone(grpc.exc_to_code(grpc.RpcError())) 106 | -------------------------------------------------------------------------------- /tests/test_path_template.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring,no-self-use,no-init,invalid-name 31 | """Unit tests for the path_template module.""" 32 | 33 | from __future__ import absolute_import 34 | 35 | import unittest2 36 | 37 | from google.gax.path_template import PathTemplate, ValidationException 38 | 39 | 40 | class TestPathTemplate(unittest2.TestCase): 41 | """Unit tests for PathTemplate.""" 42 | 43 | def test_len(self): 44 | self.assertEqual(len(PathTemplate('a/b/**/*/{a=hello/world}')), 6) 45 | 46 | def test_fail_invalid_token(self): 47 | self.assertRaises(ValidationException, 48 | PathTemplate, 'hello/wor*ld') 49 | 50 | def test_fail_when_impossible_match(self): 51 | template = PathTemplate('hello/world') 52 | self.assertRaises(ValidationException, 53 | template.match, 'hello') 54 | template = PathTemplate('hello/world') 55 | self.assertRaises(ValidationException, 56 | template.match, 'hello/world/fail') 57 | 58 | def test_fail_mismatched_literal(self): 59 | template = PathTemplate('hello/world') 60 | self.assertRaises(ValidationException, 61 | template.match, 'hello/world2') 62 | 63 | def test_fail_when_multiple_path_wildcards(self): 64 | self.assertRaises(ValidationException, 65 | PathTemplate, 'buckets/*/**/**/objects/*') 66 | 67 | def test_fail_if_inner_binding(self): 68 | self.assertRaises(ValidationException, 69 | PathTemplate, 'buckets/{hello={world}}') 70 | 71 | def test_fail_unexpected_eof(self): 72 | self.assertRaises(ValidationException, 73 | PathTemplate, 'a/{hello=world') 74 | 75 | def test_match_atomic_resource_name(self): 76 | template = PathTemplate('buckets/*/*/objects/*') 77 | self.assertEqual({'$0': 'f', '$1': 'o', '$2': 'bar'}, 78 | template.match('buckets/f/o/objects/bar')) 79 | template = PathTemplate('/buckets/{hello}') 80 | self.assertEqual({'hello': 'world'}, 81 | template.match('buckets/world')) 82 | template = PathTemplate('/buckets/{hello=*}') 83 | self.assertEqual({'hello': 'world'}, 84 | template.match('buckets/world')) 85 | 86 | def test_match_escaped_chars(self): 87 | template = PathTemplate('buckets/*/objects') 88 | self.assertEqual({'$0': 'hello%2F%2Bworld'}, 89 | template.match('buckets/hello%2F%2Bworld/objects')) 90 | 91 | def test_match_template_with_unbounded_wildcard(self): 92 | template = PathTemplate('buckets/*/objects/**') 93 | self.assertEqual({'$0': 'foo', '$1': 'bar/baz'}, 94 | template.match('buckets/foo/objects/bar/baz')) 95 | 96 | def test_match_with_unbound_in_middle(self): 97 | template = PathTemplate('bar/**/foo/*') 98 | self.assertEqual({'$0': 'foo/foo', '$1': 'bar'}, 99 | template.match('bar/foo/foo/foo/bar')) 100 | 101 | def test_render_atomic_resource(self): 102 | template = PathTemplate('buckets/*/*/*/objects/*') 103 | url = template.render({ 104 | '$0': 'f', '$1': 'o', '$2': 'o', '$3': 'google.com:a-b'}) 105 | self.assertEqual(url, 'buckets/f/o/o/objects/google.com:a-b') 106 | 107 | def test_render_fail_when_too_few_variables(self): 108 | template = PathTemplate('buckets/*/*/*/objects/*') 109 | self.assertRaises(ValidationException, 110 | template.render, 111 | {'$0': 'f', '$1': 'l', '$2': 'o'}) 112 | 113 | def test_render_with_unbound_in_middle(self): 114 | template = PathTemplate('bar/**/foo/*') 115 | url = template.render({'$0': '1/2', '$1': '3'}) 116 | self.assertEqual(url, 'bar/1/2/foo/3') 117 | 118 | def test_to_string(self): 119 | template = PathTemplate('bar/**/foo/*') 120 | self.assertEqual(str(template), 'bar/{$0=**}/foo/{$1=*}') 121 | template = PathTemplate('buckets/*/objects/*') 122 | self.assertEqual(str(template), 'buckets/{$0=*}/objects/{$1=*}') 123 | template = PathTemplate('/buckets/{hello}') 124 | self.assertEqual(str(template), 'buckets/{hello=*}') 125 | template = PathTemplate('/buckets/{hello=what}/{world}') 126 | self.assertEqual(str(template), 'buckets/{hello=what}/{world=*}') 127 | template = PathTemplate('/buckets/helloazAZ09-.~_what') 128 | self.assertEqual(str(template), 'buckets/helloazAZ09-.~_what') 129 | -------------------------------------------------------------------------------- /tests/test_retry.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | # pylint: disable=missing-docstring,invalid-name 31 | """Unit tests for retry""" 32 | 33 | from __future__ import absolute_import, division 34 | 35 | import mock 36 | import unittest2 37 | 38 | from google.gax import BackoffSettings, errors, retry, RetryOptions 39 | 40 | _MILLIS_PER_SEC = 1000 41 | 42 | 43 | _FAKE_STATUS_CODE_1 = object() 44 | 45 | 46 | _FAKE_STATUS_CODE_2 = object() 47 | 48 | 49 | class CustomException(Exception): 50 | def __init__(self, msg, code): 51 | super(CustomException, self).__init__(msg) 52 | self.code = code 53 | 54 | 55 | class TestRetry(unittest2.TestCase): 56 | 57 | @mock.patch('google.gax.config.exc_to_code') 58 | @mock.patch('time.time') 59 | def test_retryable_without_timeout(self, mock_time, mock_exc_to_code): 60 | mock_time.return_value = 0 61 | mock_exc_to_code.side_effect = lambda e: e.code 62 | 63 | to_attempt = 3 64 | mock_call = mock.Mock() 65 | mock_call.side_effect = ([CustomException('', _FAKE_STATUS_CODE_1)] * 66 | (to_attempt - 1) + [mock.DEFAULT]) 67 | mock_call.return_value = 1729 68 | 69 | retry_options = RetryOptions( 70 | [_FAKE_STATUS_CODE_1], 71 | BackoffSettings(0, 0, 0, None, None, None, None)) 72 | 73 | my_callable = retry.retryable(mock_call, retry_options) 74 | 75 | self.assertEqual(my_callable(None), 1729) 76 | self.assertEqual(to_attempt, mock_call.call_count) 77 | 78 | @mock.patch('google.gax.config.exc_to_code') 79 | @mock.patch('time.time') 80 | def test_retryable_with_timeout(self, mock_time, mock_exc_to_code): 81 | mock_time.return_value = 1 82 | mock_exc_to_code.side_effect = lambda e: e.code 83 | 84 | mock_call = mock.Mock() 85 | mock_call.side_effect = [CustomException('', _FAKE_STATUS_CODE_1), 86 | mock.DEFAULT] 87 | mock_call.return_value = 1729 88 | 89 | retry_options = RetryOptions( 90 | [_FAKE_STATUS_CODE_1], 91 | BackoffSettings(0, 0, 0, 0, 0, 0, 0)) 92 | 93 | my_callable = retry.retryable(mock_call, retry_options) 94 | 95 | self.assertRaises(errors.RetryError, my_callable) 96 | self.assertEqual(0, mock_call.call_count) 97 | 98 | @mock.patch('google.gax.config.exc_to_code') 99 | @mock.patch('time.time') 100 | def test_retryable_when_no_codes(self, mock_time, mock_exc_to_code): 101 | mock_time.return_value = 0 102 | mock_exc_to_code.side_effect = lambda e: e.code 103 | 104 | mock_call = mock.Mock() 105 | mock_call.side_effect = [CustomException('', _FAKE_STATUS_CODE_1), 106 | mock.DEFAULT] 107 | mock_call.return_value = 1729 108 | 109 | retry_options = RetryOptions( 110 | [], 111 | BackoffSettings(0, 0, 0, 0, 0, 0, 1)) 112 | 113 | my_callable = retry.retryable(mock_call, retry_options) 114 | 115 | try: 116 | my_callable(None) 117 | self.fail('Should not have been reached') 118 | except errors.RetryError as exc: 119 | self.assertIsInstance(exc.cause, CustomException) 120 | 121 | self.assertEqual(1, mock_call.call_count) 122 | 123 | @mock.patch('google.gax.config.exc_to_code') 124 | @mock.patch('time.time') 125 | def test_retryable_aborts_on_unexpected_exception( 126 | self, mock_time, mock_exc_to_code): 127 | mock_time.return_value = 0 128 | mock_exc_to_code.side_effect = lambda e: e.code 129 | 130 | mock_call = mock.Mock() 131 | mock_call.side_effect = [CustomException('', _FAKE_STATUS_CODE_2), 132 | mock.DEFAULT] 133 | mock_call.return_value = 1729 134 | 135 | retry_options = RetryOptions( 136 | [_FAKE_STATUS_CODE_1], 137 | BackoffSettings(0, 0, 0, 0, 0, 0, 1)) 138 | 139 | my_callable = retry.retryable(mock_call, retry_options) 140 | 141 | try: 142 | my_callable(None) 143 | self.fail('Should not have been reached') 144 | except errors.RetryError as exc: 145 | self.assertIsInstance(exc.cause, CustomException) 146 | 147 | self.assertEqual(1, mock_call.call_count) 148 | 149 | @mock.patch('google.gax.config.exc_to_code') 150 | @mock.patch('time.sleep') 151 | @mock.patch('time.time') 152 | def test_retryable_exponential_backoff( 153 | self, mock_time, mock_sleep, mock_exc_to_code): 154 | def incr_time(secs): 155 | mock_time.return_value += secs 156 | 157 | def api_call(timeout): 158 | incr_time(timeout) 159 | raise CustomException(str(timeout), _FAKE_STATUS_CODE_1) 160 | 161 | mock_time.return_value = 0 162 | mock_sleep.side_effect = incr_time 163 | mock_exc_to_code.side_effect = lambda e: e.code 164 | 165 | mock_call = mock.Mock() 166 | mock_call.side_effect = api_call 167 | 168 | params = BackoffSettings(3, 2, 24, 5, 2, 80, 2500) 169 | retry_options = RetryOptions([_FAKE_STATUS_CODE_1], params) 170 | 171 | my_callable = retry.retryable(mock_call, retry_options) 172 | 173 | try: 174 | my_callable() 175 | self.fail('Should not have been reached') 176 | except errors.RetryError as exc: 177 | self.assertIsInstance(exc.cause, CustomException) 178 | 179 | self.assertGreaterEqual(mock_time(), 180 | params.total_timeout_millis / _MILLIS_PER_SEC) 181 | 182 | # Very rough bounds 183 | calls_lower_bound = params.total_timeout_millis / ( 184 | params.max_retry_delay_millis + params.max_rpc_timeout_millis) 185 | self.assertGreater(mock_call.call_count, calls_lower_bound) 186 | 187 | calls_upper_bound = (params.total_timeout_millis / 188 | params.initial_retry_delay_millis) 189 | self.assertLess(mock_call.call_count, calls_upper_bound) 190 | -------------------------------------------------------------------------------- /tests/test_utils_messages.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | from __future__ import absolute_import 31 | 32 | import unittest 33 | 34 | from google.gax.utils import messages 35 | from google.protobuf.message import Message 36 | from google.type import date_pb2 37 | 38 | 39 | class MessagesTests(unittest.TestCase): 40 | def test_get_messages(self): 41 | answer = messages.get_messages(date_pb2) 42 | 43 | # Ensure that Date was exported properly. 44 | assert answer['Date'] is date_pb2.Date 45 | 46 | # Ensure that no non-Message objects were exported. 47 | for value in answer.values(): 48 | assert issubclass(value, Message) 49 | -------------------------------------------------------------------------------- /tests/test_utils_metrics.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | from __future__ import absolute_import 31 | 32 | import collections 33 | import platform 34 | import unittest 35 | 36 | import pkg_resources 37 | 38 | from google import gax 39 | from google.gax.utils import metrics 40 | 41 | 42 | # pylint: disable=no-member 43 | GRPC_VERSION = pkg_resources.get_distribution('grpcio').version 44 | # pylint: enable=no-member 45 | 46 | 47 | class TestFill(unittest.TestCase): 48 | def test_no_argument(self): 49 | headers = metrics.fill() 50 | 51 | # Assert that the headers are set appropriately. 52 | self.assertEqual(headers['gl-python'], platform.python_version()) 53 | self.assertEqual(headers['gax'], gax.__version__) 54 | self.assertEqual(headers['grpc'], GRPC_VERSION) 55 | 56 | # Assert that the headers are in the correct order. 57 | self.assertEqual( 58 | [k for k in headers.keys()], 59 | ['gl-python', 'gax', 'grpc'], 60 | ) 61 | 62 | def test_gapic(self): 63 | headers = metrics.fill(collections.OrderedDict(( 64 | ('gl-python', platform.python_version()), 65 | ('gapic', '1.0.0'), 66 | ))) 67 | 68 | # Assert that the headers are set appropriately. 69 | self.assertEqual(headers['gl-python'], platform.python_version()) 70 | self.assertEqual(headers['gapic'], '1.0.0') 71 | self.assertEqual(headers['gax'], gax.__version__) 72 | self.assertEqual(headers['grpc'], GRPC_VERSION) 73 | 74 | # Assert that the headers are in the correct order. 75 | self.assertEqual( 76 | [k for k in headers.keys()], 77 | ['gl-python', 'gapic', 'gax', 'grpc'], 78 | ) 79 | 80 | 81 | class TestStringify(unittest.TestCase): 82 | def test_no_argument(self): 83 | self.assertEqual(metrics.stringify(), '') 84 | 85 | def test_with_unordered_argument(self): 86 | string = metrics.stringify({ 87 | 'gl-python': platform.python_version(), 88 | 'gapic': '1.0.0', 89 | 'gax': gax.__version__, 90 | 'grpc': GRPC_VERSION, 91 | }) 92 | 93 | # Assert that each section of the string is present, but 94 | # ignore ordering. 95 | self.assertIn('gl-python/%s' % platform.python_version(), string) 96 | self.assertIn('gapic/1.0.0', string) 97 | self.assertIn('gax/%s' % gax.__version__, string) 98 | self.assertIn('grpc/%s' % GRPC_VERSION, string) 99 | 100 | def test_with_ordered_argument(self): 101 | headers = collections.OrderedDict() 102 | headers['gl-python'] = platform.python_version() 103 | headers['gapic'] = '1.0.0' 104 | headers['gax'] = gax.__version__ 105 | headers['grpc'] = GRPC_VERSION 106 | string = metrics.stringify(headers) 107 | 108 | # Check for the exact string, order and all. 109 | expected = 'gl-python/{python} gapic/{gapic} gax/{gax} grpc/{grpc}' 110 | self.assertEqual(string, expected.format( 111 | gapic='1.0.0', 112 | gax=gax.__version__, 113 | grpc=GRPC_VERSION, 114 | python=platform.python_version(), 115 | )) 116 | -------------------------------------------------------------------------------- /tests/test_utils_oneof.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | from __future__ import absolute_import 31 | 32 | import unittest 33 | 34 | from google.gax.utils import oneof 35 | 36 | 37 | class TestOneof(unittest.TestCase): 38 | def test_check_ok(self): 39 | self.assertIsNone(oneof.check_oneof()) 40 | self.assertIsNone(oneof.check_oneof(foo='bar')) 41 | self.assertIsNone(oneof.check_oneof(foo='bar', baz=None)) 42 | self.assertIsNone(oneof.check_oneof(foo=None, baz='bacon')) 43 | self.assertIsNone(oneof.check_oneof(foo='bar', spam=None, eggs=None)) 44 | 45 | def test_check_failures(self): 46 | with self.assertRaises(ValueError): 47 | oneof.check_oneof(foo='bar', spam='eggs') 48 | with self.assertRaises(ValueError): 49 | oneof.check_oneof(foo='bar', baz='bacon', spam='eggs') 50 | with self.assertRaises(ValueError): 51 | oneof.check_oneof(foo='bar', spam=0, eggs=None) 52 | -------------------------------------------------------------------------------- /tests/test_utils_protobuf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017, Google Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are 6 | # met: 7 | # 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above 11 | # copyright notice, this list of conditions and the following disclaimer 12 | # in the documentation and/or other materials provided with the 13 | # distribution. 14 | # * Neither the name of Google Inc. nor the names of its 15 | # contributors may be used to endorse or promote products derived from 16 | # this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | import unittest 31 | 32 | import pytest 33 | 34 | from google.api import http_pb2 35 | from google.gax.utils import protobuf 36 | from google.longrunning import operations_proto_pb2 as ops 37 | 38 | 39 | class GetTests(unittest.TestCase): 40 | def test_get_dict_sentinel(self): 41 | with pytest.raises(KeyError): 42 | assert protobuf.get({}, 'foo') 43 | 44 | def test_get_dict_present(self): 45 | assert protobuf.get({'foo': 'bar'}, 'foo') == 'bar' 46 | 47 | def test_get_dict_default(self): 48 | assert protobuf.get({}, 'foo', default='bar') == 'bar' 49 | 50 | def test_get_dict_nested(self): 51 | assert protobuf.get({'foo': {'bar': 'baz'}}, 'foo.bar') == 'baz' 52 | 53 | def test_get_dict_nested_default(self): 54 | assert protobuf.get({}, 'foo.baz', default='bacon') == 'bacon' 55 | assert protobuf.get({'foo': {}}, 'foo.baz', default='bacon') == 'bacon' 56 | 57 | def test_get_pb2_sentinel(self): 58 | operation = ops.Operation() 59 | with pytest.raises(KeyError): 60 | assert protobuf.get(operation, 'foo') 61 | 62 | def test_get_pb2_present(self): 63 | operation = ops.Operation(name='foo') 64 | assert protobuf.get(operation, 'name') == 'foo' 65 | 66 | def test_get_pb2_default(self): 67 | operation = ops.Operation() 68 | assert protobuf.get(operation, 'foo', default='bar') == 'bar' 69 | 70 | def test_invalid_object(self): 71 | obj = object() 72 | with pytest.raises(TypeError): 73 | protobuf.get(obj, 'foo', 'bar') 74 | 75 | 76 | class SetTests(unittest.TestCase): 77 | def test_set_dict(self): 78 | mapping = {} 79 | protobuf.set(mapping, 'foo', 'bar') 80 | assert mapping == {'foo': 'bar'} 81 | 82 | def test_set_pb2(self): 83 | operation = ops.Operation() 84 | protobuf.set(operation, 'name', 'foo') 85 | assert operation.name == 'foo' 86 | 87 | def test_set_nested(self): 88 | mapping = {} 89 | protobuf.set(mapping, 'foo.bar', 'baz') 90 | assert mapping == {'foo': {'bar': 'baz'}} 91 | 92 | def test_invalid_object(self): 93 | obj = object() 94 | with pytest.raises(TypeError): 95 | protobuf.set(obj, 'foo', 'bar') 96 | 97 | def test_set_list(self): 98 | list_ops_response = ops.ListOperationsResponse() 99 | protobuf.set(list_ops_response, 'operations', [ 100 | {'name': 'foo'}, 101 | ops.Operation(name='bar'), 102 | ]) 103 | assert len(list_ops_response.operations) == 2 104 | for operation in list_ops_response.operations: 105 | assert isinstance(operation, ops.Operation) 106 | assert list_ops_response.operations[0].name == 'foo' 107 | assert list_ops_response.operations[1].name == 'bar' 108 | 109 | def test_set_list_clear_existing(self): 110 | list_ops_response = ops.ListOperationsResponse( 111 | operations=[{'name': 'baz'}], 112 | ) 113 | protobuf.set(list_ops_response, 'operations', [ 114 | {'name': 'foo'}, 115 | ops.Operation(name='bar'), 116 | ]) 117 | assert len(list_ops_response.operations) == 2 118 | for operation in list_ops_response.operations: 119 | assert isinstance(operation, ops.Operation) 120 | assert list_ops_response.operations[0].name == 'foo' 121 | assert list_ops_response.operations[1].name == 'bar' 122 | 123 | def test_set_dict_nested_with_message(self): 124 | rule = http_pb2.HttpRule() 125 | pattern = http_pb2.CustomHttpPattern(kind='foo', path='bar') 126 | protobuf.set(rule, 'custom', pattern) 127 | assert rule.custom.kind == 'foo' 128 | assert rule.custom.path == 'bar' 129 | 130 | def test_set_dict_nested_with_dict(self): 131 | rule = http_pb2.HttpRule() 132 | pattern = {'kind': 'foo', 'path': 'bar'} 133 | protobuf.set(rule, 'custom', pattern) 134 | assert rule.custom.kind == 'foo' 135 | assert rule.custom.path == 'bar' 136 | 137 | 138 | class SetDefaultTests(unittest.TestCase): 139 | def test_dict_unset(self): 140 | mapping = {} 141 | protobuf.setdefault(mapping, 'foo', 'bar') 142 | assert mapping == {'foo': 'bar'} 143 | 144 | def test_dict_falsy(self): 145 | mapping = {'foo': None} 146 | protobuf.setdefault(mapping, 'foo', 'bar') 147 | assert mapping == {'foo': 'bar'} 148 | 149 | def test_dict_truthy(self): 150 | mapping = {'foo': 'bar'} 151 | protobuf.setdefault(mapping, 'foo', 'baz') 152 | assert mapping == {'foo': 'bar'} 153 | 154 | def test_pb2_falsy(self): 155 | operation = ops.Operation() 156 | protobuf.setdefault(operation, 'name', 'foo') 157 | assert operation.name == 'foo' 158 | 159 | def test_pb2_truthy(self): 160 | operation = ops.Operation(name='bar') 161 | protobuf.setdefault(operation, 'name', 'foo') 162 | assert operation.name == 'bar' 163 | --------------------------------------------------------------------------------