Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/INSTALLER +1 -0
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/LICENSE.txt +13 -0
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/METADATA +250 -0
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/RECORD +131 -0
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/WHEEL +6 -0
- .venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/top_level.txt +1 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__about__.py +28 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__init__.py +67 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/__about__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/abc.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/cors_config.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/mixin.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/preflight_handler.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/resource_options.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/urldispatcher_router_adapter.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/abc.py +100 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/cors_config.py +263 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/mixin.py +47 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/preflight_handler.py +130 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/resource_options.py +153 -0
- .venv/lib/python3.11/site-packages/aiohttp_cors/urldispatcher_router_adapter.py +324 -0
- .venv/lib/python3.11/site-packages/frozenlist/__init__.py +98 -0
- .venv/lib/python3.11/site-packages/frozenlist/__init__.pyi +47 -0
- .venv/lib/python3.11/site-packages/frozenlist/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/frozenlist/_frozenlist.pyx +123 -0
- .venv/lib/python3.11/site-packages/frozenlist/py.typed +1 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__init__.py +27 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/_auth.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/_helpers.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/channel.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/discovery.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/errors.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/http.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/mimeparse.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/model.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/sample_tools.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/schema.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/__pycache__/version.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/_auth.py +167 -0
- .venv/lib/python3.11/site-packages/googleapiclient/_helpers.py +207 -0
- .venv/lib/python3.11/site-packages/googleapiclient/channel.py +315 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery.py +1662 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__init__.py +78 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/appengine_memcache.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/base.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/file_cache.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/appengine_memcache.py +55 -0
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/LICENSE.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright aio-libs contributors.
|
| 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.
|
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/METADATA
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.2
|
| 2 |
+
Name: aiohttp
|
| 3 |
+
Version: 3.11.12
|
| 4 |
+
Summary: Async http client/server framework (asyncio)
|
| 5 |
+
Home-page: https://github.com/aio-libs/aiohttp
|
| 6 |
+
Maintainer: aiohttp team <team@aiohttp.org>
|
| 7 |
+
Maintainer-email: team@aiohttp.org
|
| 8 |
+
License: Apache-2.0
|
| 9 |
+
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
|
| 10 |
+
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
|
| 11 |
+
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
| 12 |
+
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
|
| 13 |
+
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
|
| 14 |
+
Project-URL: Docs: RTD, https://docs.aiohttp.org
|
| 15 |
+
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
|
| 16 |
+
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
|
| 17 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 18 |
+
Classifier: Framework :: AsyncIO
|
| 19 |
+
Classifier: Intended Audience :: Developers
|
| 20 |
+
Classifier: License :: OSI Approved :: Apache Software License
|
| 21 |
+
Classifier: Operating System :: POSIX
|
| 22 |
+
Classifier: Operating System :: MacOS :: MacOS X
|
| 23 |
+
Classifier: Operating System :: Microsoft :: Windows
|
| 24 |
+
Classifier: Programming Language :: Python
|
| 25 |
+
Classifier: Programming Language :: Python :: 3
|
| 26 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 27 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 28 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 29 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 30 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 31 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 32 |
+
Requires-Python: >=3.9
|
| 33 |
+
Description-Content-Type: text/x-rst
|
| 34 |
+
License-File: LICENSE.txt
|
| 35 |
+
Requires-Dist: aiohappyeyeballs>=2.3.0
|
| 36 |
+
Requires-Dist: aiosignal>=1.1.2
|
| 37 |
+
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
|
| 38 |
+
Requires-Dist: attrs>=17.3.0
|
| 39 |
+
Requires-Dist: frozenlist>=1.1.1
|
| 40 |
+
Requires-Dist: multidict<7.0,>=4.5
|
| 41 |
+
Requires-Dist: propcache>=0.2.0
|
| 42 |
+
Requires-Dist: yarl<2.0,>=1.17.0
|
| 43 |
+
Provides-Extra: speedups
|
| 44 |
+
Requires-Dist: aiodns>=3.2.0; (sys_platform == "linux" or sys_platform == "darwin") and extra == "speedups"
|
| 45 |
+
Requires-Dist: Brotli; platform_python_implementation == "CPython" and extra == "speedups"
|
| 46 |
+
Requires-Dist: brotlicffi; platform_python_implementation != "CPython" and extra == "speedups"
|
| 47 |
+
|
| 48 |
+
==================================
|
| 49 |
+
Async http client/server framework
|
| 50 |
+
==================================
|
| 51 |
+
|
| 52 |
+
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
|
| 53 |
+
:height: 64px
|
| 54 |
+
:width: 64px
|
| 55 |
+
:alt: aiohttp logo
|
| 56 |
+
|
| 57 |
+
|
|
| 58 |
+
|
| 59 |
+
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
|
| 60 |
+
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
|
| 61 |
+
:alt: GitHub Actions status for master branch
|
| 62 |
+
|
| 63 |
+
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
|
| 64 |
+
:target: https://codecov.io/gh/aio-libs/aiohttp
|
| 65 |
+
:alt: codecov.io status for master branch
|
| 66 |
+
|
| 67 |
+
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
|
| 68 |
+
:target: https://codspeed.io/aio-libs/aiohttp
|
| 69 |
+
:alt: Codspeed.io status for aiohttp
|
| 70 |
+
|
| 71 |
+
.. image:: https://badge.fury.io/py/aiohttp.svg
|
| 72 |
+
:target: https://pypi.org/project/aiohttp
|
| 73 |
+
:alt: Latest PyPI package version
|
| 74 |
+
|
| 75 |
+
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
|
| 76 |
+
:target: https://docs.aiohttp.org/
|
| 77 |
+
:alt: Latest Read The Docs
|
| 78 |
+
|
| 79 |
+
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
| 80 |
+
:target: https://matrix.to/#/%23aio-libs:matrix.org
|
| 81 |
+
:alt: Matrix Room — #aio-libs:matrix.org
|
| 82 |
+
|
| 83 |
+
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
|
| 84 |
+
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
|
| 85 |
+
:alt: Matrix Space — #aio-libs-space:matrix.org
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
Key Features
|
| 89 |
+
============
|
| 90 |
+
|
| 91 |
+
- Supports both client and server side of HTTP protocol.
|
| 92 |
+
- Supports both client and server Web-Sockets out-of-the-box and avoids
|
| 93 |
+
Callback Hell.
|
| 94 |
+
- Provides Web-server with middleware and pluggable routing.
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
Getting started
|
| 98 |
+
===============
|
| 99 |
+
|
| 100 |
+
Client
|
| 101 |
+
------
|
| 102 |
+
|
| 103 |
+
To get something from the web:
|
| 104 |
+
|
| 105 |
+
.. code-block:: python
|
| 106 |
+
|
| 107 |
+
import aiohttp
|
| 108 |
+
import asyncio
|
| 109 |
+
|
| 110 |
+
async def main():
|
| 111 |
+
|
| 112 |
+
async with aiohttp.ClientSession() as session:
|
| 113 |
+
async with session.get('http://python.org') as response:
|
| 114 |
+
|
| 115 |
+
print("Status:", response.status)
|
| 116 |
+
print("Content-type:", response.headers['content-type'])
|
| 117 |
+
|
| 118 |
+
html = await response.text()
|
| 119 |
+
print("Body:", html[:15], "...")
|
| 120 |
+
|
| 121 |
+
asyncio.run(main())
|
| 122 |
+
|
| 123 |
+
This prints:
|
| 124 |
+
|
| 125 |
+
.. code-block::
|
| 126 |
+
|
| 127 |
+
Status: 200
|
| 128 |
+
Content-type: text/html; charset=utf-8
|
| 129 |
+
Body: <!doctype html> ...
|
| 130 |
+
|
| 131 |
+
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
|
| 132 |
+
|
| 133 |
+
Server
|
| 134 |
+
------
|
| 135 |
+
|
| 136 |
+
An example using a simple server:
|
| 137 |
+
|
| 138 |
+
.. code-block:: python
|
| 139 |
+
|
| 140 |
+
# examples/server_simple.py
|
| 141 |
+
from aiohttp import web
|
| 142 |
+
|
| 143 |
+
async def handle(request):
|
| 144 |
+
name = request.match_info.get('name', "Anonymous")
|
| 145 |
+
text = "Hello, " + name
|
| 146 |
+
return web.Response(text=text)
|
| 147 |
+
|
| 148 |
+
async def wshandle(request):
|
| 149 |
+
ws = web.WebSocketResponse()
|
| 150 |
+
await ws.prepare(request)
|
| 151 |
+
|
| 152 |
+
async for msg in ws:
|
| 153 |
+
if msg.type == web.WSMsgType.text:
|
| 154 |
+
await ws.send_str("Hello, {}".format(msg.data))
|
| 155 |
+
elif msg.type == web.WSMsgType.binary:
|
| 156 |
+
await ws.send_bytes(msg.data)
|
| 157 |
+
elif msg.type == web.WSMsgType.close:
|
| 158 |
+
break
|
| 159 |
+
|
| 160 |
+
return ws
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
app = web.Application()
|
| 164 |
+
app.add_routes([web.get('/', handle),
|
| 165 |
+
web.get('/echo', wshandle),
|
| 166 |
+
web.get('/{name}', handle)])
|
| 167 |
+
|
| 168 |
+
if __name__ == '__main__':
|
| 169 |
+
web.run_app(app)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
Documentation
|
| 173 |
+
=============
|
| 174 |
+
|
| 175 |
+
https://aiohttp.readthedocs.io/
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
Demos
|
| 179 |
+
=====
|
| 180 |
+
|
| 181 |
+
https://github.com/aio-libs/aiohttp-demos
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
External links
|
| 185 |
+
==============
|
| 186 |
+
|
| 187 |
+
* `Third party libraries
|
| 188 |
+
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
|
| 189 |
+
* `Built with aiohttp
|
| 190 |
+
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
|
| 191 |
+
* `Powered by aiohttp
|
| 192 |
+
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
|
| 193 |
+
|
| 194 |
+
Feel free to make a Pull Request for adding your link to these pages!
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
Communication channels
|
| 198 |
+
======================
|
| 199 |
+
|
| 200 |
+
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
|
| 201 |
+
|
| 202 |
+
*Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
|
| 203 |
+
|
| 204 |
+
We support `Stack Overflow
|
| 205 |
+
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
|
| 206 |
+
Please add *aiohttp* tag to your question there.
|
| 207 |
+
|
| 208 |
+
Requirements
|
| 209 |
+
============
|
| 210 |
+
|
| 211 |
+
- attrs_
|
| 212 |
+
- multidict_
|
| 213 |
+
- yarl_
|
| 214 |
+
- frozenlist_
|
| 215 |
+
|
| 216 |
+
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
|
| 217 |
+
|
| 218 |
+
.. _aiodns: https://pypi.python.org/pypi/aiodns
|
| 219 |
+
.. _attrs: https://github.com/python-attrs/attrs
|
| 220 |
+
.. _multidict: https://pypi.python.org/pypi/multidict
|
| 221 |
+
.. _frozenlist: https://pypi.org/project/frozenlist/
|
| 222 |
+
.. _yarl: https://pypi.python.org/pypi/yarl
|
| 223 |
+
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
|
| 224 |
+
|
| 225 |
+
License
|
| 226 |
+
=======
|
| 227 |
+
|
| 228 |
+
``aiohttp`` is offered under the Apache 2 license.
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
Keepsafe
|
| 232 |
+
========
|
| 233 |
+
|
| 234 |
+
The aiohttp community would like to thank Keepsafe
|
| 235 |
+
(https://www.getkeepsafe.com) for its support in the early days of
|
| 236 |
+
the project.
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
Source code
|
| 240 |
+
===========
|
| 241 |
+
|
| 242 |
+
The latest developer version is available in a GitHub repository:
|
| 243 |
+
https://github.com/aio-libs/aiohttp
|
| 244 |
+
|
| 245 |
+
Benchmarks
|
| 246 |
+
==========
|
| 247 |
+
|
| 248 |
+
If you are interested in efficiency, the AsyncIO community maintains a
|
| 249 |
+
list of benchmarks on the official wiki:
|
| 250 |
+
https://github.com/python/asyncio/wiki/Benchmarks
|
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/RECORD
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohttp-3.11.12.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
aiohttp-3.11.12.dist-info/LICENSE.txt,sha256=n4DQ2311WpQdtFchcsJw7L2PCCuiFd3QlZhZQu2Uqes,588
|
| 3 |
+
aiohttp-3.11.12.dist-info/METADATA,sha256=d7xgT5HQPw4R1cWYIK6lQbOoCkFYV5505hAhDZWALcA,7712
|
| 4 |
+
aiohttp-3.11.12.dist-info/RECORD,,
|
| 5 |
+
aiohttp-3.11.12.dist-info/WHEEL,sha256=bWgrYGwmr1oqID2askDfuFruhD_BtRXYD33vDqPTClc,151
|
| 6 |
+
aiohttp-3.11.12.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
|
| 7 |
+
aiohttp/.hash/_cparser.pxd.hash,sha256=hYa9Vje-oMs2eh_7MfCPOh2QW_1x1yCjcZuc7AmwLd0,121
|
| 8 |
+
aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125
|
| 9 |
+
aiohttp/.hash/_http_parser.pyx.hash,sha256=GBgZjCNbtZApPhf9-gHpS5Z2WMIzM-vgp5VSZIEvZfk,125
|
| 10 |
+
aiohttp/.hash/_http_writer.pyx.hash,sha256=-UgSF82qclpxjP0og_gcFEsstXRKF9e3Ou4wziAyDvI,125
|
| 11 |
+
aiohttp/.hash/hdrs.py.hash,sha256=v6IaKbsxjsdQxBzhb5AjP0x_9G3rUe84D7avf7AI4cs,116
|
| 12 |
+
aiohttp/__init__.py,sha256=NFDgrak_EkgNNzRQ9Ka63rIzkN_IYocFcIERyltx5ys,7840
|
| 13 |
+
aiohttp/__pycache__/__init__.cpython-311.pyc,,
|
| 14 |
+
aiohttp/__pycache__/abc.cpython-311.pyc,,
|
| 15 |
+
aiohttp/__pycache__/base_protocol.cpython-311.pyc,,
|
| 16 |
+
aiohttp/__pycache__/client.cpython-311.pyc,,
|
| 17 |
+
aiohttp/__pycache__/client_exceptions.cpython-311.pyc,,
|
| 18 |
+
aiohttp/__pycache__/client_proto.cpython-311.pyc,,
|
| 19 |
+
aiohttp/__pycache__/client_reqrep.cpython-311.pyc,,
|
| 20 |
+
aiohttp/__pycache__/client_ws.cpython-311.pyc,,
|
| 21 |
+
aiohttp/__pycache__/compression_utils.cpython-311.pyc,,
|
| 22 |
+
aiohttp/__pycache__/connector.cpython-311.pyc,,
|
| 23 |
+
aiohttp/__pycache__/cookiejar.cpython-311.pyc,,
|
| 24 |
+
aiohttp/__pycache__/formdata.cpython-311.pyc,,
|
| 25 |
+
aiohttp/__pycache__/hdrs.cpython-311.pyc,,
|
| 26 |
+
aiohttp/__pycache__/helpers.cpython-311.pyc,,
|
| 27 |
+
aiohttp/__pycache__/http.cpython-311.pyc,,
|
| 28 |
+
aiohttp/__pycache__/http_exceptions.cpython-311.pyc,,
|
| 29 |
+
aiohttp/__pycache__/http_parser.cpython-311.pyc,,
|
| 30 |
+
aiohttp/__pycache__/http_websocket.cpython-311.pyc,,
|
| 31 |
+
aiohttp/__pycache__/http_writer.cpython-311.pyc,,
|
| 32 |
+
aiohttp/__pycache__/log.cpython-311.pyc,,
|
| 33 |
+
aiohttp/__pycache__/multipart.cpython-311.pyc,,
|
| 34 |
+
aiohttp/__pycache__/payload.cpython-311.pyc,,
|
| 35 |
+
aiohttp/__pycache__/payload_streamer.cpython-311.pyc,,
|
| 36 |
+
aiohttp/__pycache__/pytest_plugin.cpython-311.pyc,,
|
| 37 |
+
aiohttp/__pycache__/resolver.cpython-311.pyc,,
|
| 38 |
+
aiohttp/__pycache__/streams.cpython-311.pyc,,
|
| 39 |
+
aiohttp/__pycache__/tcp_helpers.cpython-311.pyc,,
|
| 40 |
+
aiohttp/__pycache__/test_utils.cpython-311.pyc,,
|
| 41 |
+
aiohttp/__pycache__/tracing.cpython-311.pyc,,
|
| 42 |
+
aiohttp/__pycache__/typedefs.cpython-311.pyc,,
|
| 43 |
+
aiohttp/__pycache__/web.cpython-311.pyc,,
|
| 44 |
+
aiohttp/__pycache__/web_app.cpython-311.pyc,,
|
| 45 |
+
aiohttp/__pycache__/web_exceptions.cpython-311.pyc,,
|
| 46 |
+
aiohttp/__pycache__/web_fileresponse.cpython-311.pyc,,
|
| 47 |
+
aiohttp/__pycache__/web_log.cpython-311.pyc,,
|
| 48 |
+
aiohttp/__pycache__/web_middlewares.cpython-311.pyc,,
|
| 49 |
+
aiohttp/__pycache__/web_protocol.cpython-311.pyc,,
|
| 50 |
+
aiohttp/__pycache__/web_request.cpython-311.pyc,,
|
| 51 |
+
aiohttp/__pycache__/web_response.cpython-311.pyc,,
|
| 52 |
+
aiohttp/__pycache__/web_routedef.cpython-311.pyc,,
|
| 53 |
+
aiohttp/__pycache__/web_runner.cpython-311.pyc,,
|
| 54 |
+
aiohttp/__pycache__/web_server.cpython-311.pyc,,
|
| 55 |
+
aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc,,
|
| 56 |
+
aiohttp/__pycache__/web_ws.cpython-311.pyc,,
|
| 57 |
+
aiohttp/__pycache__/worker.cpython-311.pyc,,
|
| 58 |
+
aiohttp/_cparser.pxd,sha256=8jGIg-VJ9p3llwCakUYDsPGxA4HiZe9dmK9Jmtlz-5g,4318
|
| 59 |
+
aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68
|
| 60 |
+
aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007
|
| 61 |
+
aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so,sha256=rulksFv5PZ7AOPoQuR2P1THFflfukMumxkEfRfdFk0Y,2826344
|
| 62 |
+
aiohttp/_http_parser.pyx,sha256=wQdADj5LizwC_7nFGr8nIlk6GpoaQeQ0359H0HMKGuM,28241
|
| 63 |
+
aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so,sha256=zMYCtCYo-xjYJpSCcVx6UdBpGSHm_pMgnpM7JUmsWXA,463752
|
| 64 |
+
aiohttp/_http_writer.pyx,sha256=fiCck_EVgRiTX7VtAoV2AldjuesJMFPev4TWd9Fx8jo,4597
|
| 65 |
+
aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128
|
| 66 |
+
aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128
|
| 67 |
+
aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=EoZjkF_tAFEbGvV0oRY2GZOSuAfWFWFjMhXgq6mQExo,132
|
| 68 |
+
aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44
|
| 69 |
+
aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc,,
|
| 70 |
+
aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc,,
|
| 71 |
+
aiohttp/_websocket/__pycache__/models.cpython-311.pyc,,
|
| 72 |
+
aiohttp/_websocket/__pycache__/reader.cpython-311.pyc,,
|
| 73 |
+
aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc,,
|
| 74 |
+
aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc,,
|
| 75 |
+
aiohttp/_websocket/__pycache__/writer.cpython-311.pyc,,
|
| 76 |
+
aiohttp/_websocket/helpers.py,sha256=P-XLv8IUaihKzDenVUqfKU5DJbWE5HvG8uhvUZK8Ic4,5038
|
| 77 |
+
aiohttp/_websocket/mask.cpython-311-x86_64-linux-gnu.so,sha256=hq3aHe5ZVl5ENFRtaXjZcSrbS-ITBwqGgEneGVphY1w,245952
|
| 78 |
+
aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112
|
| 79 |
+
aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397
|
| 80 |
+
aiohttp/_websocket/models.py,sha256=XAzjs_8JYszWXIgZ6R3ZRrF-tX9Q_6LiD49WRYojopM,2121
|
| 81 |
+
aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030
|
| 82 |
+
aiohttp/_websocket/reader_c.cpython-311-x86_64-linux-gnu.so,sha256=bnETOmm4gSxKkarM68sAEhBjSd_bQt0GgDD2W7V8cUQ,1906008
|
| 83 |
+
aiohttp/_websocket/reader_c.pxd,sha256=9rMWCpAC1jng7_gtqLjRlqQv9q7UkOn63tIQfq2k8Gc,2444
|
| 84 |
+
aiohttp/_websocket/reader_c.py,sha256=anZsBKZWlL8SO8gArsZMDstH37qBuZOvJA7jtj0Z95M,17975
|
| 85 |
+
aiohttp/_websocket/reader_py.py,sha256=anZsBKZWlL8SO8gArsZMDstH37qBuZOvJA7jtj0Z95M,17975
|
| 86 |
+
aiohttp/_websocket/writer.py,sha256=T3P36iMrzVPPC2XeScserHMD5vd9an6yizWzqDUkRZ0,7077
|
| 87 |
+
aiohttp/abc.py,sha256=JLMOxrKLGTDaPRLfraY1pl-xka53YiHhAH9yaF9QRXQ,6512
|
| 88 |
+
aiohttp/base_protocol.py,sha256=Tp8cxUPQvv9kUPk3w6lAzk6d2MAzV3scwI_3Go3C47c,3025
|
| 89 |
+
aiohttp/client.py,sha256=isdfGlM4O5ILr4F4gBABlybxo4MQ1tNaMm7zjMcrfrM,54309
|
| 90 |
+
aiohttp/client_exceptions.py,sha256=uyKbxI2peZhKl7lELBMx3UeusNkfpemPWpGFq0r6JeM,11367
|
| 91 |
+
aiohttp/client_proto.py,sha256=dV7u9floGWG-_xtD2fLUYqiANG6VsJtq0HMlTjf1g-g,10015
|
| 92 |
+
aiohttp/client_reqrep.py,sha256=VAgh0NxP2HvYWx6nX1Pr8FINc1m-W8-5q2zKeZV68n8,43925
|
| 93 |
+
aiohttp/client_ws.py,sha256=_n4hVk71H5rK8TFOIYT0bPTIHOmMCQ3FDFSrU7ctpfI,15031
|
| 94 |
+
aiohttp/compression_utils.py,sha256=0J3EAOR-0HehlYIudJXRu_Kr6hrYCY0IfuJ1px9MhQs,5681
|
| 95 |
+
aiohttp/connector.py,sha256=yW8vyZz4cmXbScbBkCneMF0giSl0WZPJ2NnNw-TegbQ,60225
|
| 96 |
+
aiohttp/cookiejar.py,sha256=PYR1K1mkLa24Hm6c9UEJnAitccNzz97CbsJyQ2ULAlU,17615
|
| 97 |
+
aiohttp/formdata.py,sha256=CUJnCWDNHFcXSYZ_TupaT6rHkY-Q7ghssvWzaYBPIo0,6552
|
| 98 |
+
aiohttp/hdrs.py,sha256=2rj5MyA-6yRdYPhW5UKkW4iNWhEAlGIOSBH5D4FmKNE,5111
|
| 99 |
+
aiohttp/helpers.py,sha256=KqPQECeiJ_EhA93k7-5ZoVdZH0sk_4n0tCoM_E-iMnE,29091
|
| 100 |
+
aiohttp/http.py,sha256=8o8j8xH70OWjnfTWA9V44NR785QPxEPrUtzMXiAVpwc,1842
|
| 101 |
+
aiohttp/http_exceptions.py,sha256=RYmBycJvvPerKkgXXm8v145I1N-fbsgSpcsbNIC-gdE,2961
|
| 102 |
+
aiohttp/http_parser.py,sha256=UqerYPJzA1MqLmeG1jURhTNO1YhwUASK3QVcNEz0me8,36851
|
| 103 |
+
aiohttp/http_websocket.py,sha256=8VXFKw6KQUEmPg48GtRMB37v0gTK7A0inoxXuDxMZEc,842
|
| 104 |
+
aiohttp/http_writer.py,sha256=pRIyfOmL3cZmdWDWBBJ2cZEwEJzLWzlPPAJInaPLThI,7595
|
| 105 |
+
aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325
|
| 106 |
+
aiohttp/multipart.py,sha256=SABIvo3vhXzG4bLDZ0C4V3yG_86vAb-3Zb9Li7BVmI8,36944
|
| 107 |
+
aiohttp/payload.py,sha256=rCA9JJI_RMCik_7qNIaC1Bh21aXhABGYK2tsYeaHRQ4,15793
|
| 108 |
+
aiohttp/payload_streamer.py,sha256=ZzEYyfzcjGWkVkK3XR2pBthSCSIykYvY3Wr5cGQ2eTc,2211
|
| 109 |
+
aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
| 110 |
+
aiohttp/pytest_plugin.py,sha256=AfJ6VIWzsp5KgpXRREsX3yqGUZrJyfb7zzcMqzWxz7I,12768
|
| 111 |
+
aiohttp/resolver.py,sha256=sJ8-LYCtl_g9f6gn_5X2NFQ9FQ72Q2Mr4_rLxo9NVeI,6375
|
| 112 |
+
aiohttp/streams.py,sha256=U-qTkuAqIfpJChuKEy-vYn8nQ_Z1MVcW0WO2DHiJz_o,22329
|
| 113 |
+
aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961
|
| 114 |
+
aiohttp/test_utils.py,sha256=r7kBasmZtC3tQY5OmyMaIl1B9P8Bnnq1oM3npVcAPKs,22811
|
| 115 |
+
aiohttp/tracing.py,sha256=66XQwtdR5DHv8p953eeNL0l8o6iHDaNwH9bBaybHXD4,15137
|
| 116 |
+
aiohttp/typedefs.py,sha256=wUlqwe9Mw9W8jT3HsYJcYk00qP3EMPz3nTkYXmeNN48,1657
|
| 117 |
+
aiohttp/web.py,sha256=As5nqGQy4QXWMXSaOsh0JudSVVJVIt_nr3n0b8CaMb0,18422
|
| 118 |
+
aiohttp/web_app.py,sha256=Zre0QHM9JAp4d7jrj5NRxlPnfTrKLNuA42Rdsh8Q2TI,19554
|
| 119 |
+
aiohttp/web_exceptions.py,sha256=7nIuiwhZ39vJJ9KrWqArA5QcWbUdqkz2CLwEpJapeN8,10360
|
| 120 |
+
aiohttp/web_fileresponse.py,sha256=FRsS0p9r1KU5y8ceG0QXBYnrL6xggjbxcXSmI6qIR4k,16504
|
| 121 |
+
aiohttp/web_log.py,sha256=rX5D7xLOX2B6BMdiZ-chme_KfJfW5IXEoFwLfkfkajs,7865
|
| 122 |
+
aiohttp/web_middlewares.py,sha256=sFI0AgeNjdyAjuz92QtMIpngmJSOxrqe2Jfbs4BNUu0,4165
|
| 123 |
+
aiohttp/web_protocol.py,sha256=PMCLcrUlC_-2U9ZxnMyWFiaekNvbswmXiIdmhRcMLHM,25630
|
| 124 |
+
aiohttp/web_request.py,sha256=j_SSX9s-d3ZeNyqUTpFIaPUaNdSqHwb7yfc0ufL8xFA,29750
|
| 125 |
+
aiohttp/web_response.py,sha256=65aliDETi7rZ8P76ksuHQI0ZTu1cKpclCSailNu105M,28696
|
| 126 |
+
aiohttp/web_routedef.py,sha256=VT1GAx6BrawoDh5RwBwBu5wSABSqgWwAe74AUCyZAEo,6110
|
| 127 |
+
aiohttp/web_runner.py,sha256=v1G1nKiOOQgFnTSR4IMc6I9ReEFDMaHtMLvO_roDM-A,11786
|
| 128 |
+
aiohttp/web_server.py,sha256=-9WDKUAiR9ll-rSdwXSqG6YjaoW79d1R4y0BGSqgUMA,2888
|
| 129 |
+
aiohttp/web_urldispatcher.py,sha256=TIMxFmhLjERseG0xcZv2Ef9Xuo_GTBRqBqeMkCgL0K8,43825
|
| 130 |
+
aiohttp/web_ws.py,sha256=EOQX3LYqlrkNQHlFTaNpZkQpOYRCZfR-m1bHT4Iseq8,22488
|
| 131 |
+
aiohttp/worker.py,sha256=0lvxRNMjGM47ddlQWtci53ri9YN42Su1Vdd_Z7zMMH0,8040
|
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (75.8.0)
|
| 3 |
+
Root-Is-Purelib: false
|
| 4 |
+
Tag: cp311-cp311-manylinux_2_17_x86_64
|
| 5 |
+
Tag: cp311-cp311-manylinux2014_x86_64
|
| 6 |
+
|
.venv/lib/python3.11/site-packages/aiohttp-3.11.12.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
aiohttp
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__about__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""
|
| 16 |
+
Library meta information.
|
| 17 |
+
|
| 18 |
+
This module must be stand-alone executable.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
__title__ = "aiohttp-cors"
|
| 22 |
+
__version__ = "0.7.0"
|
| 23 |
+
__author__ = "Vladimir Rutsky and aio-libs team"
|
| 24 |
+
__email__ = "vladimir@rutsky.org"
|
| 25 |
+
__summary__ = "CORS support for aiohttp"
|
| 26 |
+
__uri__ = "https://github.com/aio-libs/aiohttp-cors"
|
| 27 |
+
__license__ = "Apache License, Version 2.0"
|
| 28 |
+
__copyright__ = "2015-2018 {}".format(__author__)
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""CORS support for aiohttp.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from typing import Mapping, Union, Any
|
| 19 |
+
|
| 20 |
+
from aiohttp import web
|
| 21 |
+
|
| 22 |
+
from .__about__ import (
|
| 23 |
+
__title__, __version__, __author__, __email__, __summary__, __uri__,
|
| 24 |
+
__license__, __copyright__,
|
| 25 |
+
)
|
| 26 |
+
from .resource_options import ResourceOptions
|
| 27 |
+
from .cors_config import CorsConfig
|
| 28 |
+
from .mixin import CorsViewMixin, custom_cors
|
| 29 |
+
|
| 30 |
+
__all__ = (
|
| 31 |
+
"__title__", "__version__", "__author__", "__email__", "__summary__",
|
| 32 |
+
"__uri__", "__license__", "__copyright__",
|
| 33 |
+
"setup", "CorsConfig", "ResourceOptions", "CorsViewMixin", "custom_cors"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
APP_CONFIG_KEY = "aiohttp_cors"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def setup(app: web.Application, *,
|
| 41 |
+
defaults: Mapping[str, Union[ResourceOptions,
|
| 42 |
+
Mapping[str, Any]]]=None) -> CorsConfig:
|
| 43 |
+
"""Setup CORS processing for the application.
|
| 44 |
+
|
| 45 |
+
To enable CORS for a resource you need to explicitly add route for
|
| 46 |
+
that resource using `CorsConfig.add()` method::
|
| 47 |
+
|
| 48 |
+
app = aiohttp.web.Application()
|
| 49 |
+
cors = aiohttp_cors.setup(app)
|
| 50 |
+
cors.add(
|
| 51 |
+
app.router.add_route("GET", "/resource", handler),
|
| 52 |
+
{
|
| 53 |
+
"*": aiohttp_cors.ResourceOptions(
|
| 54 |
+
allow_credentials=True,
|
| 55 |
+
expose_headers="*",
|
| 56 |
+
allow_headers="*"),
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
:param app:
|
| 60 |
+
The application for which CORS will be configured.
|
| 61 |
+
:param defaults:
|
| 62 |
+
Default settings for origins.
|
| 63 |
+
)
|
| 64 |
+
"""
|
| 65 |
+
cors = CorsConfig(app, defaults=defaults)
|
| 66 |
+
app[APP_CONFIG_KEY] = cors
|
| 67 |
+
return cors
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/__about__.cpython-311.pyc
ADDED
|
Binary file (693 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (2.15 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/abc.cpython-311.pyc
ADDED
|
Binary file (4.33 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/cors_config.cpython-311.pyc
ADDED
|
Binary file (9.28 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/mixin.cpython-311.pyc
ADDED
|
Binary file (2.63 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/preflight_handler.cpython-311.pyc
ADDED
|
Binary file (5.25 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/resource_options.cpython-311.pyc
ADDED
|
Binary file (6.04 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/__pycache__/urldispatcher_router_adapter.cpython-311.pyc
ADDED
|
Binary file (11.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/aiohttp_cors/abc.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""Abstract base classes.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from abc import ABCMeta, abstractmethod
|
| 19 |
+
|
| 20 |
+
from aiohttp import web
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
__all__ = ("AbstractRouterAdapter",)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class AbstractRouterAdapter(metaclass=ABCMeta):
|
| 27 |
+
"""Router adapter for handling CORS configuration interface.
|
| 28 |
+
|
| 29 |
+
`AbstractRouter` doesn't specify how HTTP requests are delivered
|
| 30 |
+
to handlers, and aiohttp_cors doesn't rely on specific implementation
|
| 31 |
+
details.
|
| 32 |
+
|
| 33 |
+
In general Router can be seen as a substance that allows to setup handlers
|
| 34 |
+
for specific HTTP methods and requests paths, lets call these Router's
|
| 35 |
+
items routing entities.
|
| 36 |
+
Generic Router is configured with set of routing entities and their
|
| 37 |
+
handlers.
|
| 38 |
+
|
| 39 |
+
This adapter assumes that its reasonable to configure CORS for same
|
| 40 |
+
routing entities as used in `AbstractRouter`.
|
| 41 |
+
Routing entities will be added to CorsConfig to enable CORS for them.
|
| 42 |
+
|
| 43 |
+
For example, for aiohttp < 0.21.0 routing entity would be
|
| 44 |
+
`aiohttp.web.Route` — tuple of (HTTP method, URI path).
|
| 45 |
+
And CORS can be configured for each `aiohttp.web.Route`.
|
| 46 |
+
|
| 47 |
+
In aiohttp >= 0.21.0 there are two routing entities: Resource and Route.
|
| 48 |
+
You can configure CORS for Resource (which will be interpreted as default
|
| 49 |
+
for all Routes on Resoures), and configure CORS for specific Route.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
@abstractmethod
|
| 53 |
+
def add_preflight_handler(self,
|
| 54 |
+
routing_entity,
|
| 55 |
+
handler,
|
| 56 |
+
webview: bool=False):
|
| 57 |
+
"""Add OPTIONS handler for all routes defined by `routing_entity`.
|
| 58 |
+
|
| 59 |
+
Does nothing if CORS handler already handles routing entity.
|
| 60 |
+
Should fail if there are conflicting user-defined OPTIONS handlers.
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
@abstractmethod
|
| 64 |
+
def is_preflight_request(self, request: web.Request) -> bool:
|
| 65 |
+
"""Is `request` is a CORS preflight request."""
|
| 66 |
+
|
| 67 |
+
@abstractmethod
|
| 68 |
+
def is_cors_enabled_on_request(self, request: web.Request) -> bool:
|
| 69 |
+
"""Is `request` is a request for CORS-enabled resource."""
|
| 70 |
+
|
| 71 |
+
@abstractmethod
|
| 72 |
+
def set_config_for_routing_entity(self,
|
| 73 |
+
routing_entity,
|
| 74 |
+
config):
|
| 75 |
+
"""Record configuration for routing entity.
|
| 76 |
+
|
| 77 |
+
If router implements hierarchical routing entities, stored config
|
| 78 |
+
can be used in hierarchical manner too.
|
| 79 |
+
|
| 80 |
+
Should raise if there is conflicting configuration for the routing
|
| 81 |
+
entity.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
@abstractmethod
|
| 85 |
+
async def get_preflight_request_config(
|
| 86 |
+
self,
|
| 87 |
+
preflight_request: web.Request,
|
| 88 |
+
origin: str,
|
| 89 |
+
requested_method: str):
|
| 90 |
+
"""Get stored CORS configuration for specified HTTP method and origin
|
| 91 |
+
that corresponds to preflight request.
|
| 92 |
+
|
| 93 |
+
Should raise KeyError if CORS is not configured or not enabled
|
| 94 |
+
for specified HTTP method.
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
@abstractmethod
|
| 98 |
+
def get_non_preflight_request_config(self, request: web.Request):
|
| 99 |
+
"""Get stored CORS configuration for routing entity that handles
|
| 100 |
+
specified request."""
|
.venv/lib/python3.11/site-packages/aiohttp_cors/cors_config.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""CORS configuration container class definition.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import collections
|
| 19 |
+
import warnings
|
| 20 |
+
from typing import Mapping, Union, Any
|
| 21 |
+
|
| 22 |
+
from aiohttp import hdrs, web
|
| 23 |
+
|
| 24 |
+
from .urldispatcher_router_adapter import ResourcesUrlDispatcherRouterAdapter
|
| 25 |
+
from .abc import AbstractRouterAdapter
|
| 26 |
+
from .resource_options import ResourceOptions
|
| 27 |
+
from .preflight_handler import _PreflightHandler
|
| 28 |
+
|
| 29 |
+
__all__ = (
|
| 30 |
+
"CorsConfig",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Positive response to Access-Control-Allow-Credentials
|
| 34 |
+
_TRUE = "true"
|
| 35 |
+
# CORS simple response headers:
|
| 36 |
+
# <http://www.w3.org/TR/cors/#simple-response-header>
|
| 37 |
+
_SIMPLE_RESPONSE_HEADERS = frozenset([
|
| 38 |
+
hdrs.CACHE_CONTROL,
|
| 39 |
+
hdrs.CONTENT_LANGUAGE,
|
| 40 |
+
hdrs.CONTENT_TYPE,
|
| 41 |
+
hdrs.EXPIRES,
|
| 42 |
+
hdrs.LAST_MODIFIED,
|
| 43 |
+
hdrs.PRAGMA
|
| 44 |
+
])
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _parse_config_options(
|
| 48 |
+
config: Mapping[str, Union[ResourceOptions, Mapping[str, Any]]]=None):
|
| 49 |
+
"""Parse CORS configuration (default or per-route)
|
| 50 |
+
|
| 51 |
+
:param config:
|
| 52 |
+
Mapping from Origin to Resource configuration (allowed headers etc)
|
| 53 |
+
defined either as mapping or `ResourceOptions` instance.
|
| 54 |
+
|
| 55 |
+
Raises `ValueError` if configuration is not correct.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
if config is None:
|
| 59 |
+
return {}
|
| 60 |
+
|
| 61 |
+
if not isinstance(config, collections.abc.Mapping):
|
| 62 |
+
raise ValueError(
|
| 63 |
+
"Config must be mapping, got '{}'".format(config))
|
| 64 |
+
|
| 65 |
+
parsed = {}
|
| 66 |
+
|
| 67 |
+
options_keys = {
|
| 68 |
+
"allow_credentials", "expose_headers", "allow_headers", "max_age"
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
for origin, options in config.items():
|
| 72 |
+
# TODO: check that all origins are properly formatted.
|
| 73 |
+
# This is not a security issue, since origin is compared as strings.
|
| 74 |
+
if not isinstance(origin, str):
|
| 75 |
+
raise ValueError(
|
| 76 |
+
"Origin must be string, got '{}'".format(origin))
|
| 77 |
+
|
| 78 |
+
if isinstance(options, ResourceOptions):
|
| 79 |
+
resource_options = options
|
| 80 |
+
|
| 81 |
+
else:
|
| 82 |
+
if not isinstance(options, collections.abc.Mapping):
|
| 83 |
+
raise ValueError(
|
| 84 |
+
"Origin options must be either "
|
| 85 |
+
"aiohttp_cors.ResourceOptions instance or mapping, "
|
| 86 |
+
"got '{}'".format(options))
|
| 87 |
+
|
| 88 |
+
unexpected_args = frozenset(options.keys()) - options_keys
|
| 89 |
+
if unexpected_args:
|
| 90 |
+
raise ValueError(
|
| 91 |
+
"Unexpected keywords in resource options: {}".format(
|
| 92 |
+
# pylint: disable=bad-builtin
|
| 93 |
+
",".join(map(str, unexpected_args))))
|
| 94 |
+
|
| 95 |
+
resource_options = ResourceOptions(**options)
|
| 96 |
+
|
| 97 |
+
parsed[origin] = resource_options
|
| 98 |
+
|
| 99 |
+
return parsed
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
_ConfigType = Mapping[str, Union[ResourceOptions, Mapping[str, Any]]]
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class _CorsConfigImpl(_PreflightHandler):
|
| 106 |
+
|
| 107 |
+
def __init__(self,
|
| 108 |
+
app: web.Application,
|
| 109 |
+
router_adapter: AbstractRouterAdapter):
|
| 110 |
+
self._app = app
|
| 111 |
+
|
| 112 |
+
self._router_adapter = router_adapter
|
| 113 |
+
|
| 114 |
+
# Register hook for all responses. This hook handles CORS-related
|
| 115 |
+
# headers on non-preflight requests.
|
| 116 |
+
self._app.on_response_prepare.append(self._on_response_prepare)
|
| 117 |
+
|
| 118 |
+
def add(self,
|
| 119 |
+
routing_entity,
|
| 120 |
+
config: _ConfigType=None):
|
| 121 |
+
"""Enable CORS for specific route or resource.
|
| 122 |
+
|
| 123 |
+
If route is passed CORS is enabled for route's resource.
|
| 124 |
+
|
| 125 |
+
:param routing_entity:
|
| 126 |
+
Route or Resource for which CORS should be enabled.
|
| 127 |
+
:param config:
|
| 128 |
+
CORS options for the route.
|
| 129 |
+
:return: `routing_entity`.
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
parsed_config = _parse_config_options(config)
|
| 133 |
+
|
| 134 |
+
self._router_adapter.add_preflight_handler(
|
| 135 |
+
routing_entity, self._preflight_handler)
|
| 136 |
+
self._router_adapter.set_config_for_routing_entity(
|
| 137 |
+
routing_entity, parsed_config)
|
| 138 |
+
|
| 139 |
+
return routing_entity
|
| 140 |
+
|
| 141 |
+
async def _on_response_prepare(self,
|
| 142 |
+
request: web.Request,
|
| 143 |
+
response: web.StreamResponse):
|
| 144 |
+
"""Non-preflight CORS request response processor.
|
| 145 |
+
|
| 146 |
+
If request is done on CORS-enabled route, process request parameters
|
| 147 |
+
and set appropriate CORS response headers.
|
| 148 |
+
"""
|
| 149 |
+
if (not self._router_adapter.is_cors_enabled_on_request(request) or
|
| 150 |
+
self._router_adapter.is_preflight_request(request)):
|
| 151 |
+
# Either not CORS enabled route, or preflight request which is
|
| 152 |
+
# handled in its own handler.
|
| 153 |
+
return
|
| 154 |
+
|
| 155 |
+
# Processing response of non-preflight CORS-enabled request.
|
| 156 |
+
|
| 157 |
+
config = self._router_adapter.get_non_preflight_request_config(request)
|
| 158 |
+
|
| 159 |
+
# Handle according to part 6.1 of the CORS specification.
|
| 160 |
+
|
| 161 |
+
origin = request.headers.get(hdrs.ORIGIN)
|
| 162 |
+
if origin is None:
|
| 163 |
+
# Terminate CORS according to CORS 6.1.1.
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
options = config.get(origin, config.get("*"))
|
| 167 |
+
if options is None:
|
| 168 |
+
# Terminate CORS according to CORS 6.1.2.
|
| 169 |
+
return
|
| 170 |
+
|
| 171 |
+
assert hdrs.ACCESS_CONTROL_ALLOW_ORIGIN not in response.headers
|
| 172 |
+
assert hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS not in response.headers
|
| 173 |
+
assert hdrs.ACCESS_CONTROL_EXPOSE_HEADERS not in response.headers
|
| 174 |
+
|
| 175 |
+
# Process according to CORS 6.1.4.
|
| 176 |
+
# Set exposed headers (server headers exposed to client) before
|
| 177 |
+
# setting any other headers.
|
| 178 |
+
if options.expose_headers == "*":
|
| 179 |
+
# Expose all headers that are set in response.
|
| 180 |
+
exposed_headers = \
|
| 181 |
+
frozenset(response.headers.keys()) - _SIMPLE_RESPONSE_HEADERS
|
| 182 |
+
response.headers[hdrs.ACCESS_CONTROL_EXPOSE_HEADERS] = \
|
| 183 |
+
",".join(exposed_headers)
|
| 184 |
+
|
| 185 |
+
elif options.expose_headers:
|
| 186 |
+
# Expose predefined list of headers.
|
| 187 |
+
response.headers[hdrs.ACCESS_CONTROL_EXPOSE_HEADERS] = \
|
| 188 |
+
",".join(options.expose_headers)
|
| 189 |
+
|
| 190 |
+
# Process according to CORS 6.1.3.
|
| 191 |
+
# Set allowed origin.
|
| 192 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_ORIGIN] = origin
|
| 193 |
+
if options.allow_credentials:
|
| 194 |
+
# Set allowed credentials.
|
| 195 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS] = _TRUE
|
| 196 |
+
|
| 197 |
+
async def _get_config(self, request, origin, request_method):
|
| 198 |
+
config = \
|
| 199 |
+
await self._router_adapter.get_preflight_request_config(
|
| 200 |
+
request, origin, request_method)
|
| 201 |
+
return config
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class CorsConfig:
|
| 205 |
+
"""CORS configuration instance.
|
| 206 |
+
|
| 207 |
+
The instance holds default CORS parameters and per-route options specified
|
| 208 |
+
in `add()` method.
|
| 209 |
+
|
| 210 |
+
Each `aiohttp.web.Application` can have exactly one instance of this class.
|
| 211 |
+
"""
|
| 212 |
+
|
| 213 |
+
def __init__(self, app: web.Application, *,
|
| 214 |
+
defaults: _ConfigType=None,
|
| 215 |
+
router_adapter: AbstractRouterAdapter=None):
|
| 216 |
+
"""Construct CORS configuration.
|
| 217 |
+
|
| 218 |
+
:param app:
|
| 219 |
+
Application for which CORS configuration is built.
|
| 220 |
+
:param defaults:
|
| 221 |
+
Default CORS settings for origins.
|
| 222 |
+
:param router_adapter:
|
| 223 |
+
Router adapter. Required if application uses non-default router.
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
self.defaults = _parse_config_options(defaults)
|
| 227 |
+
|
| 228 |
+
self._cors_impl = None
|
| 229 |
+
|
| 230 |
+
self._resources_router_adapter = None
|
| 231 |
+
self._resources_cors_impl = None
|
| 232 |
+
|
| 233 |
+
self._old_routes_cors_impl = None
|
| 234 |
+
|
| 235 |
+
if router_adapter is None:
|
| 236 |
+
router_adapter = \
|
| 237 |
+
ResourcesUrlDispatcherRouterAdapter(app.router, self.defaults)
|
| 238 |
+
|
| 239 |
+
self._cors_impl = _CorsConfigImpl(app, router_adapter)
|
| 240 |
+
|
| 241 |
+
def add(self,
|
| 242 |
+
routing_entity,
|
| 243 |
+
config: _ConfigType = None,
|
| 244 |
+
webview: bool=False):
|
| 245 |
+
"""Enable CORS for specific route or resource.
|
| 246 |
+
|
| 247 |
+
If route is passed CORS is enabled for route's resource.
|
| 248 |
+
|
| 249 |
+
:param routing_entity:
|
| 250 |
+
Route or Resource for which CORS should be enabled.
|
| 251 |
+
:param config:
|
| 252 |
+
CORS options for the route.
|
| 253 |
+
:return: `routing_entity`.
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
if webview:
|
| 257 |
+
warnings.warn('webview argument is deprecated, '
|
| 258 |
+
'views are handled authomatically without '
|
| 259 |
+
'extra settings',
|
| 260 |
+
DeprecationWarning,
|
| 261 |
+
stacklevel=2)
|
| 262 |
+
|
| 263 |
+
return self._cors_impl.add(routing_entity, config)
|
.venv/lib/python3.11/site-packages/aiohttp_cors/mixin.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
|
| 3 |
+
from .preflight_handler import _PreflightHandler
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def custom_cors(config):
|
| 7 |
+
def wrapper(function):
|
| 8 |
+
name = "{}_cors_config".format(function.__name__)
|
| 9 |
+
setattr(function, name, config)
|
| 10 |
+
return function
|
| 11 |
+
return wrapper
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class CorsViewMixin(_PreflightHandler):
|
| 15 |
+
cors_config = None
|
| 16 |
+
|
| 17 |
+
@classmethod
|
| 18 |
+
def get_request_config(cls, request, request_method):
|
| 19 |
+
try:
|
| 20 |
+
from . import APP_CONFIG_KEY
|
| 21 |
+
cors = request.app[APP_CONFIG_KEY]
|
| 22 |
+
except KeyError:
|
| 23 |
+
raise ValueError("aiohttp-cors is not configured.")
|
| 24 |
+
|
| 25 |
+
method = getattr(cls, request_method.lower(), None)
|
| 26 |
+
|
| 27 |
+
if not method:
|
| 28 |
+
raise KeyError()
|
| 29 |
+
|
| 30 |
+
config_property_key = "{}_cors_config".format(request_method.lower())
|
| 31 |
+
|
| 32 |
+
custom_config = getattr(method, config_property_key, None)
|
| 33 |
+
if not custom_config:
|
| 34 |
+
custom_config = {}
|
| 35 |
+
|
| 36 |
+
class_config = cls.cors_config
|
| 37 |
+
if not class_config:
|
| 38 |
+
class_config = {}
|
| 39 |
+
|
| 40 |
+
return collections.ChainMap(custom_config, class_config, cors.defaults)
|
| 41 |
+
|
| 42 |
+
async def _get_config(self, request, origin, request_method):
|
| 43 |
+
return self.get_request_config(request, request_method)
|
| 44 |
+
|
| 45 |
+
async def options(self):
|
| 46 |
+
response = await self._preflight_handler(self.request)
|
| 47 |
+
return response
|
.venv/lib/python3.11/site-packages/aiohttp_cors/preflight_handler.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from aiohttp import hdrs, web
|
| 2 |
+
|
| 3 |
+
# Positive response to Access-Control-Allow-Credentials
|
| 4 |
+
_TRUE = "true"
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class _PreflightHandler:
|
| 8 |
+
|
| 9 |
+
@staticmethod
|
| 10 |
+
def _parse_request_method(request: web.Request):
|
| 11 |
+
"""Parse Access-Control-Request-Method header of the preflight request
|
| 12 |
+
"""
|
| 13 |
+
method = request.headers.get(hdrs.ACCESS_CONTROL_REQUEST_METHOD)
|
| 14 |
+
if method is None:
|
| 15 |
+
raise web.HTTPForbidden(
|
| 16 |
+
text="CORS preflight request failed: "
|
| 17 |
+
"'Access-Control-Request-Method' header is not specified")
|
| 18 |
+
|
| 19 |
+
# FIXME: validate method string (ABNF: method = token), if parsing
|
| 20 |
+
# fails, raise HTTPForbidden.
|
| 21 |
+
|
| 22 |
+
return method
|
| 23 |
+
|
| 24 |
+
@staticmethod
|
| 25 |
+
def _parse_request_headers(request: web.Request):
|
| 26 |
+
"""Parse Access-Control-Request-Headers header or the preflight request
|
| 27 |
+
|
| 28 |
+
Returns set of headers in upper case.
|
| 29 |
+
"""
|
| 30 |
+
headers = request.headers.get(hdrs.ACCESS_CONTROL_REQUEST_HEADERS)
|
| 31 |
+
if headers is None:
|
| 32 |
+
return frozenset()
|
| 33 |
+
|
| 34 |
+
# FIXME: validate each header string, if parsing fails, raise
|
| 35 |
+
# HTTPForbidden.
|
| 36 |
+
# FIXME: check, that headers split and stripped correctly (according
|
| 37 |
+
# to ABNF).
|
| 38 |
+
headers = (h.strip(" \t").upper() for h in headers.split(","))
|
| 39 |
+
# pylint: disable=bad-builtin
|
| 40 |
+
return frozenset(filter(None, headers))
|
| 41 |
+
|
| 42 |
+
async def _get_config(self, request, origin, request_method):
|
| 43 |
+
raise NotImplementedError()
|
| 44 |
+
|
| 45 |
+
async def _preflight_handler(self, request: web.Request):
|
| 46 |
+
"""CORS preflight request handler"""
|
| 47 |
+
|
| 48 |
+
# Handle according to part 6.2 of the CORS specification.
|
| 49 |
+
|
| 50 |
+
origin = request.headers.get(hdrs.ORIGIN)
|
| 51 |
+
if origin is None:
|
| 52 |
+
# Terminate CORS according to CORS 6.2.1.
|
| 53 |
+
raise web.HTTPForbidden(
|
| 54 |
+
text="CORS preflight request failed: "
|
| 55 |
+
"origin header is not specified in the request")
|
| 56 |
+
|
| 57 |
+
# CORS 6.2.3. Doing it out of order is not an error.
|
| 58 |
+
request_method = self._parse_request_method(request)
|
| 59 |
+
|
| 60 |
+
# CORS 6.2.5. Doing it out of order is not an error.
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
config = \
|
| 64 |
+
await self._get_config(request, origin, request_method)
|
| 65 |
+
except KeyError:
|
| 66 |
+
raise web.HTTPForbidden(
|
| 67 |
+
text="CORS preflight request failed: "
|
| 68 |
+
"request method {!r} is not allowed "
|
| 69 |
+
"for {!r} origin".format(request_method, origin))
|
| 70 |
+
|
| 71 |
+
if not config:
|
| 72 |
+
# No allowed origins for the route.
|
| 73 |
+
# Terminate CORS according to CORS 6.2.1.
|
| 74 |
+
raise web.HTTPForbidden(
|
| 75 |
+
text="CORS preflight request failed: "
|
| 76 |
+
"no origins are allowed")
|
| 77 |
+
|
| 78 |
+
options = config.get(origin, config.get("*"))
|
| 79 |
+
if options is None:
|
| 80 |
+
# No configuration for the origin - deny.
|
| 81 |
+
# Terminate CORS according to CORS 6.2.2.
|
| 82 |
+
raise web.HTTPForbidden(
|
| 83 |
+
text="CORS preflight request failed: "
|
| 84 |
+
"origin '{}' is not allowed".format(origin))
|
| 85 |
+
|
| 86 |
+
# CORS 6.2.4
|
| 87 |
+
request_headers = self._parse_request_headers(request)
|
| 88 |
+
|
| 89 |
+
# CORS 6.2.6
|
| 90 |
+
if options.allow_headers == "*":
|
| 91 |
+
pass
|
| 92 |
+
else:
|
| 93 |
+
disallowed_headers = request_headers - options.allow_headers
|
| 94 |
+
if disallowed_headers:
|
| 95 |
+
raise web.HTTPForbidden(
|
| 96 |
+
text="CORS preflight request failed: "
|
| 97 |
+
"headers are not allowed: {}".format(
|
| 98 |
+
", ".join(disallowed_headers)))
|
| 99 |
+
|
| 100 |
+
# Ok, CORS actual request with specified in the preflight request
|
| 101 |
+
# parameters is allowed.
|
| 102 |
+
# Set appropriate headers and return 200 response.
|
| 103 |
+
|
| 104 |
+
response = web.Response()
|
| 105 |
+
|
| 106 |
+
# CORS 6.2.7
|
| 107 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_ORIGIN] = origin
|
| 108 |
+
if options.allow_credentials:
|
| 109 |
+
# Set allowed credentials.
|
| 110 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS] = _TRUE
|
| 111 |
+
|
| 112 |
+
# CORS 6.2.8
|
| 113 |
+
if options.max_age is not None:
|
| 114 |
+
response.headers[hdrs.ACCESS_CONTROL_MAX_AGE] = \
|
| 115 |
+
str(options.max_age)
|
| 116 |
+
|
| 117 |
+
# CORS 6.2.9
|
| 118 |
+
# TODO: more optimal for client preflight request cache would be to
|
| 119 |
+
# respond with ALL allowed methods.
|
| 120 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_METHODS] = request_method
|
| 121 |
+
|
| 122 |
+
# CORS 6.2.10
|
| 123 |
+
if request_headers:
|
| 124 |
+
# Note: case of the headers in the request is changed, but this
|
| 125 |
+
# shouldn't be a problem, since the headers should be compared in
|
| 126 |
+
# the case-insensitive way.
|
| 127 |
+
response.headers[hdrs.ACCESS_CONTROL_ALLOW_HEADERS] = \
|
| 128 |
+
",".join(request_headers)
|
| 129 |
+
|
| 130 |
+
return response
|
.venv/lib/python3.11/site-packages/aiohttp_cors/resource_options.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""Resource CORS options class definition.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import numbers
|
| 19 |
+
import collections
|
| 20 |
+
import collections.abc
|
| 21 |
+
|
| 22 |
+
__all__ = ("ResourceOptions",)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _is_proper_sequence(seq):
|
| 26 |
+
"""Returns is seq is sequence and not string."""
|
| 27 |
+
return (isinstance(seq, collections.abc.Sequence) and
|
| 28 |
+
not isinstance(seq, str))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ResourceOptions(collections.namedtuple(
|
| 32 |
+
"Base",
|
| 33 |
+
("allow_credentials", "expose_headers", "allow_headers", "max_age",
|
| 34 |
+
"allow_methods"))):
|
| 35 |
+
"""Resource CORS options."""
|
| 36 |
+
|
| 37 |
+
__slots__ = ()
|
| 38 |
+
|
| 39 |
+
def __init__(self, *, allow_credentials=False, expose_headers=(),
|
| 40 |
+
allow_headers=(), max_age=None, allow_methods=None):
|
| 41 |
+
"""Construct resource CORS options.
|
| 42 |
+
|
| 43 |
+
Options will be normalized.
|
| 44 |
+
|
| 45 |
+
:param allow_credentials:
|
| 46 |
+
Is passing client credentials to the resource from other origin
|
| 47 |
+
is allowed.
|
| 48 |
+
See <http://www.w3.org/TR/cors/#user-credentials> for
|
| 49 |
+
the definition.
|
| 50 |
+
:type allow_credentials: bool
|
| 51 |
+
Is passing client credentials to the resource from other origin
|
| 52 |
+
is allowed.
|
| 53 |
+
|
| 54 |
+
:param expose_headers:
|
| 55 |
+
Server headers that are allowed to be exposed to the client.
|
| 56 |
+
Simple response headers are excluded from this set, see
|
| 57 |
+
<http://www.w3.org/TR/cors/#list-of-exposed-headers>.
|
| 58 |
+
:type expose_headers: sequence of strings or ``*`` string.
|
| 59 |
+
|
| 60 |
+
:param allow_headers:
|
| 61 |
+
Client headers that are allowed to be passed to the resource.
|
| 62 |
+
See <http://www.w3.org/TR/cors/#list-of-headers>.
|
| 63 |
+
:type allow_headers: sequence of strings or ``*`` string.
|
| 64 |
+
|
| 65 |
+
:param max_age:
|
| 66 |
+
How long the results of a preflight request can be cached in a
|
| 67 |
+
preflight result cache (in seconds).
|
| 68 |
+
See <http://www.w3.org/TR/cors/#http-access-control-max-age>.
|
| 69 |
+
|
| 70 |
+
:param allow_methods:
|
| 71 |
+
List of allowed methods or ``*``string. Can be used in resource or
|
| 72 |
+
global defaults, but not in specific route.
|
| 73 |
+
|
| 74 |
+
It's not required to specify all allowed methods for specific
|
| 75 |
+
resource, routes that have explicit CORS configuration will be
|
| 76 |
+
treated as if their methods are allowed.
|
| 77 |
+
"""
|
| 78 |
+
super().__init__()
|
| 79 |
+
|
| 80 |
+
def __new__(cls, *, allow_credentials=False, expose_headers=(),
|
| 81 |
+
allow_headers=(), max_age=None, allow_methods=None):
|
| 82 |
+
"""Normalize source parameters and store them in namedtuple."""
|
| 83 |
+
|
| 84 |
+
if not isinstance(allow_credentials, bool):
|
| 85 |
+
raise ValueError(
|
| 86 |
+
"'allow_credentials' must be boolean, "
|
| 87 |
+
"got '{!r}'".format(allow_credentials))
|
| 88 |
+
_allow_credentials = allow_credentials
|
| 89 |
+
|
| 90 |
+
# `expose_headers` is either "*", or sequence of strings.
|
| 91 |
+
if expose_headers == "*":
|
| 92 |
+
_expose_headers = expose_headers
|
| 93 |
+
elif not _is_proper_sequence(expose_headers):
|
| 94 |
+
raise ValueError(
|
| 95 |
+
"'expose_headers' must be either '*', or sequence of strings, "
|
| 96 |
+
"got '{!r}'".format(expose_headers))
|
| 97 |
+
elif expose_headers:
|
| 98 |
+
# "Access-Control-Expose-Headers" ":" #field-name
|
| 99 |
+
# TODO: Check that headers are valid.
|
| 100 |
+
# TODO: Remove headers that in the _SIMPLE_RESPONSE_HEADERS set
|
| 101 |
+
# according to
|
| 102 |
+
# <http://www.w3.org/TR/cors/#list-of-exposed-headers>.
|
| 103 |
+
_expose_headers = frozenset(expose_headers)
|
| 104 |
+
else:
|
| 105 |
+
# No headers exposed.
|
| 106 |
+
_expose_headers = frozenset()
|
| 107 |
+
|
| 108 |
+
# `allow_headers` is either "*", or set of headers in upper case.
|
| 109 |
+
if allow_headers == "*":
|
| 110 |
+
_allow_headers = allow_headers
|
| 111 |
+
elif not _is_proper_sequence(allow_headers):
|
| 112 |
+
raise ValueError(
|
| 113 |
+
"'allow_headers' must be either '*', or sequence of strings, "
|
| 114 |
+
"got '{!r}'".format(allow_headers))
|
| 115 |
+
else:
|
| 116 |
+
# TODO: Check that headers are valid.
|
| 117 |
+
_allow_headers = frozenset(h.upper() for h in allow_headers)
|
| 118 |
+
|
| 119 |
+
if max_age is None:
|
| 120 |
+
_max_age = None
|
| 121 |
+
else:
|
| 122 |
+
if not isinstance(max_age, numbers.Integral) or max_age < 0:
|
| 123 |
+
raise ValueError(
|
| 124 |
+
"'max_age' must be non-negative integer, "
|
| 125 |
+
"got '{!r}'".format(max_age))
|
| 126 |
+
_max_age = max_age
|
| 127 |
+
|
| 128 |
+
if allow_methods is None or allow_methods == "*":
|
| 129 |
+
_allow_methods = allow_methods
|
| 130 |
+
elif not _is_proper_sequence(allow_methods):
|
| 131 |
+
raise ValueError(
|
| 132 |
+
"'allow_methods' must be either '*', or sequence of strings, "
|
| 133 |
+
"got '{!r}'".format(allow_methods))
|
| 134 |
+
else:
|
| 135 |
+
# TODO: Check that methods are valid.
|
| 136 |
+
_allow_methods = frozenset(m.upper() for m in allow_methods)
|
| 137 |
+
|
| 138 |
+
return super().__new__(
|
| 139 |
+
cls,
|
| 140 |
+
allow_credentials=_allow_credentials,
|
| 141 |
+
expose_headers=_expose_headers,
|
| 142 |
+
allow_headers=_allow_headers,
|
| 143 |
+
max_age=_max_age,
|
| 144 |
+
allow_methods=_allow_methods)
|
| 145 |
+
|
| 146 |
+
def is_method_allowed(self, method):
|
| 147 |
+
if self.allow_methods is None:
|
| 148 |
+
return False
|
| 149 |
+
|
| 150 |
+
if self.allow_methods == '*':
|
| 151 |
+
return True
|
| 152 |
+
|
| 153 |
+
return method.upper() in self.allow_methods
|
.venv/lib/python3.11/site-packages/aiohttp_cors/urldispatcher_router_adapter.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Vladimir Rutsky <vladimir@rutsky.org>
|
| 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 |
+
|
| 15 |
+
"""AbstractRouterAdapter for aiohttp.web.UrlDispatcher.
|
| 16 |
+
"""
|
| 17 |
+
import collections
|
| 18 |
+
|
| 19 |
+
from typing import Union
|
| 20 |
+
|
| 21 |
+
from aiohttp import web
|
| 22 |
+
from aiohttp import hdrs
|
| 23 |
+
|
| 24 |
+
from .abc import AbstractRouterAdapter
|
| 25 |
+
from .mixin import CorsViewMixin
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# There several usage patterns of routes which should be handled
|
| 29 |
+
# differently.
|
| 30 |
+
#
|
| 31 |
+
# 1. Using new Resources:
|
| 32 |
+
#
|
| 33 |
+
# resource = app.router.add_resource(path)
|
| 34 |
+
# cors.add(resource, resource_defaults=...)
|
| 35 |
+
# cors.add(resource.add_route(method1, handler1), config=...)
|
| 36 |
+
# cors.add(resource.add_route(method2, handler2), config=...)
|
| 37 |
+
# cors.add(resource.add_route(method3, handler3), config=...)
|
| 38 |
+
#
|
| 39 |
+
# Here all related Routes (i.e. routes with the same path) are in
|
| 40 |
+
# a single Resource.
|
| 41 |
+
#
|
| 42 |
+
# 2. Using `router.add_static()`:
|
| 43 |
+
#
|
| 44 |
+
# route1 = app.router.add_static(
|
| 45 |
+
# "/images", "/usr/share/app/images/")
|
| 46 |
+
# cors.add(route1, config=...)
|
| 47 |
+
#
|
| 48 |
+
# Here old-style `web.StaticRoute` is created and wrapped with
|
| 49 |
+
# `web.ResourceAdapter`.
|
| 50 |
+
#
|
| 51 |
+
# 3. Using old `router.add_route()`:
|
| 52 |
+
#
|
| 53 |
+
# cors.add(app.router.add_route(method1, path, hand1), config=...)
|
| 54 |
+
# cors.add(app.router.add_route(method2, path, hand2), config=...)
|
| 55 |
+
# cors.add(app.router.add_route(method3, path, hand3), config=...)
|
| 56 |
+
#
|
| 57 |
+
# This creates three Resources with single Route in each.
|
| 58 |
+
#
|
| 59 |
+
# 4. Using deprecated `register_route` with manually created
|
| 60 |
+
# `web.Route`:
|
| 61 |
+
#
|
| 62 |
+
# route1 = RouteSubclass(...)
|
| 63 |
+
# app.router.register_route(route1)
|
| 64 |
+
# cors.add(route1, config=...)
|
| 65 |
+
#
|
| 66 |
+
# Here old-style route is wrapped with `web.ResourceAdapter`.
|
| 67 |
+
#
|
| 68 |
+
# Preflight requests is roughly an OPTIONS request with query
|
| 69 |
+
# "is specific HTTP method is allowed".
|
| 70 |
+
# In order to properly handle preflight request we need to know which
|
| 71 |
+
# routes have enabled CORS on the request path and CORS configuration
|
| 72 |
+
# for requested HTTP method.
|
| 73 |
+
#
|
| 74 |
+
# In case of new usage pattern it's simple: we need to take a look at
|
| 75 |
+
# self._resource_config[resource][method] for the processing resource.
|
| 76 |
+
#
|
| 77 |
+
# In case of old usage pattern we need to iterate over routes with
|
| 78 |
+
# enabled CORS and check is requested path and HTTP method is accepted
|
| 79 |
+
# by a route.
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class _ResourceConfig:
|
| 83 |
+
def __init__(self, default_config):
|
| 84 |
+
# Resource default config.
|
| 85 |
+
self.default_config = default_config
|
| 86 |
+
|
| 87 |
+
# HTTP method to route configuration.
|
| 88 |
+
self.method_config = {}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _is_web_view(entity, strict=True):
|
| 92 |
+
webview = False
|
| 93 |
+
if isinstance(entity, web.AbstractRoute):
|
| 94 |
+
handler = entity.handler
|
| 95 |
+
if isinstance(handler, type) and issubclass(handler, web.View):
|
| 96 |
+
webview = True
|
| 97 |
+
if not issubclass(handler, CorsViewMixin):
|
| 98 |
+
if strict:
|
| 99 |
+
raise ValueError("web view should be derived from "
|
| 100 |
+
"aiohttp_cors.WebViewMixig for working "
|
| 101 |
+
"with the library")
|
| 102 |
+
else:
|
| 103 |
+
return False
|
| 104 |
+
return webview
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class ResourcesUrlDispatcherRouterAdapter(AbstractRouterAdapter):
|
| 108 |
+
"""Adapter for `UrlDispatcher` for Resources-based routing only.
|
| 109 |
+
|
| 110 |
+
Should be used with routes added in the following way:
|
| 111 |
+
|
| 112 |
+
resource = app.router.add_resource(path)
|
| 113 |
+
cors.add(resource, resource_defaults=...)
|
| 114 |
+
cors.add(resource.add_route(method1, handler1), config=...)
|
| 115 |
+
cors.add(resource.add_route(method2, handler2), config=...)
|
| 116 |
+
cors.add(resource.add_route(method3, handler3), config=...)
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
def __init__(self,
|
| 120 |
+
router: web.UrlDispatcher,
|
| 121 |
+
defaults):
|
| 122 |
+
"""
|
| 123 |
+
:param defaults:
|
| 124 |
+
Default CORS configuration.
|
| 125 |
+
"""
|
| 126 |
+
self._router = router
|
| 127 |
+
|
| 128 |
+
# Default configuration for all routes.
|
| 129 |
+
self._default_config = defaults
|
| 130 |
+
|
| 131 |
+
# Mapping from Resource to _ResourceConfig.
|
| 132 |
+
self._resource_config = {}
|
| 133 |
+
|
| 134 |
+
self._resources_with_preflight_handlers = set()
|
| 135 |
+
self._preflight_routes = set()
|
| 136 |
+
|
| 137 |
+
def add_preflight_handler(
|
| 138 |
+
self,
|
| 139 |
+
routing_entity: Union[web.Resource, web.StaticResource,
|
| 140 |
+
web.ResourceRoute],
|
| 141 |
+
handler):
|
| 142 |
+
"""Add OPTIONS handler for all routes defined by `routing_entity`.
|
| 143 |
+
|
| 144 |
+
Does nothing if CORS handler already handles routing entity.
|
| 145 |
+
Should fail if there are conflicting user-defined OPTIONS handlers.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
if isinstance(routing_entity, web.Resource):
|
| 149 |
+
resource = routing_entity
|
| 150 |
+
|
| 151 |
+
# Add preflight handler for Resource, if not yet added.
|
| 152 |
+
|
| 153 |
+
if resource in self._resources_with_preflight_handlers:
|
| 154 |
+
# Preflight handler already added for this resource.
|
| 155 |
+
return
|
| 156 |
+
for route_obj in resource:
|
| 157 |
+
if route_obj.method == hdrs.METH_OPTIONS:
|
| 158 |
+
if route_obj.handler is handler:
|
| 159 |
+
return # already added
|
| 160 |
+
else:
|
| 161 |
+
raise ValueError(
|
| 162 |
+
"{!r} already has OPTIONS handler {!r}"
|
| 163 |
+
.format(resource, route_obj.handler))
|
| 164 |
+
elif route_obj.method == hdrs.METH_ANY:
|
| 165 |
+
if _is_web_view(route_obj):
|
| 166 |
+
self._preflight_routes.add(route_obj)
|
| 167 |
+
self._resources_with_preflight_handlers.add(resource)
|
| 168 |
+
return
|
| 169 |
+
else:
|
| 170 |
+
raise ValueError("{!r} already has a '*' handler "
|
| 171 |
+
"for all methods".format(resource))
|
| 172 |
+
|
| 173 |
+
preflight_route = resource.add_route(hdrs.METH_OPTIONS, handler)
|
| 174 |
+
self._preflight_routes.add(preflight_route)
|
| 175 |
+
self._resources_with_preflight_handlers.add(resource)
|
| 176 |
+
|
| 177 |
+
elif isinstance(routing_entity, web.StaticResource):
|
| 178 |
+
resource = routing_entity
|
| 179 |
+
|
| 180 |
+
# Add preflight handler for Resource, if not yet added.
|
| 181 |
+
|
| 182 |
+
if resource in self._resources_with_preflight_handlers:
|
| 183 |
+
# Preflight handler already added for this resource.
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
resource.set_options_route(handler)
|
| 187 |
+
preflight_route = resource._routes[hdrs.METH_OPTIONS]
|
| 188 |
+
self._preflight_routes.add(preflight_route)
|
| 189 |
+
self._resources_with_preflight_handlers.add(resource)
|
| 190 |
+
|
| 191 |
+
elif isinstance(routing_entity, web.ResourceRoute):
|
| 192 |
+
route = routing_entity
|
| 193 |
+
|
| 194 |
+
if not self.is_cors_for_resource(route.resource):
|
| 195 |
+
self.add_preflight_handler(route.resource, handler)
|
| 196 |
+
|
| 197 |
+
else:
|
| 198 |
+
raise ValueError(
|
| 199 |
+
"Resource or ResourceRoute expected, got {!r}".format(
|
| 200 |
+
routing_entity))
|
| 201 |
+
|
| 202 |
+
def is_cors_for_resource(self, resource: web.Resource) -> bool:
|
| 203 |
+
"""Is CORS is configured for the resource"""
|
| 204 |
+
return resource in self._resources_with_preflight_handlers
|
| 205 |
+
|
| 206 |
+
def _request_route(self, request: web.Request) -> web.ResourceRoute:
|
| 207 |
+
match_info = request.match_info
|
| 208 |
+
assert isinstance(match_info, web.UrlMappingMatchInfo)
|
| 209 |
+
return match_info.route
|
| 210 |
+
|
| 211 |
+
def _request_resource(self, request: web.Request) -> web.Resource:
|
| 212 |
+
return self._request_route(request).resource
|
| 213 |
+
|
| 214 |
+
def is_preflight_request(self, request: web.Request) -> bool:
|
| 215 |
+
"""Is `request` is a CORS preflight request."""
|
| 216 |
+
route = self._request_route(request)
|
| 217 |
+
if _is_web_view(route, strict=False):
|
| 218 |
+
return request.method == 'OPTIONS'
|
| 219 |
+
return route in self._preflight_routes
|
| 220 |
+
|
| 221 |
+
def is_cors_enabled_on_request(self, request: web.Request) -> bool:
|
| 222 |
+
"""Is `request` is a request for CORS-enabled resource."""
|
| 223 |
+
|
| 224 |
+
return self._request_resource(request) in self._resource_config
|
| 225 |
+
|
| 226 |
+
def set_config_for_routing_entity(
|
| 227 |
+
self,
|
| 228 |
+
routing_entity: Union[web.Resource, web.StaticResource,
|
| 229 |
+
web.ResourceRoute],
|
| 230 |
+
config):
|
| 231 |
+
"""Record configuration for resource or it's route."""
|
| 232 |
+
|
| 233 |
+
if isinstance(routing_entity, (web.Resource, web.StaticResource)):
|
| 234 |
+
resource = routing_entity
|
| 235 |
+
|
| 236 |
+
# Add resource configuration or fail if it's already added.
|
| 237 |
+
if resource in self._resource_config:
|
| 238 |
+
raise ValueError(
|
| 239 |
+
"CORS is already configured for {!r} resource.".format(
|
| 240 |
+
resource))
|
| 241 |
+
|
| 242 |
+
self._resource_config[resource] = _ResourceConfig(
|
| 243 |
+
default_config=config)
|
| 244 |
+
|
| 245 |
+
elif isinstance(routing_entity, web.ResourceRoute):
|
| 246 |
+
route = routing_entity
|
| 247 |
+
|
| 248 |
+
# Add resource's route configuration or fail if it's already added.
|
| 249 |
+
if route.resource not in self._resource_config:
|
| 250 |
+
self.set_config_for_routing_entity(route.resource, config)
|
| 251 |
+
|
| 252 |
+
if route.resource not in self._resource_config:
|
| 253 |
+
raise ValueError(
|
| 254 |
+
"Can't setup CORS for {!r} request, "
|
| 255 |
+
"CORS must be enabled for route's resource first.".format(
|
| 256 |
+
route))
|
| 257 |
+
|
| 258 |
+
resource_config = self._resource_config[route.resource]
|
| 259 |
+
|
| 260 |
+
if route.method in resource_config.method_config:
|
| 261 |
+
raise ValueError(
|
| 262 |
+
"Can't setup CORS for {!r} route: CORS already "
|
| 263 |
+
"configured on resource {!r} for {} method".format(
|
| 264 |
+
route, route.resource, route.method))
|
| 265 |
+
|
| 266 |
+
resource_config.method_config[route.method] = config
|
| 267 |
+
|
| 268 |
+
else:
|
| 269 |
+
raise ValueError(
|
| 270 |
+
"Resource or ResourceRoute expected, got {!r}".format(
|
| 271 |
+
routing_entity))
|
| 272 |
+
|
| 273 |
+
async def get_preflight_request_config(
|
| 274 |
+
self,
|
| 275 |
+
preflight_request: web.Request,
|
| 276 |
+
origin: str,
|
| 277 |
+
requested_method: str):
|
| 278 |
+
assert self.is_preflight_request(preflight_request)
|
| 279 |
+
|
| 280 |
+
resource = self._request_resource(preflight_request)
|
| 281 |
+
resource_config = self._resource_config[resource]
|
| 282 |
+
defaulted_config = collections.ChainMap(
|
| 283 |
+
resource_config.default_config,
|
| 284 |
+
self._default_config)
|
| 285 |
+
|
| 286 |
+
options = defaulted_config.get(origin, defaulted_config.get("*"))
|
| 287 |
+
if options is not None and options.is_method_allowed(requested_method):
|
| 288 |
+
# Requested method enabled for CORS in defaults, override it with
|
| 289 |
+
# explicit route configuration (if any).
|
| 290 |
+
route_config = resource_config.method_config.get(
|
| 291 |
+
requested_method, {})
|
| 292 |
+
|
| 293 |
+
else:
|
| 294 |
+
# Requested method is not enabled in defaults.
|
| 295 |
+
# Enable CORS for it only if explicit configuration exists.
|
| 296 |
+
route_config = resource_config.method_config[requested_method]
|
| 297 |
+
|
| 298 |
+
defaulted_config = collections.ChainMap(route_config, defaulted_config)
|
| 299 |
+
|
| 300 |
+
return defaulted_config
|
| 301 |
+
|
| 302 |
+
def get_non_preflight_request_config(self, request: web.Request):
|
| 303 |
+
"""Get stored CORS configuration for routing entity that handles
|
| 304 |
+
specified request."""
|
| 305 |
+
|
| 306 |
+
assert self.is_cors_enabled_on_request(request)
|
| 307 |
+
|
| 308 |
+
resource = self._request_resource(request)
|
| 309 |
+
resource_config = self._resource_config[resource]
|
| 310 |
+
# Take Route config (if any) with defaults from Resource CORS
|
| 311 |
+
# configuration and global defaults.
|
| 312 |
+
route = request.match_info.route
|
| 313 |
+
if _is_web_view(route, strict=False):
|
| 314 |
+
method_config = request.match_info.handler.get_request_config(
|
| 315 |
+
request, request.method)
|
| 316 |
+
else:
|
| 317 |
+
method_config = resource_config.method_config.get(request.method,
|
| 318 |
+
{})
|
| 319 |
+
defaulted_config = collections.ChainMap(
|
| 320 |
+
method_config,
|
| 321 |
+
resource_config.default_config,
|
| 322 |
+
self._default_config)
|
| 323 |
+
|
| 324 |
+
return defaulted_config
|
.venv/lib/python3.11/site-packages/frozenlist/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import types
|
| 4 |
+
from collections.abc import MutableSequence
|
| 5 |
+
from functools import total_ordering
|
| 6 |
+
from typing import Any, Type
|
| 7 |
+
|
| 8 |
+
__version__ = "1.5.0"
|
| 9 |
+
|
| 10 |
+
__all__ = ("FrozenList", "PyFrozenList") # type: Tuple[str, ...]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
NO_EXTENSIONS = bool(os.environ.get("FROZENLIST_NO_EXTENSIONS")) # type: bool
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@total_ordering
|
| 17 |
+
class FrozenList(MutableSequence):
|
| 18 |
+
__slots__ = ("_frozen", "_items")
|
| 19 |
+
|
| 20 |
+
if sys.version_info >= (3, 9):
|
| 21 |
+
__class_getitem__ = classmethod(types.GenericAlias)
|
| 22 |
+
else:
|
| 23 |
+
|
| 24 |
+
@classmethod
|
| 25 |
+
def __class_getitem__(
|
| 26 |
+
cls: Type["FrozenList"],
|
| 27 |
+
cls_item: Any,
|
| 28 |
+
) -> Type["FrozenList"]:
|
| 29 |
+
return cls
|
| 30 |
+
|
| 31 |
+
def __init__(self, items=None):
|
| 32 |
+
self._frozen = False
|
| 33 |
+
if items is not None:
|
| 34 |
+
items = list(items)
|
| 35 |
+
else:
|
| 36 |
+
items = []
|
| 37 |
+
self._items = items
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def frozen(self):
|
| 41 |
+
return self._frozen
|
| 42 |
+
|
| 43 |
+
def freeze(self):
|
| 44 |
+
self._frozen = True
|
| 45 |
+
|
| 46 |
+
def __getitem__(self, index):
|
| 47 |
+
return self._items[index]
|
| 48 |
+
|
| 49 |
+
def __setitem__(self, index, value):
|
| 50 |
+
if self._frozen:
|
| 51 |
+
raise RuntimeError("Cannot modify frozen list.")
|
| 52 |
+
self._items[index] = value
|
| 53 |
+
|
| 54 |
+
def __delitem__(self, index):
|
| 55 |
+
if self._frozen:
|
| 56 |
+
raise RuntimeError("Cannot modify frozen list.")
|
| 57 |
+
del self._items[index]
|
| 58 |
+
|
| 59 |
+
def __len__(self):
|
| 60 |
+
return self._items.__len__()
|
| 61 |
+
|
| 62 |
+
def __iter__(self):
|
| 63 |
+
return self._items.__iter__()
|
| 64 |
+
|
| 65 |
+
def __reversed__(self):
|
| 66 |
+
return self._items.__reversed__()
|
| 67 |
+
|
| 68 |
+
def __eq__(self, other):
|
| 69 |
+
return list(self) == other
|
| 70 |
+
|
| 71 |
+
def __le__(self, other):
|
| 72 |
+
return list(self) <= other
|
| 73 |
+
|
| 74 |
+
def insert(self, pos, item):
|
| 75 |
+
if self._frozen:
|
| 76 |
+
raise RuntimeError("Cannot modify frozen list.")
|
| 77 |
+
self._items.insert(pos, item)
|
| 78 |
+
|
| 79 |
+
def __repr__(self):
|
| 80 |
+
return f"<FrozenList(frozen={self._frozen}, {self._items!r})>"
|
| 81 |
+
|
| 82 |
+
def __hash__(self):
|
| 83 |
+
if self._frozen:
|
| 84 |
+
return hash(tuple(self))
|
| 85 |
+
else:
|
| 86 |
+
raise RuntimeError("Cannot hash unfrozen list.")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
PyFrozenList = FrozenList
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if not NO_EXTENSIONS:
|
| 93 |
+
try:
|
| 94 |
+
from ._frozenlist import FrozenList as CFrozenList # type: ignore
|
| 95 |
+
except ImportError: # pragma: no cover
|
| 96 |
+
pass
|
| 97 |
+
else:
|
| 98 |
+
FrozenList = CFrozenList # type: ignore
|
.venv/lib/python3.11/site-packages/frozenlist/__init__.pyi
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import (
|
| 2 |
+
Generic,
|
| 3 |
+
Iterable,
|
| 4 |
+
Iterator,
|
| 5 |
+
List,
|
| 6 |
+
MutableSequence,
|
| 7 |
+
Optional,
|
| 8 |
+
TypeVar,
|
| 9 |
+
Union,
|
| 10 |
+
overload,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
_T = TypeVar("_T")
|
| 14 |
+
_Arg = Union[List[_T], Iterable[_T]]
|
| 15 |
+
|
| 16 |
+
class FrozenList(MutableSequence[_T], Generic[_T]):
|
| 17 |
+
def __init__(self, items: Optional[_Arg[_T]] = None) -> None: ...
|
| 18 |
+
@property
|
| 19 |
+
def frozen(self) -> bool: ...
|
| 20 |
+
def freeze(self) -> None: ...
|
| 21 |
+
@overload
|
| 22 |
+
def __getitem__(self, i: int) -> _T: ...
|
| 23 |
+
@overload
|
| 24 |
+
def __getitem__(self, s: slice) -> FrozenList[_T]: ...
|
| 25 |
+
@overload
|
| 26 |
+
def __setitem__(self, i: int, o: _T) -> None: ...
|
| 27 |
+
@overload
|
| 28 |
+
def __setitem__(self, s: slice, o: Iterable[_T]) -> None: ...
|
| 29 |
+
@overload
|
| 30 |
+
def __delitem__(self, i: int) -> None: ...
|
| 31 |
+
@overload
|
| 32 |
+
def __delitem__(self, i: slice) -> None: ...
|
| 33 |
+
def __len__(self) -> int: ...
|
| 34 |
+
def __iter__(self) -> Iterator[_T]: ...
|
| 35 |
+
def __reversed__(self) -> Iterator[_T]: ...
|
| 36 |
+
def __eq__(self, other: object) -> bool: ...
|
| 37 |
+
def __le__(self, other: FrozenList[_T]) -> bool: ...
|
| 38 |
+
def __ne__(self, other: object) -> bool: ...
|
| 39 |
+
def __lt__(self, other: FrozenList[_T]) -> bool: ...
|
| 40 |
+
def __ge__(self, other: FrozenList[_T]) -> bool: ...
|
| 41 |
+
def __gt__(self, other: FrozenList[_T]) -> bool: ...
|
| 42 |
+
def insert(self, pos: int, item: _T) -> None: ...
|
| 43 |
+
def __repr__(self) -> str: ...
|
| 44 |
+
def __hash__(self) -> int: ...
|
| 45 |
+
|
| 46 |
+
# types for C accelerators are the same
|
| 47 |
+
CFrozenList = PyFrozenList = FrozenList
|
.venv/lib/python3.11/site-packages/frozenlist/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (4.84 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/frozenlist/_frozenlist.pyx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import types
|
| 3 |
+
from collections.abc import MutableSequence
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
cdef class FrozenList:
|
| 7 |
+
|
| 8 |
+
if sys.version_info >= (3, 9):
|
| 9 |
+
__class_getitem__ = classmethod(types.GenericAlias)
|
| 10 |
+
else:
|
| 11 |
+
@classmethod
|
| 12 |
+
def __class_getitem__(cls, cls_item):
|
| 13 |
+
return cls
|
| 14 |
+
|
| 15 |
+
cdef readonly bint frozen
|
| 16 |
+
cdef list _items
|
| 17 |
+
|
| 18 |
+
def __init__(self, items=None):
|
| 19 |
+
self.frozen = False
|
| 20 |
+
if items is not None:
|
| 21 |
+
items = list(items)
|
| 22 |
+
else:
|
| 23 |
+
items = []
|
| 24 |
+
self._items = items
|
| 25 |
+
|
| 26 |
+
cdef object _check_frozen(self):
|
| 27 |
+
if self.frozen:
|
| 28 |
+
raise RuntimeError("Cannot modify frozen list.")
|
| 29 |
+
|
| 30 |
+
cdef inline object _fast_len(self):
|
| 31 |
+
return len(self._items)
|
| 32 |
+
|
| 33 |
+
def freeze(self):
|
| 34 |
+
self.frozen = True
|
| 35 |
+
|
| 36 |
+
def __getitem__(self, index):
|
| 37 |
+
return self._items[index]
|
| 38 |
+
|
| 39 |
+
def __setitem__(self, index, value):
|
| 40 |
+
self._check_frozen()
|
| 41 |
+
self._items[index] = value
|
| 42 |
+
|
| 43 |
+
def __delitem__(self, index):
|
| 44 |
+
self._check_frozen()
|
| 45 |
+
del self._items[index]
|
| 46 |
+
|
| 47 |
+
def __len__(self):
|
| 48 |
+
return self._fast_len()
|
| 49 |
+
|
| 50 |
+
def __iter__(self):
|
| 51 |
+
return self._items.__iter__()
|
| 52 |
+
|
| 53 |
+
def __reversed__(self):
|
| 54 |
+
return self._items.__reversed__()
|
| 55 |
+
|
| 56 |
+
def __richcmp__(self, other, op):
|
| 57 |
+
if op == 0: # <
|
| 58 |
+
return list(self) < other
|
| 59 |
+
if op == 1: # <=
|
| 60 |
+
return list(self) <= other
|
| 61 |
+
if op == 2: # ==
|
| 62 |
+
return list(self) == other
|
| 63 |
+
if op == 3: # !=
|
| 64 |
+
return list(self) != other
|
| 65 |
+
if op == 4: # >
|
| 66 |
+
return list(self) > other
|
| 67 |
+
if op == 5: # =>
|
| 68 |
+
return list(self) >= other
|
| 69 |
+
|
| 70 |
+
def insert(self, pos, item):
|
| 71 |
+
self._check_frozen()
|
| 72 |
+
self._items.insert(pos, item)
|
| 73 |
+
|
| 74 |
+
def __contains__(self, item):
|
| 75 |
+
return item in self._items
|
| 76 |
+
|
| 77 |
+
def __iadd__(self, items):
|
| 78 |
+
self._check_frozen()
|
| 79 |
+
self._items += list(items)
|
| 80 |
+
return self
|
| 81 |
+
|
| 82 |
+
def index(self, item):
|
| 83 |
+
return self._items.index(item)
|
| 84 |
+
|
| 85 |
+
def remove(self, item):
|
| 86 |
+
self._check_frozen()
|
| 87 |
+
self._items.remove(item)
|
| 88 |
+
|
| 89 |
+
def clear(self):
|
| 90 |
+
self._check_frozen()
|
| 91 |
+
self._items.clear()
|
| 92 |
+
|
| 93 |
+
def extend(self, items):
|
| 94 |
+
self._check_frozen()
|
| 95 |
+
self._items += list(items)
|
| 96 |
+
|
| 97 |
+
def reverse(self):
|
| 98 |
+
self._check_frozen()
|
| 99 |
+
self._items.reverse()
|
| 100 |
+
|
| 101 |
+
def pop(self, index=-1):
|
| 102 |
+
self._check_frozen()
|
| 103 |
+
return self._items.pop(index)
|
| 104 |
+
|
| 105 |
+
def append(self, item):
|
| 106 |
+
self._check_frozen()
|
| 107 |
+
return self._items.append(item)
|
| 108 |
+
|
| 109 |
+
def count(self, item):
|
| 110 |
+
return self._items.count(item)
|
| 111 |
+
|
| 112 |
+
def __repr__(self):
|
| 113 |
+
return '<FrozenList(frozen={}, {!r})>'.format(self.frozen,
|
| 114 |
+
self._items)
|
| 115 |
+
|
| 116 |
+
def __hash__(self):
|
| 117 |
+
if self.frozen:
|
| 118 |
+
return hash(tuple(self._items))
|
| 119 |
+
else:
|
| 120 |
+
raise RuntimeError("Cannot hash unfrozen list.")
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
MutableSequence.register(FrozenList)
|
.venv/lib/python3.11/site-packages/frozenlist/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Marker
|
.venv/lib/python3.11/site-packages/googleapiclient/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
# Set default logging handler to avoid "No handler found" warnings.
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
try: # Python 2.7+
|
| 19 |
+
from logging import NullHandler
|
| 20 |
+
except ImportError:
|
| 21 |
+
|
| 22 |
+
class NullHandler(logging.Handler):
|
| 23 |
+
def emit(self, record):
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logging.getLogger(__name__).addHandler(NullHandler())
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (895 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/_auth.cpython-311.pyc
ADDED
|
Binary file (6.27 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/_helpers.cpython-311.pyc
ADDED
|
Binary file (7.56 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/channel.cpython-311.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/discovery.cpython-311.pyc
ADDED
|
Binary file (68.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/errors.cpython-311.pyc
ADDED
|
Binary file (9.17 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/http.cpython-311.pyc
ADDED
|
Binary file (75.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/mimeparse.cpython-311.pyc
ADDED
|
Binary file (9.01 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/model.cpython-311.pyc
ADDED
|
Binary file (17.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/sample_tools.cpython-311.pyc
ADDED
|
Binary file (4.24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/schema.cpython-311.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/__pycache__/version.cpython-311.pyc
ADDED
|
Binary file (211 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/_auth.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
"""Helpers for authentication using oauth2client or google-auth."""
|
| 16 |
+
|
| 17 |
+
import httplib2
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
import google.auth
|
| 21 |
+
import google.auth.credentials
|
| 22 |
+
|
| 23 |
+
HAS_GOOGLE_AUTH = True
|
| 24 |
+
except ImportError: # pragma: NO COVER
|
| 25 |
+
HAS_GOOGLE_AUTH = False
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
import google_auth_httplib2
|
| 29 |
+
except ImportError: # pragma: NO COVER
|
| 30 |
+
google_auth_httplib2 = None
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
import oauth2client
|
| 34 |
+
import oauth2client.client
|
| 35 |
+
|
| 36 |
+
HAS_OAUTH2CLIENT = True
|
| 37 |
+
except ImportError: # pragma: NO COVER
|
| 38 |
+
HAS_OAUTH2CLIENT = False
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def credentials_from_file(filename, scopes=None, quota_project_id=None):
|
| 42 |
+
"""Returns credentials loaded from a file."""
|
| 43 |
+
if HAS_GOOGLE_AUTH:
|
| 44 |
+
credentials, _ = google.auth.load_credentials_from_file(
|
| 45 |
+
filename, scopes=scopes, quota_project_id=quota_project_id
|
| 46 |
+
)
|
| 47 |
+
return credentials
|
| 48 |
+
else:
|
| 49 |
+
raise EnvironmentError(
|
| 50 |
+
"client_options.credentials_file is only supported in google-auth."
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def default_credentials(scopes=None, quota_project_id=None):
|
| 55 |
+
"""Returns Application Default Credentials."""
|
| 56 |
+
if HAS_GOOGLE_AUTH:
|
| 57 |
+
credentials, _ = google.auth.default(
|
| 58 |
+
scopes=scopes, quota_project_id=quota_project_id
|
| 59 |
+
)
|
| 60 |
+
return credentials
|
| 61 |
+
elif HAS_OAUTH2CLIENT:
|
| 62 |
+
if scopes is not None or quota_project_id is not None:
|
| 63 |
+
raise EnvironmentError(
|
| 64 |
+
"client_options.scopes and client_options.quota_project_id are not supported in oauth2client."
|
| 65 |
+
"Please install google-auth."
|
| 66 |
+
)
|
| 67 |
+
return oauth2client.client.GoogleCredentials.get_application_default()
|
| 68 |
+
else:
|
| 69 |
+
raise EnvironmentError(
|
| 70 |
+
"No authentication library is available. Please install either "
|
| 71 |
+
"google-auth or oauth2client."
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def with_scopes(credentials, scopes):
|
| 76 |
+
"""Scopes the credentials if necessary.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
credentials (Union[
|
| 80 |
+
google.auth.credentials.Credentials,
|
| 81 |
+
oauth2client.client.Credentials]): The credentials to scope.
|
| 82 |
+
scopes (Sequence[str]): The list of scopes.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
Union[google.auth.credentials.Credentials,
|
| 86 |
+
oauth2client.client.Credentials]: The scoped credentials.
|
| 87 |
+
"""
|
| 88 |
+
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
| 89 |
+
return google.auth.credentials.with_scopes_if_required(credentials, scopes)
|
| 90 |
+
else:
|
| 91 |
+
try:
|
| 92 |
+
if credentials.create_scoped_required():
|
| 93 |
+
return credentials.create_scoped(scopes)
|
| 94 |
+
else:
|
| 95 |
+
return credentials
|
| 96 |
+
except AttributeError:
|
| 97 |
+
return credentials
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def authorized_http(credentials):
|
| 101 |
+
"""Returns an http client that is authorized with the given credentials.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
credentials (Union[
|
| 105 |
+
google.auth.credentials.Credentials,
|
| 106 |
+
oauth2client.client.Credentials]): The credentials to use.
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Union[httplib2.Http, google_auth_httplib2.AuthorizedHttp]: An
|
| 110 |
+
authorized http client.
|
| 111 |
+
"""
|
| 112 |
+
from googleapiclient.http import build_http
|
| 113 |
+
|
| 114 |
+
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
| 115 |
+
if google_auth_httplib2 is None:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
"Credentials from google.auth specified, but "
|
| 118 |
+
"google-api-python-client is unable to use these credentials "
|
| 119 |
+
"unless google-auth-httplib2 is installed. Please install "
|
| 120 |
+
"google-auth-httplib2."
|
| 121 |
+
)
|
| 122 |
+
return google_auth_httplib2.AuthorizedHttp(credentials, http=build_http())
|
| 123 |
+
else:
|
| 124 |
+
return credentials.authorize(build_http())
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def refresh_credentials(credentials):
|
| 128 |
+
# Refresh must use a new http instance, as the one associated with the
|
| 129 |
+
# credentials could be a AuthorizedHttp or an oauth2client-decorated
|
| 130 |
+
# Http instance which would cause a weird recursive loop of refreshing
|
| 131 |
+
# and likely tear a hole in spacetime.
|
| 132 |
+
refresh_http = httplib2.Http()
|
| 133 |
+
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
| 134 |
+
request = google_auth_httplib2.Request(refresh_http)
|
| 135 |
+
return credentials.refresh(request)
|
| 136 |
+
else:
|
| 137 |
+
return credentials.refresh(refresh_http)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def apply_credentials(credentials, headers):
|
| 141 |
+
# oauth2client and google-auth have the same interface for this.
|
| 142 |
+
if not is_valid(credentials):
|
| 143 |
+
refresh_credentials(credentials)
|
| 144 |
+
return credentials.apply(headers)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def is_valid(credentials):
|
| 148 |
+
if HAS_GOOGLE_AUTH and isinstance(credentials, google.auth.credentials.Credentials):
|
| 149 |
+
return credentials.valid
|
| 150 |
+
else:
|
| 151 |
+
return (
|
| 152 |
+
credentials.access_token is not None
|
| 153 |
+
and not credentials.access_token_expired
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def get_credentials_from_http(http):
|
| 158 |
+
if http is None:
|
| 159 |
+
return None
|
| 160 |
+
elif hasattr(http.request, "credentials"):
|
| 161 |
+
return http.request.credentials
|
| 162 |
+
elif hasattr(http, "credentials") and not isinstance(
|
| 163 |
+
http.credentials, httplib2.Credentials
|
| 164 |
+
):
|
| 165 |
+
return http.credentials
|
| 166 |
+
else:
|
| 167 |
+
return None
|
.venv/lib/python3.11/site-packages/googleapiclient/_helpers.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc. All rights reserved.
|
| 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 |
+
|
| 15 |
+
"""Helper functions for commonly used utilities."""
|
| 16 |
+
|
| 17 |
+
import functools
|
| 18 |
+
import inspect
|
| 19 |
+
import logging
|
| 20 |
+
import urllib
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
POSITIONAL_WARNING = "WARNING"
|
| 25 |
+
POSITIONAL_EXCEPTION = "EXCEPTION"
|
| 26 |
+
POSITIONAL_IGNORE = "IGNORE"
|
| 27 |
+
POSITIONAL_SET = frozenset(
|
| 28 |
+
[POSITIONAL_WARNING, POSITIONAL_EXCEPTION, POSITIONAL_IGNORE]
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
positional_parameters_enforcement = POSITIONAL_WARNING
|
| 32 |
+
|
| 33 |
+
_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link."
|
| 34 |
+
_IS_DIR_MESSAGE = "{0}: Is a directory"
|
| 35 |
+
_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def positional(max_positional_args):
|
| 39 |
+
"""A decorator to declare that only the first N arguments may be positional.
|
| 40 |
+
|
| 41 |
+
This decorator makes it easy to support Python 3 style keyword-only
|
| 42 |
+
parameters. For example, in Python 3 it is possible to write::
|
| 43 |
+
|
| 44 |
+
def fn(pos1, *, kwonly1=None, kwonly2=None):
|
| 45 |
+
...
|
| 46 |
+
|
| 47 |
+
All named parameters after ``*`` must be a keyword::
|
| 48 |
+
|
| 49 |
+
fn(10, 'kw1', 'kw2') # Raises exception.
|
| 50 |
+
fn(10, kwonly1='kw1') # Ok.
|
| 51 |
+
|
| 52 |
+
Example
|
| 53 |
+
^^^^^^^
|
| 54 |
+
|
| 55 |
+
To define a function like above, do::
|
| 56 |
+
|
| 57 |
+
@positional(1)
|
| 58 |
+
def fn(pos1, kwonly1=None, kwonly2=None):
|
| 59 |
+
...
|
| 60 |
+
|
| 61 |
+
If no default value is provided to a keyword argument, it becomes a
|
| 62 |
+
required keyword argument::
|
| 63 |
+
|
| 64 |
+
@positional(0)
|
| 65 |
+
def fn(required_kw):
|
| 66 |
+
...
|
| 67 |
+
|
| 68 |
+
This must be called with the keyword parameter::
|
| 69 |
+
|
| 70 |
+
fn() # Raises exception.
|
| 71 |
+
fn(10) # Raises exception.
|
| 72 |
+
fn(required_kw=10) # Ok.
|
| 73 |
+
|
| 74 |
+
When defining instance or class methods always remember to account for
|
| 75 |
+
``self`` and ``cls``::
|
| 76 |
+
|
| 77 |
+
class MyClass(object):
|
| 78 |
+
|
| 79 |
+
@positional(2)
|
| 80 |
+
def my_method(self, pos1, kwonly1=None):
|
| 81 |
+
...
|
| 82 |
+
|
| 83 |
+
@classmethod
|
| 84 |
+
@positional(2)
|
| 85 |
+
def my_method(cls, pos1, kwonly1=None):
|
| 86 |
+
...
|
| 87 |
+
|
| 88 |
+
The positional decorator behavior is controlled by
|
| 89 |
+
``_helpers.positional_parameters_enforcement``, which may be set to
|
| 90 |
+
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
| 91 |
+
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
| 92 |
+
nothing, respectively, if a declaration is violated.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
max_positional_arguments: Maximum number of positional arguments. All
|
| 96 |
+
parameters after this index must be
|
| 97 |
+
keyword only.
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
A decorator that prevents using arguments after max_positional_args
|
| 101 |
+
from being used as positional parameters.
|
| 102 |
+
|
| 103 |
+
Raises:
|
| 104 |
+
TypeError: if a keyword-only argument is provided as a positional
|
| 105 |
+
parameter, but only if
|
| 106 |
+
_helpers.positional_parameters_enforcement is set to
|
| 107 |
+
POSITIONAL_EXCEPTION.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def positional_decorator(wrapped):
|
| 111 |
+
@functools.wraps(wrapped)
|
| 112 |
+
def positional_wrapper(*args, **kwargs):
|
| 113 |
+
if len(args) > max_positional_args:
|
| 114 |
+
plural_s = ""
|
| 115 |
+
if max_positional_args != 1:
|
| 116 |
+
plural_s = "s"
|
| 117 |
+
message = (
|
| 118 |
+
"{function}() takes at most {args_max} positional "
|
| 119 |
+
"argument{plural} ({args_given} given)".format(
|
| 120 |
+
function=wrapped.__name__,
|
| 121 |
+
args_max=max_positional_args,
|
| 122 |
+
args_given=len(args),
|
| 123 |
+
plural=plural_s,
|
| 124 |
+
)
|
| 125 |
+
)
|
| 126 |
+
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
| 127 |
+
raise TypeError(message)
|
| 128 |
+
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
| 129 |
+
logger.warning(message)
|
| 130 |
+
return wrapped(*args, **kwargs)
|
| 131 |
+
|
| 132 |
+
return positional_wrapper
|
| 133 |
+
|
| 134 |
+
if isinstance(max_positional_args, int):
|
| 135 |
+
return positional_decorator
|
| 136 |
+
else:
|
| 137 |
+
args, _, _, defaults, _, _, _ = inspect.getfullargspec(max_positional_args)
|
| 138 |
+
return positional(len(args) - len(defaults))(max_positional_args)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def parse_unique_urlencoded(content):
|
| 142 |
+
"""Parses unique key-value parameters from urlencoded content.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
content: string, URL-encoded key-value pairs.
|
| 146 |
+
|
| 147 |
+
Returns:
|
| 148 |
+
dict, The key-value pairs from ``content``.
|
| 149 |
+
|
| 150 |
+
Raises:
|
| 151 |
+
ValueError: if one of the keys is repeated.
|
| 152 |
+
"""
|
| 153 |
+
urlencoded_params = urllib.parse.parse_qs(content)
|
| 154 |
+
params = {}
|
| 155 |
+
for key, value in urlencoded_params.items():
|
| 156 |
+
if len(value) != 1:
|
| 157 |
+
msg = "URL-encoded content contains a repeated value:" "%s -> %s" % (
|
| 158 |
+
key,
|
| 159 |
+
", ".join(value),
|
| 160 |
+
)
|
| 161 |
+
raise ValueError(msg)
|
| 162 |
+
params[key] = value[0]
|
| 163 |
+
return params
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def update_query_params(uri, params):
|
| 167 |
+
"""Updates a URI with new query parameters.
|
| 168 |
+
|
| 169 |
+
If a given key from ``params`` is repeated in the ``uri``, then
|
| 170 |
+
the URI will be considered invalid and an error will occur.
|
| 171 |
+
|
| 172 |
+
If the URI is valid, then each value from ``params`` will
|
| 173 |
+
replace the corresponding value in the query parameters (if
|
| 174 |
+
it exists).
|
| 175 |
+
|
| 176 |
+
Args:
|
| 177 |
+
uri: string, A valid URI, with potential existing query parameters.
|
| 178 |
+
params: dict, A dictionary of query parameters.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
The same URI but with the new query parameters added.
|
| 182 |
+
"""
|
| 183 |
+
parts = urllib.parse.urlparse(uri)
|
| 184 |
+
query_params = parse_unique_urlencoded(parts.query)
|
| 185 |
+
query_params.update(params)
|
| 186 |
+
new_query = urllib.parse.urlencode(query_params)
|
| 187 |
+
new_parts = parts._replace(query=new_query)
|
| 188 |
+
return urllib.parse.urlunparse(new_parts)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _add_query_parameter(url, name, value):
|
| 192 |
+
"""Adds a query parameter to a url.
|
| 193 |
+
|
| 194 |
+
Replaces the current value if it already exists in the URL.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
url: string, url to add the query parameter to.
|
| 198 |
+
name: string, query parameter name.
|
| 199 |
+
value: string, query parameter value.
|
| 200 |
+
|
| 201 |
+
Returns:
|
| 202 |
+
Updated query parameter. Does not update the url if value is None.
|
| 203 |
+
"""
|
| 204 |
+
if value is None:
|
| 205 |
+
return url
|
| 206 |
+
else:
|
| 207 |
+
return update_query_params(url, {name: value})
|
.venv/lib/python3.11/site-packages/googleapiclient/channel.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
"""Channel notifications support.
|
| 16 |
+
|
| 17 |
+
Classes and functions to support channel subscriptions and notifications
|
| 18 |
+
on those channels.
|
| 19 |
+
|
| 20 |
+
Notes:
|
| 21 |
+
- This code is based on experimental APIs and is subject to change.
|
| 22 |
+
- Notification does not do deduplication of notification ids, that's up to
|
| 23 |
+
the receiver.
|
| 24 |
+
- Storing the Channel between calls is up to the caller.
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
Example setting up a channel:
|
| 28 |
+
|
| 29 |
+
# Create a new channel that gets notifications via webhook.
|
| 30 |
+
channel = new_webhook_channel("https://example.com/my_web_hook")
|
| 31 |
+
|
| 32 |
+
# Store the channel, keyed by 'channel.id'. Store it before calling the
|
| 33 |
+
# watch method because notifications may start arriving before the watch
|
| 34 |
+
# method returns.
|
| 35 |
+
...
|
| 36 |
+
|
| 37 |
+
resp = service.objects().watchAll(
|
| 38 |
+
bucket="some_bucket_id", body=channel.body()).execute()
|
| 39 |
+
channel.update(resp)
|
| 40 |
+
|
| 41 |
+
# Store the channel, keyed by 'channel.id'. Store it after being updated
|
| 42 |
+
# since the resource_id value will now be correct, and that's needed to
|
| 43 |
+
# stop a subscription.
|
| 44 |
+
...
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
An example Webhook implementation using webapp2. Note that webapp2 puts
|
| 48 |
+
headers in a case insensitive dictionary, as headers aren't guaranteed to
|
| 49 |
+
always be upper case.
|
| 50 |
+
|
| 51 |
+
id = self.request.headers[X_GOOG_CHANNEL_ID]
|
| 52 |
+
|
| 53 |
+
# Retrieve the channel by id.
|
| 54 |
+
channel = ...
|
| 55 |
+
|
| 56 |
+
# Parse notification from the headers, including validating the id.
|
| 57 |
+
n = notification_from_headers(channel, self.request.headers)
|
| 58 |
+
|
| 59 |
+
# Do app specific stuff with the notification here.
|
| 60 |
+
if n.resource_state == 'sync':
|
| 61 |
+
# Code to handle sync state.
|
| 62 |
+
elif n.resource_state == 'exists':
|
| 63 |
+
# Code to handle the exists state.
|
| 64 |
+
elif n.resource_state == 'not_exists':
|
| 65 |
+
# Code to handle the not exists state.
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
Example of unsubscribing.
|
| 69 |
+
|
| 70 |
+
service.channels().stop(channel.body()).execute()
|
| 71 |
+
"""
|
| 72 |
+
from __future__ import absolute_import
|
| 73 |
+
|
| 74 |
+
import datetime
|
| 75 |
+
import uuid
|
| 76 |
+
|
| 77 |
+
from googleapiclient import _helpers as util
|
| 78 |
+
from googleapiclient import errors
|
| 79 |
+
|
| 80 |
+
# The unix time epoch starts at midnight 1970.
|
| 81 |
+
EPOCH = datetime.datetime(1970, 1, 1)
|
| 82 |
+
|
| 83 |
+
# Map the names of the parameters in the JSON channel description to
|
| 84 |
+
# the parameter names we use in the Channel class.
|
| 85 |
+
CHANNEL_PARAMS = {
|
| 86 |
+
"address": "address",
|
| 87 |
+
"id": "id",
|
| 88 |
+
"expiration": "expiration",
|
| 89 |
+
"params": "params",
|
| 90 |
+
"resourceId": "resource_id",
|
| 91 |
+
"resourceUri": "resource_uri",
|
| 92 |
+
"type": "type",
|
| 93 |
+
"token": "token",
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
X_GOOG_CHANNEL_ID = "X-GOOG-CHANNEL-ID"
|
| 97 |
+
X_GOOG_MESSAGE_NUMBER = "X-GOOG-MESSAGE-NUMBER"
|
| 98 |
+
X_GOOG_RESOURCE_STATE = "X-GOOG-RESOURCE-STATE"
|
| 99 |
+
X_GOOG_RESOURCE_URI = "X-GOOG-RESOURCE-URI"
|
| 100 |
+
X_GOOG_RESOURCE_ID = "X-GOOG-RESOURCE-ID"
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _upper_header_keys(headers):
|
| 104 |
+
new_headers = {}
|
| 105 |
+
for k, v in headers.items():
|
| 106 |
+
new_headers[k.upper()] = v
|
| 107 |
+
return new_headers
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class Notification(object):
|
| 111 |
+
"""A Notification from a Channel.
|
| 112 |
+
|
| 113 |
+
Notifications are not usually constructed directly, but are returned
|
| 114 |
+
from functions like notification_from_headers().
|
| 115 |
+
|
| 116 |
+
Attributes:
|
| 117 |
+
message_number: int, The unique id number of this notification.
|
| 118 |
+
state: str, The state of the resource being monitored.
|
| 119 |
+
uri: str, The address of the resource being monitored.
|
| 120 |
+
resource_id: str, The unique identifier of the version of the resource at
|
| 121 |
+
this event.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
@util.positional(5)
|
| 125 |
+
def __init__(self, message_number, state, resource_uri, resource_id):
|
| 126 |
+
"""Notification constructor.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
message_number: int, The unique id number of this notification.
|
| 130 |
+
state: str, The state of the resource being monitored. Can be one
|
| 131 |
+
of "exists", "not_exists", or "sync".
|
| 132 |
+
resource_uri: str, The address of the resource being monitored.
|
| 133 |
+
resource_id: str, The identifier of the watched resource.
|
| 134 |
+
"""
|
| 135 |
+
self.message_number = message_number
|
| 136 |
+
self.state = state
|
| 137 |
+
self.resource_uri = resource_uri
|
| 138 |
+
self.resource_id = resource_id
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class Channel(object):
|
| 142 |
+
"""A Channel for notifications.
|
| 143 |
+
|
| 144 |
+
Usually not constructed directly, instead it is returned from helper
|
| 145 |
+
functions like new_webhook_channel().
|
| 146 |
+
|
| 147 |
+
Attributes:
|
| 148 |
+
type: str, The type of delivery mechanism used by this channel. For
|
| 149 |
+
example, 'web_hook'.
|
| 150 |
+
id: str, A UUID for the channel.
|
| 151 |
+
token: str, An arbitrary string associated with the channel that
|
| 152 |
+
is delivered to the target address with each event delivered
|
| 153 |
+
over this channel.
|
| 154 |
+
address: str, The address of the receiving entity where events are
|
| 155 |
+
delivered. Specific to the channel type.
|
| 156 |
+
expiration: int, The time, in milliseconds from the epoch, when this
|
| 157 |
+
channel will expire.
|
| 158 |
+
params: dict, A dictionary of string to string, with additional parameters
|
| 159 |
+
controlling delivery channel behavior.
|
| 160 |
+
resource_id: str, An opaque id that identifies the resource that is
|
| 161 |
+
being watched. Stable across different API versions.
|
| 162 |
+
resource_uri: str, The canonicalized ID of the watched resource.
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
@util.positional(5)
|
| 166 |
+
def __init__(
|
| 167 |
+
self,
|
| 168 |
+
type,
|
| 169 |
+
id,
|
| 170 |
+
token,
|
| 171 |
+
address,
|
| 172 |
+
expiration=None,
|
| 173 |
+
params=None,
|
| 174 |
+
resource_id="",
|
| 175 |
+
resource_uri="",
|
| 176 |
+
):
|
| 177 |
+
"""Create a new Channel.
|
| 178 |
+
|
| 179 |
+
In user code, this Channel constructor will not typically be called
|
| 180 |
+
manually since there are functions for creating channels for each specific
|
| 181 |
+
type with a more customized set of arguments to pass.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
type: str, The type of delivery mechanism used by this channel. For
|
| 185 |
+
example, 'web_hook'.
|
| 186 |
+
id: str, A UUID for the channel.
|
| 187 |
+
token: str, An arbitrary string associated with the channel that
|
| 188 |
+
is delivered to the target address with each event delivered
|
| 189 |
+
over this channel.
|
| 190 |
+
address: str, The address of the receiving entity where events are
|
| 191 |
+
delivered. Specific to the channel type.
|
| 192 |
+
expiration: int, The time, in milliseconds from the epoch, when this
|
| 193 |
+
channel will expire.
|
| 194 |
+
params: dict, A dictionary of string to string, with additional parameters
|
| 195 |
+
controlling delivery channel behavior.
|
| 196 |
+
resource_id: str, An opaque id that identifies the resource that is
|
| 197 |
+
being watched. Stable across different API versions.
|
| 198 |
+
resource_uri: str, The canonicalized ID of the watched resource.
|
| 199 |
+
"""
|
| 200 |
+
self.type = type
|
| 201 |
+
self.id = id
|
| 202 |
+
self.token = token
|
| 203 |
+
self.address = address
|
| 204 |
+
self.expiration = expiration
|
| 205 |
+
self.params = params
|
| 206 |
+
self.resource_id = resource_id
|
| 207 |
+
self.resource_uri = resource_uri
|
| 208 |
+
|
| 209 |
+
def body(self):
|
| 210 |
+
"""Build a body from the Channel.
|
| 211 |
+
|
| 212 |
+
Constructs a dictionary that's appropriate for passing into watch()
|
| 213 |
+
methods as the value of body argument.
|
| 214 |
+
|
| 215 |
+
Returns:
|
| 216 |
+
A dictionary representation of the channel.
|
| 217 |
+
"""
|
| 218 |
+
result = {
|
| 219 |
+
"id": self.id,
|
| 220 |
+
"token": self.token,
|
| 221 |
+
"type": self.type,
|
| 222 |
+
"address": self.address,
|
| 223 |
+
}
|
| 224 |
+
if self.params:
|
| 225 |
+
result["params"] = self.params
|
| 226 |
+
if self.resource_id:
|
| 227 |
+
result["resourceId"] = self.resource_id
|
| 228 |
+
if self.resource_uri:
|
| 229 |
+
result["resourceUri"] = self.resource_uri
|
| 230 |
+
if self.expiration:
|
| 231 |
+
result["expiration"] = self.expiration
|
| 232 |
+
|
| 233 |
+
return result
|
| 234 |
+
|
| 235 |
+
def update(self, resp):
|
| 236 |
+
"""Update a channel with information from the response of watch().
|
| 237 |
+
|
| 238 |
+
When a request is sent to watch() a resource, the response returned
|
| 239 |
+
from the watch() request is a dictionary with updated channel information,
|
| 240 |
+
such as the resource_id, which is needed when stopping a subscription.
|
| 241 |
+
|
| 242 |
+
Args:
|
| 243 |
+
resp: dict, The response from a watch() method.
|
| 244 |
+
"""
|
| 245 |
+
for json_name, param_name in CHANNEL_PARAMS.items():
|
| 246 |
+
value = resp.get(json_name)
|
| 247 |
+
if value is not None:
|
| 248 |
+
setattr(self, param_name, value)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def notification_from_headers(channel, headers):
|
| 252 |
+
"""Parse a notification from the webhook request headers, validate
|
| 253 |
+
the notification, and return a Notification object.
|
| 254 |
+
|
| 255 |
+
Args:
|
| 256 |
+
channel: Channel, The channel that the notification is associated with.
|
| 257 |
+
headers: dict, A dictionary like object that contains the request headers
|
| 258 |
+
from the webhook HTTP request.
|
| 259 |
+
|
| 260 |
+
Returns:
|
| 261 |
+
A Notification object.
|
| 262 |
+
|
| 263 |
+
Raises:
|
| 264 |
+
errors.InvalidNotificationError if the notification is invalid.
|
| 265 |
+
ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
|
| 266 |
+
"""
|
| 267 |
+
headers = _upper_header_keys(headers)
|
| 268 |
+
channel_id = headers[X_GOOG_CHANNEL_ID]
|
| 269 |
+
if channel.id != channel_id:
|
| 270 |
+
raise errors.InvalidNotificationError(
|
| 271 |
+
"Channel id mismatch: %s != %s" % (channel.id, channel_id)
|
| 272 |
+
)
|
| 273 |
+
else:
|
| 274 |
+
message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
|
| 275 |
+
state = headers[X_GOOG_RESOURCE_STATE]
|
| 276 |
+
resource_uri = headers[X_GOOG_RESOURCE_URI]
|
| 277 |
+
resource_id = headers[X_GOOG_RESOURCE_ID]
|
| 278 |
+
return Notification(message_number, state, resource_uri, resource_id)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@util.positional(2)
|
| 282 |
+
def new_webhook_channel(url, token=None, expiration=None, params=None):
|
| 283 |
+
"""Create a new webhook Channel.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
url: str, URL to post notifications to.
|
| 287 |
+
token: str, An arbitrary string associated with the channel that
|
| 288 |
+
is delivered to the target address with each notification delivered
|
| 289 |
+
over this channel.
|
| 290 |
+
expiration: datetime.datetime, A time in the future when the channel
|
| 291 |
+
should expire. Can also be None if the subscription should use the
|
| 292 |
+
default expiration. Note that different services may have different
|
| 293 |
+
limits on how long a subscription lasts. Check the response from the
|
| 294 |
+
watch() method to see the value the service has set for an expiration
|
| 295 |
+
time.
|
| 296 |
+
params: dict, Extra parameters to pass on channel creation. Currently
|
| 297 |
+
not used for webhook channels.
|
| 298 |
+
"""
|
| 299 |
+
expiration_ms = 0
|
| 300 |
+
if expiration:
|
| 301 |
+
delta = expiration - EPOCH
|
| 302 |
+
expiration_ms = (
|
| 303 |
+
delta.microseconds / 1000 + (delta.seconds + delta.days * 24 * 3600) * 1000
|
| 304 |
+
)
|
| 305 |
+
if expiration_ms < 0:
|
| 306 |
+
expiration_ms = 0
|
| 307 |
+
|
| 308 |
+
return Channel(
|
| 309 |
+
"web_hook",
|
| 310 |
+
str(uuid.uuid4()),
|
| 311 |
+
token,
|
| 312 |
+
url,
|
| 313 |
+
expiration=expiration_ms,
|
| 314 |
+
params=params,
|
| 315 |
+
)
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery.py
ADDED
|
@@ -0,0 +1,1662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
"""Client for discovery based APIs.
|
| 16 |
+
|
| 17 |
+
A client library for Google's discovery based APIs.
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import absolute_import
|
| 20 |
+
|
| 21 |
+
__author__ = "jcgregorio@google.com (Joe Gregorio)"
|
| 22 |
+
__all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
|
| 23 |
+
|
| 24 |
+
from collections import OrderedDict
|
| 25 |
+
import collections.abc
|
| 26 |
+
|
| 27 |
+
# Standard library imports
|
| 28 |
+
import copy
|
| 29 |
+
from email.generator import BytesGenerator
|
| 30 |
+
from email.mime.multipart import MIMEMultipart
|
| 31 |
+
from email.mime.nonmultipart import MIMENonMultipart
|
| 32 |
+
import http.client as http_client
|
| 33 |
+
import io
|
| 34 |
+
import json
|
| 35 |
+
import keyword
|
| 36 |
+
import logging
|
| 37 |
+
import mimetypes
|
| 38 |
+
import os
|
| 39 |
+
import re
|
| 40 |
+
import urllib
|
| 41 |
+
|
| 42 |
+
import google.api_core.client_options
|
| 43 |
+
from google.auth.exceptions import MutualTLSChannelError
|
| 44 |
+
from google.auth.transport import mtls
|
| 45 |
+
from google.oauth2 import service_account
|
| 46 |
+
|
| 47 |
+
# Third-party imports
|
| 48 |
+
import httplib2
|
| 49 |
+
import uritemplate
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
import google_auth_httplib2
|
| 53 |
+
except ImportError: # pragma: NO COVER
|
| 54 |
+
google_auth_httplib2 = None
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
from google.api_core import universe
|
| 58 |
+
|
| 59 |
+
HAS_UNIVERSE = True
|
| 60 |
+
except ImportError:
|
| 61 |
+
HAS_UNIVERSE = False
|
| 62 |
+
|
| 63 |
+
# Local imports
|
| 64 |
+
from googleapiclient import _auth, mimeparse
|
| 65 |
+
from googleapiclient._helpers import _add_query_parameter, positional
|
| 66 |
+
from googleapiclient.errors import (
|
| 67 |
+
HttpError,
|
| 68 |
+
InvalidJsonError,
|
| 69 |
+
MediaUploadSizeError,
|
| 70 |
+
UnacceptableMimeTypeError,
|
| 71 |
+
UnknownApiNameOrVersion,
|
| 72 |
+
UnknownFileType,
|
| 73 |
+
)
|
| 74 |
+
from googleapiclient.http import (
|
| 75 |
+
BatchHttpRequest,
|
| 76 |
+
HttpMock,
|
| 77 |
+
HttpMockSequence,
|
| 78 |
+
HttpRequest,
|
| 79 |
+
MediaFileUpload,
|
| 80 |
+
MediaUpload,
|
| 81 |
+
build_http,
|
| 82 |
+
)
|
| 83 |
+
from googleapiclient.model import JsonModel, MediaModel, RawModel
|
| 84 |
+
from googleapiclient.schema import Schemas
|
| 85 |
+
|
| 86 |
+
# The client library requires a version of httplib2 that supports RETRIES.
|
| 87 |
+
httplib2.RETRIES = 1
|
| 88 |
+
|
| 89 |
+
logger = logging.getLogger(__name__)
|
| 90 |
+
|
| 91 |
+
URITEMPLATE = re.compile("{[^}]*}")
|
| 92 |
+
VARNAME = re.compile("[a-zA-Z0-9_-]+")
|
| 93 |
+
DISCOVERY_URI = (
|
| 94 |
+
"https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
|
| 95 |
+
)
|
| 96 |
+
V1_DISCOVERY_URI = DISCOVERY_URI
|
| 97 |
+
V2_DISCOVERY_URI = (
|
| 98 |
+
"https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
|
| 99 |
+
)
|
| 100 |
+
DEFAULT_METHOD_DOC = "A description of how to use this function"
|
| 101 |
+
HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
|
| 102 |
+
|
| 103 |
+
_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
|
| 104 |
+
BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
|
| 105 |
+
MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
|
| 106 |
+
"description": (
|
| 107 |
+
"The filename of the media request body, or an instance "
|
| 108 |
+
"of a MediaUpload object."
|
| 109 |
+
),
|
| 110 |
+
"type": "string",
|
| 111 |
+
"required": False,
|
| 112 |
+
}
|
| 113 |
+
MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
|
| 114 |
+
"description": (
|
| 115 |
+
"The MIME type of the media request body, or an instance "
|
| 116 |
+
"of a MediaUpload object."
|
| 117 |
+
),
|
| 118 |
+
"type": "string",
|
| 119 |
+
"required": False,
|
| 120 |
+
}
|
| 121 |
+
_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
|
| 122 |
+
|
| 123 |
+
# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114.
|
| 124 |
+
GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
|
| 125 |
+
GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
|
| 126 |
+
GOOGLE_CLOUD_UNIVERSE_DOMAIN = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
|
| 127 |
+
DEFAULT_UNIVERSE = "googleapis.com"
|
| 128 |
+
# Parameters accepted by the stack, but not visible via discovery.
|
| 129 |
+
# TODO(dhermes): Remove 'userip' in 'v2'.
|
| 130 |
+
STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
|
| 131 |
+
STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class APICoreVersionError(ValueError):
|
| 135 |
+
def __init__(self):
|
| 136 |
+
message = (
|
| 137 |
+
"google-api-core >= 2.18.0 is required to use the universe domain feature."
|
| 138 |
+
)
|
| 139 |
+
super().__init__(message)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
# Library-specific reserved words beyond Python keywords.
|
| 143 |
+
RESERVED_WORDS = frozenset(["body"])
|
| 144 |
+
|
| 145 |
+
# patch _write_lines to avoid munging '\r' into '\n'
|
| 146 |
+
# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
|
| 147 |
+
class _BytesGenerator(BytesGenerator):
|
| 148 |
+
_write_lines = BytesGenerator.write
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def fix_method_name(name):
|
| 152 |
+
"""Fix method names to avoid '$' characters and reserved word conflicts.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
name: string, method name.
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
The name with '_' appended if the name is a reserved word and '$' and '-'
|
| 159 |
+
replaced with '_'.
|
| 160 |
+
"""
|
| 161 |
+
name = name.replace("$", "_").replace("-", "_")
|
| 162 |
+
if keyword.iskeyword(name) or name in RESERVED_WORDS:
|
| 163 |
+
return name + "_"
|
| 164 |
+
else:
|
| 165 |
+
return name
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def key2param(key):
|
| 169 |
+
"""Converts key names into parameter names.
|
| 170 |
+
|
| 171 |
+
For example, converting "max-results" -> "max_results"
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
key: string, the method key name.
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
A safe method name based on the key name.
|
| 178 |
+
"""
|
| 179 |
+
result = []
|
| 180 |
+
key = list(key)
|
| 181 |
+
if not key[0].isalpha():
|
| 182 |
+
result.append("x")
|
| 183 |
+
for c in key:
|
| 184 |
+
if c.isalnum():
|
| 185 |
+
result.append(c)
|
| 186 |
+
else:
|
| 187 |
+
result.append("_")
|
| 188 |
+
|
| 189 |
+
return "".join(result)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@positional(2)
|
| 193 |
+
def build(
|
| 194 |
+
serviceName,
|
| 195 |
+
version,
|
| 196 |
+
http=None,
|
| 197 |
+
discoveryServiceUrl=None,
|
| 198 |
+
developerKey=None,
|
| 199 |
+
model=None,
|
| 200 |
+
requestBuilder=HttpRequest,
|
| 201 |
+
credentials=None,
|
| 202 |
+
cache_discovery=True,
|
| 203 |
+
cache=None,
|
| 204 |
+
client_options=None,
|
| 205 |
+
adc_cert_path=None,
|
| 206 |
+
adc_key_path=None,
|
| 207 |
+
num_retries=1,
|
| 208 |
+
static_discovery=None,
|
| 209 |
+
always_use_jwt_access=False,
|
| 210 |
+
):
|
| 211 |
+
"""Construct a Resource for interacting with an API.
|
| 212 |
+
|
| 213 |
+
Construct a Resource object for interacting with an API. The serviceName and
|
| 214 |
+
version are the names from the Discovery service.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
serviceName: string, name of the service.
|
| 218 |
+
version: string, the version of the service.
|
| 219 |
+
http: httplib2.Http, An instance of httplib2.Http or something that acts
|
| 220 |
+
like it that HTTP requests will be made through.
|
| 221 |
+
discoveryServiceUrl: string, a URI Template that points to the location of
|
| 222 |
+
the discovery service. It should have two parameters {api} and
|
| 223 |
+
{apiVersion} that when filled in produce an absolute URI to the discovery
|
| 224 |
+
document for that service.
|
| 225 |
+
developerKey: string, key obtained from
|
| 226 |
+
https://code.google.com/apis/console.
|
| 227 |
+
model: googleapiclient.Model, converts to and from the wire format.
|
| 228 |
+
requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
|
| 229 |
+
request.
|
| 230 |
+
credentials: oauth2client.Credentials or
|
| 231 |
+
google.auth.credentials.Credentials, credentials to be used for
|
| 232 |
+
authentication.
|
| 233 |
+
cache_discovery: Boolean, whether or not to cache the discovery doc.
|
| 234 |
+
cache: googleapiclient.discovery_cache.base.CacheBase, an optional
|
| 235 |
+
cache object for the discovery documents.
|
| 236 |
+
client_options: Mapping object or google.api_core.client_options, client
|
| 237 |
+
options to set user options on the client.
|
| 238 |
+
(1) The API endpoint should be set through client_options. If API endpoint
|
| 239 |
+
is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
|
| 240 |
+
to control which endpoint to use.
|
| 241 |
+
(2) client_cert_source is not supported, client cert should be provided using
|
| 242 |
+
client_encrypted_cert_source instead. In order to use the provided client
|
| 243 |
+
cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
|
| 244 |
+
set to `true`.
|
| 245 |
+
More details on the environment variables are here:
|
| 246 |
+
https://google.aip.dev/auth/4114
|
| 247 |
+
adc_cert_path: str, client certificate file path to save the application
|
| 248 |
+
default client certificate for mTLS. This field is required if you want to
|
| 249 |
+
use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 250 |
+
environment variable must be set to `true` in order to use this field,
|
| 251 |
+
otherwise this field doesn't nothing.
|
| 252 |
+
More details on the environment variables are here:
|
| 253 |
+
https://google.aip.dev/auth/4114
|
| 254 |
+
adc_key_path: str, client encrypted private key file path to save the
|
| 255 |
+
application default client encrypted private key for mTLS. This field is
|
| 256 |
+
required if you want to use the default client certificate.
|
| 257 |
+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
|
| 258 |
+
`true` in order to use this field, otherwise this field doesn't nothing.
|
| 259 |
+
More details on the environment variables are here:
|
| 260 |
+
https://google.aip.dev/auth/4114
|
| 261 |
+
num_retries: Integer, number of times to retry discovery with
|
| 262 |
+
randomized exponential backoff in case of intermittent/connection issues.
|
| 263 |
+
static_discovery: Boolean, whether or not to use the static discovery docs
|
| 264 |
+
included in the library. The default value for `static_discovery` depends
|
| 265 |
+
on the value of `discoveryServiceUrl`. `static_discovery` will default to
|
| 266 |
+
`True` when `discoveryServiceUrl` is also not provided, otherwise it will
|
| 267 |
+
default to `False`.
|
| 268 |
+
always_use_jwt_access: Boolean, whether always use self signed JWT for service
|
| 269 |
+
account credentials. This only applies to
|
| 270 |
+
google.oauth2.service_account.Credentials.
|
| 271 |
+
|
| 272 |
+
Returns:
|
| 273 |
+
A Resource object with methods for interacting with the service.
|
| 274 |
+
|
| 275 |
+
Raises:
|
| 276 |
+
google.auth.exceptions.MutualTLSChannelError: if there are any problems
|
| 277 |
+
setting up mutual TLS channel.
|
| 278 |
+
"""
|
| 279 |
+
params = {"api": serviceName, "apiVersion": version}
|
| 280 |
+
|
| 281 |
+
# The default value for `static_discovery` depends on the value of
|
| 282 |
+
# `discoveryServiceUrl`. `static_discovery` will default to `True` when
|
| 283 |
+
# `discoveryServiceUrl` is also not provided, otherwise it will default to
|
| 284 |
+
# `False`. This is added for backwards compatability with
|
| 285 |
+
# google-api-python-client 1.x which does not support the `static_discovery`
|
| 286 |
+
# parameter.
|
| 287 |
+
if static_discovery is None:
|
| 288 |
+
if discoveryServiceUrl is None:
|
| 289 |
+
static_discovery = True
|
| 290 |
+
else:
|
| 291 |
+
static_discovery = False
|
| 292 |
+
|
| 293 |
+
if http is None:
|
| 294 |
+
discovery_http = build_http()
|
| 295 |
+
else:
|
| 296 |
+
discovery_http = http
|
| 297 |
+
|
| 298 |
+
service = None
|
| 299 |
+
|
| 300 |
+
for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
|
| 301 |
+
requested_url = uritemplate.expand(discovery_url, params)
|
| 302 |
+
|
| 303 |
+
try:
|
| 304 |
+
content = _retrieve_discovery_doc(
|
| 305 |
+
requested_url,
|
| 306 |
+
discovery_http,
|
| 307 |
+
cache_discovery,
|
| 308 |
+
serviceName,
|
| 309 |
+
version,
|
| 310 |
+
cache,
|
| 311 |
+
developerKey,
|
| 312 |
+
num_retries=num_retries,
|
| 313 |
+
static_discovery=static_discovery,
|
| 314 |
+
)
|
| 315 |
+
service = build_from_document(
|
| 316 |
+
content,
|
| 317 |
+
base=discovery_url,
|
| 318 |
+
http=http,
|
| 319 |
+
developerKey=developerKey,
|
| 320 |
+
model=model,
|
| 321 |
+
requestBuilder=requestBuilder,
|
| 322 |
+
credentials=credentials,
|
| 323 |
+
client_options=client_options,
|
| 324 |
+
adc_cert_path=adc_cert_path,
|
| 325 |
+
adc_key_path=adc_key_path,
|
| 326 |
+
always_use_jwt_access=always_use_jwt_access,
|
| 327 |
+
)
|
| 328 |
+
break # exit if a service was created
|
| 329 |
+
except HttpError as e:
|
| 330 |
+
if e.resp.status == http_client.NOT_FOUND:
|
| 331 |
+
continue
|
| 332 |
+
else:
|
| 333 |
+
raise e
|
| 334 |
+
|
| 335 |
+
# If discovery_http was created by this function, we are done with it
|
| 336 |
+
# and can safely close it
|
| 337 |
+
if http is None:
|
| 338 |
+
discovery_http.close()
|
| 339 |
+
|
| 340 |
+
if service is None:
|
| 341 |
+
raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
|
| 342 |
+
else:
|
| 343 |
+
return service
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _discovery_service_uri_options(discoveryServiceUrl, version):
|
| 347 |
+
"""
|
| 348 |
+
Returns Discovery URIs to be used for attempting to build the API Resource.
|
| 349 |
+
|
| 350 |
+
Args:
|
| 351 |
+
discoveryServiceUrl:
|
| 352 |
+
string, the Original Discovery Service URL preferred by the customer.
|
| 353 |
+
version:
|
| 354 |
+
string, API Version requested
|
| 355 |
+
|
| 356 |
+
Returns:
|
| 357 |
+
A list of URIs to be tried for the Service Discovery, in order.
|
| 358 |
+
"""
|
| 359 |
+
|
| 360 |
+
if discoveryServiceUrl is not None:
|
| 361 |
+
return [discoveryServiceUrl]
|
| 362 |
+
if version is None:
|
| 363 |
+
# V1 Discovery won't work if the requested version is None
|
| 364 |
+
logger.warning(
|
| 365 |
+
"Discovery V1 does not support empty versions. Defaulting to V2..."
|
| 366 |
+
)
|
| 367 |
+
return [V2_DISCOVERY_URI]
|
| 368 |
+
else:
|
| 369 |
+
return [DISCOVERY_URI, V2_DISCOVERY_URI]
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def _retrieve_discovery_doc(
|
| 373 |
+
url,
|
| 374 |
+
http,
|
| 375 |
+
cache_discovery,
|
| 376 |
+
serviceName,
|
| 377 |
+
version,
|
| 378 |
+
cache=None,
|
| 379 |
+
developerKey=None,
|
| 380 |
+
num_retries=1,
|
| 381 |
+
static_discovery=True,
|
| 382 |
+
):
|
| 383 |
+
"""Retrieves the discovery_doc from cache or the internet.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
url: string, the URL of the discovery document.
|
| 387 |
+
http: httplib2.Http, An instance of httplib2.Http or something that acts
|
| 388 |
+
like it through which HTTP requests will be made.
|
| 389 |
+
cache_discovery: Boolean, whether or not to cache the discovery doc.
|
| 390 |
+
serviceName: string, name of the service.
|
| 391 |
+
version: string, the version of the service.
|
| 392 |
+
cache: googleapiclient.discovery_cache.base.Cache, an optional cache
|
| 393 |
+
object for the discovery documents.
|
| 394 |
+
developerKey: string, Key for controlling API usage, generated
|
| 395 |
+
from the API Console.
|
| 396 |
+
num_retries: Integer, number of times to retry discovery with
|
| 397 |
+
randomized exponential backoff in case of intermittent/connection issues.
|
| 398 |
+
static_discovery: Boolean, whether or not to use the static discovery docs
|
| 399 |
+
included in the library.
|
| 400 |
+
|
| 401 |
+
Returns:
|
| 402 |
+
A unicode string representation of the discovery document.
|
| 403 |
+
"""
|
| 404 |
+
from . import discovery_cache
|
| 405 |
+
|
| 406 |
+
if cache_discovery:
|
| 407 |
+
if cache is None:
|
| 408 |
+
cache = discovery_cache.autodetect()
|
| 409 |
+
if cache:
|
| 410 |
+
content = cache.get(url)
|
| 411 |
+
if content:
|
| 412 |
+
return content
|
| 413 |
+
|
| 414 |
+
# When `static_discovery=True`, use static discovery artifacts included
|
| 415 |
+
# with the library
|
| 416 |
+
if static_discovery:
|
| 417 |
+
content = discovery_cache.get_static_doc(serviceName, version)
|
| 418 |
+
if content:
|
| 419 |
+
return content
|
| 420 |
+
else:
|
| 421 |
+
raise UnknownApiNameOrVersion(
|
| 422 |
+
"name: %s version: %s" % (serviceName, version)
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
actual_url = url
|
| 426 |
+
# REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
|
| 427 |
+
# variable that contains the network address of the client sending the
|
| 428 |
+
# request. If it exists then add that to the request for the discovery
|
| 429 |
+
# document to avoid exceeding the quota on discovery requests.
|
| 430 |
+
if "REMOTE_ADDR" in os.environ:
|
| 431 |
+
actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
|
| 432 |
+
if developerKey:
|
| 433 |
+
actual_url = _add_query_parameter(url, "key", developerKey)
|
| 434 |
+
logger.debug("URL being requested: GET %s", actual_url)
|
| 435 |
+
|
| 436 |
+
# Execute this request with retries build into HttpRequest
|
| 437 |
+
# Note that it will already raise an error if we don't get a 2xx response
|
| 438 |
+
req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
|
| 439 |
+
resp, content = req.execute(num_retries=num_retries)
|
| 440 |
+
|
| 441 |
+
try:
|
| 442 |
+
content = content.decode("utf-8")
|
| 443 |
+
except AttributeError:
|
| 444 |
+
pass
|
| 445 |
+
|
| 446 |
+
try:
|
| 447 |
+
service = json.loads(content)
|
| 448 |
+
except ValueError as e:
|
| 449 |
+
logger.error("Failed to parse as JSON: " + content)
|
| 450 |
+
raise InvalidJsonError()
|
| 451 |
+
if cache_discovery and cache:
|
| 452 |
+
cache.set(url, content)
|
| 453 |
+
return content
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def _check_api_core_compatible_with_credentials_universe(credentials):
|
| 457 |
+
if not HAS_UNIVERSE:
|
| 458 |
+
credentials_universe = getattr(credentials, "universe_domain", None)
|
| 459 |
+
if credentials_universe and credentials_universe != DEFAULT_UNIVERSE:
|
| 460 |
+
raise APICoreVersionError
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
@positional(1)
|
| 464 |
+
def build_from_document(
|
| 465 |
+
service,
|
| 466 |
+
base=None,
|
| 467 |
+
future=None,
|
| 468 |
+
http=None,
|
| 469 |
+
developerKey=None,
|
| 470 |
+
model=None,
|
| 471 |
+
requestBuilder=HttpRequest,
|
| 472 |
+
credentials=None,
|
| 473 |
+
client_options=None,
|
| 474 |
+
adc_cert_path=None,
|
| 475 |
+
adc_key_path=None,
|
| 476 |
+
always_use_jwt_access=False,
|
| 477 |
+
):
|
| 478 |
+
"""Create a Resource for interacting with an API.
|
| 479 |
+
|
| 480 |
+
Same as `build()`, but constructs the Resource object from a discovery
|
| 481 |
+
document that is it given, as opposed to retrieving one over HTTP.
|
| 482 |
+
|
| 483 |
+
Args:
|
| 484 |
+
service: string or object, the JSON discovery document describing the API.
|
| 485 |
+
The value passed in may either be the JSON string or the deserialized
|
| 486 |
+
JSON.
|
| 487 |
+
base: string, base URI for all HTTP requests, usually the discovery URI.
|
| 488 |
+
This parameter is no longer used as rootUrl and servicePath are included
|
| 489 |
+
within the discovery document. (deprecated)
|
| 490 |
+
future: string, discovery document with future capabilities (deprecated).
|
| 491 |
+
http: httplib2.Http, An instance of httplib2.Http or something that acts
|
| 492 |
+
like it that HTTP requests will be made through.
|
| 493 |
+
developerKey: string, Key for controlling API usage, generated
|
| 494 |
+
from the API Console.
|
| 495 |
+
model: Model class instance that serializes and de-serializes requests and
|
| 496 |
+
responses.
|
| 497 |
+
requestBuilder: Takes an http request and packages it up to be executed.
|
| 498 |
+
credentials: oauth2client.Credentials or
|
| 499 |
+
google.auth.credentials.Credentials, credentials to be used for
|
| 500 |
+
authentication.
|
| 501 |
+
client_options: Mapping object or google.api_core.client_options, client
|
| 502 |
+
options to set user options on the client.
|
| 503 |
+
(1) The API endpoint should be set through client_options. If API endpoint
|
| 504 |
+
is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
|
| 505 |
+
to control which endpoint to use.
|
| 506 |
+
(2) client_cert_source is not supported, client cert should be provided using
|
| 507 |
+
client_encrypted_cert_source instead. In order to use the provided client
|
| 508 |
+
cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
|
| 509 |
+
set to `true`.
|
| 510 |
+
More details on the environment variables are here:
|
| 511 |
+
https://google.aip.dev/auth/4114
|
| 512 |
+
adc_cert_path: str, client certificate file path to save the application
|
| 513 |
+
default client certificate for mTLS. This field is required if you want to
|
| 514 |
+
use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
|
| 515 |
+
environment variable must be set to `true` in order to use this field,
|
| 516 |
+
otherwise this field doesn't nothing.
|
| 517 |
+
More details on the environment variables are here:
|
| 518 |
+
https://google.aip.dev/auth/4114
|
| 519 |
+
adc_key_path: str, client encrypted private key file path to save the
|
| 520 |
+
application default client encrypted private key for mTLS. This field is
|
| 521 |
+
required if you want to use the default client certificate.
|
| 522 |
+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
|
| 523 |
+
`true` in order to use this field, otherwise this field doesn't nothing.
|
| 524 |
+
More details on the environment variables are here:
|
| 525 |
+
https://google.aip.dev/auth/4114
|
| 526 |
+
always_use_jwt_access: Boolean, whether always use self signed JWT for service
|
| 527 |
+
account credentials. This only applies to
|
| 528 |
+
google.oauth2.service_account.Credentials.
|
| 529 |
+
|
| 530 |
+
Returns:
|
| 531 |
+
A Resource object with methods for interacting with the service.
|
| 532 |
+
|
| 533 |
+
Raises:
|
| 534 |
+
google.auth.exceptions.MutualTLSChannelError: if there are any problems
|
| 535 |
+
setting up mutual TLS channel.
|
| 536 |
+
"""
|
| 537 |
+
|
| 538 |
+
if client_options is None:
|
| 539 |
+
client_options = google.api_core.client_options.ClientOptions()
|
| 540 |
+
if isinstance(client_options, collections.abc.Mapping):
|
| 541 |
+
client_options = google.api_core.client_options.from_dict(client_options)
|
| 542 |
+
|
| 543 |
+
if http is not None:
|
| 544 |
+
# if http is passed, the user cannot provide credentials
|
| 545 |
+
banned_options = [
|
| 546 |
+
(credentials, "credentials"),
|
| 547 |
+
(client_options.credentials_file, "client_options.credentials_file"),
|
| 548 |
+
]
|
| 549 |
+
for option, name in banned_options:
|
| 550 |
+
if option is not None:
|
| 551 |
+
raise ValueError(
|
| 552 |
+
"Arguments http and {} are mutually exclusive".format(name)
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
if isinstance(service, str):
|
| 556 |
+
service = json.loads(service)
|
| 557 |
+
elif isinstance(service, bytes):
|
| 558 |
+
service = json.loads(service.decode("utf-8"))
|
| 559 |
+
|
| 560 |
+
if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
|
| 561 |
+
logger.error(
|
| 562 |
+
"You are using HttpMock or HttpMockSequence without"
|
| 563 |
+
+ "having the service discovery doc in cache. Try calling "
|
| 564 |
+
+ "build() without mocking once first to populate the "
|
| 565 |
+
+ "cache."
|
| 566 |
+
)
|
| 567 |
+
raise InvalidJsonError()
|
| 568 |
+
|
| 569 |
+
# If an API Endpoint is provided on client options, use that as the base URL
|
| 570 |
+
base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"])
|
| 571 |
+
universe_domain = None
|
| 572 |
+
if HAS_UNIVERSE:
|
| 573 |
+
universe_domain_env = os.getenv(GOOGLE_CLOUD_UNIVERSE_DOMAIN, None)
|
| 574 |
+
universe_domain = universe.determine_domain(
|
| 575 |
+
client_options.universe_domain, universe_domain_env
|
| 576 |
+
)
|
| 577 |
+
base = base.replace(universe.DEFAULT_UNIVERSE, universe_domain)
|
| 578 |
+
else:
|
| 579 |
+
client_universe = getattr(client_options, "universe_domain", None)
|
| 580 |
+
if client_universe:
|
| 581 |
+
raise APICoreVersionError
|
| 582 |
+
|
| 583 |
+
audience_for_self_signed_jwt = base
|
| 584 |
+
if client_options.api_endpoint:
|
| 585 |
+
base = client_options.api_endpoint
|
| 586 |
+
|
| 587 |
+
schema = Schemas(service)
|
| 588 |
+
|
| 589 |
+
# If the http client is not specified, then we must construct an http client
|
| 590 |
+
# to make requests. If the service has scopes, then we also need to setup
|
| 591 |
+
# authentication.
|
| 592 |
+
if http is None:
|
| 593 |
+
# Does the service require scopes?
|
| 594 |
+
scopes = list(
|
| 595 |
+
service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
# If so, then the we need to setup authentication if no developerKey is
|
| 599 |
+
# specified.
|
| 600 |
+
if scopes and not developerKey:
|
| 601 |
+
# Make sure the user didn't pass multiple credentials
|
| 602 |
+
if client_options.credentials_file and credentials:
|
| 603 |
+
raise google.api_core.exceptions.DuplicateCredentialArgs(
|
| 604 |
+
"client_options.credentials_file and credentials are mutually exclusive."
|
| 605 |
+
)
|
| 606 |
+
# Check for credentials file via client options
|
| 607 |
+
if client_options.credentials_file:
|
| 608 |
+
credentials = _auth.credentials_from_file(
|
| 609 |
+
client_options.credentials_file,
|
| 610 |
+
scopes=client_options.scopes,
|
| 611 |
+
quota_project_id=client_options.quota_project_id,
|
| 612 |
+
)
|
| 613 |
+
# If the user didn't pass in credentials, attempt to acquire application
|
| 614 |
+
# default credentials.
|
| 615 |
+
if credentials is None:
|
| 616 |
+
credentials = _auth.default_credentials(
|
| 617 |
+
scopes=client_options.scopes,
|
| 618 |
+
quota_project_id=client_options.quota_project_id,
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
# Check google-api-core >= 2.18.0 if credentials' universe != "googleapis.com".
|
| 622 |
+
_check_api_core_compatible_with_credentials_universe(credentials)
|
| 623 |
+
|
| 624 |
+
# The credentials need to be scoped.
|
| 625 |
+
# If the user provided scopes via client_options don't override them
|
| 626 |
+
if not client_options.scopes:
|
| 627 |
+
credentials = _auth.with_scopes(credentials, scopes)
|
| 628 |
+
|
| 629 |
+
# For google-auth service account credentials, enable self signed JWT if
|
| 630 |
+
# always_use_jwt_access is true.
|
| 631 |
+
if (
|
| 632 |
+
credentials
|
| 633 |
+
and isinstance(credentials, service_account.Credentials)
|
| 634 |
+
and always_use_jwt_access
|
| 635 |
+
and hasattr(service_account.Credentials, "with_always_use_jwt_access")
|
| 636 |
+
):
|
| 637 |
+
credentials = credentials.with_always_use_jwt_access(always_use_jwt_access)
|
| 638 |
+
credentials._create_self_signed_jwt(audience_for_self_signed_jwt)
|
| 639 |
+
|
| 640 |
+
# If credentials are provided, create an authorized http instance;
|
| 641 |
+
# otherwise, skip authentication.
|
| 642 |
+
if credentials:
|
| 643 |
+
http = _auth.authorized_http(credentials)
|
| 644 |
+
|
| 645 |
+
# If the service doesn't require scopes then there is no need for
|
| 646 |
+
# authentication.
|
| 647 |
+
else:
|
| 648 |
+
http = build_http()
|
| 649 |
+
|
| 650 |
+
# Obtain client cert and create mTLS http channel if cert exists.
|
| 651 |
+
client_cert_to_use = None
|
| 652 |
+
use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
|
| 653 |
+
if not use_client_cert in ("true", "false"):
|
| 654 |
+
raise MutualTLSChannelError(
|
| 655 |
+
"Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
|
| 656 |
+
)
|
| 657 |
+
if client_options and client_options.client_cert_source:
|
| 658 |
+
raise MutualTLSChannelError(
|
| 659 |
+
"ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
|
| 660 |
+
)
|
| 661 |
+
if use_client_cert == "true":
|
| 662 |
+
if (
|
| 663 |
+
client_options
|
| 664 |
+
and hasattr(client_options, "client_encrypted_cert_source")
|
| 665 |
+
and client_options.client_encrypted_cert_source
|
| 666 |
+
):
|
| 667 |
+
client_cert_to_use = client_options.client_encrypted_cert_source
|
| 668 |
+
elif (
|
| 669 |
+
adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
|
| 670 |
+
):
|
| 671 |
+
client_cert_to_use = mtls.default_client_encrypted_cert_source(
|
| 672 |
+
adc_cert_path, adc_key_path
|
| 673 |
+
)
|
| 674 |
+
if client_cert_to_use:
|
| 675 |
+
cert_path, key_path, passphrase = client_cert_to_use()
|
| 676 |
+
|
| 677 |
+
# The http object we built could be google_auth_httplib2.AuthorizedHttp
|
| 678 |
+
# or httplib2.Http. In the first case we need to extract the wrapped
|
| 679 |
+
# httplib2.Http object from google_auth_httplib2.AuthorizedHttp.
|
| 680 |
+
http_channel = (
|
| 681 |
+
http.http
|
| 682 |
+
if google_auth_httplib2
|
| 683 |
+
and isinstance(http, google_auth_httplib2.AuthorizedHttp)
|
| 684 |
+
else http
|
| 685 |
+
)
|
| 686 |
+
http_channel.add_certificate(key_path, cert_path, "", passphrase)
|
| 687 |
+
|
| 688 |
+
# If user doesn't provide api endpoint via client options, decide which
|
| 689 |
+
# api endpoint to use.
|
| 690 |
+
if "mtlsRootUrl" in service and (
|
| 691 |
+
not client_options or not client_options.api_endpoint
|
| 692 |
+
):
|
| 693 |
+
mtls_endpoint = urllib.parse.urljoin(
|
| 694 |
+
service["mtlsRootUrl"], service["servicePath"]
|
| 695 |
+
)
|
| 696 |
+
use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
|
| 697 |
+
|
| 698 |
+
if not use_mtls_endpoint in ("never", "auto", "always"):
|
| 699 |
+
raise MutualTLSChannelError(
|
| 700 |
+
"Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
# Switch to mTLS endpoint, if environment variable is "always", or
|
| 704 |
+
# environment varibable is "auto" and client cert exists.
|
| 705 |
+
if use_mtls_endpoint == "always" or (
|
| 706 |
+
use_mtls_endpoint == "auto" and client_cert_to_use
|
| 707 |
+
):
|
| 708 |
+
if HAS_UNIVERSE and universe_domain != universe.DEFAULT_UNIVERSE:
|
| 709 |
+
raise MutualTLSChannelError(
|
| 710 |
+
f"mTLS is not supported in any universe other than {universe.DEFAULT_UNIVERSE}."
|
| 711 |
+
)
|
| 712 |
+
base = mtls_endpoint
|
| 713 |
+
else:
|
| 714 |
+
# Check google-api-core >= 2.18.0 if credentials' universe != "googleapis.com".
|
| 715 |
+
http_credentials = getattr(http, "credentials", None)
|
| 716 |
+
_check_api_core_compatible_with_credentials_universe(http_credentials)
|
| 717 |
+
|
| 718 |
+
if model is None:
|
| 719 |
+
features = service.get("features", [])
|
| 720 |
+
model = JsonModel("dataWrapper" in features)
|
| 721 |
+
|
| 722 |
+
return Resource(
|
| 723 |
+
http=http,
|
| 724 |
+
baseUrl=base,
|
| 725 |
+
model=model,
|
| 726 |
+
developerKey=developerKey,
|
| 727 |
+
requestBuilder=requestBuilder,
|
| 728 |
+
resourceDesc=service,
|
| 729 |
+
rootDesc=service,
|
| 730 |
+
schema=schema,
|
| 731 |
+
universe_domain=universe_domain,
|
| 732 |
+
)
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
def _cast(value, schema_type):
|
| 736 |
+
"""Convert value to a string based on JSON Schema type.
|
| 737 |
+
|
| 738 |
+
See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
|
| 739 |
+
JSON Schema.
|
| 740 |
+
|
| 741 |
+
Args:
|
| 742 |
+
value: any, the value to convert
|
| 743 |
+
schema_type: string, the type that value should be interpreted as
|
| 744 |
+
|
| 745 |
+
Returns:
|
| 746 |
+
A string representation of 'value' based on the schema_type.
|
| 747 |
+
"""
|
| 748 |
+
if schema_type == "string":
|
| 749 |
+
if type(value) == type("") or type(value) == type(""):
|
| 750 |
+
return value
|
| 751 |
+
else:
|
| 752 |
+
return str(value)
|
| 753 |
+
elif schema_type == "integer":
|
| 754 |
+
return str(int(value))
|
| 755 |
+
elif schema_type == "number":
|
| 756 |
+
return str(float(value))
|
| 757 |
+
elif schema_type == "boolean":
|
| 758 |
+
return str(bool(value)).lower()
|
| 759 |
+
else:
|
| 760 |
+
if type(value) == type("") or type(value) == type(""):
|
| 761 |
+
return value
|
| 762 |
+
else:
|
| 763 |
+
return str(value)
|
| 764 |
+
|
| 765 |
+
|
| 766 |
+
def _media_size_to_long(maxSize):
|
| 767 |
+
"""Convert a string media size, such as 10GB or 3TB into an integer.
|
| 768 |
+
|
| 769 |
+
Args:
|
| 770 |
+
maxSize: string, size as a string, such as 2MB or 7GB.
|
| 771 |
+
|
| 772 |
+
Returns:
|
| 773 |
+
The size as an integer value.
|
| 774 |
+
"""
|
| 775 |
+
if len(maxSize) < 2:
|
| 776 |
+
return 0
|
| 777 |
+
units = maxSize[-2:].upper()
|
| 778 |
+
bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
|
| 779 |
+
if bit_shift is not None:
|
| 780 |
+
return int(maxSize[:-2]) << bit_shift
|
| 781 |
+
else:
|
| 782 |
+
return int(maxSize)
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def _media_path_url_from_info(root_desc, path_url):
|
| 786 |
+
"""Creates an absolute media path URL.
|
| 787 |
+
|
| 788 |
+
Constructed using the API root URI and service path from the discovery
|
| 789 |
+
document and the relative path for the API method.
|
| 790 |
+
|
| 791 |
+
Args:
|
| 792 |
+
root_desc: Dictionary; the entire original deserialized discovery document.
|
| 793 |
+
path_url: String; the relative URL for the API method. Relative to the API
|
| 794 |
+
root, which is specified in the discovery document.
|
| 795 |
+
|
| 796 |
+
Returns:
|
| 797 |
+
String; the absolute URI for media upload for the API method.
|
| 798 |
+
"""
|
| 799 |
+
return "%(root)supload/%(service_path)s%(path)s" % {
|
| 800 |
+
"root": root_desc["rootUrl"],
|
| 801 |
+
"service_path": root_desc["servicePath"],
|
| 802 |
+
"path": path_url,
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _fix_up_parameters(method_desc, root_desc, http_method, schema):
|
| 807 |
+
"""Updates parameters of an API method with values specific to this library.
|
| 808 |
+
|
| 809 |
+
Specifically, adds whatever global parameters are specified by the API to the
|
| 810 |
+
parameters for the individual method. Also adds parameters which don't
|
| 811 |
+
appear in the discovery document, but are available to all discovery based
|
| 812 |
+
APIs (these are listed in STACK_QUERY_PARAMETERS).
|
| 813 |
+
|
| 814 |
+
SIDE EFFECTS: This updates the parameters dictionary object in the method
|
| 815 |
+
description.
|
| 816 |
+
|
| 817 |
+
Args:
|
| 818 |
+
method_desc: Dictionary with metadata describing an API method. Value comes
|
| 819 |
+
from the dictionary of methods stored in the 'methods' key in the
|
| 820 |
+
deserialized discovery document.
|
| 821 |
+
root_desc: Dictionary; the entire original deserialized discovery document.
|
| 822 |
+
http_method: String; the HTTP method used to call the API method described
|
| 823 |
+
in method_desc.
|
| 824 |
+
schema: Object, mapping of schema names to schema descriptions.
|
| 825 |
+
|
| 826 |
+
Returns:
|
| 827 |
+
The updated Dictionary stored in the 'parameters' key of the method
|
| 828 |
+
description dictionary.
|
| 829 |
+
"""
|
| 830 |
+
parameters = method_desc.setdefault("parameters", {})
|
| 831 |
+
|
| 832 |
+
# Add in the parameters common to all methods.
|
| 833 |
+
for name, description in root_desc.get("parameters", {}).items():
|
| 834 |
+
parameters[name] = description
|
| 835 |
+
|
| 836 |
+
# Add in undocumented query parameters.
|
| 837 |
+
for name in STACK_QUERY_PARAMETERS:
|
| 838 |
+
parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
|
| 839 |
+
|
| 840 |
+
# Add 'body' (our own reserved word) to parameters if the method supports
|
| 841 |
+
# a request payload.
|
| 842 |
+
if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
|
| 843 |
+
body = BODY_PARAMETER_DEFAULT_VALUE.copy()
|
| 844 |
+
body.update(method_desc["request"])
|
| 845 |
+
parameters["body"] = body
|
| 846 |
+
|
| 847 |
+
return parameters
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
|
| 851 |
+
"""Adds 'media_body' and 'media_mime_type' parameters if supported by method.
|
| 852 |
+
|
| 853 |
+
SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
|
| 854 |
+
'media_upload' key to parameters.
|
| 855 |
+
|
| 856 |
+
Args:
|
| 857 |
+
method_desc: Dictionary with metadata describing an API method. Value comes
|
| 858 |
+
from the dictionary of methods stored in the 'methods' key in the
|
| 859 |
+
deserialized discovery document.
|
| 860 |
+
root_desc: Dictionary; the entire original deserialized discovery document.
|
| 861 |
+
path_url: String; the relative URL for the API method. Relative to the API
|
| 862 |
+
root, which is specified in the discovery document.
|
| 863 |
+
parameters: A dictionary describing method parameters for method described
|
| 864 |
+
in method_desc.
|
| 865 |
+
|
| 866 |
+
Returns:
|
| 867 |
+
Triple (accept, max_size, media_path_url) where:
|
| 868 |
+
- accept is a list of strings representing what content types are
|
| 869 |
+
accepted for media upload. Defaults to empty list if not in the
|
| 870 |
+
discovery document.
|
| 871 |
+
- max_size is a long representing the max size in bytes allowed for a
|
| 872 |
+
media upload. Defaults to 0L if not in the discovery document.
|
| 873 |
+
- media_path_url is a String; the absolute URI for media upload for the
|
| 874 |
+
API method. Constructed using the API root URI and service path from
|
| 875 |
+
the discovery document and the relative path for the API method. If
|
| 876 |
+
media upload is not supported, this is None.
|
| 877 |
+
"""
|
| 878 |
+
media_upload = method_desc.get("mediaUpload", {})
|
| 879 |
+
accept = media_upload.get("accept", [])
|
| 880 |
+
max_size = _media_size_to_long(media_upload.get("maxSize", ""))
|
| 881 |
+
media_path_url = None
|
| 882 |
+
|
| 883 |
+
if media_upload:
|
| 884 |
+
media_path_url = _media_path_url_from_info(root_desc, path_url)
|
| 885 |
+
parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
|
| 886 |
+
parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
|
| 887 |
+
|
| 888 |
+
return accept, max_size, media_path_url
|
| 889 |
+
|
| 890 |
+
|
| 891 |
+
def _fix_up_method_description(method_desc, root_desc, schema):
|
| 892 |
+
"""Updates a method description in a discovery document.
|
| 893 |
+
|
| 894 |
+
SIDE EFFECTS: Changes the parameters dictionary in the method description with
|
| 895 |
+
extra parameters which are used locally.
|
| 896 |
+
|
| 897 |
+
Args:
|
| 898 |
+
method_desc: Dictionary with metadata describing an API method. Value comes
|
| 899 |
+
from the dictionary of methods stored in the 'methods' key in the
|
| 900 |
+
deserialized discovery document.
|
| 901 |
+
root_desc: Dictionary; the entire original deserialized discovery document.
|
| 902 |
+
schema: Object, mapping of schema names to schema descriptions.
|
| 903 |
+
|
| 904 |
+
Returns:
|
| 905 |
+
Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
|
| 906 |
+
where:
|
| 907 |
+
- path_url is a String; the relative URL for the API method. Relative to
|
| 908 |
+
the API root, which is specified in the discovery document.
|
| 909 |
+
- http_method is a String; the HTTP method used to call the API method
|
| 910 |
+
described in the method description.
|
| 911 |
+
- method_id is a String; the name of the RPC method associated with the
|
| 912 |
+
API method, and is in the method description in the 'id' key.
|
| 913 |
+
- accept is a list of strings representing what content types are
|
| 914 |
+
accepted for media upload. Defaults to empty list if not in the
|
| 915 |
+
discovery document.
|
| 916 |
+
- max_size is a long representing the max size in bytes allowed for a
|
| 917 |
+
media upload. Defaults to 0L if not in the discovery document.
|
| 918 |
+
- media_path_url is a String; the absolute URI for media upload for the
|
| 919 |
+
API method. Constructed using the API root URI and service path from
|
| 920 |
+
the discovery document and the relative path for the API method. If
|
| 921 |
+
media upload is not supported, this is None.
|
| 922 |
+
"""
|
| 923 |
+
path_url = method_desc["path"]
|
| 924 |
+
http_method = method_desc["httpMethod"]
|
| 925 |
+
method_id = method_desc["id"]
|
| 926 |
+
|
| 927 |
+
parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
|
| 928 |
+
# Order is important. `_fix_up_media_upload` needs `method_desc` to have a
|
| 929 |
+
# 'parameters' key and needs to know if there is a 'body' parameter because it
|
| 930 |
+
# also sets a 'media_body' parameter.
|
| 931 |
+
accept, max_size, media_path_url = _fix_up_media_upload(
|
| 932 |
+
method_desc, root_desc, path_url, parameters
|
| 933 |
+
)
|
| 934 |
+
|
| 935 |
+
return path_url, http_method, method_id, accept, max_size, media_path_url
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
def _fix_up_media_path_base_url(media_path_url, base_url):
|
| 939 |
+
"""
|
| 940 |
+
Update the media upload base url if its netloc doesn't match base url netloc.
|
| 941 |
+
|
| 942 |
+
This can happen in case the base url was overridden by
|
| 943 |
+
client_options.api_endpoint.
|
| 944 |
+
|
| 945 |
+
Args:
|
| 946 |
+
media_path_url: String; the absolute URI for media upload.
|
| 947 |
+
base_url: string, base URL for the API. All requests are relative to this URI.
|
| 948 |
+
|
| 949 |
+
Returns:
|
| 950 |
+
String; the absolute URI for media upload.
|
| 951 |
+
"""
|
| 952 |
+
parsed_media_url = urllib.parse.urlparse(media_path_url)
|
| 953 |
+
parsed_base_url = urllib.parse.urlparse(base_url)
|
| 954 |
+
if parsed_media_url.netloc == parsed_base_url.netloc:
|
| 955 |
+
return media_path_url
|
| 956 |
+
return urllib.parse.urlunparse(
|
| 957 |
+
parsed_media_url._replace(netloc=parsed_base_url.netloc)
|
| 958 |
+
)
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def _urljoin(base, url):
|
| 962 |
+
"""Custom urljoin replacement supporting : before / in url."""
|
| 963 |
+
# In general, it's unsafe to simply join base and url. However, for
|
| 964 |
+
# the case of discovery documents, we know:
|
| 965 |
+
# * base will never contain params, query, or fragment
|
| 966 |
+
# * url will never contain a scheme or net_loc.
|
| 967 |
+
# In general, this means we can safely join on /; we just need to
|
| 968 |
+
# ensure we end up with precisely one / joining base and url. The
|
| 969 |
+
# exception here is the case of media uploads, where url will be an
|
| 970 |
+
# absolute url.
|
| 971 |
+
if url.startswith("http://") or url.startswith("https://"):
|
| 972 |
+
return urllib.parse.urljoin(base, url)
|
| 973 |
+
new_base = base if base.endswith("/") else base + "/"
|
| 974 |
+
new_url = url[1:] if url.startswith("/") else url
|
| 975 |
+
return new_base + new_url
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
# TODO(dhermes): Convert this class to ResourceMethod and make it callable
|
| 979 |
+
class ResourceMethodParameters(object):
|
| 980 |
+
"""Represents the parameters associated with a method.
|
| 981 |
+
|
| 982 |
+
Attributes:
|
| 983 |
+
argmap: Map from method parameter name (string) to query parameter name
|
| 984 |
+
(string).
|
| 985 |
+
required_params: List of required parameters (represented by parameter
|
| 986 |
+
name as string).
|
| 987 |
+
repeated_params: List of repeated parameters (represented by parameter
|
| 988 |
+
name as string).
|
| 989 |
+
pattern_params: Map from method parameter name (string) to regular
|
| 990 |
+
expression (as a string). If the pattern is set for a parameter, the
|
| 991 |
+
value for that parameter must match the regular expression.
|
| 992 |
+
query_params: List of parameters (represented by parameter name as string)
|
| 993 |
+
that will be used in the query string.
|
| 994 |
+
path_params: Set of parameters (represented by parameter name as string)
|
| 995 |
+
that will be used in the base URL path.
|
| 996 |
+
param_types: Map from method parameter name (string) to parameter type. Type
|
| 997 |
+
can be any valid JSON schema type; valid values are 'any', 'array',
|
| 998 |
+
'boolean', 'integer', 'number', 'object', or 'string'. Reference:
|
| 999 |
+
http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
|
| 1000 |
+
enum_params: Map from method parameter name (string) to list of strings,
|
| 1001 |
+
where each list of strings is the list of acceptable enum values.
|
| 1002 |
+
"""
|
| 1003 |
+
|
| 1004 |
+
def __init__(self, method_desc):
|
| 1005 |
+
"""Constructor for ResourceMethodParameters.
|
| 1006 |
+
|
| 1007 |
+
Sets default values and defers to set_parameters to populate.
|
| 1008 |
+
|
| 1009 |
+
Args:
|
| 1010 |
+
method_desc: Dictionary with metadata describing an API method. Value
|
| 1011 |
+
comes from the dictionary of methods stored in the 'methods' key in
|
| 1012 |
+
the deserialized discovery document.
|
| 1013 |
+
"""
|
| 1014 |
+
self.argmap = {}
|
| 1015 |
+
self.required_params = []
|
| 1016 |
+
self.repeated_params = []
|
| 1017 |
+
self.pattern_params = {}
|
| 1018 |
+
self.query_params = []
|
| 1019 |
+
# TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
|
| 1020 |
+
# parsing is gotten rid of.
|
| 1021 |
+
self.path_params = set()
|
| 1022 |
+
self.param_types = {}
|
| 1023 |
+
self.enum_params = {}
|
| 1024 |
+
|
| 1025 |
+
self.set_parameters(method_desc)
|
| 1026 |
+
|
| 1027 |
+
def set_parameters(self, method_desc):
|
| 1028 |
+
"""Populates maps and lists based on method description.
|
| 1029 |
+
|
| 1030 |
+
Iterates through each parameter for the method and parses the values from
|
| 1031 |
+
the parameter dictionary.
|
| 1032 |
+
|
| 1033 |
+
Args:
|
| 1034 |
+
method_desc: Dictionary with metadata describing an API method. Value
|
| 1035 |
+
comes from the dictionary of methods stored in the 'methods' key in
|
| 1036 |
+
the deserialized discovery document.
|
| 1037 |
+
"""
|
| 1038 |
+
parameters = method_desc.get("parameters", {})
|
| 1039 |
+
sorted_parameters = OrderedDict(sorted(parameters.items()))
|
| 1040 |
+
for arg, desc in sorted_parameters.items():
|
| 1041 |
+
param = key2param(arg)
|
| 1042 |
+
self.argmap[param] = arg
|
| 1043 |
+
|
| 1044 |
+
if desc.get("pattern"):
|
| 1045 |
+
self.pattern_params[param] = desc["pattern"]
|
| 1046 |
+
if desc.get("enum"):
|
| 1047 |
+
self.enum_params[param] = desc["enum"]
|
| 1048 |
+
if desc.get("required"):
|
| 1049 |
+
self.required_params.append(param)
|
| 1050 |
+
if desc.get("repeated"):
|
| 1051 |
+
self.repeated_params.append(param)
|
| 1052 |
+
if desc.get("location") == "query":
|
| 1053 |
+
self.query_params.append(param)
|
| 1054 |
+
if desc.get("location") == "path":
|
| 1055 |
+
self.path_params.add(param)
|
| 1056 |
+
self.param_types[param] = desc.get("type", "string")
|
| 1057 |
+
|
| 1058 |
+
# TODO(dhermes): Determine if this is still necessary. Discovery based APIs
|
| 1059 |
+
# should have all path parameters already marked with
|
| 1060 |
+
# 'location: path'.
|
| 1061 |
+
for match in URITEMPLATE.finditer(method_desc["path"]):
|
| 1062 |
+
for namematch in VARNAME.finditer(match.group(0)):
|
| 1063 |
+
name = key2param(namematch.group(0))
|
| 1064 |
+
self.path_params.add(name)
|
| 1065 |
+
if name in self.query_params:
|
| 1066 |
+
self.query_params.remove(name)
|
| 1067 |
+
|
| 1068 |
+
|
| 1069 |
+
def createMethod(methodName, methodDesc, rootDesc, schema):
|
| 1070 |
+
"""Creates a method for attaching to a Resource.
|
| 1071 |
+
|
| 1072 |
+
Args:
|
| 1073 |
+
methodName: string, name of the method to use.
|
| 1074 |
+
methodDesc: object, fragment of deserialized discovery document that
|
| 1075 |
+
describes the method.
|
| 1076 |
+
rootDesc: object, the entire deserialized discovery document.
|
| 1077 |
+
schema: object, mapping of schema names to schema descriptions.
|
| 1078 |
+
"""
|
| 1079 |
+
methodName = fix_method_name(methodName)
|
| 1080 |
+
(
|
| 1081 |
+
pathUrl,
|
| 1082 |
+
httpMethod,
|
| 1083 |
+
methodId,
|
| 1084 |
+
accept,
|
| 1085 |
+
maxSize,
|
| 1086 |
+
mediaPathUrl,
|
| 1087 |
+
) = _fix_up_method_description(methodDesc, rootDesc, schema)
|
| 1088 |
+
|
| 1089 |
+
parameters = ResourceMethodParameters(methodDesc)
|
| 1090 |
+
|
| 1091 |
+
def method(self, **kwargs):
|
| 1092 |
+
# Don't bother with doc string, it will be over-written by createMethod.
|
| 1093 |
+
|
| 1094 |
+
# Validate credentials for the configured universe.
|
| 1095 |
+
self._validate_credentials()
|
| 1096 |
+
|
| 1097 |
+
for name in kwargs:
|
| 1098 |
+
if name not in parameters.argmap:
|
| 1099 |
+
raise TypeError("Got an unexpected keyword argument {}".format(name))
|
| 1100 |
+
|
| 1101 |
+
# Remove args that have a value of None.
|
| 1102 |
+
keys = list(kwargs.keys())
|
| 1103 |
+
for name in keys:
|
| 1104 |
+
if kwargs[name] is None:
|
| 1105 |
+
del kwargs[name]
|
| 1106 |
+
|
| 1107 |
+
for name in parameters.required_params:
|
| 1108 |
+
if name not in kwargs:
|
| 1109 |
+
# temporary workaround for non-paging methods incorrectly requiring
|
| 1110 |
+
# page token parameter (cf. drive.changes.watch vs. drive.changes.list)
|
| 1111 |
+
if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
|
| 1112 |
+
_methodProperties(methodDesc, schema, "response")
|
| 1113 |
+
):
|
| 1114 |
+
raise TypeError('Missing required parameter "%s"' % name)
|
| 1115 |
+
|
| 1116 |
+
for name, regex in parameters.pattern_params.items():
|
| 1117 |
+
if name in kwargs:
|
| 1118 |
+
if isinstance(kwargs[name], str):
|
| 1119 |
+
pvalues = [kwargs[name]]
|
| 1120 |
+
else:
|
| 1121 |
+
pvalues = kwargs[name]
|
| 1122 |
+
for pvalue in pvalues:
|
| 1123 |
+
if re.match(regex, pvalue) is None:
|
| 1124 |
+
raise TypeError(
|
| 1125 |
+
'Parameter "%s" value "%s" does not match the pattern "%s"'
|
| 1126 |
+
% (name, pvalue, regex)
|
| 1127 |
+
)
|
| 1128 |
+
|
| 1129 |
+
for name, enums in parameters.enum_params.items():
|
| 1130 |
+
if name in kwargs:
|
| 1131 |
+
# We need to handle the case of a repeated enum
|
| 1132 |
+
# name differently, since we want to handle both
|
| 1133 |
+
# arg='value' and arg=['value1', 'value2']
|
| 1134 |
+
if name in parameters.repeated_params and not isinstance(
|
| 1135 |
+
kwargs[name], str
|
| 1136 |
+
):
|
| 1137 |
+
values = kwargs[name]
|
| 1138 |
+
else:
|
| 1139 |
+
values = [kwargs[name]]
|
| 1140 |
+
for value in values:
|
| 1141 |
+
if value not in enums:
|
| 1142 |
+
raise TypeError(
|
| 1143 |
+
'Parameter "%s" value "%s" is not an allowed value in "%s"'
|
| 1144 |
+
% (name, value, str(enums))
|
| 1145 |
+
)
|
| 1146 |
+
|
| 1147 |
+
actual_query_params = {}
|
| 1148 |
+
actual_path_params = {}
|
| 1149 |
+
for key, value in kwargs.items():
|
| 1150 |
+
to_type = parameters.param_types.get(key, "string")
|
| 1151 |
+
# For repeated parameters we cast each member of the list.
|
| 1152 |
+
if key in parameters.repeated_params and type(value) == type([]):
|
| 1153 |
+
cast_value = [_cast(x, to_type) for x in value]
|
| 1154 |
+
else:
|
| 1155 |
+
cast_value = _cast(value, to_type)
|
| 1156 |
+
if key in parameters.query_params:
|
| 1157 |
+
actual_query_params[parameters.argmap[key]] = cast_value
|
| 1158 |
+
if key in parameters.path_params:
|
| 1159 |
+
actual_path_params[parameters.argmap[key]] = cast_value
|
| 1160 |
+
body_value = kwargs.get("body", None)
|
| 1161 |
+
media_filename = kwargs.get("media_body", None)
|
| 1162 |
+
media_mime_type = kwargs.get("media_mime_type", None)
|
| 1163 |
+
|
| 1164 |
+
if self._developerKey:
|
| 1165 |
+
actual_query_params["key"] = self._developerKey
|
| 1166 |
+
|
| 1167 |
+
model = self._model
|
| 1168 |
+
if methodName.endswith("_media"):
|
| 1169 |
+
model = MediaModel()
|
| 1170 |
+
elif "response" not in methodDesc:
|
| 1171 |
+
model = RawModel()
|
| 1172 |
+
|
| 1173 |
+
api_version = methodDesc.get("apiVersion", None)
|
| 1174 |
+
|
| 1175 |
+
headers = {}
|
| 1176 |
+
headers, params, query, body = model.request(
|
| 1177 |
+
headers, actual_path_params, actual_query_params, body_value, api_version
|
| 1178 |
+
)
|
| 1179 |
+
|
| 1180 |
+
expanded_url = uritemplate.expand(pathUrl, params)
|
| 1181 |
+
url = _urljoin(self._baseUrl, expanded_url + query)
|
| 1182 |
+
|
| 1183 |
+
resumable = None
|
| 1184 |
+
multipart_boundary = ""
|
| 1185 |
+
|
| 1186 |
+
if media_filename:
|
| 1187 |
+
# Ensure we end up with a valid MediaUpload object.
|
| 1188 |
+
if isinstance(media_filename, str):
|
| 1189 |
+
if media_mime_type is None:
|
| 1190 |
+
logger.warning(
|
| 1191 |
+
"media_mime_type argument not specified: trying to auto-detect for %s",
|
| 1192 |
+
media_filename,
|
| 1193 |
+
)
|
| 1194 |
+
media_mime_type, _ = mimetypes.guess_type(media_filename)
|
| 1195 |
+
if media_mime_type is None:
|
| 1196 |
+
raise UnknownFileType(media_filename)
|
| 1197 |
+
if not mimeparse.best_match([media_mime_type], ",".join(accept)):
|
| 1198 |
+
raise UnacceptableMimeTypeError(media_mime_type)
|
| 1199 |
+
media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
|
| 1200 |
+
elif isinstance(media_filename, MediaUpload):
|
| 1201 |
+
media_upload = media_filename
|
| 1202 |
+
else:
|
| 1203 |
+
raise TypeError("media_filename must be str or MediaUpload.")
|
| 1204 |
+
|
| 1205 |
+
# Check the maxSize
|
| 1206 |
+
if media_upload.size() is not None and media_upload.size() > maxSize > 0:
|
| 1207 |
+
raise MediaUploadSizeError("Media larger than: %s" % maxSize)
|
| 1208 |
+
|
| 1209 |
+
# Use the media path uri for media uploads
|
| 1210 |
+
expanded_url = uritemplate.expand(mediaPathUrl, params)
|
| 1211 |
+
url = _urljoin(self._baseUrl, expanded_url + query)
|
| 1212 |
+
url = _fix_up_media_path_base_url(url, self._baseUrl)
|
| 1213 |
+
if media_upload.resumable():
|
| 1214 |
+
url = _add_query_parameter(url, "uploadType", "resumable")
|
| 1215 |
+
|
| 1216 |
+
if media_upload.resumable():
|
| 1217 |
+
# This is all we need to do for resumable, if the body exists it gets
|
| 1218 |
+
# sent in the first request, otherwise an empty body is sent.
|
| 1219 |
+
resumable = media_upload
|
| 1220 |
+
else:
|
| 1221 |
+
# A non-resumable upload
|
| 1222 |
+
if body is None:
|
| 1223 |
+
# This is a simple media upload
|
| 1224 |
+
headers["content-type"] = media_upload.mimetype()
|
| 1225 |
+
body = media_upload.getbytes(0, media_upload.size())
|
| 1226 |
+
url = _add_query_parameter(url, "uploadType", "media")
|
| 1227 |
+
else:
|
| 1228 |
+
# This is a multipart/related upload.
|
| 1229 |
+
msgRoot = MIMEMultipart("related")
|
| 1230 |
+
# msgRoot should not write out it's own headers
|
| 1231 |
+
setattr(msgRoot, "_write_headers", lambda self: None)
|
| 1232 |
+
|
| 1233 |
+
# attach the body as one part
|
| 1234 |
+
msg = MIMENonMultipart(*headers["content-type"].split("/"))
|
| 1235 |
+
msg.set_payload(body)
|
| 1236 |
+
msgRoot.attach(msg)
|
| 1237 |
+
|
| 1238 |
+
# attach the media as the second part
|
| 1239 |
+
msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
|
| 1240 |
+
msg["Content-Transfer-Encoding"] = "binary"
|
| 1241 |
+
|
| 1242 |
+
payload = media_upload.getbytes(0, media_upload.size())
|
| 1243 |
+
msg.set_payload(payload)
|
| 1244 |
+
msgRoot.attach(msg)
|
| 1245 |
+
# encode the body: note that we can't use `as_string`, because
|
| 1246 |
+
# it plays games with `From ` lines.
|
| 1247 |
+
fp = io.BytesIO()
|
| 1248 |
+
g = _BytesGenerator(fp, mangle_from_=False)
|
| 1249 |
+
g.flatten(msgRoot, unixfrom=False)
|
| 1250 |
+
body = fp.getvalue()
|
| 1251 |
+
|
| 1252 |
+
multipart_boundary = msgRoot.get_boundary()
|
| 1253 |
+
headers["content-type"] = (
|
| 1254 |
+
"multipart/related; " 'boundary="%s"'
|
| 1255 |
+
) % multipart_boundary
|
| 1256 |
+
url = _add_query_parameter(url, "uploadType", "multipart")
|
| 1257 |
+
|
| 1258 |
+
logger.debug("URL being requested: %s %s" % (httpMethod, url))
|
| 1259 |
+
return self._requestBuilder(
|
| 1260 |
+
self._http,
|
| 1261 |
+
model.response,
|
| 1262 |
+
url,
|
| 1263 |
+
method=httpMethod,
|
| 1264 |
+
body=body,
|
| 1265 |
+
headers=headers,
|
| 1266 |
+
methodId=methodId,
|
| 1267 |
+
resumable=resumable,
|
| 1268 |
+
)
|
| 1269 |
+
|
| 1270 |
+
docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
|
| 1271 |
+
if len(parameters.argmap) > 0:
|
| 1272 |
+
docs.append("Args:\n")
|
| 1273 |
+
|
| 1274 |
+
# Skip undocumented params and params common to all methods.
|
| 1275 |
+
skip_parameters = list(rootDesc.get("parameters", {}).keys())
|
| 1276 |
+
skip_parameters.extend(STACK_QUERY_PARAMETERS)
|
| 1277 |
+
|
| 1278 |
+
all_args = list(parameters.argmap.keys())
|
| 1279 |
+
args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
|
| 1280 |
+
|
| 1281 |
+
# Move body to the front of the line.
|
| 1282 |
+
if "body" in all_args:
|
| 1283 |
+
args_ordered.append("body")
|
| 1284 |
+
|
| 1285 |
+
for name in sorted(all_args):
|
| 1286 |
+
if name not in args_ordered:
|
| 1287 |
+
args_ordered.append(name)
|
| 1288 |
+
|
| 1289 |
+
for arg in args_ordered:
|
| 1290 |
+
if arg in skip_parameters:
|
| 1291 |
+
continue
|
| 1292 |
+
|
| 1293 |
+
repeated = ""
|
| 1294 |
+
if arg in parameters.repeated_params:
|
| 1295 |
+
repeated = " (repeated)"
|
| 1296 |
+
required = ""
|
| 1297 |
+
if arg in parameters.required_params:
|
| 1298 |
+
required = " (required)"
|
| 1299 |
+
paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
|
| 1300 |
+
paramdoc = paramdesc.get("description", "A parameter")
|
| 1301 |
+
if "$ref" in paramdesc:
|
| 1302 |
+
docs.append(
|
| 1303 |
+
(" %s: object, %s%s%s\n The object takes the form of:\n\n%s\n\n")
|
| 1304 |
+
% (
|
| 1305 |
+
arg,
|
| 1306 |
+
paramdoc,
|
| 1307 |
+
required,
|
| 1308 |
+
repeated,
|
| 1309 |
+
schema.prettyPrintByName(paramdesc["$ref"]),
|
| 1310 |
+
)
|
| 1311 |
+
)
|
| 1312 |
+
else:
|
| 1313 |
+
paramtype = paramdesc.get("type", "string")
|
| 1314 |
+
docs.append(
|
| 1315 |
+
" %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
|
| 1316 |
+
)
|
| 1317 |
+
enum = paramdesc.get("enum", [])
|
| 1318 |
+
enumDesc = paramdesc.get("enumDescriptions", [])
|
| 1319 |
+
if enum and enumDesc:
|
| 1320 |
+
docs.append(" Allowed values\n")
|
| 1321 |
+
for (name, desc) in zip(enum, enumDesc):
|
| 1322 |
+
docs.append(" %s - %s\n" % (name, desc))
|
| 1323 |
+
if "response" in methodDesc:
|
| 1324 |
+
if methodName.endswith("_media"):
|
| 1325 |
+
docs.append("\nReturns:\n The media object as a string.\n\n ")
|
| 1326 |
+
else:
|
| 1327 |
+
docs.append("\nReturns:\n An object of the form:\n\n ")
|
| 1328 |
+
docs.append(schema.prettyPrintSchema(methodDesc["response"]))
|
| 1329 |
+
|
| 1330 |
+
setattr(method, "__doc__", "".join(docs))
|
| 1331 |
+
return (methodName, method)
|
| 1332 |
+
|
| 1333 |
+
|
| 1334 |
+
def createNextMethod(
|
| 1335 |
+
methodName,
|
| 1336 |
+
pageTokenName="pageToken",
|
| 1337 |
+
nextPageTokenName="nextPageToken",
|
| 1338 |
+
isPageTokenParameter=True,
|
| 1339 |
+
):
|
| 1340 |
+
"""Creates any _next methods for attaching to a Resource.
|
| 1341 |
+
|
| 1342 |
+
The _next methods allow for easy iteration through list() responses.
|
| 1343 |
+
|
| 1344 |
+
Args:
|
| 1345 |
+
methodName: string, name of the method to use.
|
| 1346 |
+
pageTokenName: string, name of request page token field.
|
| 1347 |
+
nextPageTokenName: string, name of response page token field.
|
| 1348 |
+
isPageTokenParameter: Boolean, True if request page token is a query
|
| 1349 |
+
parameter, False if request page token is a field of the request body.
|
| 1350 |
+
"""
|
| 1351 |
+
methodName = fix_method_name(methodName)
|
| 1352 |
+
|
| 1353 |
+
def methodNext(self, previous_request, previous_response):
|
| 1354 |
+
"""Retrieves the next page of results.
|
| 1355 |
+
|
| 1356 |
+
Args:
|
| 1357 |
+
previous_request: The request for the previous page. (required)
|
| 1358 |
+
previous_response: The response from the request for the previous page. (required)
|
| 1359 |
+
|
| 1360 |
+
Returns:
|
| 1361 |
+
A request object that you can call 'execute()' on to request the next
|
| 1362 |
+
page. Returns None if there are no more items in the collection.
|
| 1363 |
+
"""
|
| 1364 |
+
# Retrieve nextPageToken from previous_response
|
| 1365 |
+
# Use as pageToken in previous_request to create new request.
|
| 1366 |
+
|
| 1367 |
+
nextPageToken = previous_response.get(nextPageTokenName, None)
|
| 1368 |
+
if not nextPageToken:
|
| 1369 |
+
return None
|
| 1370 |
+
|
| 1371 |
+
request = copy.copy(previous_request)
|
| 1372 |
+
|
| 1373 |
+
if isPageTokenParameter:
|
| 1374 |
+
# Replace pageToken value in URI
|
| 1375 |
+
request.uri = _add_query_parameter(
|
| 1376 |
+
request.uri, pageTokenName, nextPageToken
|
| 1377 |
+
)
|
| 1378 |
+
logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
|
| 1379 |
+
else:
|
| 1380 |
+
# Replace pageToken value in request body
|
| 1381 |
+
model = self._model
|
| 1382 |
+
body = model.deserialize(request.body)
|
| 1383 |
+
body[pageTokenName] = nextPageToken
|
| 1384 |
+
request.body = model.serialize(body)
|
| 1385 |
+
request.body_size = len(request.body)
|
| 1386 |
+
if "content-length" in request.headers:
|
| 1387 |
+
del request.headers["content-length"]
|
| 1388 |
+
logger.debug("Next page request body: %s %s" % (methodName, body))
|
| 1389 |
+
|
| 1390 |
+
return request
|
| 1391 |
+
|
| 1392 |
+
return (methodName, methodNext)
|
| 1393 |
+
|
| 1394 |
+
|
| 1395 |
+
class Resource(object):
|
| 1396 |
+
"""A class for interacting with a resource."""
|
| 1397 |
+
|
| 1398 |
+
def __init__(
|
| 1399 |
+
self,
|
| 1400 |
+
http,
|
| 1401 |
+
baseUrl,
|
| 1402 |
+
model,
|
| 1403 |
+
requestBuilder,
|
| 1404 |
+
developerKey,
|
| 1405 |
+
resourceDesc,
|
| 1406 |
+
rootDesc,
|
| 1407 |
+
schema,
|
| 1408 |
+
universe_domain=universe.DEFAULT_UNIVERSE if HAS_UNIVERSE else "",
|
| 1409 |
+
):
|
| 1410 |
+
"""Build a Resource from the API description.
|
| 1411 |
+
|
| 1412 |
+
Args:
|
| 1413 |
+
http: httplib2.Http, Object to make http requests with.
|
| 1414 |
+
baseUrl: string, base URL for the API. All requests are relative to this
|
| 1415 |
+
URI.
|
| 1416 |
+
model: googleapiclient.Model, converts to and from the wire format.
|
| 1417 |
+
requestBuilder: class or callable that instantiates an
|
| 1418 |
+
googleapiclient.HttpRequest object.
|
| 1419 |
+
developerKey: string, key obtained from
|
| 1420 |
+
https://code.google.com/apis/console
|
| 1421 |
+
resourceDesc: object, section of deserialized discovery document that
|
| 1422 |
+
describes a resource. Note that the top level discovery document
|
| 1423 |
+
is considered a resource.
|
| 1424 |
+
rootDesc: object, the entire deserialized discovery document.
|
| 1425 |
+
schema: object, mapping of schema names to schema descriptions.
|
| 1426 |
+
universe_domain: string, the universe for the API. The default universe
|
| 1427 |
+
is "googleapis.com".
|
| 1428 |
+
"""
|
| 1429 |
+
self._dynamic_attrs = []
|
| 1430 |
+
|
| 1431 |
+
self._http = http
|
| 1432 |
+
self._baseUrl = baseUrl
|
| 1433 |
+
self._model = model
|
| 1434 |
+
self._developerKey = developerKey
|
| 1435 |
+
self._requestBuilder = requestBuilder
|
| 1436 |
+
self._resourceDesc = resourceDesc
|
| 1437 |
+
self._rootDesc = rootDesc
|
| 1438 |
+
self._schema = schema
|
| 1439 |
+
self._universe_domain = universe_domain
|
| 1440 |
+
self._credentials_validated = False
|
| 1441 |
+
|
| 1442 |
+
self._set_service_methods()
|
| 1443 |
+
|
| 1444 |
+
def _set_dynamic_attr(self, attr_name, value):
|
| 1445 |
+
"""Sets an instance attribute and tracks it in a list of dynamic attributes.
|
| 1446 |
+
|
| 1447 |
+
Args:
|
| 1448 |
+
attr_name: string; The name of the attribute to be set
|
| 1449 |
+
value: The value being set on the object and tracked in the dynamic cache.
|
| 1450 |
+
"""
|
| 1451 |
+
self._dynamic_attrs.append(attr_name)
|
| 1452 |
+
self.__dict__[attr_name] = value
|
| 1453 |
+
|
| 1454 |
+
def __getstate__(self):
|
| 1455 |
+
"""Trim the state down to something that can be pickled.
|
| 1456 |
+
|
| 1457 |
+
Uses the fact that the instance variable _dynamic_attrs holds attrs that
|
| 1458 |
+
will be wiped and restored on pickle serialization.
|
| 1459 |
+
"""
|
| 1460 |
+
state_dict = copy.copy(self.__dict__)
|
| 1461 |
+
for dynamic_attr in self._dynamic_attrs:
|
| 1462 |
+
del state_dict[dynamic_attr]
|
| 1463 |
+
del state_dict["_dynamic_attrs"]
|
| 1464 |
+
return state_dict
|
| 1465 |
+
|
| 1466 |
+
def __setstate__(self, state):
|
| 1467 |
+
"""Reconstitute the state of the object from being pickled.
|
| 1468 |
+
|
| 1469 |
+
Uses the fact that the instance variable _dynamic_attrs holds attrs that
|
| 1470 |
+
will be wiped and restored on pickle serialization.
|
| 1471 |
+
"""
|
| 1472 |
+
self.__dict__.update(state)
|
| 1473 |
+
self._dynamic_attrs = []
|
| 1474 |
+
self._set_service_methods()
|
| 1475 |
+
|
| 1476 |
+
def __enter__(self):
|
| 1477 |
+
return self
|
| 1478 |
+
|
| 1479 |
+
def __exit__(self, exc_type, exc, exc_tb):
|
| 1480 |
+
self.close()
|
| 1481 |
+
|
| 1482 |
+
def close(self):
|
| 1483 |
+
"""Close httplib2 connections."""
|
| 1484 |
+
# httplib2 leaves sockets open by default.
|
| 1485 |
+
# Cleanup using the `close` method.
|
| 1486 |
+
# https://github.com/httplib2/httplib2/issues/148
|
| 1487 |
+
self._http.close()
|
| 1488 |
+
|
| 1489 |
+
def _set_service_methods(self):
|
| 1490 |
+
self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
|
| 1491 |
+
self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
|
| 1492 |
+
self._add_next_methods(self._resourceDesc, self._schema)
|
| 1493 |
+
|
| 1494 |
+
def _add_basic_methods(self, resourceDesc, rootDesc, schema):
|
| 1495 |
+
# If this is the root Resource, add a new_batch_http_request() method.
|
| 1496 |
+
if resourceDesc == rootDesc:
|
| 1497 |
+
batch_uri = "%s%s" % (
|
| 1498 |
+
rootDesc["rootUrl"],
|
| 1499 |
+
rootDesc.get("batchPath", "batch"),
|
| 1500 |
+
)
|
| 1501 |
+
|
| 1502 |
+
def new_batch_http_request(callback=None):
|
| 1503 |
+
"""Create a BatchHttpRequest object based on the discovery document.
|
| 1504 |
+
|
| 1505 |
+
Args:
|
| 1506 |
+
callback: callable, A callback to be called for each response, of the
|
| 1507 |
+
form callback(id, response, exception). The first parameter is the
|
| 1508 |
+
request id, and the second is the deserialized response object. The
|
| 1509 |
+
third is an apiclient.errors.HttpError exception object if an HTTP
|
| 1510 |
+
error occurred while processing the request, or None if no error
|
| 1511 |
+
occurred.
|
| 1512 |
+
|
| 1513 |
+
Returns:
|
| 1514 |
+
A BatchHttpRequest object based on the discovery document.
|
| 1515 |
+
"""
|
| 1516 |
+
return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
|
| 1517 |
+
|
| 1518 |
+
self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
|
| 1519 |
+
|
| 1520 |
+
# Add basic methods to Resource
|
| 1521 |
+
if "methods" in resourceDesc:
|
| 1522 |
+
for methodName, methodDesc in resourceDesc["methods"].items():
|
| 1523 |
+
fixedMethodName, method = createMethod(
|
| 1524 |
+
methodName, methodDesc, rootDesc, schema
|
| 1525 |
+
)
|
| 1526 |
+
self._set_dynamic_attr(
|
| 1527 |
+
fixedMethodName, method.__get__(self, self.__class__)
|
| 1528 |
+
)
|
| 1529 |
+
# Add in _media methods. The functionality of the attached method will
|
| 1530 |
+
# change when it sees that the method name ends in _media.
|
| 1531 |
+
if methodDesc.get("supportsMediaDownload", False):
|
| 1532 |
+
fixedMethodName, method = createMethod(
|
| 1533 |
+
methodName + "_media", methodDesc, rootDesc, schema
|
| 1534 |
+
)
|
| 1535 |
+
self._set_dynamic_attr(
|
| 1536 |
+
fixedMethodName, method.__get__(self, self.__class__)
|
| 1537 |
+
)
|
| 1538 |
+
|
| 1539 |
+
def _add_nested_resources(self, resourceDesc, rootDesc, schema):
|
| 1540 |
+
# Add in nested resources
|
| 1541 |
+
if "resources" in resourceDesc:
|
| 1542 |
+
|
| 1543 |
+
def createResourceMethod(methodName, methodDesc):
|
| 1544 |
+
"""Create a method on the Resource to access a nested Resource.
|
| 1545 |
+
|
| 1546 |
+
Args:
|
| 1547 |
+
methodName: string, name of the method to use.
|
| 1548 |
+
methodDesc: object, fragment of deserialized discovery document that
|
| 1549 |
+
describes the method.
|
| 1550 |
+
"""
|
| 1551 |
+
methodName = fix_method_name(methodName)
|
| 1552 |
+
|
| 1553 |
+
def methodResource(self):
|
| 1554 |
+
return Resource(
|
| 1555 |
+
http=self._http,
|
| 1556 |
+
baseUrl=self._baseUrl,
|
| 1557 |
+
model=self._model,
|
| 1558 |
+
developerKey=self._developerKey,
|
| 1559 |
+
requestBuilder=self._requestBuilder,
|
| 1560 |
+
resourceDesc=methodDesc,
|
| 1561 |
+
rootDesc=rootDesc,
|
| 1562 |
+
schema=schema,
|
| 1563 |
+
universe_domain=self._universe_domain,
|
| 1564 |
+
)
|
| 1565 |
+
|
| 1566 |
+
setattr(methodResource, "__doc__", "A collection resource.")
|
| 1567 |
+
setattr(methodResource, "__is_resource__", True)
|
| 1568 |
+
|
| 1569 |
+
return (methodName, methodResource)
|
| 1570 |
+
|
| 1571 |
+
for methodName, methodDesc in resourceDesc["resources"].items():
|
| 1572 |
+
fixedMethodName, method = createResourceMethod(methodName, methodDesc)
|
| 1573 |
+
self._set_dynamic_attr(
|
| 1574 |
+
fixedMethodName, method.__get__(self, self.__class__)
|
| 1575 |
+
)
|
| 1576 |
+
|
| 1577 |
+
def _add_next_methods(self, resourceDesc, schema):
|
| 1578 |
+
# Add _next() methods if and only if one of the names 'pageToken' or
|
| 1579 |
+
# 'nextPageToken' occurs among the fields of both the method's response
|
| 1580 |
+
# type either the method's request (query parameters) or request body.
|
| 1581 |
+
if "methods" not in resourceDesc:
|
| 1582 |
+
return
|
| 1583 |
+
for methodName, methodDesc in resourceDesc["methods"].items():
|
| 1584 |
+
nextPageTokenName = _findPageTokenName(
|
| 1585 |
+
_methodProperties(methodDesc, schema, "response")
|
| 1586 |
+
)
|
| 1587 |
+
if not nextPageTokenName:
|
| 1588 |
+
continue
|
| 1589 |
+
isPageTokenParameter = True
|
| 1590 |
+
pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
|
| 1591 |
+
if not pageTokenName:
|
| 1592 |
+
isPageTokenParameter = False
|
| 1593 |
+
pageTokenName = _findPageTokenName(
|
| 1594 |
+
_methodProperties(methodDesc, schema, "request")
|
| 1595 |
+
)
|
| 1596 |
+
if not pageTokenName:
|
| 1597 |
+
continue
|
| 1598 |
+
fixedMethodName, method = createNextMethod(
|
| 1599 |
+
methodName + "_next",
|
| 1600 |
+
pageTokenName,
|
| 1601 |
+
nextPageTokenName,
|
| 1602 |
+
isPageTokenParameter,
|
| 1603 |
+
)
|
| 1604 |
+
self._set_dynamic_attr(
|
| 1605 |
+
fixedMethodName, method.__get__(self, self.__class__)
|
| 1606 |
+
)
|
| 1607 |
+
|
| 1608 |
+
def _validate_credentials(self):
|
| 1609 |
+
"""Validates client's and credentials' universe domains are consistent.
|
| 1610 |
+
|
| 1611 |
+
Returns:
|
| 1612 |
+
bool: True iff the configured universe domain is valid.
|
| 1613 |
+
|
| 1614 |
+
Raises:
|
| 1615 |
+
UniverseMismatchError: If the configured universe domain is not valid.
|
| 1616 |
+
"""
|
| 1617 |
+
credentials = getattr(self._http, "credentials", None)
|
| 1618 |
+
|
| 1619 |
+
self._credentials_validated = (
|
| 1620 |
+
(
|
| 1621 |
+
self._credentials_validated
|
| 1622 |
+
or universe.compare_domains(self._universe_domain, credentials)
|
| 1623 |
+
)
|
| 1624 |
+
if HAS_UNIVERSE
|
| 1625 |
+
else True
|
| 1626 |
+
)
|
| 1627 |
+
return self._credentials_validated
|
| 1628 |
+
|
| 1629 |
+
|
| 1630 |
+
def _findPageTokenName(fields):
|
| 1631 |
+
"""Search field names for one like a page token.
|
| 1632 |
+
|
| 1633 |
+
Args:
|
| 1634 |
+
fields: container of string, names of fields.
|
| 1635 |
+
|
| 1636 |
+
Returns:
|
| 1637 |
+
First name that is either 'pageToken' or 'nextPageToken' if one exists,
|
| 1638 |
+
otherwise None.
|
| 1639 |
+
"""
|
| 1640 |
+
return next(
|
| 1641 |
+
(tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
|
| 1642 |
+
)
|
| 1643 |
+
|
| 1644 |
+
|
| 1645 |
+
def _methodProperties(methodDesc, schema, name):
|
| 1646 |
+
"""Get properties of a field in a method description.
|
| 1647 |
+
|
| 1648 |
+
Args:
|
| 1649 |
+
methodDesc: object, fragment of deserialized discovery document that
|
| 1650 |
+
describes the method.
|
| 1651 |
+
schema: object, mapping of schema names to schema descriptions.
|
| 1652 |
+
name: string, name of top-level field in method description.
|
| 1653 |
+
|
| 1654 |
+
Returns:
|
| 1655 |
+
Object representing fragment of deserialized discovery document
|
| 1656 |
+
corresponding to 'properties' field of object corresponding to named field
|
| 1657 |
+
in method description, if it exists, otherwise empty dict.
|
| 1658 |
+
"""
|
| 1659 |
+
desc = methodDesc.get(name, {})
|
| 1660 |
+
if "$ref" in desc:
|
| 1661 |
+
desc = schema.get(desc["$ref"], {})
|
| 1662 |
+
return desc.get("properties", {})
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
"""Caching utility for the discovery document."""
|
| 16 |
+
|
| 17 |
+
from __future__ import absolute_import
|
| 18 |
+
|
| 19 |
+
import logging
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
LOGGER = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
DISCOVERY_DOC_MAX_AGE = 60 * 60 * 24 # 1 day
|
| 25 |
+
DISCOVERY_DOC_DIR = os.path.join(
|
| 26 |
+
os.path.dirname(os.path.realpath(__file__)), "documents"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def autodetect():
|
| 31 |
+
"""Detects an appropriate cache module and returns it.
|
| 32 |
+
|
| 33 |
+
Returns:
|
| 34 |
+
googleapiclient.discovery_cache.base.Cache, a cache object which
|
| 35 |
+
is auto detected, or None if no cache object is available.
|
| 36 |
+
"""
|
| 37 |
+
if "GAE_ENV" in os.environ:
|
| 38 |
+
try:
|
| 39 |
+
from . import appengine_memcache
|
| 40 |
+
|
| 41 |
+
return appengine_memcache.cache
|
| 42 |
+
except Exception:
|
| 43 |
+
pass
|
| 44 |
+
try:
|
| 45 |
+
from . import file_cache
|
| 46 |
+
|
| 47 |
+
return file_cache.cache
|
| 48 |
+
except Exception:
|
| 49 |
+
LOGGER.info(
|
| 50 |
+
"file_cache is only supported with oauth2client<4.0.0", exc_info=False
|
| 51 |
+
)
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_static_doc(serviceName, version):
|
| 56 |
+
"""Retrieves the discovery document from the directory defined in
|
| 57 |
+
DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
serviceName: string, name of the service.
|
| 61 |
+
version: string, the version of the service.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
A string containing the contents of the JSON discovery document,
|
| 65 |
+
otherwise None if the JSON discovery document was not found.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
content = None
|
| 69 |
+
doc_name = "{}.{}.json".format(serviceName, version)
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), "r") as f:
|
| 73 |
+
content = f.read()
|
| 74 |
+
except FileNotFoundError:
|
| 75 |
+
# File does not exist. Nothing to do here.
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
return content
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (2.87 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/appengine_memcache.cpython-311.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/base.cpython-311.pyc
ADDED
|
Binary file (1.49 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/__pycache__/file_cache.cpython-311.pyc
ADDED
|
Binary file (7.44 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/googleapiclient/discovery_cache/appengine_memcache.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
| 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 |
+
|
| 15 |
+
"""App Engine memcache based cache for the discovery document."""
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
|
| 19 |
+
# This is only an optional dependency because we only import this
|
| 20 |
+
# module when google.appengine.api.memcache is available.
|
| 21 |
+
from google.appengine.api import memcache
|
| 22 |
+
|
| 23 |
+
from . import base
|
| 24 |
+
from ..discovery_cache import DISCOVERY_DOC_MAX_AGE
|
| 25 |
+
|
| 26 |
+
LOGGER = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
NAMESPACE = "google-api-client"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Cache(base.Cache):
|
| 32 |
+
"""A cache with app engine memcache API."""
|
| 33 |
+
|
| 34 |
+
def __init__(self, max_age):
|
| 35 |
+
"""Constructor.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
max_age: Cache expiration in seconds.
|
| 39 |
+
"""
|
| 40 |
+
self._max_age = max_age
|
| 41 |
+
|
| 42 |
+
def get(self, url):
|
| 43 |
+
try:
|
| 44 |
+
return memcache.get(url, namespace=NAMESPACE)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
LOGGER.warning(e, exc_info=True)
|
| 47 |
+
|
| 48 |
+
def set(self, url, content):
|
| 49 |
+
try:
|
| 50 |
+
memcache.set(url, content, time=int(self._max_age), namespace=NAMESPACE)
|
| 51 |
+
except Exception as e:
|
| 52 |
+
LOGGER.warning(e, exc_info=True)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
cache = Cache(max_age=DISCOVERY_DOC_MAX_AGE)
|