Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- py311/lib/python3.11/site-packages/aiofiles-25.1.0.dist-info/licenses/LICENSE +202 -0
- py311/lib/python3.11/site-packages/aiofiles-25.1.0.dist-info/licenses/NOTICE +2 -0
- py311/lib/python3.11/site-packages/aiofiles/tempfile/__init__.py +357 -0
- py311/lib/python3.11/site-packages/aiofiles/tempfile/temptypes.py +70 -0
- py311/lib/python3.11/site-packages/aiofiles/threadpool/__init__.py +141 -0
- py311/lib/python3.11/site-packages/aiofiles/threadpool/binary.py +104 -0
- py311/lib/python3.11/site-packages/aiofiles/threadpool/text.py +64 -0
- py311/lib/python3.11/site-packages/aiofiles/threadpool/utils.py +71 -0
- py311/lib/python3.11/site-packages/anyio-4.12.1.dist-info/licenses/LICENSE +20 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/__init__.py +4 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/conv.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/conv.py +256 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/driver.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/driver.py +313 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/grammar.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/grammar.py +228 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/literals.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/literals.py +65 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/parse.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/parse.py +395 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/pgen.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/pgen.py +411 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/token.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/token.py +95 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/tokenize.cpython-311-x86_64-linux-gnu.so +0 -0
- py311/lib/python3.11/site-packages/blib2to3/pgen2/tokenize.py +226 -0
- py311/lib/python3.11/site-packages/google/api_core/__init__.py +41 -0
- py311/lib/python3.11/site-packages/google/api_core/_python_package_support.py +234 -0
- py311/lib/python3.11/site-packages/google/api_core/_python_version_support.py +278 -0
- py311/lib/python3.11/site-packages/google/api_core/_rest_streaming_base.py +118 -0
- py311/lib/python3.11/site-packages/google/api_core/bidi.py +735 -0
- py311/lib/python3.11/site-packages/google/api_core/bidi_async.py +244 -0
- py311/lib/python3.11/site-packages/google/api_core/bidi_base.py +88 -0
- py311/lib/python3.11/site-packages/google/api_core/client_info.py +114 -0
- py311/lib/python3.11/site-packages/google/api_core/client_logging.py +144 -0
- py311/lib/python3.11/site-packages/google/api_core/client_options.py +160 -0
- py311/lib/python3.11/site-packages/google/api_core/datetime_helpers.py +298 -0
- py311/lib/python3.11/site-packages/google/api_core/exceptions.py +670 -0
- py311/lib/python3.11/site-packages/google/api_core/extended_operation.py +225 -0
- py311/lib/python3.11/site-packages/google/api_core/general_helpers.py +52 -0
- py311/lib/python3.11/site-packages/google/api_core/grpc_helpers.py +649 -0
- py311/lib/python3.11/site-packages/google/api_core/grpc_helpers_async.py +348 -0
- py311/lib/python3.11/site-packages/google/api_core/iam.py +427 -0
- py311/lib/python3.11/site-packages/google/api_core/operation.py +365 -0
- py311/lib/python3.11/site-packages/google/api_core/operation_async.py +225 -0
- py311/lib/python3.11/site-packages/google/api_core/page_iterator.py +571 -0
- py311/lib/python3.11/site-packages/google/api_core/page_iterator_async.py +285 -0
- py311/lib/python3.11/site-packages/google/api_core/path_template.py +346 -0
- py311/lib/python3.11/site-packages/google/api_core/protobuf_helpers.py +371 -0
- py311/lib/python3.11/site-packages/google/api_core/py.typed +2 -0
py311/lib/python3.11/site-packages/aiofiles-25.1.0.dist-info/licenses/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "{}"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright {yyyy} {name of copyright owner}
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
| 202 |
+
|
py311/lib/python3.11/site-packages/aiofiles-25.1.0.dist-info/licenses/NOTICE
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Asyncio support for files
|
| 2 |
+
Copyright 2016 Tin Tvrtkovic
|
py311/lib/python3.11/site-packages/aiofiles/tempfile/__init__.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import sys
|
| 3 |
+
from functools import partial, singledispatch
|
| 4 |
+
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOBase
|
| 5 |
+
from tempfile import NamedTemporaryFile as syncNamedTemporaryFile
|
| 6 |
+
from tempfile import SpooledTemporaryFile as syncSpooledTemporaryFile
|
| 7 |
+
from tempfile import TemporaryDirectory as syncTemporaryDirectory
|
| 8 |
+
from tempfile import TemporaryFile as syncTemporaryFile
|
| 9 |
+
from tempfile import _TemporaryFileWrapper as syncTemporaryFileWrapper
|
| 10 |
+
|
| 11 |
+
from ..base import AiofilesContextManager
|
| 12 |
+
from ..threadpool.binary import AsyncBufferedIOBase, AsyncBufferedReader, AsyncFileIO
|
| 13 |
+
from ..threadpool.text import AsyncTextIOWrapper
|
| 14 |
+
from .temptypes import AsyncSpooledTemporaryFile, AsyncTemporaryDirectory
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
"NamedTemporaryFile",
|
| 18 |
+
"TemporaryFile",
|
| 19 |
+
"SpooledTemporaryFile",
|
| 20 |
+
"TemporaryDirectory",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# ================================================================
|
| 25 |
+
# Public methods for async open and return of temp file/directory
|
| 26 |
+
# objects with async interface
|
| 27 |
+
# ================================================================
|
| 28 |
+
if sys.version_info >= (3, 12):
|
| 29 |
+
|
| 30 |
+
def NamedTemporaryFile(
|
| 31 |
+
mode="w+b",
|
| 32 |
+
buffering=-1,
|
| 33 |
+
encoding=None,
|
| 34 |
+
newline=None,
|
| 35 |
+
suffix=None,
|
| 36 |
+
prefix=None,
|
| 37 |
+
dir=None,
|
| 38 |
+
delete=True,
|
| 39 |
+
delete_on_close=True,
|
| 40 |
+
loop=None,
|
| 41 |
+
executor=None,
|
| 42 |
+
):
|
| 43 |
+
"""Async open a named temporary file"""
|
| 44 |
+
return AiofilesContextManager(
|
| 45 |
+
_temporary_file(
|
| 46 |
+
named=True,
|
| 47 |
+
mode=mode,
|
| 48 |
+
buffering=buffering,
|
| 49 |
+
encoding=encoding,
|
| 50 |
+
newline=newline,
|
| 51 |
+
suffix=suffix,
|
| 52 |
+
prefix=prefix,
|
| 53 |
+
dir=dir,
|
| 54 |
+
delete=delete,
|
| 55 |
+
delete_on_close=delete_on_close,
|
| 56 |
+
loop=loop,
|
| 57 |
+
executor=executor,
|
| 58 |
+
)
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
else:
|
| 62 |
+
|
| 63 |
+
def NamedTemporaryFile(
|
| 64 |
+
mode="w+b",
|
| 65 |
+
buffering=-1,
|
| 66 |
+
encoding=None,
|
| 67 |
+
newline=None,
|
| 68 |
+
suffix=None,
|
| 69 |
+
prefix=None,
|
| 70 |
+
dir=None,
|
| 71 |
+
delete=True,
|
| 72 |
+
loop=None,
|
| 73 |
+
executor=None,
|
| 74 |
+
):
|
| 75 |
+
"""Async open a named temporary file"""
|
| 76 |
+
return AiofilesContextManager(
|
| 77 |
+
_temporary_file(
|
| 78 |
+
named=True,
|
| 79 |
+
mode=mode,
|
| 80 |
+
buffering=buffering,
|
| 81 |
+
encoding=encoding,
|
| 82 |
+
newline=newline,
|
| 83 |
+
suffix=suffix,
|
| 84 |
+
prefix=prefix,
|
| 85 |
+
dir=dir,
|
| 86 |
+
delete=delete,
|
| 87 |
+
loop=loop,
|
| 88 |
+
executor=executor,
|
| 89 |
+
)
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def TemporaryFile(
|
| 94 |
+
mode="w+b",
|
| 95 |
+
buffering=-1,
|
| 96 |
+
encoding=None,
|
| 97 |
+
newline=None,
|
| 98 |
+
suffix=None,
|
| 99 |
+
prefix=None,
|
| 100 |
+
dir=None,
|
| 101 |
+
loop=None,
|
| 102 |
+
executor=None,
|
| 103 |
+
):
|
| 104 |
+
"""Async open an unnamed temporary file"""
|
| 105 |
+
return AiofilesContextManager(
|
| 106 |
+
_temporary_file(
|
| 107 |
+
named=False,
|
| 108 |
+
mode=mode,
|
| 109 |
+
buffering=buffering,
|
| 110 |
+
encoding=encoding,
|
| 111 |
+
newline=newline,
|
| 112 |
+
suffix=suffix,
|
| 113 |
+
prefix=prefix,
|
| 114 |
+
dir=dir,
|
| 115 |
+
loop=loop,
|
| 116 |
+
executor=executor,
|
| 117 |
+
)
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def SpooledTemporaryFile(
|
| 122 |
+
max_size=0,
|
| 123 |
+
mode="w+b",
|
| 124 |
+
buffering=-1,
|
| 125 |
+
encoding=None,
|
| 126 |
+
newline=None,
|
| 127 |
+
suffix=None,
|
| 128 |
+
prefix=None,
|
| 129 |
+
dir=None,
|
| 130 |
+
loop=None,
|
| 131 |
+
executor=None,
|
| 132 |
+
):
|
| 133 |
+
"""Async open a spooled temporary file"""
|
| 134 |
+
return AiofilesContextManager(
|
| 135 |
+
_spooled_temporary_file(
|
| 136 |
+
max_size=max_size,
|
| 137 |
+
mode=mode,
|
| 138 |
+
buffering=buffering,
|
| 139 |
+
encoding=encoding,
|
| 140 |
+
newline=newline,
|
| 141 |
+
suffix=suffix,
|
| 142 |
+
prefix=prefix,
|
| 143 |
+
dir=dir,
|
| 144 |
+
loop=loop,
|
| 145 |
+
executor=executor,
|
| 146 |
+
)
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def TemporaryDirectory(suffix=None, prefix=None, dir=None, loop=None, executor=None):
|
| 151 |
+
"""Async open a temporary directory"""
|
| 152 |
+
return AiofilesContextManagerTempDir(
|
| 153 |
+
_temporary_directory(
|
| 154 |
+
suffix=suffix, prefix=prefix, dir=dir, loop=loop, executor=executor
|
| 155 |
+
)
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# =========================================================
|
| 160 |
+
# Internal coroutines to open new temp files/directories
|
| 161 |
+
# =========================================================
|
| 162 |
+
if sys.version_info >= (3, 12):
|
| 163 |
+
|
| 164 |
+
async def _temporary_file(
|
| 165 |
+
named=True,
|
| 166 |
+
mode="w+b",
|
| 167 |
+
buffering=-1,
|
| 168 |
+
encoding=None,
|
| 169 |
+
newline=None,
|
| 170 |
+
suffix=None,
|
| 171 |
+
prefix=None,
|
| 172 |
+
dir=None,
|
| 173 |
+
delete=True,
|
| 174 |
+
delete_on_close=True,
|
| 175 |
+
loop=None,
|
| 176 |
+
executor=None,
|
| 177 |
+
max_size=0,
|
| 178 |
+
):
|
| 179 |
+
"""Async method to open a temporary file with async interface"""
|
| 180 |
+
if loop is None:
|
| 181 |
+
loop = asyncio.get_running_loop()
|
| 182 |
+
|
| 183 |
+
if named:
|
| 184 |
+
cb = partial(
|
| 185 |
+
syncNamedTemporaryFile,
|
| 186 |
+
mode=mode,
|
| 187 |
+
buffering=buffering,
|
| 188 |
+
encoding=encoding,
|
| 189 |
+
newline=newline,
|
| 190 |
+
suffix=suffix,
|
| 191 |
+
prefix=prefix,
|
| 192 |
+
dir=dir,
|
| 193 |
+
delete=delete,
|
| 194 |
+
delete_on_close=delete_on_close,
|
| 195 |
+
)
|
| 196 |
+
else:
|
| 197 |
+
cb = partial(
|
| 198 |
+
syncTemporaryFile,
|
| 199 |
+
mode=mode,
|
| 200 |
+
buffering=buffering,
|
| 201 |
+
encoding=encoding,
|
| 202 |
+
newline=newline,
|
| 203 |
+
suffix=suffix,
|
| 204 |
+
prefix=prefix,
|
| 205 |
+
dir=dir,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
f = await loop.run_in_executor(executor, cb)
|
| 209 |
+
|
| 210 |
+
# Wrap based on type of underlying IO object
|
| 211 |
+
if type(f) is syncTemporaryFileWrapper:
|
| 212 |
+
# _TemporaryFileWrapper was used (named files)
|
| 213 |
+
result = wrap(f.file, f, loop=loop, executor=executor)
|
| 214 |
+
result._closer = f._closer
|
| 215 |
+
return result
|
| 216 |
+
# IO object was returned directly without wrapper
|
| 217 |
+
return wrap(f, f, loop=loop, executor=executor)
|
| 218 |
+
|
| 219 |
+
else:
|
| 220 |
+
|
| 221 |
+
async def _temporary_file(
|
| 222 |
+
named=True,
|
| 223 |
+
mode="w+b",
|
| 224 |
+
buffering=-1,
|
| 225 |
+
encoding=None,
|
| 226 |
+
newline=None,
|
| 227 |
+
suffix=None,
|
| 228 |
+
prefix=None,
|
| 229 |
+
dir=None,
|
| 230 |
+
delete=True,
|
| 231 |
+
loop=None,
|
| 232 |
+
executor=None,
|
| 233 |
+
max_size=0,
|
| 234 |
+
):
|
| 235 |
+
"""Async method to open a temporary file with async interface"""
|
| 236 |
+
if loop is None:
|
| 237 |
+
loop = asyncio.get_running_loop()
|
| 238 |
+
|
| 239 |
+
if named:
|
| 240 |
+
cb = partial(
|
| 241 |
+
syncNamedTemporaryFile,
|
| 242 |
+
mode=mode,
|
| 243 |
+
buffering=buffering,
|
| 244 |
+
encoding=encoding,
|
| 245 |
+
newline=newline,
|
| 246 |
+
suffix=suffix,
|
| 247 |
+
prefix=prefix,
|
| 248 |
+
dir=dir,
|
| 249 |
+
delete=delete,
|
| 250 |
+
)
|
| 251 |
+
else:
|
| 252 |
+
cb = partial(
|
| 253 |
+
syncTemporaryFile,
|
| 254 |
+
mode=mode,
|
| 255 |
+
buffering=buffering,
|
| 256 |
+
encoding=encoding,
|
| 257 |
+
newline=newline,
|
| 258 |
+
suffix=suffix,
|
| 259 |
+
prefix=prefix,
|
| 260 |
+
dir=dir,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
f = await loop.run_in_executor(executor, cb)
|
| 264 |
+
|
| 265 |
+
# Wrap based on type of underlying IO object
|
| 266 |
+
if type(f) is syncTemporaryFileWrapper:
|
| 267 |
+
# _TemporaryFileWrapper was used (named files)
|
| 268 |
+
result = wrap(f.file, f, loop=loop, executor=executor)
|
| 269 |
+
# add delete property
|
| 270 |
+
result.delete = f.delete
|
| 271 |
+
return result
|
| 272 |
+
# IO object was returned directly without wrapper
|
| 273 |
+
return wrap(f, f, loop=loop, executor=executor)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
async def _spooled_temporary_file(
|
| 277 |
+
max_size=0,
|
| 278 |
+
mode="w+b",
|
| 279 |
+
buffering=-1,
|
| 280 |
+
encoding=None,
|
| 281 |
+
newline=None,
|
| 282 |
+
suffix=None,
|
| 283 |
+
prefix=None,
|
| 284 |
+
dir=None,
|
| 285 |
+
loop=None,
|
| 286 |
+
executor=None,
|
| 287 |
+
):
|
| 288 |
+
"""Open a spooled temporary file with async interface"""
|
| 289 |
+
if loop is None:
|
| 290 |
+
loop = asyncio.get_running_loop()
|
| 291 |
+
|
| 292 |
+
cb = partial(
|
| 293 |
+
syncSpooledTemporaryFile,
|
| 294 |
+
max_size=max_size,
|
| 295 |
+
mode=mode,
|
| 296 |
+
buffering=buffering,
|
| 297 |
+
encoding=encoding,
|
| 298 |
+
newline=newline,
|
| 299 |
+
suffix=suffix,
|
| 300 |
+
prefix=prefix,
|
| 301 |
+
dir=dir,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
f = await loop.run_in_executor(executor, cb)
|
| 305 |
+
|
| 306 |
+
# Single interface provided by SpooledTemporaryFile for all modes
|
| 307 |
+
return AsyncSpooledTemporaryFile(f, loop=loop, executor=executor)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
async def _temporary_directory(
|
| 311 |
+
suffix=None, prefix=None, dir=None, loop=None, executor=None
|
| 312 |
+
):
|
| 313 |
+
"""Async method to open a temporary directory with async interface"""
|
| 314 |
+
if loop is None:
|
| 315 |
+
loop = asyncio.get_running_loop()
|
| 316 |
+
|
| 317 |
+
cb = partial(syncTemporaryDirectory, suffix, prefix, dir)
|
| 318 |
+
f = await loop.run_in_executor(executor, cb)
|
| 319 |
+
|
| 320 |
+
return AsyncTemporaryDirectory(f, loop=loop, executor=executor)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
class AiofilesContextManagerTempDir(AiofilesContextManager):
|
| 324 |
+
"""With returns the directory location, not the object (matching sync lib)"""
|
| 325 |
+
|
| 326 |
+
async def __aenter__(self):
|
| 327 |
+
self._obj = await self._coro
|
| 328 |
+
return self._obj.name
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@singledispatch
|
| 332 |
+
def wrap(base_io_obj, file, *, loop=None, executor=None):
|
| 333 |
+
"""Wrap the object with interface based on type of underlying IO"""
|
| 334 |
+
|
| 335 |
+
msg = f"Unsupported IO type: {base_io_obj}"
|
| 336 |
+
raise TypeError(msg)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@wrap.register(TextIOBase)
|
| 340 |
+
def _(base_io_obj, file, *, loop=None, executor=None):
|
| 341 |
+
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
@wrap.register(BufferedWriter)
|
| 345 |
+
def _(base_io_obj, file, *, loop=None, executor=None):
|
| 346 |
+
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
@wrap.register(BufferedReader)
|
| 350 |
+
@wrap.register(BufferedRandom)
|
| 351 |
+
def _(base_io_obj, file, *, loop=None, executor=None):
|
| 352 |
+
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@wrap.register(FileIO)
|
| 356 |
+
def _(base_io_obj, file, *, loop=None, executor=None):
|
| 357 |
+
return AsyncFileIO(file, loop=loop, executor=executor)
|
py311/lib/python3.11/site-packages/aiofiles/tempfile/temptypes.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Async wrappers for spooled temp files and temp directory objects"""
|
| 2 |
+
|
| 3 |
+
from functools import partial
|
| 4 |
+
|
| 5 |
+
from ..base import AsyncBase
|
| 6 |
+
from ..threadpool.utils import (
|
| 7 |
+
cond_delegate_to_executor,
|
| 8 |
+
delegate_to_executor,
|
| 9 |
+
proxy_property_directly,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@delegate_to_executor("fileno", "rollover")
|
| 14 |
+
@cond_delegate_to_executor(
|
| 15 |
+
"close",
|
| 16 |
+
"flush",
|
| 17 |
+
"isatty",
|
| 18 |
+
"read",
|
| 19 |
+
"readline",
|
| 20 |
+
"readlines",
|
| 21 |
+
"seek",
|
| 22 |
+
"tell",
|
| 23 |
+
"truncate",
|
| 24 |
+
)
|
| 25 |
+
@proxy_property_directly("closed", "encoding", "mode", "name", "newlines")
|
| 26 |
+
class AsyncSpooledTemporaryFile(AsyncBase):
|
| 27 |
+
"""Async wrapper for SpooledTemporaryFile class"""
|
| 28 |
+
|
| 29 |
+
async def _check(self):
|
| 30 |
+
if self._file._rolled:
|
| 31 |
+
return
|
| 32 |
+
max_size = self._file._max_size
|
| 33 |
+
if max_size and self._file.tell() > max_size:
|
| 34 |
+
await self.rollover()
|
| 35 |
+
|
| 36 |
+
async def write(self, s):
|
| 37 |
+
"""Implementation to anticipate rollover"""
|
| 38 |
+
if self._file._rolled:
|
| 39 |
+
cb = partial(self._file.write, s)
|
| 40 |
+
return await self._loop.run_in_executor(self._executor, cb)
|
| 41 |
+
|
| 42 |
+
file = self._file._file # reference underlying base IO object
|
| 43 |
+
rv = file.write(s)
|
| 44 |
+
await self._check()
|
| 45 |
+
return rv
|
| 46 |
+
|
| 47 |
+
async def writelines(self, iterable):
|
| 48 |
+
"""Implementation to anticipate rollover"""
|
| 49 |
+
if self._file._rolled:
|
| 50 |
+
cb = partial(self._file.writelines, iterable)
|
| 51 |
+
return await self._loop.run_in_executor(self._executor, cb)
|
| 52 |
+
|
| 53 |
+
file = self._file._file # reference underlying base IO object
|
| 54 |
+
rv = file.writelines(iterable)
|
| 55 |
+
await self._check()
|
| 56 |
+
return rv
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@delegate_to_executor("cleanup")
|
| 60 |
+
@proxy_property_directly("name")
|
| 61 |
+
class AsyncTemporaryDirectory:
|
| 62 |
+
"""Async wrapper for TemporaryDirectory class"""
|
| 63 |
+
|
| 64 |
+
def __init__(self, file, loop, executor):
|
| 65 |
+
self._file = file
|
| 66 |
+
self._loop = loop
|
| 67 |
+
self._executor = executor
|
| 68 |
+
|
| 69 |
+
async def close(self):
|
| 70 |
+
await self.cleanup()
|
py311/lib/python3.11/site-packages/aiofiles/threadpool/__init__.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Handle files using a thread pool executor."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import sys
|
| 5 |
+
from functools import partial, singledispatch
|
| 6 |
+
from io import (
|
| 7 |
+
BufferedIOBase,
|
| 8 |
+
BufferedRandom,
|
| 9 |
+
BufferedReader,
|
| 10 |
+
BufferedWriter,
|
| 11 |
+
FileIO,
|
| 12 |
+
TextIOBase,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
from ..base import AiofilesContextManager
|
| 16 |
+
from .binary import (
|
| 17 |
+
AsyncBufferedIOBase,
|
| 18 |
+
AsyncBufferedReader,
|
| 19 |
+
AsyncFileIO,
|
| 20 |
+
AsyncIndirectBufferedIOBase,
|
| 21 |
+
)
|
| 22 |
+
from .text import AsyncTextIndirectIOWrapper, AsyncTextIOWrapper
|
| 23 |
+
|
| 24 |
+
sync_open = open
|
| 25 |
+
|
| 26 |
+
__all__ = (
|
| 27 |
+
"open",
|
| 28 |
+
"stdin",
|
| 29 |
+
"stdout",
|
| 30 |
+
"stderr",
|
| 31 |
+
"stdin_bytes",
|
| 32 |
+
"stdout_bytes",
|
| 33 |
+
"stderr_bytes",
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def open(
|
| 38 |
+
file,
|
| 39 |
+
mode="r",
|
| 40 |
+
buffering=-1,
|
| 41 |
+
encoding=None,
|
| 42 |
+
errors=None,
|
| 43 |
+
newline=None,
|
| 44 |
+
closefd=True,
|
| 45 |
+
opener=None,
|
| 46 |
+
*,
|
| 47 |
+
loop=None,
|
| 48 |
+
executor=None,
|
| 49 |
+
):
|
| 50 |
+
return AiofilesContextManager(
|
| 51 |
+
_open(
|
| 52 |
+
file,
|
| 53 |
+
mode=mode,
|
| 54 |
+
buffering=buffering,
|
| 55 |
+
encoding=encoding,
|
| 56 |
+
errors=errors,
|
| 57 |
+
newline=newline,
|
| 58 |
+
closefd=closefd,
|
| 59 |
+
opener=opener,
|
| 60 |
+
loop=loop,
|
| 61 |
+
executor=executor,
|
| 62 |
+
)
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
async def _open(
|
| 67 |
+
file,
|
| 68 |
+
mode="r",
|
| 69 |
+
buffering=-1,
|
| 70 |
+
encoding=None,
|
| 71 |
+
errors=None,
|
| 72 |
+
newline=None,
|
| 73 |
+
closefd=True,
|
| 74 |
+
opener=None,
|
| 75 |
+
*,
|
| 76 |
+
loop=None,
|
| 77 |
+
executor=None,
|
| 78 |
+
):
|
| 79 |
+
"""Open an asyncio file."""
|
| 80 |
+
if loop is None:
|
| 81 |
+
loop = asyncio.get_running_loop()
|
| 82 |
+
cb = partial(
|
| 83 |
+
sync_open,
|
| 84 |
+
file,
|
| 85 |
+
mode=mode,
|
| 86 |
+
buffering=buffering,
|
| 87 |
+
encoding=encoding,
|
| 88 |
+
errors=errors,
|
| 89 |
+
newline=newline,
|
| 90 |
+
closefd=closefd,
|
| 91 |
+
opener=opener,
|
| 92 |
+
)
|
| 93 |
+
f = await loop.run_in_executor(executor, cb)
|
| 94 |
+
|
| 95 |
+
return wrap(f, loop=loop, executor=executor)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@singledispatch
|
| 99 |
+
def wrap(file, *, loop=None, executor=None):
|
| 100 |
+
msg = f"Unsupported io type: {file}."
|
| 101 |
+
raise TypeError(msg)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@wrap.register(TextIOBase)
|
| 105 |
+
def _(file, *, loop=None, executor=None):
|
| 106 |
+
return AsyncTextIOWrapper(file, loop=loop, executor=executor)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@wrap.register(BufferedWriter)
|
| 110 |
+
@wrap.register(BufferedIOBase)
|
| 111 |
+
def _(file, *, loop=None, executor=None):
|
| 112 |
+
return AsyncBufferedIOBase(file, loop=loop, executor=executor)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@wrap.register(BufferedReader)
|
| 116 |
+
@wrap.register(BufferedRandom)
|
| 117 |
+
def _(file, *, loop=None, executor=None):
|
| 118 |
+
return AsyncBufferedReader(file, loop=loop, executor=executor)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@wrap.register(FileIO)
|
| 122 |
+
def _(file, *, loop=None, executor=None):
|
| 123 |
+
return AsyncFileIO(file, loop=loop, executor=executor)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
stdin = AsyncTextIndirectIOWrapper("sys.stdin", None, None, indirect=lambda: sys.stdin)
|
| 127 |
+
stdout = AsyncTextIndirectIOWrapper(
|
| 128 |
+
"sys.stdout", None, None, indirect=lambda: sys.stdout
|
| 129 |
+
)
|
| 130 |
+
stderr = AsyncTextIndirectIOWrapper(
|
| 131 |
+
"sys.stderr", None, None, indirect=lambda: sys.stderr
|
| 132 |
+
)
|
| 133 |
+
stdin_bytes = AsyncIndirectBufferedIOBase(
|
| 134 |
+
"sys.stdin.buffer", None, None, indirect=lambda: sys.stdin.buffer
|
| 135 |
+
)
|
| 136 |
+
stdout_bytes = AsyncIndirectBufferedIOBase(
|
| 137 |
+
"sys.stdout.buffer", None, None, indirect=lambda: sys.stdout.buffer
|
| 138 |
+
)
|
| 139 |
+
stderr_bytes = AsyncIndirectBufferedIOBase(
|
| 140 |
+
"sys.stderr.buffer", None, None, indirect=lambda: sys.stderr.buffer
|
| 141 |
+
)
|
py311/lib/python3.11/site-packages/aiofiles/threadpool/binary.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..base import AsyncBase, AsyncIndirectBase
|
| 2 |
+
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@delegate_to_executor(
|
| 6 |
+
"close",
|
| 7 |
+
"flush",
|
| 8 |
+
"isatty",
|
| 9 |
+
"read",
|
| 10 |
+
"read1",
|
| 11 |
+
"readinto",
|
| 12 |
+
"readline",
|
| 13 |
+
"readlines",
|
| 14 |
+
"seek",
|
| 15 |
+
"seekable",
|
| 16 |
+
"tell",
|
| 17 |
+
"truncate",
|
| 18 |
+
"writable",
|
| 19 |
+
"write",
|
| 20 |
+
"writelines",
|
| 21 |
+
)
|
| 22 |
+
@proxy_method_directly("detach", "fileno", "readable")
|
| 23 |
+
@proxy_property_directly("closed", "raw", "name", "mode")
|
| 24 |
+
class AsyncBufferedIOBase(AsyncBase):
|
| 25 |
+
"""The asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@delegate_to_executor("peek")
|
| 29 |
+
class AsyncBufferedReader(AsyncBufferedIOBase):
|
| 30 |
+
"""The asyncio executor version of io.BufferedReader and Random."""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@delegate_to_executor(
|
| 34 |
+
"close",
|
| 35 |
+
"flush",
|
| 36 |
+
"isatty",
|
| 37 |
+
"read",
|
| 38 |
+
"readall",
|
| 39 |
+
"readinto",
|
| 40 |
+
"readline",
|
| 41 |
+
"readlines",
|
| 42 |
+
"seek",
|
| 43 |
+
"seekable",
|
| 44 |
+
"tell",
|
| 45 |
+
"truncate",
|
| 46 |
+
"writable",
|
| 47 |
+
"write",
|
| 48 |
+
"writelines",
|
| 49 |
+
)
|
| 50 |
+
@proxy_method_directly("fileno", "readable")
|
| 51 |
+
@proxy_property_directly("closed", "name", "mode")
|
| 52 |
+
class AsyncFileIO(AsyncBase):
|
| 53 |
+
"""The asyncio executor version of io.FileIO."""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@delegate_to_executor(
|
| 57 |
+
"close",
|
| 58 |
+
"flush",
|
| 59 |
+
"isatty",
|
| 60 |
+
"read",
|
| 61 |
+
"read1",
|
| 62 |
+
"readinto",
|
| 63 |
+
"readline",
|
| 64 |
+
"readlines",
|
| 65 |
+
"seek",
|
| 66 |
+
"seekable",
|
| 67 |
+
"tell",
|
| 68 |
+
"truncate",
|
| 69 |
+
"writable",
|
| 70 |
+
"write",
|
| 71 |
+
"writelines",
|
| 72 |
+
)
|
| 73 |
+
@proxy_method_directly("detach", "fileno", "readable")
|
| 74 |
+
@proxy_property_directly("closed", "raw", "name", "mode")
|
| 75 |
+
class AsyncIndirectBufferedIOBase(AsyncIndirectBase):
|
| 76 |
+
"""The indirect asyncio executor version of io.BufferedWriter and BufferedIOBase."""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@delegate_to_executor("peek")
|
| 80 |
+
class AsyncIndirectBufferedReader(AsyncIndirectBufferedIOBase):
|
| 81 |
+
"""The indirect asyncio executor version of io.BufferedReader and Random."""
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@delegate_to_executor(
|
| 85 |
+
"close",
|
| 86 |
+
"flush",
|
| 87 |
+
"isatty",
|
| 88 |
+
"read",
|
| 89 |
+
"readall",
|
| 90 |
+
"readinto",
|
| 91 |
+
"readline",
|
| 92 |
+
"readlines",
|
| 93 |
+
"seek",
|
| 94 |
+
"seekable",
|
| 95 |
+
"tell",
|
| 96 |
+
"truncate",
|
| 97 |
+
"writable",
|
| 98 |
+
"write",
|
| 99 |
+
"writelines",
|
| 100 |
+
)
|
| 101 |
+
@proxy_method_directly("fileno", "readable")
|
| 102 |
+
@proxy_property_directly("closed", "name", "mode")
|
| 103 |
+
class AsyncIndirectFileIO(AsyncIndirectBase):
|
| 104 |
+
"""The indirect asyncio executor version of io.FileIO."""
|
py311/lib/python3.11/site-packages/aiofiles/threadpool/text.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..base import AsyncBase, AsyncIndirectBase
|
| 2 |
+
from .utils import delegate_to_executor, proxy_method_directly, proxy_property_directly
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@delegate_to_executor(
|
| 6 |
+
"close",
|
| 7 |
+
"flush",
|
| 8 |
+
"isatty",
|
| 9 |
+
"read",
|
| 10 |
+
"readable",
|
| 11 |
+
"readline",
|
| 12 |
+
"readlines",
|
| 13 |
+
"seek",
|
| 14 |
+
"seekable",
|
| 15 |
+
"tell",
|
| 16 |
+
"truncate",
|
| 17 |
+
"write",
|
| 18 |
+
"writable",
|
| 19 |
+
"writelines",
|
| 20 |
+
)
|
| 21 |
+
@proxy_method_directly("detach", "fileno", "readable")
|
| 22 |
+
@proxy_property_directly(
|
| 23 |
+
"buffer",
|
| 24 |
+
"closed",
|
| 25 |
+
"encoding",
|
| 26 |
+
"errors",
|
| 27 |
+
"line_buffering",
|
| 28 |
+
"newlines",
|
| 29 |
+
"name",
|
| 30 |
+
"mode",
|
| 31 |
+
)
|
| 32 |
+
class AsyncTextIOWrapper(AsyncBase):
|
| 33 |
+
"""The asyncio executor version of io.TextIOWrapper."""
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@delegate_to_executor(
|
| 37 |
+
"close",
|
| 38 |
+
"flush",
|
| 39 |
+
"isatty",
|
| 40 |
+
"read",
|
| 41 |
+
"readable",
|
| 42 |
+
"readline",
|
| 43 |
+
"readlines",
|
| 44 |
+
"seek",
|
| 45 |
+
"seekable",
|
| 46 |
+
"tell",
|
| 47 |
+
"truncate",
|
| 48 |
+
"write",
|
| 49 |
+
"writable",
|
| 50 |
+
"writelines",
|
| 51 |
+
)
|
| 52 |
+
@proxy_method_directly("detach", "fileno", "readable")
|
| 53 |
+
@proxy_property_directly(
|
| 54 |
+
"buffer",
|
| 55 |
+
"closed",
|
| 56 |
+
"encoding",
|
| 57 |
+
"errors",
|
| 58 |
+
"line_buffering",
|
| 59 |
+
"newlines",
|
| 60 |
+
"name",
|
| 61 |
+
"mode",
|
| 62 |
+
)
|
| 63 |
+
class AsyncTextIndirectIOWrapper(AsyncIndirectBase):
|
| 64 |
+
"""The indirect asyncio executor version of io.TextIOWrapper."""
|
py311/lib/python3.11/site-packages/aiofiles/threadpool/utils.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def delegate_to_executor(*attrs):
|
| 5 |
+
def cls_builder(cls):
|
| 6 |
+
for attr_name in attrs:
|
| 7 |
+
setattr(cls, attr_name, _make_delegate_method(attr_name))
|
| 8 |
+
return cls
|
| 9 |
+
|
| 10 |
+
return cls_builder
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def proxy_method_directly(*attrs):
|
| 14 |
+
def cls_builder(cls):
|
| 15 |
+
for attr_name in attrs:
|
| 16 |
+
setattr(cls, attr_name, _make_proxy_method(attr_name))
|
| 17 |
+
return cls
|
| 18 |
+
|
| 19 |
+
return cls_builder
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def proxy_property_directly(*attrs):
|
| 23 |
+
def cls_builder(cls):
|
| 24 |
+
for attr_name in attrs:
|
| 25 |
+
setattr(cls, attr_name, _make_proxy_property(attr_name))
|
| 26 |
+
return cls
|
| 27 |
+
|
| 28 |
+
return cls_builder
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def cond_delegate_to_executor(*attrs):
|
| 32 |
+
def cls_builder(cls):
|
| 33 |
+
for attr_name in attrs:
|
| 34 |
+
setattr(cls, attr_name, _make_cond_delegate_method(attr_name))
|
| 35 |
+
return cls
|
| 36 |
+
|
| 37 |
+
return cls_builder
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _make_delegate_method(attr_name):
|
| 41 |
+
async def method(self, *args, **kwargs):
|
| 42 |
+
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
| 43 |
+
return await self._loop.run_in_executor(self._executor, cb)
|
| 44 |
+
|
| 45 |
+
return method
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _make_proxy_method(attr_name):
|
| 49 |
+
def method(self, *args, **kwargs):
|
| 50 |
+
return getattr(self._file, attr_name)(*args, **kwargs)
|
| 51 |
+
|
| 52 |
+
return method
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _make_proxy_property(attr_name):
|
| 56 |
+
def proxy_property(self):
|
| 57 |
+
return getattr(self._file, attr_name)
|
| 58 |
+
|
| 59 |
+
return property(proxy_property)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _make_cond_delegate_method(attr_name):
|
| 63 |
+
"""For spooled temp files, delegate only if rolled to file object"""
|
| 64 |
+
|
| 65 |
+
async def method(self, *args, **kwargs):
|
| 66 |
+
if self._file._rolled:
|
| 67 |
+
cb = functools.partial(getattr(self._file, attr_name), *args, **kwargs)
|
| 68 |
+
return await self._loop.run_in_executor(self._executor, cb)
|
| 69 |
+
return getattr(self._file, attr_name)(*args, **kwargs)
|
| 70 |
+
|
| 71 |
+
return method
|
py311/lib/python3.11/site-packages/anyio-4.12.1.dist-info/licenses/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2018 Alex Grönholm
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
| 6 |
+
this software and associated documentation files (the "Software"), to deal in
|
| 7 |
+
the Software without restriction, including without limitation the rights to
|
| 8 |
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
| 9 |
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
| 10 |
+
subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
| 17 |
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
| 18 |
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
| 19 |
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
| 20 |
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
"""The pgen2 package."""
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/conv.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/conv.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
# mypy: ignore-errors
|
| 5 |
+
|
| 6 |
+
"""Convert graminit.[ch] spit out by pgen to Python code.
|
| 7 |
+
|
| 8 |
+
Pgen is the Python parser generator. It is useful to quickly create a
|
| 9 |
+
parser from a grammar file in Python's grammar notation. But I don't
|
| 10 |
+
want my parsers to be written in C (yet), so I'm translating the
|
| 11 |
+
parsing tables to Python data structures and writing a Python parse
|
| 12 |
+
engine.
|
| 13 |
+
|
| 14 |
+
Note that the token numbers are constants determined by the standard
|
| 15 |
+
Python tokenizer. The standard token module defines these numbers and
|
| 16 |
+
their names (the names are not used much). The token numbers are
|
| 17 |
+
hardcoded into the Python tokenizer and into pgen. A Python
|
| 18 |
+
implementation of the Python tokenizer is also available, in the
|
| 19 |
+
standard tokenize module.
|
| 20 |
+
|
| 21 |
+
On the other hand, symbol numbers (representing the grammar's
|
| 22 |
+
non-terminals) are assigned by pgen based on the actual grammar
|
| 23 |
+
input.
|
| 24 |
+
|
| 25 |
+
Note: this module is pretty much obsolete; the pgen module generates
|
| 26 |
+
equivalent grammar tables directly from the Grammar.txt input file
|
| 27 |
+
without having to invoke the Python pgen C program.
|
| 28 |
+
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
# Python imports
|
| 32 |
+
import re
|
| 33 |
+
|
| 34 |
+
# Local imports
|
| 35 |
+
from blib2to3.pgen2 import grammar, token
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Converter(grammar.Grammar):
|
| 39 |
+
"""Grammar subclass that reads classic pgen output files.
|
| 40 |
+
|
| 41 |
+
The run() method reads the tables as produced by the pgen parser
|
| 42 |
+
generator, typically contained in two C files, graminit.h and
|
| 43 |
+
graminit.c. The other methods are for internal use only.
|
| 44 |
+
|
| 45 |
+
See the base class for more documentation.
|
| 46 |
+
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def run(self, graminit_h, graminit_c):
|
| 50 |
+
"""Load the grammar tables from the text files written by pgen."""
|
| 51 |
+
self.parse_graminit_h(graminit_h)
|
| 52 |
+
self.parse_graminit_c(graminit_c)
|
| 53 |
+
self.finish_off()
|
| 54 |
+
|
| 55 |
+
def parse_graminit_h(self, filename):
|
| 56 |
+
"""Parse the .h file written by pgen. (Internal)
|
| 57 |
+
|
| 58 |
+
This file is a sequence of #define statements defining the
|
| 59 |
+
nonterminals of the grammar as numbers. We build two tables
|
| 60 |
+
mapping the numbers to names and back.
|
| 61 |
+
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
f = open(filename)
|
| 65 |
+
except OSError as err:
|
| 66 |
+
print(f"Can't open {filename}: {err}")
|
| 67 |
+
return False
|
| 68 |
+
self.symbol2number = {}
|
| 69 |
+
self.number2symbol = {}
|
| 70 |
+
lineno = 0
|
| 71 |
+
for line in f:
|
| 72 |
+
lineno += 1
|
| 73 |
+
mo = re.match(r"^#define\s+(\w+)\s+(\d+)$", line)
|
| 74 |
+
if not mo and line.strip():
|
| 75 |
+
print(f"{filename}({lineno}): can't parse {line.strip()}")
|
| 76 |
+
else:
|
| 77 |
+
symbol, number = mo.groups()
|
| 78 |
+
number = int(number)
|
| 79 |
+
assert symbol not in self.symbol2number
|
| 80 |
+
assert number not in self.number2symbol
|
| 81 |
+
self.symbol2number[symbol] = number
|
| 82 |
+
self.number2symbol[number] = symbol
|
| 83 |
+
return True
|
| 84 |
+
|
| 85 |
+
def parse_graminit_c(self, filename):
|
| 86 |
+
"""Parse the .c file written by pgen. (Internal)
|
| 87 |
+
|
| 88 |
+
The file looks as follows. The first two lines are always this:
|
| 89 |
+
|
| 90 |
+
#include "pgenheaders.h"
|
| 91 |
+
#include "grammar.h"
|
| 92 |
+
|
| 93 |
+
After that come four blocks:
|
| 94 |
+
|
| 95 |
+
1) one or more state definitions
|
| 96 |
+
2) a table defining dfas
|
| 97 |
+
3) a table defining labels
|
| 98 |
+
4) a struct defining the grammar
|
| 99 |
+
|
| 100 |
+
A state definition has the following form:
|
| 101 |
+
- one or more arc arrays, each of the form:
|
| 102 |
+
static arc arcs_<n>_<m>[<k>] = {
|
| 103 |
+
{<i>, <j>},
|
| 104 |
+
...
|
| 105 |
+
};
|
| 106 |
+
- followed by a state array, of the form:
|
| 107 |
+
static state states_<s>[<t>] = {
|
| 108 |
+
{<k>, arcs_<n>_<m>},
|
| 109 |
+
...
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
"""
|
| 113 |
+
try:
|
| 114 |
+
f = open(filename)
|
| 115 |
+
except OSError as err:
|
| 116 |
+
print(f"Can't open {filename}: {err}")
|
| 117 |
+
return False
|
| 118 |
+
# The code below essentially uses f's iterator-ness!
|
| 119 |
+
lineno = 0
|
| 120 |
+
|
| 121 |
+
# Expect the two #include lines
|
| 122 |
+
lineno, line = lineno + 1, next(f)
|
| 123 |
+
assert line == '#include "pgenheaders.h"\n', (lineno, line)
|
| 124 |
+
lineno, line = lineno + 1, next(f)
|
| 125 |
+
assert line == '#include "grammar.h"\n', (lineno, line)
|
| 126 |
+
|
| 127 |
+
# Parse the state definitions
|
| 128 |
+
lineno, line = lineno + 1, next(f)
|
| 129 |
+
allarcs = {}
|
| 130 |
+
states = []
|
| 131 |
+
while line.startswith("static arc "):
|
| 132 |
+
while line.startswith("static arc "):
|
| 133 |
+
mo = re.match(r"static arc arcs_(\d+)_(\d+)\[(\d+)\] = {$", line)
|
| 134 |
+
assert mo, (lineno, line)
|
| 135 |
+
n, m, k = list(map(int, mo.groups()))
|
| 136 |
+
arcs = []
|
| 137 |
+
for _ in range(k):
|
| 138 |
+
lineno, line = lineno + 1, next(f)
|
| 139 |
+
mo = re.match(r"\s+{(\d+), (\d+)},$", line)
|
| 140 |
+
assert mo, (lineno, line)
|
| 141 |
+
i, j = list(map(int, mo.groups()))
|
| 142 |
+
arcs.append((i, j))
|
| 143 |
+
lineno, line = lineno + 1, next(f)
|
| 144 |
+
assert line == "};\n", (lineno, line)
|
| 145 |
+
allarcs[(n, m)] = arcs
|
| 146 |
+
lineno, line = lineno + 1, next(f)
|
| 147 |
+
mo = re.match(r"static state states_(\d+)\[(\d+)\] = {$", line)
|
| 148 |
+
assert mo, (lineno, line)
|
| 149 |
+
s, t = list(map(int, mo.groups()))
|
| 150 |
+
assert s == len(states), (lineno, line)
|
| 151 |
+
state = []
|
| 152 |
+
for _ in range(t):
|
| 153 |
+
lineno, line = lineno + 1, next(f)
|
| 154 |
+
mo = re.match(r"\s+{(\d+), arcs_(\d+)_(\d+)},$", line)
|
| 155 |
+
assert mo, (lineno, line)
|
| 156 |
+
k, n, m = list(map(int, mo.groups()))
|
| 157 |
+
arcs = allarcs[n, m]
|
| 158 |
+
assert k == len(arcs), (lineno, line)
|
| 159 |
+
state.append(arcs)
|
| 160 |
+
states.append(state)
|
| 161 |
+
lineno, line = lineno + 1, next(f)
|
| 162 |
+
assert line == "};\n", (lineno, line)
|
| 163 |
+
lineno, line = lineno + 1, next(f)
|
| 164 |
+
self.states = states
|
| 165 |
+
|
| 166 |
+
# Parse the dfas
|
| 167 |
+
dfas = {}
|
| 168 |
+
mo = re.match(r"static dfa dfas\[(\d+)\] = {$", line)
|
| 169 |
+
assert mo, (lineno, line)
|
| 170 |
+
ndfas = int(mo.group(1))
|
| 171 |
+
for i in range(ndfas):
|
| 172 |
+
lineno, line = lineno + 1, next(f)
|
| 173 |
+
mo = re.match(r'\s+{(\d+), "(\w+)", (\d+), (\d+), states_(\d+),$', line)
|
| 174 |
+
assert mo, (lineno, line)
|
| 175 |
+
symbol = mo.group(2)
|
| 176 |
+
number, x, y, z = list(map(int, mo.group(1, 3, 4, 5)))
|
| 177 |
+
assert self.symbol2number[symbol] == number, (lineno, line)
|
| 178 |
+
assert self.number2symbol[number] == symbol, (lineno, line)
|
| 179 |
+
assert x == 0, (lineno, line)
|
| 180 |
+
state = states[z]
|
| 181 |
+
assert y == len(state), (lineno, line)
|
| 182 |
+
lineno, line = lineno + 1, next(f)
|
| 183 |
+
mo = re.match(r'\s+("(?:\\\d\d\d)*")},$', line)
|
| 184 |
+
assert mo, (lineno, line)
|
| 185 |
+
first = {}
|
| 186 |
+
rawbitset = eval(mo.group(1))
|
| 187 |
+
for i, c in enumerate(rawbitset):
|
| 188 |
+
byte = ord(c)
|
| 189 |
+
for j in range(8):
|
| 190 |
+
if byte & (1 << j):
|
| 191 |
+
first[i * 8 + j] = 1
|
| 192 |
+
dfas[number] = (state, first)
|
| 193 |
+
lineno, line = lineno + 1, next(f)
|
| 194 |
+
assert line == "};\n", (lineno, line)
|
| 195 |
+
self.dfas = dfas
|
| 196 |
+
|
| 197 |
+
# Parse the labels
|
| 198 |
+
labels = []
|
| 199 |
+
lineno, line = lineno + 1, next(f)
|
| 200 |
+
mo = re.match(r"static label labels\[(\d+)\] = {$", line)
|
| 201 |
+
assert mo, (lineno, line)
|
| 202 |
+
nlabels = int(mo.group(1))
|
| 203 |
+
for i in range(nlabels):
|
| 204 |
+
lineno, line = lineno + 1, next(f)
|
| 205 |
+
mo = re.match(r'\s+{(\d+), (0|"\w+")},$', line)
|
| 206 |
+
assert mo, (lineno, line)
|
| 207 |
+
x, y = mo.groups()
|
| 208 |
+
x = int(x)
|
| 209 |
+
if y == "0":
|
| 210 |
+
y = None
|
| 211 |
+
else:
|
| 212 |
+
y = eval(y)
|
| 213 |
+
labels.append((x, y))
|
| 214 |
+
lineno, line = lineno + 1, next(f)
|
| 215 |
+
assert line == "};\n", (lineno, line)
|
| 216 |
+
self.labels = labels
|
| 217 |
+
|
| 218 |
+
# Parse the grammar struct
|
| 219 |
+
lineno, line = lineno + 1, next(f)
|
| 220 |
+
assert line == "grammar _PyParser_Grammar = {\n", (lineno, line)
|
| 221 |
+
lineno, line = lineno + 1, next(f)
|
| 222 |
+
mo = re.match(r"\s+(\d+),$", line)
|
| 223 |
+
assert mo, (lineno, line)
|
| 224 |
+
ndfas = int(mo.group(1))
|
| 225 |
+
assert ndfas == len(self.dfas)
|
| 226 |
+
lineno, line = lineno + 1, next(f)
|
| 227 |
+
assert line == "\tdfas,\n", (lineno, line)
|
| 228 |
+
lineno, line = lineno + 1, next(f)
|
| 229 |
+
mo = re.match(r"\s+{(\d+), labels},$", line)
|
| 230 |
+
assert mo, (lineno, line)
|
| 231 |
+
nlabels = int(mo.group(1))
|
| 232 |
+
assert nlabels == len(self.labels), (lineno, line)
|
| 233 |
+
lineno, line = lineno + 1, next(f)
|
| 234 |
+
mo = re.match(r"\s+(\d+)$", line)
|
| 235 |
+
assert mo, (lineno, line)
|
| 236 |
+
start = int(mo.group(1))
|
| 237 |
+
assert start in self.number2symbol, (lineno, line)
|
| 238 |
+
self.start = start
|
| 239 |
+
lineno, line = lineno + 1, next(f)
|
| 240 |
+
assert line == "};\n", (lineno, line)
|
| 241 |
+
try:
|
| 242 |
+
lineno, line = lineno + 1, next(f)
|
| 243 |
+
except StopIteration:
|
| 244 |
+
pass
|
| 245 |
+
else:
|
| 246 |
+
assert 0, (lineno, line)
|
| 247 |
+
|
| 248 |
+
def finish_off(self):
|
| 249 |
+
"""Create additional useful structures. (Internal)."""
|
| 250 |
+
self.keywords = {} # map from keyword strings to arc labels
|
| 251 |
+
self.tokens = {} # map from numeric token values to arc labels
|
| 252 |
+
for ilabel, (type, value) in enumerate(self.labels):
|
| 253 |
+
if type == token.NAME and value is not None:
|
| 254 |
+
self.keywords[value] = ilabel
|
| 255 |
+
elif value is None:
|
| 256 |
+
self.tokens[type] = ilabel
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/driver.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/driver.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
# Modifications:
|
| 5 |
+
# Copyright 2006 Google, Inc. All Rights Reserved.
|
| 6 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 7 |
+
|
| 8 |
+
"""Parser driver.
|
| 9 |
+
|
| 10 |
+
This provides a high-level interface to parse a file into a syntax tree.
|
| 11 |
+
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
__author__ = "Guido van Rossum <guido@python.org>"
|
| 15 |
+
|
| 16 |
+
__all__ = ["Driver", "load_grammar"]
|
| 17 |
+
|
| 18 |
+
# Python imports
|
| 19 |
+
import io
|
| 20 |
+
import logging
|
| 21 |
+
import os
|
| 22 |
+
import pkgutil
|
| 23 |
+
import sys
|
| 24 |
+
from collections.abc import Iterable, Iterator
|
| 25 |
+
from contextlib import contextmanager
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
from logging import Logger
|
| 28 |
+
from typing import Any, Union, cast
|
| 29 |
+
|
| 30 |
+
from blib2to3.pgen2.grammar import Grammar
|
| 31 |
+
from blib2to3.pgen2.tokenize import TokenInfo
|
| 32 |
+
from blib2to3.pytree import NL
|
| 33 |
+
|
| 34 |
+
# Pgen imports
|
| 35 |
+
from . import grammar, parse, pgen, token, tokenize
|
| 36 |
+
|
| 37 |
+
Path = Union[str, "os.PathLike[str]"]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass
|
| 41 |
+
class ReleaseRange:
|
| 42 |
+
start: int
|
| 43 |
+
end: int | None = None
|
| 44 |
+
tokens: list[Any] = field(default_factory=list)
|
| 45 |
+
|
| 46 |
+
def lock(self) -> None:
|
| 47 |
+
total_eaten = len(self.tokens)
|
| 48 |
+
self.end = self.start + total_eaten
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class TokenProxy:
|
| 52 |
+
def __init__(self, generator: Any) -> None:
|
| 53 |
+
self._tokens = generator
|
| 54 |
+
self._counter = 0
|
| 55 |
+
self._release_ranges: list[ReleaseRange] = []
|
| 56 |
+
|
| 57 |
+
@contextmanager
|
| 58 |
+
def release(self) -> Iterator["TokenProxy"]:
|
| 59 |
+
release_range = ReleaseRange(self._counter)
|
| 60 |
+
self._release_ranges.append(release_range)
|
| 61 |
+
try:
|
| 62 |
+
yield self
|
| 63 |
+
finally:
|
| 64 |
+
# Lock the last release range to the final position that
|
| 65 |
+
# has been eaten.
|
| 66 |
+
release_range.lock()
|
| 67 |
+
|
| 68 |
+
def eat(self, point: int) -> Any:
|
| 69 |
+
eaten_tokens = self._release_ranges[-1].tokens
|
| 70 |
+
if point < len(eaten_tokens):
|
| 71 |
+
return eaten_tokens[point]
|
| 72 |
+
else:
|
| 73 |
+
while point >= len(eaten_tokens):
|
| 74 |
+
token = next(self._tokens)
|
| 75 |
+
eaten_tokens.append(token)
|
| 76 |
+
return token
|
| 77 |
+
|
| 78 |
+
def __iter__(self) -> "TokenProxy":
|
| 79 |
+
return self
|
| 80 |
+
|
| 81 |
+
def __next__(self) -> Any:
|
| 82 |
+
# If the current position is already compromised (looked up)
|
| 83 |
+
# return the eaten token, if not just go further on the given
|
| 84 |
+
# token producer.
|
| 85 |
+
for release_range in self._release_ranges:
|
| 86 |
+
assert release_range.end is not None
|
| 87 |
+
|
| 88 |
+
start, end = release_range.start, release_range.end
|
| 89 |
+
if start <= self._counter < end:
|
| 90 |
+
token = release_range.tokens[self._counter - start]
|
| 91 |
+
break
|
| 92 |
+
else:
|
| 93 |
+
token = next(self._tokens)
|
| 94 |
+
self._counter += 1
|
| 95 |
+
return token
|
| 96 |
+
|
| 97 |
+
def can_advance(self, to: int) -> bool:
|
| 98 |
+
# Try to eat, fail if it can't. The eat operation is cached
|
| 99 |
+
# so there won't be any additional cost of eating here
|
| 100 |
+
try:
|
| 101 |
+
self.eat(to)
|
| 102 |
+
except StopIteration:
|
| 103 |
+
return False
|
| 104 |
+
else:
|
| 105 |
+
return True
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class Driver:
|
| 109 |
+
def __init__(self, grammar: Grammar, logger: Logger | None = None) -> None:
|
| 110 |
+
self.grammar = grammar
|
| 111 |
+
if logger is None:
|
| 112 |
+
logger = logging.getLogger(__name__)
|
| 113 |
+
self.logger = logger
|
| 114 |
+
|
| 115 |
+
def parse_tokens(self, tokens: Iterable[TokenInfo], debug: bool = False) -> NL:
|
| 116 |
+
"""Parse a series of tokens and return the syntax tree."""
|
| 117 |
+
# XXX Move the prefix computation into a wrapper around tokenize.
|
| 118 |
+
proxy = TokenProxy(tokens)
|
| 119 |
+
|
| 120 |
+
p = parse.Parser(self.grammar)
|
| 121 |
+
p.setup(proxy=proxy)
|
| 122 |
+
|
| 123 |
+
lineno = 1
|
| 124 |
+
column = 0
|
| 125 |
+
indent_columns: list[int] = []
|
| 126 |
+
type = value = start = end = line_text = None
|
| 127 |
+
prefix = ""
|
| 128 |
+
|
| 129 |
+
for quintuple in proxy:
|
| 130 |
+
type, value, start, end, line_text = quintuple
|
| 131 |
+
if start != (lineno, column):
|
| 132 |
+
assert (lineno, column) <= start, ((lineno, column), start)
|
| 133 |
+
s_lineno, s_column = start
|
| 134 |
+
if lineno < s_lineno:
|
| 135 |
+
prefix += "\n" * (s_lineno - lineno)
|
| 136 |
+
lineno = s_lineno
|
| 137 |
+
column = 0
|
| 138 |
+
if column < s_column:
|
| 139 |
+
prefix += line_text[column:s_column]
|
| 140 |
+
column = s_column
|
| 141 |
+
if type in (tokenize.COMMENT, tokenize.NL):
|
| 142 |
+
prefix += value
|
| 143 |
+
lineno, column = end
|
| 144 |
+
if value.endswith("\n"):
|
| 145 |
+
lineno += 1
|
| 146 |
+
column = 0
|
| 147 |
+
continue
|
| 148 |
+
if type == token.OP:
|
| 149 |
+
type = grammar.opmap[value]
|
| 150 |
+
if debug:
|
| 151 |
+
assert type is not None
|
| 152 |
+
self.logger.debug(
|
| 153 |
+
"%s %r (prefix=%r)", token.tok_name[type], value, prefix
|
| 154 |
+
)
|
| 155 |
+
if type == token.INDENT:
|
| 156 |
+
indent_columns.append(len(value))
|
| 157 |
+
_prefix = prefix + value
|
| 158 |
+
prefix = ""
|
| 159 |
+
value = ""
|
| 160 |
+
elif type == token.DEDENT:
|
| 161 |
+
_indent_col = indent_columns.pop()
|
| 162 |
+
prefix, _prefix = self._partially_consume_prefix(prefix, _indent_col)
|
| 163 |
+
if p.addtoken(cast(int, type), value, (prefix, start)):
|
| 164 |
+
if debug:
|
| 165 |
+
self.logger.debug("Stop.")
|
| 166 |
+
break
|
| 167 |
+
prefix = ""
|
| 168 |
+
if type in {token.INDENT, token.DEDENT}:
|
| 169 |
+
prefix = _prefix
|
| 170 |
+
lineno, column = end
|
| 171 |
+
# FSTRING_MIDDLE and TSTRING_MIDDLE are the only token that can end with a
|
| 172 |
+
# newline, and `end` will point to the next line. For that case, don't
|
| 173 |
+
# increment lineno.
|
| 174 |
+
if value.endswith("\n") and type not in (
|
| 175 |
+
token.FSTRING_MIDDLE,
|
| 176 |
+
token.TSTRING_MIDDLE,
|
| 177 |
+
):
|
| 178 |
+
lineno += 1
|
| 179 |
+
column = 0
|
| 180 |
+
else:
|
| 181 |
+
# We never broke out -- EOF is too soon (how can this happen???)
|
| 182 |
+
assert start is not None
|
| 183 |
+
raise parse.ParseError("incomplete input", type, value, (prefix, start))
|
| 184 |
+
assert p.rootnode is not None
|
| 185 |
+
return p.rootnode
|
| 186 |
+
|
| 187 |
+
def parse_file(
|
| 188 |
+
self, filename: Path, encoding: str | None = None, debug: bool = False
|
| 189 |
+
) -> NL:
|
| 190 |
+
"""Parse a file and return the syntax tree."""
|
| 191 |
+
with open(filename, encoding=encoding) as stream:
|
| 192 |
+
text = stream.read()
|
| 193 |
+
return self.parse_string(text, debug)
|
| 194 |
+
|
| 195 |
+
def parse_string(self, text: str, debug: bool = False) -> NL:
|
| 196 |
+
"""Parse a string and return the syntax tree."""
|
| 197 |
+
tokens = tokenize.tokenize(text, grammar=self.grammar)
|
| 198 |
+
return self.parse_tokens(tokens, debug)
|
| 199 |
+
|
| 200 |
+
def _partially_consume_prefix(self, prefix: str, column: int) -> tuple[str, str]:
|
| 201 |
+
lines: list[str] = []
|
| 202 |
+
current_line = ""
|
| 203 |
+
current_column = 0
|
| 204 |
+
wait_for_nl = False
|
| 205 |
+
for char in prefix:
|
| 206 |
+
current_line += char
|
| 207 |
+
if wait_for_nl:
|
| 208 |
+
if char == "\n":
|
| 209 |
+
if current_line.strip() and current_column < column:
|
| 210 |
+
res = "".join(lines)
|
| 211 |
+
return res, prefix[len(res) :]
|
| 212 |
+
|
| 213 |
+
lines.append(current_line)
|
| 214 |
+
current_line = ""
|
| 215 |
+
current_column = 0
|
| 216 |
+
wait_for_nl = False
|
| 217 |
+
elif char in " \t":
|
| 218 |
+
current_column += 1
|
| 219 |
+
elif char == "\n":
|
| 220 |
+
# unexpected empty line
|
| 221 |
+
current_column = 0
|
| 222 |
+
elif char == "\f":
|
| 223 |
+
current_column = 0
|
| 224 |
+
else:
|
| 225 |
+
# indent is finished
|
| 226 |
+
wait_for_nl = True
|
| 227 |
+
return "".join(lines), current_line
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _generate_pickle_name(gt: Path, cache_dir: Path | None = None) -> str:
|
| 231 |
+
head, tail = os.path.splitext(gt)
|
| 232 |
+
if tail == ".txt":
|
| 233 |
+
tail = ""
|
| 234 |
+
name = head + tail + ".".join(map(str, sys.version_info)) + ".pickle"
|
| 235 |
+
if cache_dir:
|
| 236 |
+
return os.path.join(cache_dir, os.path.basename(name))
|
| 237 |
+
else:
|
| 238 |
+
return name
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def load_grammar(
|
| 242 |
+
gt: str = "Grammar.txt",
|
| 243 |
+
gp: str | None = None,
|
| 244 |
+
save: bool = True,
|
| 245 |
+
force: bool = False,
|
| 246 |
+
logger: Logger | None = None,
|
| 247 |
+
) -> Grammar:
|
| 248 |
+
"""Load the grammar (maybe from a pickle)."""
|
| 249 |
+
if logger is None:
|
| 250 |
+
logger = logging.getLogger(__name__)
|
| 251 |
+
gp = _generate_pickle_name(gt) if gp is None else gp
|
| 252 |
+
if force or not _newer(gp, gt):
|
| 253 |
+
g: grammar.Grammar = pgen.generate_grammar(gt)
|
| 254 |
+
if save:
|
| 255 |
+
try:
|
| 256 |
+
g.dump(gp)
|
| 257 |
+
except OSError:
|
| 258 |
+
# Ignore error, caching is not vital.
|
| 259 |
+
pass
|
| 260 |
+
else:
|
| 261 |
+
g = grammar.Grammar()
|
| 262 |
+
g.load(gp)
|
| 263 |
+
return g
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _newer(a: str, b: str) -> bool:
|
| 267 |
+
"""Inquire whether file a was written since file b."""
|
| 268 |
+
if not os.path.exists(a):
|
| 269 |
+
return False
|
| 270 |
+
if not os.path.exists(b):
|
| 271 |
+
return True
|
| 272 |
+
return os.path.getmtime(a) >= os.path.getmtime(b)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def load_packaged_grammar(
|
| 276 |
+
package: str, grammar_source: str, cache_dir: Path | None = None
|
| 277 |
+
) -> grammar.Grammar:
|
| 278 |
+
"""Normally, loads a pickled grammar by doing
|
| 279 |
+
pkgutil.get_data(package, pickled_grammar)
|
| 280 |
+
where *pickled_grammar* is computed from *grammar_source* by adding the
|
| 281 |
+
Python version and using a ``.pickle`` extension.
|
| 282 |
+
|
| 283 |
+
However, if *grammar_source* is an extant file, load_grammar(grammar_source)
|
| 284 |
+
is called instead. This facilitates using a packaged grammar file when needed
|
| 285 |
+
but preserves load_grammar's automatic regeneration behavior when possible.
|
| 286 |
+
|
| 287 |
+
"""
|
| 288 |
+
if os.path.isfile(grammar_source):
|
| 289 |
+
gp = _generate_pickle_name(grammar_source, cache_dir) if cache_dir else None
|
| 290 |
+
return load_grammar(grammar_source, gp=gp)
|
| 291 |
+
pickled_name = _generate_pickle_name(os.path.basename(grammar_source), cache_dir)
|
| 292 |
+
data = pkgutil.get_data(package, pickled_name)
|
| 293 |
+
assert data is not None
|
| 294 |
+
g = grammar.Grammar()
|
| 295 |
+
g.loads(data)
|
| 296 |
+
return g
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def main(*args: str) -> bool:
|
| 300 |
+
"""Main program, when run as a script: produce grammar pickle files.
|
| 301 |
+
|
| 302 |
+
Calls load_grammar for each argument, a path to a grammar text file.
|
| 303 |
+
"""
|
| 304 |
+
if not args:
|
| 305 |
+
args = tuple(sys.argv[1:])
|
| 306 |
+
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(message)s")
|
| 307 |
+
for gt in args:
|
| 308 |
+
load_grammar(gt, save=True, force=True)
|
| 309 |
+
return True
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
if __name__ == "__main__":
|
| 313 |
+
sys.exit(int(not main()))
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/grammar.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/grammar.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
"""This module defines the data structures used to represent a grammar.
|
| 5 |
+
|
| 6 |
+
These are a bit arcane because they are derived from the data
|
| 7 |
+
structures used by Python's 'pgen' parser generator.
|
| 8 |
+
|
| 9 |
+
There's also a table here mapping operators to their names in the
|
| 10 |
+
token module; the Python tokenize module reports all operators as the
|
| 11 |
+
fallback token code OP, but the parser needs the actual token code.
|
| 12 |
+
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
# Python imports
|
| 16 |
+
import os
|
| 17 |
+
import pickle
|
| 18 |
+
import tempfile
|
| 19 |
+
from typing import Any, Optional, TypeVar, Union
|
| 20 |
+
|
| 21 |
+
# Local imports
|
| 22 |
+
from . import token
|
| 23 |
+
|
| 24 |
+
_P = TypeVar("_P", bound="Grammar")
|
| 25 |
+
Label = tuple[int, Optional[str]]
|
| 26 |
+
DFA = list[list[tuple[int, int]]]
|
| 27 |
+
DFAS = tuple[DFA, dict[int, int]]
|
| 28 |
+
Path = Union[str, "os.PathLike[str]"]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Grammar:
|
| 32 |
+
"""Pgen parsing tables conversion class.
|
| 33 |
+
|
| 34 |
+
Once initialized, this class supplies the grammar tables for the
|
| 35 |
+
parsing engine implemented by parse.py. The parsing engine
|
| 36 |
+
accesses the instance variables directly. The class here does not
|
| 37 |
+
provide initialization of the tables; several subclasses exist to
|
| 38 |
+
do this (see the conv and pgen modules).
|
| 39 |
+
|
| 40 |
+
The load() method reads the tables from a pickle file, which is
|
| 41 |
+
much faster than the other ways offered by subclasses. The pickle
|
| 42 |
+
file is written by calling dump() (after loading the grammar
|
| 43 |
+
tables using a subclass). The report() method prints a readable
|
| 44 |
+
representation of the tables to stdout, for debugging.
|
| 45 |
+
|
| 46 |
+
The instance variables are as follows:
|
| 47 |
+
|
| 48 |
+
symbol2number -- a dict mapping symbol names to numbers. Symbol
|
| 49 |
+
numbers are always 256 or higher, to distinguish
|
| 50 |
+
them from token numbers, which are between 0 and
|
| 51 |
+
255 (inclusive).
|
| 52 |
+
|
| 53 |
+
number2symbol -- a dict mapping numbers to symbol names;
|
| 54 |
+
these two are each other's inverse.
|
| 55 |
+
|
| 56 |
+
states -- a list of DFAs, where each DFA is a list of
|
| 57 |
+
states, each state is a list of arcs, and each
|
| 58 |
+
arc is a (i, j) pair where i is a label and j is
|
| 59 |
+
a state number. The DFA number is the index into
|
| 60 |
+
this list. (This name is slightly confusing.)
|
| 61 |
+
Final states are represented by a special arc of
|
| 62 |
+
the form (0, j) where j is its own state number.
|
| 63 |
+
|
| 64 |
+
dfas -- a dict mapping symbol numbers to (DFA, first)
|
| 65 |
+
pairs, where DFA is an item from the states list
|
| 66 |
+
above, and first is a set of tokens that can
|
| 67 |
+
begin this grammar rule (represented by a dict
|
| 68 |
+
whose values are always 1).
|
| 69 |
+
|
| 70 |
+
labels -- a list of (x, y) pairs where x is either a token
|
| 71 |
+
number or a symbol number, and y is either None
|
| 72 |
+
or a string; the strings are keywords. The label
|
| 73 |
+
number is the index in this list; label numbers
|
| 74 |
+
are used to mark state transitions (arcs) in the
|
| 75 |
+
DFAs.
|
| 76 |
+
|
| 77 |
+
start -- the number of the grammar's start symbol.
|
| 78 |
+
|
| 79 |
+
keywords -- a dict mapping keyword strings to arc labels.
|
| 80 |
+
|
| 81 |
+
tokens -- a dict mapping token numbers to arc labels.
|
| 82 |
+
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(self) -> None:
|
| 86 |
+
self.symbol2number: dict[str, int] = {}
|
| 87 |
+
self.number2symbol: dict[int, str] = {}
|
| 88 |
+
self.states: list[DFA] = []
|
| 89 |
+
self.dfas: dict[int, DFAS] = {}
|
| 90 |
+
self.labels: list[Label] = [(0, "EMPTY")]
|
| 91 |
+
self.keywords: dict[str, int] = {}
|
| 92 |
+
self.soft_keywords: dict[str, int] = {}
|
| 93 |
+
self.tokens: dict[int, int] = {}
|
| 94 |
+
self.symbol2label: dict[str, int] = {}
|
| 95 |
+
self.version: tuple[int, int] = (0, 0)
|
| 96 |
+
self.start = 256
|
| 97 |
+
# Python 3.7+ parses async as a keyword, not an identifier
|
| 98 |
+
self.async_keywords = False
|
| 99 |
+
|
| 100 |
+
def dump(self, filename: Path) -> None:
|
| 101 |
+
"""Dump the grammar tables to a pickle file."""
|
| 102 |
+
|
| 103 |
+
# mypyc generates objects that don't have a __dict__, but they
|
| 104 |
+
# do have __getstate__ methods that will return an equivalent
|
| 105 |
+
# dictionary
|
| 106 |
+
if hasattr(self, "__dict__"):
|
| 107 |
+
d = self.__dict__
|
| 108 |
+
else:
|
| 109 |
+
d = self.__getstate__() # type: ignore
|
| 110 |
+
|
| 111 |
+
with tempfile.NamedTemporaryFile(
|
| 112 |
+
dir=os.path.dirname(filename), delete=False
|
| 113 |
+
) as f:
|
| 114 |
+
pickle.dump(d, f, pickle.HIGHEST_PROTOCOL)
|
| 115 |
+
os.replace(f.name, filename)
|
| 116 |
+
|
| 117 |
+
def _update(self, attrs: dict[str, Any]) -> None:
|
| 118 |
+
for k, v in attrs.items():
|
| 119 |
+
setattr(self, k, v)
|
| 120 |
+
|
| 121 |
+
def load(self, filename: Path) -> None:
|
| 122 |
+
"""Load the grammar tables from a pickle file."""
|
| 123 |
+
with open(filename, "rb") as f:
|
| 124 |
+
d = pickle.load(f)
|
| 125 |
+
self._update(d)
|
| 126 |
+
|
| 127 |
+
def loads(self, pkl: bytes) -> None:
|
| 128 |
+
"""Load the grammar tables from a pickle bytes object."""
|
| 129 |
+
self._update(pickle.loads(pkl))
|
| 130 |
+
|
| 131 |
+
def copy(self: _P) -> _P:
|
| 132 |
+
"""
|
| 133 |
+
Copy the grammar.
|
| 134 |
+
"""
|
| 135 |
+
new = self.__class__()
|
| 136 |
+
for dict_attr in (
|
| 137 |
+
"symbol2number",
|
| 138 |
+
"number2symbol",
|
| 139 |
+
"dfas",
|
| 140 |
+
"keywords",
|
| 141 |
+
"soft_keywords",
|
| 142 |
+
"tokens",
|
| 143 |
+
"symbol2label",
|
| 144 |
+
):
|
| 145 |
+
setattr(new, dict_attr, getattr(self, dict_attr).copy())
|
| 146 |
+
new.labels = self.labels[:]
|
| 147 |
+
new.states = self.states[:]
|
| 148 |
+
new.start = self.start
|
| 149 |
+
new.version = self.version
|
| 150 |
+
new.async_keywords = self.async_keywords
|
| 151 |
+
return new
|
| 152 |
+
|
| 153 |
+
def report(self) -> None:
|
| 154 |
+
"""Dump the grammar tables to standard output, for debugging."""
|
| 155 |
+
from pprint import pprint
|
| 156 |
+
|
| 157 |
+
print("s2n")
|
| 158 |
+
pprint(self.symbol2number)
|
| 159 |
+
print("n2s")
|
| 160 |
+
pprint(self.number2symbol)
|
| 161 |
+
print("states")
|
| 162 |
+
pprint(self.states)
|
| 163 |
+
print("dfas")
|
| 164 |
+
pprint(self.dfas)
|
| 165 |
+
print("labels")
|
| 166 |
+
pprint(self.labels)
|
| 167 |
+
print("start", self.start)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# Map from operator to number (since tokenize doesn't do this)
|
| 171 |
+
|
| 172 |
+
opmap_raw = """
|
| 173 |
+
( LPAR
|
| 174 |
+
) RPAR
|
| 175 |
+
[ LSQB
|
| 176 |
+
] RSQB
|
| 177 |
+
: COLON
|
| 178 |
+
, COMMA
|
| 179 |
+
; SEMI
|
| 180 |
+
+ PLUS
|
| 181 |
+
- MINUS
|
| 182 |
+
* STAR
|
| 183 |
+
/ SLASH
|
| 184 |
+
| VBAR
|
| 185 |
+
& AMPER
|
| 186 |
+
< LESS
|
| 187 |
+
> GREATER
|
| 188 |
+
= EQUAL
|
| 189 |
+
. DOT
|
| 190 |
+
% PERCENT
|
| 191 |
+
` BACKQUOTE
|
| 192 |
+
{ LBRACE
|
| 193 |
+
} RBRACE
|
| 194 |
+
@ AT
|
| 195 |
+
@= ATEQUAL
|
| 196 |
+
== EQEQUAL
|
| 197 |
+
!= NOTEQUAL
|
| 198 |
+
<> NOTEQUAL
|
| 199 |
+
<= LESSEQUAL
|
| 200 |
+
>= GREATEREQUAL
|
| 201 |
+
~ TILDE
|
| 202 |
+
^ CIRCUMFLEX
|
| 203 |
+
<< LEFTSHIFT
|
| 204 |
+
>> RIGHTSHIFT
|
| 205 |
+
** DOUBLESTAR
|
| 206 |
+
+= PLUSEQUAL
|
| 207 |
+
-= MINEQUAL
|
| 208 |
+
*= STAREQUAL
|
| 209 |
+
/= SLASHEQUAL
|
| 210 |
+
%= PERCENTEQUAL
|
| 211 |
+
&= AMPEREQUAL
|
| 212 |
+
|= VBAREQUAL
|
| 213 |
+
^= CIRCUMFLEXEQUAL
|
| 214 |
+
<<= LEFTSHIFTEQUAL
|
| 215 |
+
>>= RIGHTSHIFTEQUAL
|
| 216 |
+
**= DOUBLESTAREQUAL
|
| 217 |
+
// DOUBLESLASH
|
| 218 |
+
//= DOUBLESLASHEQUAL
|
| 219 |
+
-> RARROW
|
| 220 |
+
:= COLONEQUAL
|
| 221 |
+
! BANG
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
opmap = {}
|
| 225 |
+
for line in opmap_raw.splitlines():
|
| 226 |
+
if line:
|
| 227 |
+
op, name = line.split()
|
| 228 |
+
opmap[op] = getattr(token, name)
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/literals.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/literals.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
"""Safely evaluate Python string literals without using eval()."""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
simple_escapes: dict[str, str] = {
|
| 9 |
+
"a": "\a",
|
| 10 |
+
"b": "\b",
|
| 11 |
+
"f": "\f",
|
| 12 |
+
"n": "\n",
|
| 13 |
+
"r": "\r",
|
| 14 |
+
"t": "\t",
|
| 15 |
+
"v": "\v",
|
| 16 |
+
"'": "'",
|
| 17 |
+
'"': '"',
|
| 18 |
+
"\\": "\\",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def escape(m: re.Match[str]) -> str:
|
| 23 |
+
all, tail = m.group(0, 1)
|
| 24 |
+
assert all.startswith("\\")
|
| 25 |
+
esc = simple_escapes.get(tail)
|
| 26 |
+
if esc is not None:
|
| 27 |
+
return esc
|
| 28 |
+
if tail.startswith("x"):
|
| 29 |
+
hexes = tail[1:]
|
| 30 |
+
if len(hexes) < 2:
|
| 31 |
+
raise ValueError(f"invalid hex string escape ('\\{tail}')")
|
| 32 |
+
try:
|
| 33 |
+
i = int(hexes, 16)
|
| 34 |
+
except ValueError:
|
| 35 |
+
raise ValueError(f"invalid hex string escape ('\\{tail}')") from None
|
| 36 |
+
else:
|
| 37 |
+
try:
|
| 38 |
+
i = int(tail, 8)
|
| 39 |
+
except ValueError:
|
| 40 |
+
raise ValueError(f"invalid octal string escape ('\\{tail}')") from None
|
| 41 |
+
return chr(i)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def evalString(s: str) -> str:
|
| 45 |
+
assert s.startswith("'") or s.startswith('"'), repr(s[:1])
|
| 46 |
+
q = s[0]
|
| 47 |
+
if s[:3] == q * 3:
|
| 48 |
+
q = q * 3
|
| 49 |
+
assert s.endswith(q), repr(s[-len(q) :])
|
| 50 |
+
assert len(s) >= 2 * len(q)
|
| 51 |
+
s = s[len(q) : -len(q)]
|
| 52 |
+
return re.sub(r"\\(\'|\"|\\|[abfnrtv]|x.{0,2}|[0-7]{1,3})", escape, s)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test() -> None:
|
| 56 |
+
for i in range(256):
|
| 57 |
+
c = chr(i)
|
| 58 |
+
s = repr(c)
|
| 59 |
+
e = evalString(s)
|
| 60 |
+
if e != c:
|
| 61 |
+
print(i, c, s, e)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
test()
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/parse.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/parse.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
"""Parser engine for the grammar tables generated by pgen.
|
| 5 |
+
|
| 6 |
+
The grammar table must be loaded first.
|
| 7 |
+
|
| 8 |
+
See Parser/parser.c in the Python distribution for additional info on
|
| 9 |
+
how this parsing engine works.
|
| 10 |
+
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from collections.abc import Callable, Iterator
|
| 14 |
+
from contextlib import contextmanager
|
| 15 |
+
from typing import TYPE_CHECKING, Union, cast
|
| 16 |
+
|
| 17 |
+
from blib2to3.pgen2.grammar import Grammar
|
| 18 |
+
from blib2to3.pytree import NL, Context, Leaf, Node, RawNode, convert
|
| 19 |
+
|
| 20 |
+
# Local imports
|
| 21 |
+
from . import grammar, token, tokenize
|
| 22 |
+
|
| 23 |
+
if TYPE_CHECKING:
|
| 24 |
+
from blib2to3.pgen2.driver import TokenProxy
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
Results = dict[str, NL]
|
| 28 |
+
Convert = Callable[[Grammar, RawNode], Union[Node, Leaf]]
|
| 29 |
+
DFA = list[list[tuple[int, int]]]
|
| 30 |
+
DFAS = tuple[DFA, dict[int, int]]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def lam_sub(grammar: Grammar, node: RawNode) -> NL:
|
| 34 |
+
assert node[3] is not None
|
| 35 |
+
return Node(type=node[0], children=node[3], context=node[2])
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# A placeholder node, used when parser is backtracking.
|
| 39 |
+
DUMMY_NODE = (-1, None, None, None)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def stack_copy(
|
| 43 |
+
stack: list[tuple[DFAS, int, RawNode]],
|
| 44 |
+
) -> list[tuple[DFAS, int, RawNode]]:
|
| 45 |
+
"""Nodeless stack copy."""
|
| 46 |
+
return [(dfa, label, DUMMY_NODE) for dfa, label, _ in stack]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class Recorder:
|
| 50 |
+
def __init__(self, parser: "Parser", ilabels: list[int], context: Context) -> None:
|
| 51 |
+
self.parser = parser
|
| 52 |
+
self._ilabels = ilabels
|
| 53 |
+
self.context = context # not really matter
|
| 54 |
+
|
| 55 |
+
self._dead_ilabels: set[int] = set()
|
| 56 |
+
self._start_point = self.parser.stack
|
| 57 |
+
self._points = {ilabel: stack_copy(self._start_point) for ilabel in ilabels}
|
| 58 |
+
|
| 59 |
+
@property
|
| 60 |
+
def ilabels(self) -> set[int]:
|
| 61 |
+
return self._dead_ilabels.symmetric_difference(self._ilabels)
|
| 62 |
+
|
| 63 |
+
@contextmanager
|
| 64 |
+
def switch_to(self, ilabel: int) -> Iterator[None]:
|
| 65 |
+
with self.backtrack():
|
| 66 |
+
self.parser.stack = self._points[ilabel]
|
| 67 |
+
try:
|
| 68 |
+
yield
|
| 69 |
+
except ParseError:
|
| 70 |
+
self._dead_ilabels.add(ilabel)
|
| 71 |
+
finally:
|
| 72 |
+
self.parser.stack = self._start_point
|
| 73 |
+
|
| 74 |
+
@contextmanager
|
| 75 |
+
def backtrack(self) -> Iterator[None]:
|
| 76 |
+
"""
|
| 77 |
+
Use the node-level invariant ones for basic parsing operations (push/pop/shift).
|
| 78 |
+
These still will operate on the stack; but they won't create any new nodes, or
|
| 79 |
+
modify the contents of any other existing nodes.
|
| 80 |
+
|
| 81 |
+
This saves us a ton of time when we are backtracking, since we
|
| 82 |
+
want to restore to the initial state as quick as possible, which
|
| 83 |
+
can only be done by having as little mutatations as possible.
|
| 84 |
+
"""
|
| 85 |
+
is_backtracking = self.parser.is_backtracking
|
| 86 |
+
try:
|
| 87 |
+
self.parser.is_backtracking = True
|
| 88 |
+
yield
|
| 89 |
+
finally:
|
| 90 |
+
self.parser.is_backtracking = is_backtracking
|
| 91 |
+
|
| 92 |
+
def add_token(self, tok_type: int, tok_val: str, raw: bool = False) -> None:
|
| 93 |
+
for ilabel in self.ilabels:
|
| 94 |
+
with self.switch_to(ilabel):
|
| 95 |
+
if raw:
|
| 96 |
+
self.parser._addtoken(ilabel, tok_type, tok_val, self.context)
|
| 97 |
+
else:
|
| 98 |
+
self.parser.addtoken(tok_type, tok_val, self.context)
|
| 99 |
+
|
| 100 |
+
def determine_route(
|
| 101 |
+
self, value: str | None = None, force: bool = False
|
| 102 |
+
) -> int | None:
|
| 103 |
+
alive_ilabels = self.ilabels
|
| 104 |
+
if len(alive_ilabels) == 0:
|
| 105 |
+
*_, most_successful_ilabel = self._dead_ilabels
|
| 106 |
+
raise ParseError("bad input", most_successful_ilabel, value, self.context)
|
| 107 |
+
|
| 108 |
+
ilabel, *rest = alive_ilabels
|
| 109 |
+
if force or not rest:
|
| 110 |
+
return ilabel
|
| 111 |
+
else:
|
| 112 |
+
return None
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class ParseError(Exception):
|
| 116 |
+
"""Exception to signal the parser is stuck."""
|
| 117 |
+
|
| 118 |
+
def __init__(
|
| 119 |
+
self, msg: str, type: int | None, value: str | None, context: Context
|
| 120 |
+
) -> None:
|
| 121 |
+
Exception.__init__(
|
| 122 |
+
self, f"{msg}: type={type!r}, value={value!r}, context={context!r}"
|
| 123 |
+
)
|
| 124 |
+
self.msg = msg
|
| 125 |
+
self.type = type
|
| 126 |
+
self.value = value
|
| 127 |
+
self.context = context
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class Parser:
|
| 131 |
+
"""Parser engine.
|
| 132 |
+
|
| 133 |
+
The proper usage sequence is:
|
| 134 |
+
|
| 135 |
+
p = Parser(grammar, [converter]) # create instance
|
| 136 |
+
p.setup([start]) # prepare for parsing
|
| 137 |
+
<for each input token>:
|
| 138 |
+
if p.addtoken(...): # parse a token; may raise ParseError
|
| 139 |
+
break
|
| 140 |
+
root = p.rootnode # root of abstract syntax tree
|
| 141 |
+
|
| 142 |
+
A Parser instance may be reused by calling setup() repeatedly.
|
| 143 |
+
|
| 144 |
+
A Parser instance contains state pertaining to the current token
|
| 145 |
+
sequence, and should not be used concurrently by different threads
|
| 146 |
+
to parse separate token sequences.
|
| 147 |
+
|
| 148 |
+
See driver.py for how to get input tokens by tokenizing a file or
|
| 149 |
+
string.
|
| 150 |
+
|
| 151 |
+
Parsing is complete when addtoken() returns True; the root of the
|
| 152 |
+
abstract syntax tree can then be retrieved from the rootnode
|
| 153 |
+
instance variable. When a syntax error occurs, addtoken() raises
|
| 154 |
+
the ParseError exception. There is no error recovery; the parser
|
| 155 |
+
cannot be used after a syntax error was reported (but it can be
|
| 156 |
+
reinitialized by calling setup()).
|
| 157 |
+
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
def __init__(self, grammar: Grammar, convert: Convert | None = None) -> None:
|
| 161 |
+
"""Constructor.
|
| 162 |
+
|
| 163 |
+
The grammar argument is a grammar.Grammar instance; see the
|
| 164 |
+
grammar module for more information.
|
| 165 |
+
|
| 166 |
+
The parser is not ready yet for parsing; you must call the
|
| 167 |
+
setup() method to get it started.
|
| 168 |
+
|
| 169 |
+
The optional convert argument is a function mapping concrete
|
| 170 |
+
syntax tree nodes to abstract syntax tree nodes. If not
|
| 171 |
+
given, no conversion is done and the syntax tree produced is
|
| 172 |
+
the concrete syntax tree. If given, it must be a function of
|
| 173 |
+
two arguments, the first being the grammar (a grammar.Grammar
|
| 174 |
+
instance), and the second being the concrete syntax tree node
|
| 175 |
+
to be converted. The syntax tree is converted from the bottom
|
| 176 |
+
up.
|
| 177 |
+
|
| 178 |
+
**post-note: the convert argument is ignored since for Black's
|
| 179 |
+
usage, convert will always be blib2to3.pytree.convert. Allowing
|
| 180 |
+
this to be dynamic hurts mypyc's ability to use early binding.
|
| 181 |
+
These docs are left for historical and informational value.
|
| 182 |
+
|
| 183 |
+
A concrete syntax tree node is a (type, value, context, nodes)
|
| 184 |
+
tuple, where type is the node type (a token or symbol number),
|
| 185 |
+
value is None for symbols and a string for tokens, context is
|
| 186 |
+
None or an opaque value used for error reporting (typically a
|
| 187 |
+
(lineno, offset) pair), and nodes is a list of children for
|
| 188 |
+
symbols, and None for tokens.
|
| 189 |
+
|
| 190 |
+
An abstract syntax tree node may be anything; this is entirely
|
| 191 |
+
up to the converter function.
|
| 192 |
+
|
| 193 |
+
"""
|
| 194 |
+
self.grammar = grammar
|
| 195 |
+
# See note in docstring above. TL;DR this is ignored.
|
| 196 |
+
self.convert = convert or lam_sub
|
| 197 |
+
self.is_backtracking = False
|
| 198 |
+
self.last_token: int | None = None
|
| 199 |
+
|
| 200 |
+
def setup(self, proxy: "TokenProxy", start: int | None = None) -> None:
|
| 201 |
+
"""Prepare for parsing.
|
| 202 |
+
|
| 203 |
+
This *must* be called before starting to parse.
|
| 204 |
+
|
| 205 |
+
The optional argument is an alternative start symbol; it
|
| 206 |
+
defaults to the grammar's start symbol.
|
| 207 |
+
|
| 208 |
+
You can use a Parser instance to parse any number of programs;
|
| 209 |
+
each time you call setup() the parser is reset to an initial
|
| 210 |
+
state determined by the (implicit or explicit) start symbol.
|
| 211 |
+
|
| 212 |
+
"""
|
| 213 |
+
if start is None:
|
| 214 |
+
start = self.grammar.start
|
| 215 |
+
# Each stack entry is a tuple: (dfa, state, node).
|
| 216 |
+
# A node is a tuple: (type, value, context, children),
|
| 217 |
+
# where children is a list of nodes or None, and context may be None.
|
| 218 |
+
newnode: RawNode = (start, None, None, [])
|
| 219 |
+
stackentry = (self.grammar.dfas[start], 0, newnode)
|
| 220 |
+
self.stack: list[tuple[DFAS, int, RawNode]] = [stackentry]
|
| 221 |
+
self.rootnode: NL | None = None
|
| 222 |
+
self.used_names: set[str] = set()
|
| 223 |
+
self.proxy = proxy
|
| 224 |
+
self.last_token = None
|
| 225 |
+
|
| 226 |
+
def addtoken(self, type: int, value: str, context: Context) -> bool:
|
| 227 |
+
"""Add a token; return True iff this is the end of the program."""
|
| 228 |
+
# Map from token to label
|
| 229 |
+
ilabels = self.classify(type, value, context)
|
| 230 |
+
assert len(ilabels) >= 1
|
| 231 |
+
|
| 232 |
+
# If we have only one state to advance, we'll directly
|
| 233 |
+
# take it as is.
|
| 234 |
+
if len(ilabels) == 1:
|
| 235 |
+
[ilabel] = ilabels
|
| 236 |
+
return self._addtoken(ilabel, type, value, context)
|
| 237 |
+
|
| 238 |
+
# If there are multiple states which we can advance (only
|
| 239 |
+
# happen under soft-keywords), then we will try all of them
|
| 240 |
+
# in parallel and as soon as one state can reach further than
|
| 241 |
+
# the rest, we'll choose that one. This is a pretty hacky
|
| 242 |
+
# and hopefully temporary algorithm.
|
| 243 |
+
#
|
| 244 |
+
# For a more detailed explanation, check out this post:
|
| 245 |
+
# https://tree.science/what-the-backtracking.html
|
| 246 |
+
|
| 247 |
+
with self.proxy.release() as proxy:
|
| 248 |
+
counter, force = 0, False
|
| 249 |
+
recorder = Recorder(self, ilabels, context)
|
| 250 |
+
recorder.add_token(type, value, raw=True)
|
| 251 |
+
|
| 252 |
+
next_token_value = value
|
| 253 |
+
while recorder.determine_route(next_token_value) is None:
|
| 254 |
+
if not proxy.can_advance(counter):
|
| 255 |
+
force = True
|
| 256 |
+
break
|
| 257 |
+
|
| 258 |
+
next_token_type, next_token_value, *_ = proxy.eat(counter)
|
| 259 |
+
if next_token_type in (tokenize.COMMENT, tokenize.NL):
|
| 260 |
+
counter += 1
|
| 261 |
+
continue
|
| 262 |
+
|
| 263 |
+
if next_token_type == tokenize.OP:
|
| 264 |
+
next_token_type = grammar.opmap[next_token_value]
|
| 265 |
+
|
| 266 |
+
recorder.add_token(next_token_type, next_token_value)
|
| 267 |
+
counter += 1
|
| 268 |
+
|
| 269 |
+
ilabel = cast(int, recorder.determine_route(next_token_value, force=force))
|
| 270 |
+
assert ilabel is not None
|
| 271 |
+
|
| 272 |
+
return self._addtoken(ilabel, type, value, context)
|
| 273 |
+
|
| 274 |
+
def _addtoken(self, ilabel: int, type: int, value: str, context: Context) -> bool:
|
| 275 |
+
# Loop until the token is shifted; may raise exceptions
|
| 276 |
+
while True:
|
| 277 |
+
dfa, state, node = self.stack[-1]
|
| 278 |
+
states, first = dfa
|
| 279 |
+
arcs = states[state]
|
| 280 |
+
# Look for a state with this label
|
| 281 |
+
for i, newstate in arcs:
|
| 282 |
+
t = self.grammar.labels[i][0]
|
| 283 |
+
if t >= 256:
|
| 284 |
+
# See if it's a symbol and if we're in its first set
|
| 285 |
+
itsdfa = self.grammar.dfas[t]
|
| 286 |
+
itsstates, itsfirst = itsdfa
|
| 287 |
+
if ilabel in itsfirst:
|
| 288 |
+
# Push a symbol
|
| 289 |
+
self.push(t, itsdfa, newstate, context)
|
| 290 |
+
break # To continue the outer while loop
|
| 291 |
+
|
| 292 |
+
elif ilabel == i:
|
| 293 |
+
# Look it up in the list of labels
|
| 294 |
+
# Shift a token; we're done with it
|
| 295 |
+
self.shift(type, value, newstate, context)
|
| 296 |
+
# Pop while we are in an accept-only state
|
| 297 |
+
state = newstate
|
| 298 |
+
while states[state] == [(0, state)]:
|
| 299 |
+
self.pop()
|
| 300 |
+
if not self.stack:
|
| 301 |
+
# Done parsing!
|
| 302 |
+
return True
|
| 303 |
+
dfa, state, node = self.stack[-1]
|
| 304 |
+
states, first = dfa
|
| 305 |
+
# Done with this token
|
| 306 |
+
self.last_token = type
|
| 307 |
+
return False
|
| 308 |
+
|
| 309 |
+
else:
|
| 310 |
+
if (0, state) in arcs:
|
| 311 |
+
# An accepting state, pop it and try something else
|
| 312 |
+
self.pop()
|
| 313 |
+
if not self.stack:
|
| 314 |
+
# Done parsing, but another token is input
|
| 315 |
+
raise ParseError("too much input", type, value, context)
|
| 316 |
+
else:
|
| 317 |
+
# No success finding a transition
|
| 318 |
+
raise ParseError("bad input", type, value, context)
|
| 319 |
+
|
| 320 |
+
def classify(self, type: int, value: str, context: Context) -> list[int]:
|
| 321 |
+
"""Turn a token into a label. (Internal)
|
| 322 |
+
|
| 323 |
+
Depending on whether the value is a soft-keyword or not,
|
| 324 |
+
this function may return multiple labels to choose from."""
|
| 325 |
+
if type == token.NAME:
|
| 326 |
+
# Keep a listing of all used names
|
| 327 |
+
self.used_names.add(value)
|
| 328 |
+
# Check for reserved words
|
| 329 |
+
if value in self.grammar.keywords:
|
| 330 |
+
return [self.grammar.keywords[value]]
|
| 331 |
+
elif value in self.grammar.soft_keywords:
|
| 332 |
+
assert type in self.grammar.tokens
|
| 333 |
+
# Current soft keywords (match, case, type) can only appear at the
|
| 334 |
+
# beginning of a statement. So as a shortcut, don't try to treat them
|
| 335 |
+
# like keywords in any other context.
|
| 336 |
+
# ('_' is also a soft keyword in the real grammar, but for our grammar
|
| 337 |
+
# it's just an expression, so we don't need to treat it specially.)
|
| 338 |
+
if self.last_token not in (
|
| 339 |
+
None,
|
| 340 |
+
token.INDENT,
|
| 341 |
+
token.DEDENT,
|
| 342 |
+
token.NEWLINE,
|
| 343 |
+
token.SEMI,
|
| 344 |
+
token.COLON,
|
| 345 |
+
):
|
| 346 |
+
return [self.grammar.tokens[type]]
|
| 347 |
+
return [
|
| 348 |
+
self.grammar.tokens[type],
|
| 349 |
+
self.grammar.soft_keywords[value],
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
ilabel = self.grammar.tokens.get(type)
|
| 353 |
+
if ilabel is None:
|
| 354 |
+
raise ParseError("bad token", type, value, context)
|
| 355 |
+
return [ilabel]
|
| 356 |
+
|
| 357 |
+
def shift(self, type: int, value: str, newstate: int, context: Context) -> None:
|
| 358 |
+
"""Shift a token. (Internal)"""
|
| 359 |
+
if self.is_backtracking:
|
| 360 |
+
dfa, state, _ = self.stack[-1]
|
| 361 |
+
self.stack[-1] = (dfa, newstate, DUMMY_NODE)
|
| 362 |
+
else:
|
| 363 |
+
dfa, state, node = self.stack[-1]
|
| 364 |
+
rawnode: RawNode = (type, value, context, None)
|
| 365 |
+
newnode = convert(self.grammar, rawnode)
|
| 366 |
+
assert node[-1] is not None
|
| 367 |
+
node[-1].append(newnode)
|
| 368 |
+
self.stack[-1] = (dfa, newstate, node)
|
| 369 |
+
|
| 370 |
+
def push(self, type: int, newdfa: DFAS, newstate: int, context: Context) -> None:
|
| 371 |
+
"""Push a nonterminal. (Internal)"""
|
| 372 |
+
if self.is_backtracking:
|
| 373 |
+
dfa, state, _ = self.stack[-1]
|
| 374 |
+
self.stack[-1] = (dfa, newstate, DUMMY_NODE)
|
| 375 |
+
self.stack.append((newdfa, 0, DUMMY_NODE))
|
| 376 |
+
else:
|
| 377 |
+
dfa, state, node = self.stack[-1]
|
| 378 |
+
newnode: RawNode = (type, None, context, [])
|
| 379 |
+
self.stack[-1] = (dfa, newstate, node)
|
| 380 |
+
self.stack.append((newdfa, 0, newnode))
|
| 381 |
+
|
| 382 |
+
def pop(self) -> None:
|
| 383 |
+
"""Pop a nonterminal. (Internal)"""
|
| 384 |
+
if self.is_backtracking:
|
| 385 |
+
self.stack.pop()
|
| 386 |
+
else:
|
| 387 |
+
popdfa, popstate, popnode = self.stack.pop()
|
| 388 |
+
newnode = convert(self.grammar, popnode)
|
| 389 |
+
if self.stack:
|
| 390 |
+
dfa, state, node = self.stack[-1]
|
| 391 |
+
assert node[-1] is not None
|
| 392 |
+
node[-1].append(newnode)
|
| 393 |
+
else:
|
| 394 |
+
self.rootnode = newnode
|
| 395 |
+
self.rootnode.used_names = self.used_names
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/pgen.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/pgen.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2004-2005 Elemental Security, Inc. All Rights Reserved.
|
| 2 |
+
# Licensed to PSF under a Contributor Agreement.
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from collections.abc import Iterator, Sequence
|
| 6 |
+
from typing import IO, Any, NoReturn, Union
|
| 7 |
+
|
| 8 |
+
from blib2to3.pgen2 import grammar, token, tokenize
|
| 9 |
+
from blib2to3.pgen2.tokenize import TokenInfo
|
| 10 |
+
|
| 11 |
+
Path = Union[str, "os.PathLike[str]"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class PgenGrammar(grammar.Grammar):
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ParserGenerator:
|
| 19 |
+
filename: Path
|
| 20 |
+
stream: IO[str]
|
| 21 |
+
generator: Iterator[TokenInfo]
|
| 22 |
+
first: dict[str, dict[str, int] | None]
|
| 23 |
+
|
| 24 |
+
def __init__(self, filename: Path, stream: IO[str] | None = None) -> None:
|
| 25 |
+
close_stream = None
|
| 26 |
+
if stream is None:
|
| 27 |
+
stream = open(filename, encoding="utf-8")
|
| 28 |
+
close_stream = stream.close
|
| 29 |
+
self.filename = filename
|
| 30 |
+
self.generator = tokenize.tokenize(stream.read())
|
| 31 |
+
self.gettoken() # Initialize lookahead
|
| 32 |
+
self.dfas, self.startsymbol = self.parse()
|
| 33 |
+
if close_stream is not None:
|
| 34 |
+
close_stream()
|
| 35 |
+
self.first = {} # map from symbol name to set of tokens
|
| 36 |
+
self.addfirstsets()
|
| 37 |
+
|
| 38 |
+
def make_grammar(self) -> PgenGrammar:
|
| 39 |
+
c = PgenGrammar()
|
| 40 |
+
names = list(self.dfas.keys())
|
| 41 |
+
names.sort()
|
| 42 |
+
names.remove(self.startsymbol)
|
| 43 |
+
names.insert(0, self.startsymbol)
|
| 44 |
+
for name in names:
|
| 45 |
+
i = 256 + len(c.symbol2number)
|
| 46 |
+
c.symbol2number[name] = i
|
| 47 |
+
c.number2symbol[i] = name
|
| 48 |
+
for name in names:
|
| 49 |
+
dfa = self.dfas[name]
|
| 50 |
+
states = []
|
| 51 |
+
for state in dfa:
|
| 52 |
+
arcs = []
|
| 53 |
+
for label, next in sorted(state.arcs.items()):
|
| 54 |
+
arcs.append((self.make_label(c, label), dfa.index(next)))
|
| 55 |
+
if state.isfinal:
|
| 56 |
+
arcs.append((0, dfa.index(state)))
|
| 57 |
+
states.append(arcs)
|
| 58 |
+
c.states.append(states)
|
| 59 |
+
c.dfas[c.symbol2number[name]] = (states, self.make_first(c, name))
|
| 60 |
+
c.start = c.symbol2number[self.startsymbol]
|
| 61 |
+
return c
|
| 62 |
+
|
| 63 |
+
def make_first(self, c: PgenGrammar, name: str) -> dict[int, int]:
|
| 64 |
+
rawfirst = self.first[name]
|
| 65 |
+
assert rawfirst is not None
|
| 66 |
+
first = {}
|
| 67 |
+
for label in sorted(rawfirst):
|
| 68 |
+
ilabel = self.make_label(c, label)
|
| 69 |
+
##assert ilabel not in first # XXX failed on <> ... !=
|
| 70 |
+
first[ilabel] = 1
|
| 71 |
+
return first
|
| 72 |
+
|
| 73 |
+
def make_label(self, c: PgenGrammar, label: str) -> int:
|
| 74 |
+
# XXX Maybe this should be a method on a subclass of converter?
|
| 75 |
+
ilabel = len(c.labels)
|
| 76 |
+
if label[0].isalpha():
|
| 77 |
+
# Either a symbol name or a named token
|
| 78 |
+
if label in c.symbol2number:
|
| 79 |
+
# A symbol name (a non-terminal)
|
| 80 |
+
if label in c.symbol2label:
|
| 81 |
+
return c.symbol2label[label]
|
| 82 |
+
else:
|
| 83 |
+
c.labels.append((c.symbol2number[label], None))
|
| 84 |
+
c.symbol2label[label] = ilabel
|
| 85 |
+
return ilabel
|
| 86 |
+
else:
|
| 87 |
+
# A named token (NAME, NUMBER, STRING)
|
| 88 |
+
itoken = getattr(token, label, None)
|
| 89 |
+
assert isinstance(itoken, int), label
|
| 90 |
+
assert itoken in token.tok_name, label
|
| 91 |
+
if itoken in c.tokens:
|
| 92 |
+
return c.tokens[itoken]
|
| 93 |
+
else:
|
| 94 |
+
c.labels.append((itoken, None))
|
| 95 |
+
c.tokens[itoken] = ilabel
|
| 96 |
+
return ilabel
|
| 97 |
+
else:
|
| 98 |
+
# Either a keyword or an operator
|
| 99 |
+
assert label[0] in ('"', "'"), label
|
| 100 |
+
value = eval(label)
|
| 101 |
+
if value[0].isalpha():
|
| 102 |
+
if label[0] == '"':
|
| 103 |
+
keywords = c.soft_keywords
|
| 104 |
+
else:
|
| 105 |
+
keywords = c.keywords
|
| 106 |
+
|
| 107 |
+
# A keyword
|
| 108 |
+
if value in keywords:
|
| 109 |
+
return keywords[value]
|
| 110 |
+
else:
|
| 111 |
+
c.labels.append((token.NAME, value))
|
| 112 |
+
keywords[value] = ilabel
|
| 113 |
+
return ilabel
|
| 114 |
+
else:
|
| 115 |
+
# An operator (any non-numeric token)
|
| 116 |
+
itoken = grammar.opmap[value] # Fails if unknown token
|
| 117 |
+
if itoken in c.tokens:
|
| 118 |
+
return c.tokens[itoken]
|
| 119 |
+
else:
|
| 120 |
+
c.labels.append((itoken, None))
|
| 121 |
+
c.tokens[itoken] = ilabel
|
| 122 |
+
return ilabel
|
| 123 |
+
|
| 124 |
+
def addfirstsets(self) -> None:
|
| 125 |
+
names = list(self.dfas.keys())
|
| 126 |
+
names.sort()
|
| 127 |
+
for name in names:
|
| 128 |
+
if name not in self.first:
|
| 129 |
+
self.calcfirst(name)
|
| 130 |
+
# print name, self.first[name].keys()
|
| 131 |
+
|
| 132 |
+
def calcfirst(self, name: str) -> None:
|
| 133 |
+
dfa = self.dfas[name]
|
| 134 |
+
self.first[name] = None # dummy to detect left recursion
|
| 135 |
+
state = dfa[0]
|
| 136 |
+
totalset: dict[str, int] = {}
|
| 137 |
+
overlapcheck = {}
|
| 138 |
+
for label in state.arcs:
|
| 139 |
+
if label in self.dfas:
|
| 140 |
+
if label in self.first:
|
| 141 |
+
fset = self.first[label]
|
| 142 |
+
if fset is None:
|
| 143 |
+
raise ValueError(f"recursion for rule {name!r}")
|
| 144 |
+
else:
|
| 145 |
+
self.calcfirst(label)
|
| 146 |
+
fset = self.first[label]
|
| 147 |
+
assert fset is not None
|
| 148 |
+
totalset.update(fset)
|
| 149 |
+
overlapcheck[label] = fset
|
| 150 |
+
else:
|
| 151 |
+
totalset[label] = 1
|
| 152 |
+
overlapcheck[label] = {label: 1}
|
| 153 |
+
inverse: dict[str, str] = {}
|
| 154 |
+
for label, itsfirst in overlapcheck.items():
|
| 155 |
+
for symbol in itsfirst:
|
| 156 |
+
if symbol in inverse:
|
| 157 |
+
raise ValueError(
|
| 158 |
+
f"rule {name} is ambiguous; {symbol} is in the first sets of"
|
| 159 |
+
f" {label} as well as {inverse[symbol]}"
|
| 160 |
+
)
|
| 161 |
+
inverse[symbol] = label
|
| 162 |
+
self.first[name] = totalset
|
| 163 |
+
|
| 164 |
+
def parse(self) -> tuple[dict[str, list["DFAState"]], str]:
|
| 165 |
+
dfas = {}
|
| 166 |
+
startsymbol: str | None = None
|
| 167 |
+
# MSTART: (NEWLINE | RULE)* ENDMARKER
|
| 168 |
+
while self.type != token.ENDMARKER:
|
| 169 |
+
while self.type == token.NEWLINE:
|
| 170 |
+
self.gettoken()
|
| 171 |
+
# RULE: NAME ':' RHS NEWLINE
|
| 172 |
+
name = self.expect(token.NAME)
|
| 173 |
+
self.expect(token.OP, ":")
|
| 174 |
+
a, z = self.parse_rhs()
|
| 175 |
+
self.expect(token.NEWLINE)
|
| 176 |
+
# self.dump_nfa(name, a, z)
|
| 177 |
+
dfa = self.make_dfa(a, z)
|
| 178 |
+
# self.dump_dfa(name, dfa)
|
| 179 |
+
# oldlen = len(dfa)
|
| 180 |
+
self.simplify_dfa(dfa)
|
| 181 |
+
# newlen = len(dfa)
|
| 182 |
+
dfas[name] = dfa
|
| 183 |
+
# print name, oldlen, newlen
|
| 184 |
+
if startsymbol is None:
|
| 185 |
+
startsymbol = name
|
| 186 |
+
assert startsymbol is not None
|
| 187 |
+
return dfas, startsymbol
|
| 188 |
+
|
| 189 |
+
def make_dfa(self, start: "NFAState", finish: "NFAState") -> list["DFAState"]:
|
| 190 |
+
# To turn an NFA into a DFA, we define the states of the DFA
|
| 191 |
+
# to correspond to *sets* of states of the NFA. Then do some
|
| 192 |
+
# state reduction. Let's represent sets as dicts with 1 for
|
| 193 |
+
# values.
|
| 194 |
+
assert isinstance(start, NFAState)
|
| 195 |
+
assert isinstance(finish, NFAState)
|
| 196 |
+
|
| 197 |
+
def closure(state: NFAState) -> dict[NFAState, int]:
|
| 198 |
+
base: dict[NFAState, int] = {}
|
| 199 |
+
addclosure(state, base)
|
| 200 |
+
return base
|
| 201 |
+
|
| 202 |
+
def addclosure(state: NFAState, base: dict[NFAState, int]) -> None:
|
| 203 |
+
assert isinstance(state, NFAState)
|
| 204 |
+
if state in base:
|
| 205 |
+
return
|
| 206 |
+
base[state] = 1
|
| 207 |
+
for label, next in state.arcs:
|
| 208 |
+
if label is None:
|
| 209 |
+
addclosure(next, base)
|
| 210 |
+
|
| 211 |
+
states = [DFAState(closure(start), finish)]
|
| 212 |
+
for state in states: # NB states grows while we're iterating
|
| 213 |
+
arcs: dict[str, dict[NFAState, int]] = {}
|
| 214 |
+
for nfastate in state.nfaset:
|
| 215 |
+
for label, next in nfastate.arcs:
|
| 216 |
+
if label is not None:
|
| 217 |
+
addclosure(next, arcs.setdefault(label, {}))
|
| 218 |
+
for label, nfaset in sorted(arcs.items()):
|
| 219 |
+
for st in states:
|
| 220 |
+
if st.nfaset == nfaset:
|
| 221 |
+
break
|
| 222 |
+
else:
|
| 223 |
+
st = DFAState(nfaset, finish)
|
| 224 |
+
states.append(st)
|
| 225 |
+
state.addarc(st, label)
|
| 226 |
+
return states # List of DFAState instances; first one is start
|
| 227 |
+
|
| 228 |
+
def dump_nfa(self, name: str, start: "NFAState", finish: "NFAState") -> None:
|
| 229 |
+
print("Dump of NFA for", name)
|
| 230 |
+
todo = [start]
|
| 231 |
+
for i, state in enumerate(todo):
|
| 232 |
+
print(" State", i, state is finish and "(final)" or "")
|
| 233 |
+
for label, next in state.arcs:
|
| 234 |
+
if next in todo:
|
| 235 |
+
j = todo.index(next)
|
| 236 |
+
else:
|
| 237 |
+
j = len(todo)
|
| 238 |
+
todo.append(next)
|
| 239 |
+
if label is None:
|
| 240 |
+
print(f" -> {j}")
|
| 241 |
+
else:
|
| 242 |
+
print(f" {label} -> {j}")
|
| 243 |
+
|
| 244 |
+
def dump_dfa(self, name: str, dfa: Sequence["DFAState"]) -> None:
|
| 245 |
+
print("Dump of DFA for", name)
|
| 246 |
+
for i, state in enumerate(dfa):
|
| 247 |
+
print(" State", i, state.isfinal and "(final)" or "")
|
| 248 |
+
for label, next in sorted(state.arcs.items()):
|
| 249 |
+
print(f" {label} -> {dfa.index(next)}")
|
| 250 |
+
|
| 251 |
+
def simplify_dfa(self, dfa: list["DFAState"]) -> None:
|
| 252 |
+
# This is not theoretically optimal, but works well enough.
|
| 253 |
+
# Algorithm: repeatedly look for two states that have the same
|
| 254 |
+
# set of arcs (same labels pointing to the same nodes) and
|
| 255 |
+
# unify them, until things stop changing.
|
| 256 |
+
|
| 257 |
+
# dfa is a list of DFAState instances
|
| 258 |
+
changes = True
|
| 259 |
+
while changes:
|
| 260 |
+
changes = False
|
| 261 |
+
for i, state_i in enumerate(dfa):
|
| 262 |
+
for j in range(i + 1, len(dfa)):
|
| 263 |
+
state_j = dfa[j]
|
| 264 |
+
if state_i == state_j:
|
| 265 |
+
# print " unify", i, j
|
| 266 |
+
del dfa[j]
|
| 267 |
+
for state in dfa:
|
| 268 |
+
state.unifystate(state_j, state_i)
|
| 269 |
+
changes = True
|
| 270 |
+
break
|
| 271 |
+
|
| 272 |
+
def parse_rhs(self) -> tuple["NFAState", "NFAState"]:
|
| 273 |
+
# RHS: ALT ('|' ALT)*
|
| 274 |
+
a, z = self.parse_alt()
|
| 275 |
+
if self.value != "|":
|
| 276 |
+
return a, z
|
| 277 |
+
else:
|
| 278 |
+
aa = NFAState()
|
| 279 |
+
zz = NFAState()
|
| 280 |
+
aa.addarc(a)
|
| 281 |
+
z.addarc(zz)
|
| 282 |
+
while self.value == "|":
|
| 283 |
+
self.gettoken()
|
| 284 |
+
a, z = self.parse_alt()
|
| 285 |
+
aa.addarc(a)
|
| 286 |
+
z.addarc(zz)
|
| 287 |
+
return aa, zz
|
| 288 |
+
|
| 289 |
+
def parse_alt(self) -> tuple["NFAState", "NFAState"]:
|
| 290 |
+
# ALT: ITEM+
|
| 291 |
+
a, b = self.parse_item()
|
| 292 |
+
while self.value in ("(", "[") or self.type in (token.NAME, token.STRING):
|
| 293 |
+
c, d = self.parse_item()
|
| 294 |
+
b.addarc(c)
|
| 295 |
+
b = d
|
| 296 |
+
return a, b
|
| 297 |
+
|
| 298 |
+
def parse_item(self) -> tuple["NFAState", "NFAState"]:
|
| 299 |
+
# ITEM: '[' RHS ']' | ATOM ['+' | '*']
|
| 300 |
+
if self.value == "[":
|
| 301 |
+
self.gettoken()
|
| 302 |
+
a, z = self.parse_rhs()
|
| 303 |
+
self.expect(token.OP, "]")
|
| 304 |
+
a.addarc(z)
|
| 305 |
+
return a, z
|
| 306 |
+
else:
|
| 307 |
+
a, z = self.parse_atom()
|
| 308 |
+
value = self.value
|
| 309 |
+
if value not in ("+", "*"):
|
| 310 |
+
return a, z
|
| 311 |
+
self.gettoken()
|
| 312 |
+
z.addarc(a)
|
| 313 |
+
if value == "+":
|
| 314 |
+
return a, z
|
| 315 |
+
else:
|
| 316 |
+
return a, a
|
| 317 |
+
|
| 318 |
+
def parse_atom(self) -> tuple["NFAState", "NFAState"]:
|
| 319 |
+
# ATOM: '(' RHS ')' | NAME | STRING
|
| 320 |
+
if self.value == "(":
|
| 321 |
+
self.gettoken()
|
| 322 |
+
a, z = self.parse_rhs()
|
| 323 |
+
self.expect(token.OP, ")")
|
| 324 |
+
return a, z
|
| 325 |
+
elif self.type in (token.NAME, token.STRING):
|
| 326 |
+
a = NFAState()
|
| 327 |
+
z = NFAState()
|
| 328 |
+
a.addarc(z, self.value)
|
| 329 |
+
self.gettoken()
|
| 330 |
+
return a, z
|
| 331 |
+
else:
|
| 332 |
+
self.raise_error(
|
| 333 |
+
f"expected (...) or NAME or STRING, got {self.type}/{self.value}"
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def expect(self, type: int, value: Any | None = None) -> str:
|
| 337 |
+
if self.type != type or (value is not None and self.value != value):
|
| 338 |
+
self.raise_error(f"expected {type}/{value}, got {self.type}/{self.value}")
|
| 339 |
+
value = self.value
|
| 340 |
+
self.gettoken()
|
| 341 |
+
return value
|
| 342 |
+
|
| 343 |
+
def gettoken(self) -> None:
|
| 344 |
+
tup = next(self.generator)
|
| 345 |
+
while tup[0] in (tokenize.COMMENT, tokenize.NL):
|
| 346 |
+
tup = next(self.generator)
|
| 347 |
+
self.type, self.value, self.begin, self.end, self.line = tup
|
| 348 |
+
# print token.tok_name[self.type], repr(self.value)
|
| 349 |
+
|
| 350 |
+
def raise_error(self, msg: str) -> NoReturn:
|
| 351 |
+
raise SyntaxError(
|
| 352 |
+
msg, (str(self.filename), self.end[0], self.end[1], self.line)
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
class NFAState:
|
| 357 |
+
arcs: list[tuple[str | None, "NFAState"]]
|
| 358 |
+
|
| 359 |
+
def __init__(self) -> None:
|
| 360 |
+
self.arcs = [] # list of (label, NFAState) pairs
|
| 361 |
+
|
| 362 |
+
def addarc(self, next: "NFAState", label: str | None = None) -> None:
|
| 363 |
+
assert label is None or isinstance(label, str)
|
| 364 |
+
assert isinstance(next, NFAState)
|
| 365 |
+
self.arcs.append((label, next))
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class DFAState:
|
| 369 |
+
nfaset: dict[NFAState, Any]
|
| 370 |
+
isfinal: bool
|
| 371 |
+
arcs: dict[str, "DFAState"]
|
| 372 |
+
|
| 373 |
+
def __init__(self, nfaset: dict[NFAState, Any], final: NFAState) -> None:
|
| 374 |
+
assert isinstance(nfaset, dict)
|
| 375 |
+
assert isinstance(next(iter(nfaset)), NFAState)
|
| 376 |
+
assert isinstance(final, NFAState)
|
| 377 |
+
self.nfaset = nfaset
|
| 378 |
+
self.isfinal = final in nfaset
|
| 379 |
+
self.arcs = {} # map from label to DFAState
|
| 380 |
+
|
| 381 |
+
def addarc(self, next: "DFAState", label: str) -> None:
|
| 382 |
+
assert isinstance(label, str)
|
| 383 |
+
assert label not in self.arcs
|
| 384 |
+
assert isinstance(next, DFAState)
|
| 385 |
+
self.arcs[label] = next
|
| 386 |
+
|
| 387 |
+
def unifystate(self, old: "DFAState", new: "DFAState") -> None:
|
| 388 |
+
for label, next in self.arcs.items():
|
| 389 |
+
if next is old:
|
| 390 |
+
self.arcs[label] = new
|
| 391 |
+
|
| 392 |
+
def __eq__(self, other: Any) -> bool:
|
| 393 |
+
# Equality test -- ignore the nfaset instance variable
|
| 394 |
+
assert isinstance(other, DFAState)
|
| 395 |
+
if self.isfinal != other.isfinal:
|
| 396 |
+
return False
|
| 397 |
+
# Can't just return self.arcs == other.arcs, because that
|
| 398 |
+
# would invoke this method recursively, with cycles...
|
| 399 |
+
if len(self.arcs) != len(other.arcs):
|
| 400 |
+
return False
|
| 401 |
+
for label, next in self.arcs.items():
|
| 402 |
+
if next is not other.arcs.get(label):
|
| 403 |
+
return False
|
| 404 |
+
return True
|
| 405 |
+
|
| 406 |
+
__hash__: Any = None # For Py3 compatibility.
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def generate_grammar(filename: Path = "Grammar.txt") -> PgenGrammar:
|
| 410 |
+
p = ParserGenerator(filename)
|
| 411 |
+
return p.make_grammar()
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/token.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/token.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Token constants (from "token.h")."""
|
| 2 |
+
|
| 3 |
+
from typing import Final
|
| 4 |
+
|
| 5 |
+
# Taken from Python (r53757) and modified to include some tokens
|
| 6 |
+
# originally monkeypatched in by pgen2.tokenize
|
| 7 |
+
|
| 8 |
+
# --start constants--
|
| 9 |
+
ENDMARKER: Final = 0
|
| 10 |
+
NAME: Final = 1
|
| 11 |
+
NUMBER: Final = 2
|
| 12 |
+
STRING: Final = 3
|
| 13 |
+
NEWLINE: Final = 4
|
| 14 |
+
INDENT: Final = 5
|
| 15 |
+
DEDENT: Final = 6
|
| 16 |
+
LPAR: Final = 7
|
| 17 |
+
RPAR: Final = 8
|
| 18 |
+
LSQB: Final = 9
|
| 19 |
+
RSQB: Final = 10
|
| 20 |
+
COLON: Final = 11
|
| 21 |
+
COMMA: Final = 12
|
| 22 |
+
SEMI: Final = 13
|
| 23 |
+
PLUS: Final = 14
|
| 24 |
+
MINUS: Final = 15
|
| 25 |
+
STAR: Final = 16
|
| 26 |
+
SLASH: Final = 17
|
| 27 |
+
VBAR: Final = 18
|
| 28 |
+
AMPER: Final = 19
|
| 29 |
+
LESS: Final = 20
|
| 30 |
+
GREATER: Final = 21
|
| 31 |
+
EQUAL: Final = 22
|
| 32 |
+
DOT: Final = 23
|
| 33 |
+
PERCENT: Final = 24
|
| 34 |
+
BACKQUOTE: Final = 25
|
| 35 |
+
LBRACE: Final = 26
|
| 36 |
+
RBRACE: Final = 27
|
| 37 |
+
EQEQUAL: Final = 28
|
| 38 |
+
NOTEQUAL: Final = 29
|
| 39 |
+
LESSEQUAL: Final = 30
|
| 40 |
+
GREATEREQUAL: Final = 31
|
| 41 |
+
TILDE: Final = 32
|
| 42 |
+
CIRCUMFLEX: Final = 33
|
| 43 |
+
LEFTSHIFT: Final = 34
|
| 44 |
+
RIGHTSHIFT: Final = 35
|
| 45 |
+
DOUBLESTAR: Final = 36
|
| 46 |
+
PLUSEQUAL: Final = 37
|
| 47 |
+
MINEQUAL: Final = 38
|
| 48 |
+
STAREQUAL: Final = 39
|
| 49 |
+
SLASHEQUAL: Final = 40
|
| 50 |
+
PERCENTEQUAL: Final = 41
|
| 51 |
+
AMPEREQUAL: Final = 42
|
| 52 |
+
VBAREQUAL: Final = 43
|
| 53 |
+
CIRCUMFLEXEQUAL: Final = 44
|
| 54 |
+
LEFTSHIFTEQUAL: Final = 45
|
| 55 |
+
RIGHTSHIFTEQUAL: Final = 46
|
| 56 |
+
DOUBLESTAREQUAL: Final = 47
|
| 57 |
+
DOUBLESLASH: Final = 48
|
| 58 |
+
DOUBLESLASHEQUAL: Final = 49
|
| 59 |
+
AT: Final = 50
|
| 60 |
+
ATEQUAL: Final = 51
|
| 61 |
+
OP: Final = 52
|
| 62 |
+
COMMENT: Final = 53
|
| 63 |
+
NL: Final = 54
|
| 64 |
+
RARROW: Final = 55
|
| 65 |
+
AWAIT: Final = 56
|
| 66 |
+
ASYNC: Final = 57
|
| 67 |
+
ERRORTOKEN: Final = 58
|
| 68 |
+
COLONEQUAL: Final = 59
|
| 69 |
+
FSTRING_START: Final = 60
|
| 70 |
+
FSTRING_MIDDLE: Final = 61
|
| 71 |
+
FSTRING_END: Final = 62
|
| 72 |
+
BANG: Final = 63
|
| 73 |
+
TSTRING_START: Final = 64
|
| 74 |
+
TSTRING_MIDDLE: Final = 65
|
| 75 |
+
TSTRING_END: Final = 66
|
| 76 |
+
N_TOKENS: Final = 67
|
| 77 |
+
NT_OFFSET: Final = 256
|
| 78 |
+
# --end constants--
|
| 79 |
+
|
| 80 |
+
tok_name: Final[dict[int, str]] = {}
|
| 81 |
+
for _name, _value in list(globals().items()):
|
| 82 |
+
if type(_value) is int:
|
| 83 |
+
tok_name[_value] = _name
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def ISTERMINAL(x: int) -> bool:
|
| 87 |
+
return x < NT_OFFSET
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def ISNONTERMINAL(x: int) -> bool:
|
| 91 |
+
return x >= NT_OFFSET
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def ISEOF(x: int) -> bool:
|
| 95 |
+
return x == ENDMARKER
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/tokenize.cpython-311-x86_64-linux-gnu.so
ADDED
|
Binary file (16 kB). View file
|
|
|
py311/lib/python3.11/site-packages/blib2to3/pgen2/tokenize.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
|
| 4 |
+
# mypy: allow-untyped-defs, allow-untyped-calls
|
| 5 |
+
|
| 6 |
+
"""Tokenization help for Python programs.
|
| 7 |
+
|
| 8 |
+
generate_tokens(readline) is a generator that breaks a stream of
|
| 9 |
+
text into Python tokens. It accepts a readline-like method which is called
|
| 10 |
+
repeatedly to get the next line of input (or "" for EOF). It generates
|
| 11 |
+
5-tuples with these members:
|
| 12 |
+
|
| 13 |
+
the token type (see token.py)
|
| 14 |
+
the token (a string)
|
| 15 |
+
the starting (row, column) indices of the token (a 2-tuple of ints)
|
| 16 |
+
the ending (row, column) indices of the token (a 2-tuple of ints)
|
| 17 |
+
the original line (string)
|
| 18 |
+
|
| 19 |
+
It is designed to match the working of the Python tokenizer exactly, except
|
| 20 |
+
that it produces COMMENT tokens for comments and gives type OP for all
|
| 21 |
+
operators
|
| 22 |
+
|
| 23 |
+
Older entry points
|
| 24 |
+
tokenize_loop(readline, tokeneater)
|
| 25 |
+
tokenize(readline, tokeneater=printtoken)
|
| 26 |
+
are the same, except instead of generating tokens, tokeneater is a callback
|
| 27 |
+
function to which the 5 fields described above are passed as 5 arguments,
|
| 28 |
+
each time a new token is found."""
|
| 29 |
+
|
| 30 |
+
import sys
|
| 31 |
+
from collections.abc import Iterator
|
| 32 |
+
|
| 33 |
+
from blib2to3.pgen2.grammar import Grammar
|
| 34 |
+
from blib2to3.pgen2.token import (
|
| 35 |
+
ASYNC,
|
| 36 |
+
AWAIT,
|
| 37 |
+
COMMENT,
|
| 38 |
+
DEDENT,
|
| 39 |
+
ENDMARKER,
|
| 40 |
+
FSTRING_END,
|
| 41 |
+
FSTRING_MIDDLE,
|
| 42 |
+
FSTRING_START,
|
| 43 |
+
INDENT,
|
| 44 |
+
NAME,
|
| 45 |
+
NEWLINE,
|
| 46 |
+
NL,
|
| 47 |
+
NUMBER,
|
| 48 |
+
OP,
|
| 49 |
+
STRING,
|
| 50 |
+
TSTRING_END,
|
| 51 |
+
TSTRING_MIDDLE,
|
| 52 |
+
TSTRING_START,
|
| 53 |
+
tok_name,
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
__author__ = "Ka-Ping Yee <ping@lfw.org>"
|
| 57 |
+
__credits__ = "GvR, ESR, Tim Peters, Thomas Wouters, Fred Drake, Skip Montanaro"
|
| 58 |
+
|
| 59 |
+
import pytokens
|
| 60 |
+
from pytokens import TokenType
|
| 61 |
+
|
| 62 |
+
from . import token as _token
|
| 63 |
+
|
| 64 |
+
__all__ = [x for x in dir(_token) if x[0] != "_"] + [
|
| 65 |
+
"tokenize",
|
| 66 |
+
"generate_tokens",
|
| 67 |
+
"untokenize",
|
| 68 |
+
]
|
| 69 |
+
del _token
|
| 70 |
+
|
| 71 |
+
Coord = tuple[int, int]
|
| 72 |
+
TokenInfo = tuple[int, str, Coord, Coord, str]
|
| 73 |
+
|
| 74 |
+
TOKEN_TYPE_MAP = {
|
| 75 |
+
TokenType.indent: INDENT,
|
| 76 |
+
TokenType.dedent: DEDENT,
|
| 77 |
+
TokenType.newline: NEWLINE,
|
| 78 |
+
TokenType.nl: NL,
|
| 79 |
+
TokenType.comment: COMMENT,
|
| 80 |
+
TokenType.semicolon: OP,
|
| 81 |
+
TokenType.lparen: OP,
|
| 82 |
+
TokenType.rparen: OP,
|
| 83 |
+
TokenType.lbracket: OP,
|
| 84 |
+
TokenType.rbracket: OP,
|
| 85 |
+
TokenType.lbrace: OP,
|
| 86 |
+
TokenType.rbrace: OP,
|
| 87 |
+
TokenType.colon: OP,
|
| 88 |
+
TokenType.op: OP,
|
| 89 |
+
TokenType.identifier: NAME,
|
| 90 |
+
TokenType.number: NUMBER,
|
| 91 |
+
TokenType.string: STRING,
|
| 92 |
+
TokenType.fstring_start: FSTRING_START,
|
| 93 |
+
TokenType.fstring_middle: FSTRING_MIDDLE,
|
| 94 |
+
TokenType.fstring_end: FSTRING_END,
|
| 95 |
+
TokenType.tstring_start: TSTRING_START,
|
| 96 |
+
TokenType.tstring_middle: TSTRING_MIDDLE,
|
| 97 |
+
TokenType.tstring_end: TSTRING_END,
|
| 98 |
+
TokenType.endmarker: ENDMARKER,
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class TokenError(Exception): ...
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def transform_whitespace(
|
| 106 |
+
token: pytokens.Token, source: str, prev_token: pytokens.Token | None
|
| 107 |
+
) -> pytokens.Token:
|
| 108 |
+
r"""
|
| 109 |
+
Black treats `\\\n` at the end of a line as a 'NL' token, while it
|
| 110 |
+
is ignored as whitespace in the regular Python parser.
|
| 111 |
+
But, only the first one. If there's a `\\\n` following it
|
| 112 |
+
(as in, a \ just by itself on a line), that is not made into NL.
|
| 113 |
+
"""
|
| 114 |
+
if (
|
| 115 |
+
token.type == TokenType.whitespace
|
| 116 |
+
and prev_token is not None
|
| 117 |
+
and prev_token.type not in (TokenType.nl, TokenType.newline)
|
| 118 |
+
):
|
| 119 |
+
token_str = source[token.start_index : token.end_index]
|
| 120 |
+
if token_str.startswith("\\\r\n"):
|
| 121 |
+
return pytokens.Token(
|
| 122 |
+
TokenType.nl,
|
| 123 |
+
token.start_index,
|
| 124 |
+
token.start_index + 3,
|
| 125 |
+
token.start_line,
|
| 126 |
+
token.start_col,
|
| 127 |
+
token.start_line,
|
| 128 |
+
token.start_col + 3,
|
| 129 |
+
)
|
| 130 |
+
elif token_str.startswith("\\\n") or token_str.startswith("\\\r"):
|
| 131 |
+
return pytokens.Token(
|
| 132 |
+
TokenType.nl,
|
| 133 |
+
token.start_index,
|
| 134 |
+
token.start_index + 2,
|
| 135 |
+
token.start_line,
|
| 136 |
+
token.start_col,
|
| 137 |
+
token.start_line,
|
| 138 |
+
token.start_col + 2,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
return token
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def tokenize(source: str, grammar: Grammar | None = None) -> Iterator[TokenInfo]:
|
| 145 |
+
lines = source.split("\n")
|
| 146 |
+
lines += [""] # For newline tokens in files that don't end in a newline
|
| 147 |
+
line, column = 1, 0
|
| 148 |
+
|
| 149 |
+
prev_token: pytokens.Token | None = None
|
| 150 |
+
try:
|
| 151 |
+
for token in pytokens.tokenize(source):
|
| 152 |
+
token = transform_whitespace(token, source, prev_token)
|
| 153 |
+
|
| 154 |
+
line, column = token.start_line, token.start_col
|
| 155 |
+
if token.type == TokenType.whitespace:
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
token_str = source[token.start_index : token.end_index]
|
| 159 |
+
|
| 160 |
+
if token.type == TokenType.newline and token_str == "":
|
| 161 |
+
# Black doesn't yield empty newline tokens at the end of a file
|
| 162 |
+
# if there's no newline at the end of a file.
|
| 163 |
+
prev_token = token
|
| 164 |
+
continue
|
| 165 |
+
|
| 166 |
+
source_line = lines[token.start_line - 1]
|
| 167 |
+
|
| 168 |
+
if token.type == TokenType.identifier and token_str in ("async", "await"):
|
| 169 |
+
# Black uses `async` and `await` token types just for those two keywords
|
| 170 |
+
yield (
|
| 171 |
+
ASYNC if token_str == "async" else AWAIT,
|
| 172 |
+
token_str,
|
| 173 |
+
(token.start_line, token.start_col),
|
| 174 |
+
(token.end_line, token.end_col),
|
| 175 |
+
source_line,
|
| 176 |
+
)
|
| 177 |
+
elif token.type == TokenType.op and token_str == "...":
|
| 178 |
+
# Black doesn't have an ellipsis token yet, yield 3 DOTs instead
|
| 179 |
+
assert token.start_line == token.end_line
|
| 180 |
+
assert token.end_col == token.start_col + 3
|
| 181 |
+
|
| 182 |
+
token_str = "."
|
| 183 |
+
for start_col in range(token.start_col, token.start_col + 3):
|
| 184 |
+
end_col = start_col + 1
|
| 185 |
+
yield (
|
| 186 |
+
TOKEN_TYPE_MAP[token.type],
|
| 187 |
+
token_str,
|
| 188 |
+
(token.start_line, start_col),
|
| 189 |
+
(token.end_line, end_col),
|
| 190 |
+
source_line,
|
| 191 |
+
)
|
| 192 |
+
else:
|
| 193 |
+
token_type = TOKEN_TYPE_MAP.get(token.type)
|
| 194 |
+
if token_type is None:
|
| 195 |
+
raise ValueError(f"Unknown token type: {token.type!r}")
|
| 196 |
+
yield (
|
| 197 |
+
TOKEN_TYPE_MAP[token.type],
|
| 198 |
+
token_str,
|
| 199 |
+
(token.start_line, token.start_col),
|
| 200 |
+
(token.end_line, token.end_col),
|
| 201 |
+
source_line,
|
| 202 |
+
)
|
| 203 |
+
prev_token = token
|
| 204 |
+
|
| 205 |
+
except pytokens.UnexpectedEOF:
|
| 206 |
+
raise TokenError("Unexpected EOF in multi-line statement", (line, column))
|
| 207 |
+
except pytokens.TokenizeError as exc:
|
| 208 |
+
raise TokenError(f"Failed to parse: {type(exc).__name__}", (line, column))
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def printtoken(
|
| 212 |
+
type: int, token: str, srow_col: Coord, erow_col: Coord, line: str
|
| 213 |
+
) -> None: # for testing
|
| 214 |
+
srow, scol = srow_col
|
| 215 |
+
erow, ecol = erow_col
|
| 216 |
+
print(f"{srow},{scol}-{erow},{ecol}:\t{tok_name[type]}\t{token!r}")
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
if __name__ == "__main__": # testing
|
| 220 |
+
if len(sys.argv) > 1:
|
| 221 |
+
token_iterator = tokenize(open(sys.argv[1]).read())
|
| 222 |
+
else:
|
| 223 |
+
token_iterator = tokenize(sys.stdin.read())
|
| 224 |
+
|
| 225 |
+
for tok in token_iterator:
|
| 226 |
+
printtoken(*tok)
|
py311/lib/python3.11/site-packages/google/api_core/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 |
+
"""Google API Core.
|
| 16 |
+
|
| 17 |
+
This package contains common code and utilities used by Google client libraries.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from google.api_core import _python_package_support
|
| 21 |
+
from google.api_core import _python_version_support
|
| 22 |
+
from google.api_core import version as api_core_version
|
| 23 |
+
|
| 24 |
+
__version__ = api_core_version.__version__
|
| 25 |
+
|
| 26 |
+
# NOTE: Until dependent artifacts require this version of
|
| 27 |
+
# google.api_core, the functionality below must be made available
|
| 28 |
+
# manually in those artifacts.
|
| 29 |
+
|
| 30 |
+
# expose dependency checks for external callers
|
| 31 |
+
check_python_version = _python_version_support.check_python_version
|
| 32 |
+
check_dependency_versions = _python_package_support.check_dependency_versions
|
| 33 |
+
parse_version_to_tuple = _python_package_support.parse_version_to_tuple
|
| 34 |
+
warn_deprecation_for_versions_less_than = (
|
| 35 |
+
_python_package_support.warn_deprecation_for_versions_less_than
|
| 36 |
+
)
|
| 37 |
+
DependencyConstraint = _python_package_support.DependencyConstraint
|
| 38 |
+
|
| 39 |
+
# perform version checks against api_core, and emit warnings if needed
|
| 40 |
+
check_python_version(package="google.api_core")
|
| 41 |
+
check_dependency_versions("google.api_core")
|
py311/lib/python3.11/site-packages/google/api_core/_python_package_support.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Google LLC
|
| 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 |
+
"""Code to check versions of dependencies used by Google Cloud Client Libraries."""
|
| 16 |
+
|
| 17 |
+
import warnings
|
| 18 |
+
import sys
|
| 19 |
+
from typing import Optional, Tuple
|
| 20 |
+
|
| 21 |
+
from collections import namedtuple
|
| 22 |
+
|
| 23 |
+
from ._python_version_support import (
|
| 24 |
+
_flatten_message,
|
| 25 |
+
_get_distribution_and_import_packages,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
if sys.version_info >= (3, 8):
|
| 29 |
+
from importlib import metadata
|
| 30 |
+
else:
|
| 31 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
|
| 32 |
+
# this code path once we drop support for Python 3.7
|
| 33 |
+
import importlib_metadata as metadata
|
| 34 |
+
|
| 35 |
+
ParsedVersion = Tuple[int, ...]
|
| 36 |
+
|
| 37 |
+
# Here we list all the packages for which we want to issue warnings
|
| 38 |
+
# about deprecated and unsupported versions.
|
| 39 |
+
DependencyConstraint = namedtuple(
|
| 40 |
+
"DependencyConstraint",
|
| 41 |
+
["package_name", "minimum_fully_supported_version", "recommended_version"],
|
| 42 |
+
)
|
| 43 |
+
_PACKAGE_DEPENDENCY_WARNINGS = [
|
| 44 |
+
DependencyConstraint(
|
| 45 |
+
"google.protobuf",
|
| 46 |
+
minimum_fully_supported_version="4.25.8",
|
| 47 |
+
recommended_version="6.x",
|
| 48 |
+
)
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"])
|
| 53 |
+
# Version string we provide in a DependencyVersion when we can't determine the version of a
|
| 54 |
+
# package.
|
| 55 |
+
UNKNOWN_VERSION_STRING = "--"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def parse_version_to_tuple(version_string: str) -> ParsedVersion:
|
| 59 |
+
"""Safely converts a semantic version string to a comparable tuple of integers.
|
| 60 |
+
|
| 61 |
+
Example: "4.25.8" -> (4, 25, 8)
|
| 62 |
+
Ignores non-numeric parts and handles common version formats.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
Tuple of integers for the parsed version string.
|
| 69 |
+
"""
|
| 70 |
+
parts = []
|
| 71 |
+
for part in version_string.split("."):
|
| 72 |
+
try:
|
| 73 |
+
parts.append(int(part))
|
| 74 |
+
except ValueError:
|
| 75 |
+
# If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
|
| 76 |
+
# This is a simplification compared to 'packaging.parse_version', but sufficient
|
| 77 |
+
# for comparing strictly numeric semantic versions.
|
| 78 |
+
break
|
| 79 |
+
return tuple(parts)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def get_dependency_version(
|
| 83 |
+
dependency_name: str,
|
| 84 |
+
) -> DependencyVersion:
|
| 85 |
+
"""Get the parsed version of an installed package dependency.
|
| 86 |
+
|
| 87 |
+
This function checks for an installed package and returns its version
|
| 88 |
+
as a comparable tuple of integers object for safe comparison. It handles
|
| 89 |
+
both modern (Python 3.8+) and legacy (Python 3.7) environments.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
dependency_name: The distribution name of the package (e.g., 'requests').
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
A DependencyVersion namedtuple with `version` (a tuple of integers) and
|
| 96 |
+
`version_string` attributes, or `DependencyVersion(None,
|
| 97 |
+
UNKNOWN_VERSION_STRING)` if the package is not found or
|
| 98 |
+
another error occurs during version discovery.
|
| 99 |
+
|
| 100 |
+
"""
|
| 101 |
+
try:
|
| 102 |
+
version_string: str = metadata.version(dependency_name)
|
| 103 |
+
parsed_version = parse_version_to_tuple(version_string)
|
| 104 |
+
return DependencyVersion(parsed_version, version_string)
|
| 105 |
+
except Exception:
|
| 106 |
+
# Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
|
| 107 |
+
# or errors during parse_version_to_tuple
|
| 108 |
+
return DependencyVersion(None, UNKNOWN_VERSION_STRING)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def warn_deprecation_for_versions_less_than(
|
| 112 |
+
consumer_import_package: str,
|
| 113 |
+
dependency_import_package: str,
|
| 114 |
+
minimum_fully_supported_version: str,
|
| 115 |
+
recommended_version: Optional[str] = None,
|
| 116 |
+
message_template: Optional[str] = None,
|
| 117 |
+
):
|
| 118 |
+
"""Issue any needed deprecation warnings for `dependency_import_package`.
|
| 119 |
+
|
| 120 |
+
If `dependency_import_package` is installed at a version less than
|
| 121 |
+
`minimum_fully_supported_version`, this issues a warning using either a
|
| 122 |
+
default `message_template` or one provided by the user. The
|
| 123 |
+
default `message_template` informs the user that they will not receive
|
| 124 |
+
future updates for `consumer_import_package` if
|
| 125 |
+
`dependency_import_package` is somehow pinned to a version lower
|
| 126 |
+
than `minimum_fully_supported_version`.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
consumer_import_package: The import name of the package that
|
| 130 |
+
needs `dependency_import_package`.
|
| 131 |
+
dependency_import_package: The import name of the dependency to check.
|
| 132 |
+
minimum_fully_supported_version: The dependency_import_package version number
|
| 133 |
+
below which a deprecation warning will be logged.
|
| 134 |
+
recommended_version: If provided, the recommended next version, which
|
| 135 |
+
could be higher than `minimum_fully_supported_version`.
|
| 136 |
+
message_template: A custom default message template to replace
|
| 137 |
+
the default. This `message_template` is treated as an
|
| 138 |
+
f-string, where the following variables are defined:
|
| 139 |
+
`dependency_import_package`, `consumer_import_package` and
|
| 140 |
+
`dependency_distribution_package` and
|
| 141 |
+
`consumer_distribution_package` and `dependency_package`,
|
| 142 |
+
`consumer_package` , which contain the import packages, the
|
| 143 |
+
distribution packages, and pretty string with both the
|
| 144 |
+
distribution and import packages for the dependency and the
|
| 145 |
+
consumer, respectively; and `minimum_fully_supported_version`,
|
| 146 |
+
`version_used`, and `version_used_string`, which refer to supported
|
| 147 |
+
and currently-used versions of the dependency.
|
| 148 |
+
|
| 149 |
+
"""
|
| 150 |
+
if (
|
| 151 |
+
not consumer_import_package
|
| 152 |
+
or not dependency_import_package
|
| 153 |
+
or not minimum_fully_supported_version
|
| 154 |
+
): # pragma: NO COVER
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
+
dependency_version = get_dependency_version(dependency_import_package)
|
| 158 |
+
if not dependency_version.version:
|
| 159 |
+
return
|
| 160 |
+
|
| 161 |
+
if dependency_version.version < parse_version_to_tuple(
|
| 162 |
+
minimum_fully_supported_version
|
| 163 |
+
):
|
| 164 |
+
(
|
| 165 |
+
dependency_package,
|
| 166 |
+
dependency_distribution_package,
|
| 167 |
+
) = _get_distribution_and_import_packages(dependency_import_package)
|
| 168 |
+
(
|
| 169 |
+
consumer_package,
|
| 170 |
+
consumer_distribution_package,
|
| 171 |
+
) = _get_distribution_and_import_packages(consumer_import_package)
|
| 172 |
+
|
| 173 |
+
recommendation = (
|
| 174 |
+
" (we recommend {recommended_version})" if recommended_version else ""
|
| 175 |
+
)
|
| 176 |
+
message_template = message_template or _flatten_message(
|
| 177 |
+
"""
|
| 178 |
+
DEPRECATION: Package {consumer_package} depends on
|
| 179 |
+
{dependency_package}, currently installed at version
|
| 180 |
+
{version_used_string}. Future updates to
|
| 181 |
+
{consumer_package} will require {dependency_package} at
|
| 182 |
+
version {minimum_fully_supported_version} or
|
| 183 |
+
higher{recommendation}. Please ensure that either (a) your
|
| 184 |
+
Python environment doesn't pin the version of
|
| 185 |
+
{dependency_package}, so that updates to
|
| 186 |
+
{consumer_package} can require the higher version, or (b)
|
| 187 |
+
you manually update your Python environment to use at
|
| 188 |
+
least version {minimum_fully_supported_version} of
|
| 189 |
+
{dependency_package}.
|
| 190 |
+
"""
|
| 191 |
+
)
|
| 192 |
+
warnings.warn(
|
| 193 |
+
message_template.format(
|
| 194 |
+
consumer_import_package=consumer_import_package,
|
| 195 |
+
dependency_import_package=dependency_import_package,
|
| 196 |
+
consumer_distribution_package=consumer_distribution_package,
|
| 197 |
+
dependency_distribution_package=dependency_distribution_package,
|
| 198 |
+
dependency_package=dependency_package,
|
| 199 |
+
consumer_package=consumer_package,
|
| 200 |
+
minimum_fully_supported_version=minimum_fully_supported_version,
|
| 201 |
+
recommendation=recommendation,
|
| 202 |
+
version_used=dependency_version.version,
|
| 203 |
+
version_used_string=dependency_version.version_string,
|
| 204 |
+
),
|
| 205 |
+
FutureWarning,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def check_dependency_versions(
|
| 210 |
+
consumer_import_package: str, *package_dependency_warnings: DependencyConstraint
|
| 211 |
+
):
|
| 212 |
+
"""Bundle checks for all package dependencies.
|
| 213 |
+
|
| 214 |
+
This function can be called by all consumers of google.api_core,
|
| 215 |
+
to emit needed deprecation warnings for any of their
|
| 216 |
+
dependencies. The dependencies to check can be passed as arguments, or if
|
| 217 |
+
none are provided, it will default to the list in
|
| 218 |
+
`_PACKAGE_DEPENDENCY_WARNINGS`.
|
| 219 |
+
|
| 220 |
+
Args:
|
| 221 |
+
consumer_import_package: The distribution name of the calling package, whose
|
| 222 |
+
dependencies we're checking.
|
| 223 |
+
*package_dependency_warnings: A variable number of DependencyConstraint
|
| 224 |
+
objects, each specifying a dependency to check.
|
| 225 |
+
"""
|
| 226 |
+
if not package_dependency_warnings:
|
| 227 |
+
package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS)
|
| 228 |
+
for package_info in package_dependency_warnings:
|
| 229 |
+
warn_deprecation_for_versions_less_than(
|
| 230 |
+
consumer_import_package,
|
| 231 |
+
package_info.package_name,
|
| 232 |
+
package_info.minimum_fully_supported_version,
|
| 233 |
+
recommended_version=package_info.recommended_version,
|
| 234 |
+
)
|
py311/lib/python3.11/site-packages/google/api_core/_python_version_support.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Google LLC
|
| 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 |
+
"""Code to check Python versions supported by Google Cloud Client Libraries."""
|
| 16 |
+
|
| 17 |
+
import datetime
|
| 18 |
+
import enum
|
| 19 |
+
import logging
|
| 20 |
+
import warnings
|
| 21 |
+
import sys
|
| 22 |
+
import textwrap
|
| 23 |
+
from typing import Any, List, NamedTuple, Optional, Dict, Tuple
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
_LOGGER = logging.getLogger(__name__)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class PythonVersionStatus(enum.Enum):
|
| 30 |
+
"""Support status of a Python version in this client library artifact release.
|
| 31 |
+
|
| 32 |
+
"Support", in this context, means that this release of a client library
|
| 33 |
+
artifact is configured to run on the currently configured version of
|
| 34 |
+
Python.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED"
|
| 38 |
+
|
| 39 |
+
PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED"
|
| 40 |
+
"""This Python version is fully supported, so the artifact running on this
|
| 41 |
+
version will have all features and bug fixes."""
|
| 42 |
+
|
| 43 |
+
PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED"
|
| 44 |
+
"""This Python version is still supported, but support will end within a
|
| 45 |
+
year. At that time, there will be no more releases for this artifact
|
| 46 |
+
running under this Python version."""
|
| 47 |
+
|
| 48 |
+
PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL"
|
| 49 |
+
"""This Python version has reached its end of life in the Python community
|
| 50 |
+
(see https://devguide.python.org/versions/), and this artifact will cease
|
| 51 |
+
supporting this Python version within the next few releases."""
|
| 52 |
+
|
| 53 |
+
PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED"
|
| 54 |
+
"""This release of the client library artifact may not be the latest, since
|
| 55 |
+
current releases no longer support this Python version."""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class VersionInfo(NamedTuple):
|
| 59 |
+
"""Hold release and support date information for a Python version."""
|
| 60 |
+
|
| 61 |
+
version: str
|
| 62 |
+
python_beta: Optional[datetime.date]
|
| 63 |
+
python_start: datetime.date
|
| 64 |
+
python_eol: datetime.date
|
| 65 |
+
gapic_start: Optional[datetime.date] = None # unused
|
| 66 |
+
gapic_deprecation: Optional[datetime.date] = None
|
| 67 |
+
gapic_end: Optional[datetime.date] = None
|
| 68 |
+
dep_unpatchable_cve: Optional[datetime.date] = None # unused
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
PYTHON_VERSIONS: List[VersionInfo] = [
|
| 72 |
+
# Refer to https://devguide.python.org/versions/ and the PEPs linked therefrom.
|
| 73 |
+
VersionInfo(
|
| 74 |
+
version="3.7",
|
| 75 |
+
python_beta=None,
|
| 76 |
+
python_start=datetime.date(2018, 6, 27),
|
| 77 |
+
python_eol=datetime.date(2023, 6, 27),
|
| 78 |
+
),
|
| 79 |
+
VersionInfo(
|
| 80 |
+
version="3.8",
|
| 81 |
+
python_beta=None,
|
| 82 |
+
python_start=datetime.date(2019, 10, 14),
|
| 83 |
+
python_eol=datetime.date(2024, 10, 7),
|
| 84 |
+
),
|
| 85 |
+
VersionInfo(
|
| 86 |
+
version="3.9",
|
| 87 |
+
python_beta=datetime.date(2020, 5, 18),
|
| 88 |
+
python_start=datetime.date(2020, 10, 5),
|
| 89 |
+
python_eol=datetime.date(2025, 10, 5),
|
| 90 |
+
gapic_end=datetime.date(2025, 10, 5) + datetime.timedelta(days=90),
|
| 91 |
+
),
|
| 92 |
+
VersionInfo(
|
| 93 |
+
version="3.10",
|
| 94 |
+
python_beta=datetime.date(2021, 5, 3),
|
| 95 |
+
python_start=datetime.date(2021, 10, 4),
|
| 96 |
+
python_eol=datetime.date(2026, 10, 4), # TODO: specify day when announced
|
| 97 |
+
),
|
| 98 |
+
VersionInfo(
|
| 99 |
+
version="3.11",
|
| 100 |
+
python_beta=datetime.date(2022, 5, 8),
|
| 101 |
+
python_start=datetime.date(2022, 10, 24),
|
| 102 |
+
python_eol=datetime.date(2027, 10, 24), # TODO: specify day when announced
|
| 103 |
+
),
|
| 104 |
+
VersionInfo(
|
| 105 |
+
version="3.12",
|
| 106 |
+
python_beta=datetime.date(2023, 5, 22),
|
| 107 |
+
python_start=datetime.date(2023, 10, 2),
|
| 108 |
+
python_eol=datetime.date(2028, 10, 2), # TODO: specify day when announced
|
| 109 |
+
),
|
| 110 |
+
VersionInfo(
|
| 111 |
+
version="3.13",
|
| 112 |
+
python_beta=datetime.date(2024, 5, 8),
|
| 113 |
+
python_start=datetime.date(2024, 10, 7),
|
| 114 |
+
python_eol=datetime.date(2029, 10, 7), # TODO: specify day when announced
|
| 115 |
+
),
|
| 116 |
+
VersionInfo(
|
| 117 |
+
version="3.14",
|
| 118 |
+
python_beta=datetime.date(2025, 5, 7),
|
| 119 |
+
python_start=datetime.date(2025, 10, 7),
|
| 120 |
+
python_eol=datetime.date(2030, 10, 7), # TODO: specify day when announced
|
| 121 |
+
),
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
PYTHON_VERSION_INFO: Dict[Tuple[int, int], VersionInfo] = {}
|
| 125 |
+
for info in PYTHON_VERSIONS:
|
| 126 |
+
major, minor = map(int, info.version.split("."))
|
| 127 |
+
PYTHON_VERSION_INFO[(major, minor)] = info
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys())
|
| 131 |
+
_FAKE_PAST_DATE = datetime.date.min + datetime.timedelta(days=900)
|
| 132 |
+
_FAKE_PAST_VERSION = VersionInfo(
|
| 133 |
+
version="0.0",
|
| 134 |
+
python_beta=_FAKE_PAST_DATE,
|
| 135 |
+
python_start=_FAKE_PAST_DATE,
|
| 136 |
+
python_eol=_FAKE_PAST_DATE,
|
| 137 |
+
)
|
| 138 |
+
_FAKE_FUTURE_DATE = datetime.date.max - datetime.timedelta(days=900)
|
| 139 |
+
_FAKE_FUTURE_VERSION = VersionInfo(
|
| 140 |
+
version="999.0",
|
| 141 |
+
python_beta=_FAKE_FUTURE_DATE,
|
| 142 |
+
python_start=_FAKE_FUTURE_DATE,
|
| 143 |
+
python_eol=_FAKE_FUTURE_DATE,
|
| 144 |
+
)
|
| 145 |
+
DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365)
|
| 146 |
+
EOL_GRACE_PERIOD = datetime.timedelta(weeks=1)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _flatten_message(text: str) -> str:
|
| 150 |
+
"""Dedent a multi-line string and flatten it into a single line."""
|
| 151 |
+
return " ".join(textwrap.dedent(text).strip().split())
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/835):
|
| 155 |
+
# Remove once we no longer support Python 3.9.
|
| 156 |
+
# `importlib.metadata.packages_distributions()` is only supported in Python 3.10 and newer
|
| 157 |
+
# https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.packages_distributions
|
| 158 |
+
if sys.version_info < (3, 10):
|
| 159 |
+
|
| 160 |
+
def _get_pypi_package_name(module_name): # pragma: NO COVER
|
| 161 |
+
"""Determine the PyPI package name for a given module name."""
|
| 162 |
+
return None
|
| 163 |
+
|
| 164 |
+
else:
|
| 165 |
+
from importlib import metadata
|
| 166 |
+
|
| 167 |
+
def _get_pypi_package_name(module_name):
|
| 168 |
+
"""Determine the PyPI package name for a given module name."""
|
| 169 |
+
try:
|
| 170 |
+
# Get the mapping of modules to distributions
|
| 171 |
+
module_to_distributions = metadata.packages_distributions()
|
| 172 |
+
|
| 173 |
+
# Check if the module is found in the mapping
|
| 174 |
+
if module_name in module_to_distributions: # pragma: NO COVER
|
| 175 |
+
# The value is a list of distribution names, take the first one
|
| 176 |
+
return module_to_distributions[module_name][0]
|
| 177 |
+
except Exception as e: # pragma: NO COVER
|
| 178 |
+
_LOGGER.info(
|
| 179 |
+
"An error occurred while determining PyPI package name for %s: %s",
|
| 180 |
+
module_name,
|
| 181 |
+
e,
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _get_distribution_and_import_packages(import_package: str) -> Tuple[str, Any]:
|
| 188 |
+
"""Return a pretty string with distribution & import package names."""
|
| 189 |
+
distribution_package = _get_pypi_package_name(import_package)
|
| 190 |
+
dependency_distribution_and_import_packages = (
|
| 191 |
+
f"package {distribution_package} ({import_package})"
|
| 192 |
+
if distribution_package
|
| 193 |
+
else import_package
|
| 194 |
+
)
|
| 195 |
+
return dependency_distribution_and_import_packages, distribution_package
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def check_python_version(
|
| 199 |
+
package: str = "this package", today: Optional[datetime.date] = None
|
| 200 |
+
) -> PythonVersionStatus:
|
| 201 |
+
"""Check the running Python version and issue a support warning if needed.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
today: The date to check against. Defaults to the current date.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
The support status of the current Python version.
|
| 208 |
+
"""
|
| 209 |
+
today = today or datetime.date.today()
|
| 210 |
+
package_label, _ = _get_distribution_and_import_packages(package)
|
| 211 |
+
|
| 212 |
+
python_version = sys.version_info
|
| 213 |
+
version_tuple = (python_version.major, python_version.minor)
|
| 214 |
+
py_version_str = sys.version.split()[0]
|
| 215 |
+
|
| 216 |
+
version_info = PYTHON_VERSION_INFO.get(version_tuple)
|
| 217 |
+
|
| 218 |
+
if not version_info:
|
| 219 |
+
if version_tuple < LOWEST_TRACKED_VERSION:
|
| 220 |
+
version_info = _FAKE_PAST_VERSION
|
| 221 |
+
else:
|
| 222 |
+
version_info = _FAKE_FUTURE_VERSION
|
| 223 |
+
|
| 224 |
+
gapic_deprecation = version_info.gapic_deprecation or (
|
| 225 |
+
version_info.python_eol - DEPRECATION_WARNING_PERIOD
|
| 226 |
+
)
|
| 227 |
+
gapic_end = version_info.gapic_end or (version_info.python_eol + EOL_GRACE_PERIOD)
|
| 228 |
+
|
| 229 |
+
def min_python(date: datetime.date) -> str:
|
| 230 |
+
"""Find the minimum supported Python version for a given date."""
|
| 231 |
+
for version, info in sorted(PYTHON_VERSION_INFO.items()):
|
| 232 |
+
if info.python_start <= date < info.python_eol:
|
| 233 |
+
return f"{version[0]}.{version[1]}"
|
| 234 |
+
return "at a currently supported version [https://devguide.python.org/versions]"
|
| 235 |
+
|
| 236 |
+
if gapic_end < today:
|
| 237 |
+
message = _flatten_message(
|
| 238 |
+
f"""
|
| 239 |
+
You are using a non-supported Python version ({py_version_str}).
|
| 240 |
+
Google will not post any further updates to {package_label}
|
| 241 |
+
supporting this Python version. Please upgrade to the latest Python
|
| 242 |
+
version, or at least Python {min_python(today)}, and then update
|
| 243 |
+
{package_label}.
|
| 244 |
+
"""
|
| 245 |
+
)
|
| 246 |
+
warnings.warn(message, FutureWarning)
|
| 247 |
+
return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED
|
| 248 |
+
|
| 249 |
+
eol_date = version_info.python_eol + EOL_GRACE_PERIOD
|
| 250 |
+
if eol_date <= today <= gapic_end:
|
| 251 |
+
message = _flatten_message(
|
| 252 |
+
f"""
|
| 253 |
+
You are using a Python version ({py_version_str})
|
| 254 |
+
past its end of life. Google will update {package_label}
|
| 255 |
+
with critical bug fixes on a best-effort basis, but not
|
| 256 |
+
with any other fixes or features. Please upgrade
|
| 257 |
+
to the latest Python version, or at least Python
|
| 258 |
+
{min_python(today)}, and then update {package_label}.
|
| 259 |
+
"""
|
| 260 |
+
)
|
| 261 |
+
warnings.warn(message, FutureWarning)
|
| 262 |
+
return PythonVersionStatus.PYTHON_VERSION_EOL
|
| 263 |
+
|
| 264 |
+
if gapic_deprecation <= today <= gapic_end:
|
| 265 |
+
message = _flatten_message(
|
| 266 |
+
f"""
|
| 267 |
+
You are using a Python version ({py_version_str}) which Google will
|
| 268 |
+
stop supporting in new releases of {package_label} once it reaches
|
| 269 |
+
its end of life ({version_info.python_eol}). Please upgrade to the
|
| 270 |
+
latest Python version, or at least Python
|
| 271 |
+
{min_python(version_info.python_eol)}, to continue receiving updates
|
| 272 |
+
for {package_label} past that date.
|
| 273 |
+
"""
|
| 274 |
+
)
|
| 275 |
+
warnings.warn(message, FutureWarning)
|
| 276 |
+
return PythonVersionStatus.PYTHON_VERSION_DEPRECATED
|
| 277 |
+
|
| 278 |
+
return PythonVersionStatus.PYTHON_VERSION_SUPPORTED
|
py311/lib/python3.11/site-packages/google/api_core/_rest_streaming_base.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2024 Google LLC
|
| 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 server-side streaming in REST."""
|
| 16 |
+
|
| 17 |
+
from collections import deque
|
| 18 |
+
import string
|
| 19 |
+
from typing import Deque, Union
|
| 20 |
+
import types
|
| 21 |
+
|
| 22 |
+
import proto
|
| 23 |
+
import google.protobuf.message
|
| 24 |
+
from google.protobuf.json_format import Parse
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class BaseResponseIterator:
|
| 28 |
+
"""Base Iterator over REST API responses. This class should not be used directly.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
response_message_cls (Union[proto.Message, google.protobuf.message.Message]): A response
|
| 32 |
+
class expected to be returned from an API.
|
| 33 |
+
|
| 34 |
+
Raises:
|
| 35 |
+
ValueError: If `response_message_cls` is not a subclass of `proto.Message` or `google.protobuf.message.Message`.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def __init__(
|
| 39 |
+
self,
|
| 40 |
+
response_message_cls: Union[proto.Message, google.protobuf.message.Message],
|
| 41 |
+
):
|
| 42 |
+
self._response_message_cls = response_message_cls
|
| 43 |
+
# Contains a list of JSON responses ready to be sent to user.
|
| 44 |
+
self._ready_objs: Deque[str] = deque()
|
| 45 |
+
# Current JSON response being built.
|
| 46 |
+
self._obj = ""
|
| 47 |
+
# Keeps track of the nesting level within a JSON object.
|
| 48 |
+
self._level = 0
|
| 49 |
+
# Keeps track whether HTTP response is currently sending values
|
| 50 |
+
# inside of a string value.
|
| 51 |
+
self._in_string = False
|
| 52 |
+
# Whether an escape symbol "\" was encountered.
|
| 53 |
+
self._escape_next = False
|
| 54 |
+
|
| 55 |
+
self._grab = types.MethodType(self._create_grab(), self)
|
| 56 |
+
|
| 57 |
+
def _process_chunk(self, chunk: str):
|
| 58 |
+
if self._level == 0:
|
| 59 |
+
if chunk[0] != "[":
|
| 60 |
+
raise ValueError(
|
| 61 |
+
"Can only parse array of JSON objects, instead got %s" % chunk
|
| 62 |
+
)
|
| 63 |
+
for char in chunk:
|
| 64 |
+
if char == "{":
|
| 65 |
+
if self._level == 1:
|
| 66 |
+
# Level 1 corresponds to the outermost JSON object
|
| 67 |
+
# (i.e. the one we care about).
|
| 68 |
+
self._obj = ""
|
| 69 |
+
if not self._in_string:
|
| 70 |
+
self._level += 1
|
| 71 |
+
self._obj += char
|
| 72 |
+
elif char == "}":
|
| 73 |
+
self._obj += char
|
| 74 |
+
if not self._in_string:
|
| 75 |
+
self._level -= 1
|
| 76 |
+
if not self._in_string and self._level == 1:
|
| 77 |
+
self._ready_objs.append(self._obj)
|
| 78 |
+
elif char == '"':
|
| 79 |
+
# Helps to deal with an escaped quotes inside of a string.
|
| 80 |
+
if not self._escape_next:
|
| 81 |
+
self._in_string = not self._in_string
|
| 82 |
+
self._obj += char
|
| 83 |
+
elif char in string.whitespace:
|
| 84 |
+
if self._in_string:
|
| 85 |
+
self._obj += char
|
| 86 |
+
elif char == "[":
|
| 87 |
+
if self._level == 0:
|
| 88 |
+
self._level += 1
|
| 89 |
+
else:
|
| 90 |
+
self._obj += char
|
| 91 |
+
elif char == "]":
|
| 92 |
+
if self._level == 1:
|
| 93 |
+
self._level -= 1
|
| 94 |
+
else:
|
| 95 |
+
self._obj += char
|
| 96 |
+
else:
|
| 97 |
+
self._obj += char
|
| 98 |
+
self._escape_next = not self._escape_next if char == "\\" else False
|
| 99 |
+
|
| 100 |
+
def _create_grab(self):
|
| 101 |
+
if issubclass(self._response_message_cls, proto.Message):
|
| 102 |
+
|
| 103 |
+
def grab(this):
|
| 104 |
+
return this._response_message_cls.from_json(
|
| 105 |
+
this._ready_objs.popleft(), ignore_unknown_fields=True
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return grab
|
| 109 |
+
elif issubclass(self._response_message_cls, google.protobuf.message.Message):
|
| 110 |
+
|
| 111 |
+
def grab(this):
|
| 112 |
+
return Parse(this._ready_objs.popleft(), this._response_message_cls())
|
| 113 |
+
|
| 114 |
+
return grab
|
| 115 |
+
else:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
"Response message class must be a subclass of proto.Message or google.protobuf.message.Message."
|
| 118 |
+
)
|
py311/lib/python3.11/site-packages/google/api_core/bidi.py
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017, Google LLC
|
| 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 |
+
# https://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 synchronous bidirectional streaming RPCs."""
|
| 16 |
+
|
| 17 |
+
import collections
|
| 18 |
+
import datetime
|
| 19 |
+
import logging
|
| 20 |
+
import queue as queue_module
|
| 21 |
+
import threading
|
| 22 |
+
import time
|
| 23 |
+
|
| 24 |
+
from google.api_core import exceptions
|
| 25 |
+
from google.api_core.bidi_base import BidiRpcBase
|
| 26 |
+
|
| 27 |
+
_LOGGER = logging.getLogger(__name__)
|
| 28 |
+
_BIDIRECTIONAL_CONSUMER_NAME = "Thread-ConsumeBidirectionalStream"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class _RequestQueueGenerator(object):
|
| 32 |
+
"""A helper for sending requests to a gRPC stream from a Queue.
|
| 33 |
+
|
| 34 |
+
This generator takes requests off a given queue and yields them to gRPC.
|
| 35 |
+
|
| 36 |
+
This helper is useful when you have an indeterminate, indefinite, or
|
| 37 |
+
otherwise open-ended set of requests to send through a request-streaming
|
| 38 |
+
(or bidirectional) RPC.
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
Example::
|
| 42 |
+
|
| 43 |
+
requests = request_queue_generator(q)
|
| 44 |
+
call = stub.StreamingRequest(iter(requests))
|
| 45 |
+
requests.call = call
|
| 46 |
+
|
| 47 |
+
for response in call:
|
| 48 |
+
print(response)
|
| 49 |
+
q.put(...)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
queue (queue_module.Queue): The request queue.
|
| 54 |
+
period (float): The number of seconds to wait for items from the queue
|
| 55 |
+
before checking if the RPC is cancelled. In practice, this
|
| 56 |
+
determines the maximum amount of time the request consumption
|
| 57 |
+
thread will live after the RPC is cancelled.
|
| 58 |
+
initial_request (Union[protobuf.Message,
|
| 59 |
+
Callable[None, protobuf.Message]]): The initial request to
|
| 60 |
+
yield. This is done independently of the request queue to allow fo
|
| 61 |
+
easily restarting streams that require some initial configuration
|
| 62 |
+
request.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(self, queue, period=1, initial_request=None):
|
| 66 |
+
self._queue = queue
|
| 67 |
+
self._period = period
|
| 68 |
+
self._initial_request = initial_request
|
| 69 |
+
self.call = None
|
| 70 |
+
|
| 71 |
+
def _is_active(self):
|
| 72 |
+
# Note: there is a possibility that this starts *before* the call
|
| 73 |
+
# property is set. So we have to check if self.call is set before
|
| 74 |
+
# seeing if it's active. We need to return True if self.call is None.
|
| 75 |
+
# See https://github.com/googleapis/python-api-core/issues/560.
|
| 76 |
+
return self.call is None or self.call.is_active()
|
| 77 |
+
|
| 78 |
+
def __iter__(self):
|
| 79 |
+
# The reason this is necessary is because gRPC takes an iterator as the
|
| 80 |
+
# request for request-streaming RPCs. gRPC consumes this iterator in
|
| 81 |
+
# another thread to allow it to block while generating requests for
|
| 82 |
+
# the stream. However, if the generator blocks indefinitely gRPC will
|
| 83 |
+
# not be able to clean up the thread as it'll be blocked on
|
| 84 |
+
# `next(iterator)` and not be able to check the channel status to stop
|
| 85 |
+
# iterating. This helper mitigates that by waiting on the queue with
|
| 86 |
+
# a timeout and checking the RPC state before yielding.
|
| 87 |
+
#
|
| 88 |
+
# Finally, it allows for retrying without swapping queues because if
|
| 89 |
+
# it does pull an item off the queue when the RPC is inactive, it'll
|
| 90 |
+
# immediately put it back and then exit. This is necessary because
|
| 91 |
+
# yielding the item in this case will cause gRPC to discard it. In
|
| 92 |
+
# practice, this means that the order of messages is not guaranteed.
|
| 93 |
+
# If such a thing is necessary it would be easy to use a priority
|
| 94 |
+
# queue.
|
| 95 |
+
#
|
| 96 |
+
# Note that it is possible to accomplish this behavior without
|
| 97 |
+
# "spinning" (using a queue timeout). One possible way would be to use
|
| 98 |
+
# more threads to multiplex the grpc end event with the queue, another
|
| 99 |
+
# possible way is to use selectors and a custom event/queue object.
|
| 100 |
+
# Both of these approaches are significant from an engineering
|
| 101 |
+
# perspective for small benefit - the CPU consumed by spinning is
|
| 102 |
+
# pretty minuscule.
|
| 103 |
+
|
| 104 |
+
if self._initial_request is not None:
|
| 105 |
+
if callable(self._initial_request):
|
| 106 |
+
yield self._initial_request()
|
| 107 |
+
else:
|
| 108 |
+
yield self._initial_request
|
| 109 |
+
|
| 110 |
+
while True:
|
| 111 |
+
try:
|
| 112 |
+
item = self._queue.get(timeout=self._period)
|
| 113 |
+
except queue_module.Empty:
|
| 114 |
+
if not self._is_active():
|
| 115 |
+
_LOGGER.debug(
|
| 116 |
+
"Empty queue and inactive call, exiting request " "generator."
|
| 117 |
+
)
|
| 118 |
+
return
|
| 119 |
+
else:
|
| 120 |
+
# call is still active, keep waiting for queue items.
|
| 121 |
+
continue
|
| 122 |
+
|
| 123 |
+
# The consumer explicitly sent "None", indicating that the request
|
| 124 |
+
# should end.
|
| 125 |
+
if item is None:
|
| 126 |
+
_LOGGER.debug("Cleanly exiting request generator.")
|
| 127 |
+
return
|
| 128 |
+
|
| 129 |
+
if not self._is_active():
|
| 130 |
+
# We have an item, but the call is closed. We should put the
|
| 131 |
+
# item back on the queue so that the next call can consume it.
|
| 132 |
+
self._queue.put(item)
|
| 133 |
+
_LOGGER.debug(
|
| 134 |
+
"Inactive call, replacing item on queue and exiting "
|
| 135 |
+
"request generator."
|
| 136 |
+
)
|
| 137 |
+
return
|
| 138 |
+
|
| 139 |
+
yield item
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class _Throttle(object):
|
| 143 |
+
"""A context manager limiting the total entries in a sliding time window.
|
| 144 |
+
|
| 145 |
+
If more than ``access_limit`` attempts are made to enter the context manager
|
| 146 |
+
instance in the last ``time window`` interval, the exceeding requests block
|
| 147 |
+
until enough time elapses.
|
| 148 |
+
|
| 149 |
+
The context manager instances are thread-safe and can be shared between
|
| 150 |
+
multiple threads. If multiple requests are blocked and waiting to enter,
|
| 151 |
+
the exact order in which they are allowed to proceed is not determined.
|
| 152 |
+
|
| 153 |
+
Example::
|
| 154 |
+
|
| 155 |
+
max_three_per_second = _Throttle(
|
| 156 |
+
access_limit=3, time_window=datetime.timedelta(seconds=1)
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
for i in range(5):
|
| 160 |
+
with max_three_per_second as time_waited:
|
| 161 |
+
print("{}: Waited {} seconds to enter".format(i, time_waited))
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
access_limit (int): the maximum number of entries allowed in the time window
|
| 165 |
+
time_window (datetime.timedelta): the width of the sliding time window
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def __init__(self, access_limit, time_window):
|
| 169 |
+
if access_limit < 1:
|
| 170 |
+
raise ValueError("access_limit argument must be positive")
|
| 171 |
+
|
| 172 |
+
if time_window <= datetime.timedelta(0):
|
| 173 |
+
raise ValueError("time_window argument must be a positive timedelta")
|
| 174 |
+
|
| 175 |
+
self._time_window = time_window
|
| 176 |
+
self._access_limit = access_limit
|
| 177 |
+
self._past_entries = collections.deque(
|
| 178 |
+
maxlen=access_limit
|
| 179 |
+
) # least recent first
|
| 180 |
+
self._entry_lock = threading.Lock()
|
| 181 |
+
|
| 182 |
+
def __enter__(self):
|
| 183 |
+
with self._entry_lock:
|
| 184 |
+
cutoff_time = datetime.datetime.now() - self._time_window
|
| 185 |
+
|
| 186 |
+
# drop the entries that are too old, as they are no longer relevant
|
| 187 |
+
while self._past_entries and self._past_entries[0] < cutoff_time:
|
| 188 |
+
self._past_entries.popleft()
|
| 189 |
+
|
| 190 |
+
if len(self._past_entries) < self._access_limit:
|
| 191 |
+
self._past_entries.append(datetime.datetime.now())
|
| 192 |
+
return 0.0 # no waiting was needed
|
| 193 |
+
|
| 194 |
+
to_wait = (self._past_entries[0] - cutoff_time).total_seconds()
|
| 195 |
+
time.sleep(to_wait)
|
| 196 |
+
|
| 197 |
+
self._past_entries.append(datetime.datetime.now())
|
| 198 |
+
return to_wait
|
| 199 |
+
|
| 200 |
+
def __exit__(self, *_):
|
| 201 |
+
pass
|
| 202 |
+
|
| 203 |
+
def __repr__(self):
|
| 204 |
+
return "{}(access_limit={}, time_window={})".format(
|
| 205 |
+
self.__class__.__name__, self._access_limit, repr(self._time_window)
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class BidiRpc(BidiRpcBase):
|
| 210 |
+
"""A helper for consuming a bi-directional streaming RPC.
|
| 211 |
+
|
| 212 |
+
This maps gRPC's built-in interface which uses a request iterator and a
|
| 213 |
+
response iterator into a socket-like :func:`send` and :func:`recv`. This
|
| 214 |
+
is a more useful pattern for long-running or asymmetric streams (streams
|
| 215 |
+
where there is not a direct correlation between the requests and
|
| 216 |
+
responses).
|
| 217 |
+
|
| 218 |
+
Example::
|
| 219 |
+
|
| 220 |
+
initial_request = example_pb2.StreamingRpcRequest(
|
| 221 |
+
setting='example')
|
| 222 |
+
rpc = BidiRpc(
|
| 223 |
+
stub.StreamingRpc,
|
| 224 |
+
initial_request=initial_request,
|
| 225 |
+
metadata=[('name', 'value')]
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
rpc.open()
|
| 229 |
+
|
| 230 |
+
while rpc.is_active():
|
| 231 |
+
print(rpc.recv())
|
| 232 |
+
rpc.send(example_pb2.StreamingRpcRequest(
|
| 233 |
+
data='example'))
|
| 234 |
+
|
| 235 |
+
rpc.close()
|
| 236 |
+
|
| 237 |
+
This does *not* retry the stream on errors. See :class:`ResumableBidiRpc`.
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
start_rpc (grpc.StreamStreamMultiCallable): The gRPC method used to
|
| 241 |
+
start the RPC.
|
| 242 |
+
initial_request (Union[protobuf.Message,
|
| 243 |
+
Callable[None, protobuf.Message]]): The initial request to
|
| 244 |
+
yield. This is useful if an initial request is needed to start the
|
| 245 |
+
stream.
|
| 246 |
+
metadata (Sequence[Tuple(str, str)]): RPC metadata to include in
|
| 247 |
+
the request.
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
def _create_queue(self):
|
| 251 |
+
"""Create a queue for requests."""
|
| 252 |
+
return queue_module.Queue()
|
| 253 |
+
|
| 254 |
+
def open(self):
|
| 255 |
+
"""Opens the stream."""
|
| 256 |
+
if self.is_active:
|
| 257 |
+
raise ValueError("Cannot open an already open stream.")
|
| 258 |
+
|
| 259 |
+
request_generator = _RequestQueueGenerator(
|
| 260 |
+
self._request_queue, initial_request=self._initial_request
|
| 261 |
+
)
|
| 262 |
+
try:
|
| 263 |
+
call = self._start_rpc(iter(request_generator), metadata=self._rpc_metadata)
|
| 264 |
+
except exceptions.GoogleAPICallError as exc:
|
| 265 |
+
# The original `grpc.RpcError` (which is usually also a `grpc.Call`) is
|
| 266 |
+
# available from the ``response`` property on the mapped exception.
|
| 267 |
+
self._on_call_done(exc.response)
|
| 268 |
+
raise
|
| 269 |
+
|
| 270 |
+
request_generator.call = call
|
| 271 |
+
|
| 272 |
+
# TODO: api_core should expose the future interface for wrapped
|
| 273 |
+
# callables as well.
|
| 274 |
+
if hasattr(call, "_wrapped"): # pragma: NO COVER
|
| 275 |
+
call._wrapped.add_done_callback(self._on_call_done)
|
| 276 |
+
else:
|
| 277 |
+
call.add_done_callback(self._on_call_done)
|
| 278 |
+
|
| 279 |
+
self._request_generator = request_generator
|
| 280 |
+
self.call = call
|
| 281 |
+
|
| 282 |
+
def close(self):
|
| 283 |
+
"""Closes the stream."""
|
| 284 |
+
if self.call is not None:
|
| 285 |
+
self.call.cancel()
|
| 286 |
+
|
| 287 |
+
# Put None in request queue to signal termination.
|
| 288 |
+
self._request_queue.put(None)
|
| 289 |
+
self._request_generator = None
|
| 290 |
+
self._initial_request = None
|
| 291 |
+
self._callbacks = []
|
| 292 |
+
# Don't set self.call to None. Keep it around so that send/recv can
|
| 293 |
+
# raise the error.
|
| 294 |
+
|
| 295 |
+
def send(self, request):
|
| 296 |
+
"""Queue a message to be sent on the stream.
|
| 297 |
+
|
| 298 |
+
Send is non-blocking.
|
| 299 |
+
|
| 300 |
+
If the underlying RPC has been closed, this will raise.
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
request (protobuf.Message): The request to send.
|
| 304 |
+
"""
|
| 305 |
+
if self.call is None:
|
| 306 |
+
raise ValueError("Cannot send on an RPC stream that has never been opened.")
|
| 307 |
+
|
| 308 |
+
# Don't use self.is_active(), as ResumableBidiRpc will overload it
|
| 309 |
+
# to mean something semantically different.
|
| 310 |
+
if self.call.is_active():
|
| 311 |
+
self._request_queue.put(request)
|
| 312 |
+
else:
|
| 313 |
+
# calling next should cause the call to raise.
|
| 314 |
+
next(self.call)
|
| 315 |
+
|
| 316 |
+
def recv(self):
|
| 317 |
+
"""Wait for a message to be returned from the stream.
|
| 318 |
+
|
| 319 |
+
Recv is blocking.
|
| 320 |
+
|
| 321 |
+
If the underlying RPC has been closed, this will raise.
|
| 322 |
+
|
| 323 |
+
Returns:
|
| 324 |
+
protobuf.Message: The received message.
|
| 325 |
+
"""
|
| 326 |
+
if self.call is None:
|
| 327 |
+
raise ValueError("Cannot recv on an RPC stream that has never been opened.")
|
| 328 |
+
|
| 329 |
+
return next(self.call)
|
| 330 |
+
|
| 331 |
+
@property
|
| 332 |
+
def is_active(self):
|
| 333 |
+
"""True if this stream is currently open and active."""
|
| 334 |
+
return self.call is not None and self.call.is_active()
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _never_terminate(future_or_error):
|
| 338 |
+
"""By default, no errors cause BiDi termination."""
|
| 339 |
+
return False
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
class ResumableBidiRpc(BidiRpc):
|
| 343 |
+
"""A :class:`BidiRpc` that can automatically resume the stream on errors.
|
| 344 |
+
|
| 345 |
+
It uses the ``should_recover`` arg to determine if it should re-establish
|
| 346 |
+
the stream on error.
|
| 347 |
+
|
| 348 |
+
Example::
|
| 349 |
+
|
| 350 |
+
def should_recover(exc):
|
| 351 |
+
return (
|
| 352 |
+
isinstance(exc, grpc.RpcError) and
|
| 353 |
+
exc.code() == grpc.StatusCode.UNAVAILABLE)
|
| 354 |
+
|
| 355 |
+
initial_request = example_pb2.StreamingRpcRequest(
|
| 356 |
+
setting='example')
|
| 357 |
+
|
| 358 |
+
metadata = [('header_name', 'value')]
|
| 359 |
+
|
| 360 |
+
rpc = ResumableBidiRpc(
|
| 361 |
+
stub.StreamingRpc,
|
| 362 |
+
should_recover=should_recover,
|
| 363 |
+
initial_request=initial_request,
|
| 364 |
+
metadata=metadata
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
rpc.open()
|
| 368 |
+
|
| 369 |
+
while rpc.is_active():
|
| 370 |
+
print(rpc.recv())
|
| 371 |
+
rpc.send(example_pb2.StreamingRpcRequest(
|
| 372 |
+
data='example'))
|
| 373 |
+
|
| 374 |
+
Args:
|
| 375 |
+
start_rpc (grpc.StreamStreamMultiCallable): The gRPC method used to
|
| 376 |
+
start the RPC.
|
| 377 |
+
initial_request (Union[protobuf.Message,
|
| 378 |
+
Callable[None, protobuf.Message]]): The initial request to
|
| 379 |
+
yield. This is useful if an initial request is needed to start the
|
| 380 |
+
stream.
|
| 381 |
+
should_recover (Callable[[Exception], bool]): A function that returns
|
| 382 |
+
True if the stream should be recovered. This will be called
|
| 383 |
+
whenever an error is encountered on the stream.
|
| 384 |
+
should_terminate (Callable[[Exception], bool]): A function that returns
|
| 385 |
+
True if the stream should be terminated. This will be called
|
| 386 |
+
whenever an error is encountered on the stream.
|
| 387 |
+
metadata Sequence[Tuple(str, str)]: RPC metadata to include in
|
| 388 |
+
the request.
|
| 389 |
+
throttle_reopen (bool): If ``True``, throttling will be applied to
|
| 390 |
+
stream reopen calls. Defaults to ``False``.
|
| 391 |
+
"""
|
| 392 |
+
|
| 393 |
+
def __init__(
|
| 394 |
+
self,
|
| 395 |
+
start_rpc,
|
| 396 |
+
should_recover,
|
| 397 |
+
should_terminate=_never_terminate,
|
| 398 |
+
initial_request=None,
|
| 399 |
+
metadata=None,
|
| 400 |
+
throttle_reopen=False,
|
| 401 |
+
):
|
| 402 |
+
super(ResumableBidiRpc, self).__init__(start_rpc, initial_request, metadata)
|
| 403 |
+
self._should_recover = should_recover
|
| 404 |
+
self._should_terminate = should_terminate
|
| 405 |
+
self._operational_lock = threading.RLock()
|
| 406 |
+
self._finalized = False
|
| 407 |
+
self._finalize_lock = threading.Lock()
|
| 408 |
+
|
| 409 |
+
if throttle_reopen:
|
| 410 |
+
self._reopen_throttle = _Throttle(
|
| 411 |
+
access_limit=5, time_window=datetime.timedelta(seconds=10)
|
| 412 |
+
)
|
| 413 |
+
else:
|
| 414 |
+
self._reopen_throttle = None
|
| 415 |
+
|
| 416 |
+
def _finalize(self, result):
|
| 417 |
+
with self._finalize_lock:
|
| 418 |
+
if self._finalized:
|
| 419 |
+
return
|
| 420 |
+
|
| 421 |
+
for callback in self._callbacks:
|
| 422 |
+
callback(result)
|
| 423 |
+
|
| 424 |
+
self._finalized = True
|
| 425 |
+
|
| 426 |
+
def _on_call_done(self, future):
|
| 427 |
+
# Unlike the base class, we only execute the callbacks on a terminal
|
| 428 |
+
# error, not for errors that we can recover from. Note that grpc's
|
| 429 |
+
# "future" here is also a grpc.RpcError.
|
| 430 |
+
with self._operational_lock:
|
| 431 |
+
if self._should_terminate(future):
|
| 432 |
+
self._finalize(future)
|
| 433 |
+
elif not self._should_recover(future):
|
| 434 |
+
self._finalize(future)
|
| 435 |
+
else:
|
| 436 |
+
_LOGGER.debug("Re-opening stream from gRPC callback.")
|
| 437 |
+
self._reopen()
|
| 438 |
+
|
| 439 |
+
def _reopen(self):
|
| 440 |
+
with self._operational_lock:
|
| 441 |
+
# Another thread already managed to re-open this stream.
|
| 442 |
+
if self.call is not None and self.call.is_active():
|
| 443 |
+
_LOGGER.debug("Stream was already re-established.")
|
| 444 |
+
return
|
| 445 |
+
|
| 446 |
+
self.call = None
|
| 447 |
+
# Request generator should exit cleanly since the RPC its bound to
|
| 448 |
+
# has exited.
|
| 449 |
+
self._request_generator = None
|
| 450 |
+
|
| 451 |
+
# Note: we do not currently do any sort of backoff here. The
|
| 452 |
+
# assumption is that re-establishing the stream under normal
|
| 453 |
+
# circumstances will happen in intervals greater than 60s.
|
| 454 |
+
# However, it is possible in a degenerative case that the server
|
| 455 |
+
# closes the stream rapidly which would lead to thrashing here,
|
| 456 |
+
# but hopefully in those cases the server would return a non-
|
| 457 |
+
# retryable error.
|
| 458 |
+
|
| 459 |
+
try:
|
| 460 |
+
if self._reopen_throttle:
|
| 461 |
+
with self._reopen_throttle:
|
| 462 |
+
self.open()
|
| 463 |
+
else:
|
| 464 |
+
self.open()
|
| 465 |
+
# If re-opening or re-calling the method fails for any reason,
|
| 466 |
+
# consider it a terminal error and finalize the stream.
|
| 467 |
+
except Exception as exc:
|
| 468 |
+
_LOGGER.debug("Failed to re-open stream due to %s", exc)
|
| 469 |
+
self._finalize(exc)
|
| 470 |
+
raise
|
| 471 |
+
|
| 472 |
+
_LOGGER.info("Re-established stream")
|
| 473 |
+
|
| 474 |
+
def _recoverable(self, method, *args, **kwargs):
|
| 475 |
+
"""Wraps a method to recover the stream and retry on error.
|
| 476 |
+
|
| 477 |
+
If a retryable error occurs while making the call, then the stream will
|
| 478 |
+
be re-opened and the method will be retried. This happens indefinitely
|
| 479 |
+
so long as the error is a retryable one. If an error occurs while
|
| 480 |
+
re-opening the stream, then this method will raise immediately and
|
| 481 |
+
trigger finalization of this object.
|
| 482 |
+
|
| 483 |
+
Args:
|
| 484 |
+
method (Callable[..., Any]): The method to call.
|
| 485 |
+
args: The args to pass to the method.
|
| 486 |
+
kwargs: The kwargs to pass to the method.
|
| 487 |
+
"""
|
| 488 |
+
while True:
|
| 489 |
+
try:
|
| 490 |
+
return method(*args, **kwargs)
|
| 491 |
+
|
| 492 |
+
except Exception as exc:
|
| 493 |
+
with self._operational_lock:
|
| 494 |
+
_LOGGER.debug("Call to retryable %r caused %s.", method, exc)
|
| 495 |
+
|
| 496 |
+
if self._should_terminate(exc):
|
| 497 |
+
self.close()
|
| 498 |
+
_LOGGER.debug("Terminating %r due to %s.", method, exc)
|
| 499 |
+
self._finalize(exc)
|
| 500 |
+
break
|
| 501 |
+
|
| 502 |
+
if not self._should_recover(exc):
|
| 503 |
+
self.close()
|
| 504 |
+
_LOGGER.debug("Not retrying %r due to %s.", method, exc)
|
| 505 |
+
self._finalize(exc)
|
| 506 |
+
raise exc
|
| 507 |
+
|
| 508 |
+
_LOGGER.debug("Re-opening stream from retryable %r.", method)
|
| 509 |
+
self._reopen()
|
| 510 |
+
|
| 511 |
+
def _send(self, request):
|
| 512 |
+
# Grab a reference to the RPC call. Because another thread (notably
|
| 513 |
+
# the gRPC error thread) can modify self.call (by invoking reopen),
|
| 514 |
+
# we should ensure our reference can not change underneath us.
|
| 515 |
+
# If self.call is modified (such as replaced with a new RPC call) then
|
| 516 |
+
# this will use the "old" RPC, which should result in the same
|
| 517 |
+
# exception passed into gRPC's error handler being raised here, which
|
| 518 |
+
# will be handled by the usual error handling in retryable.
|
| 519 |
+
with self._operational_lock:
|
| 520 |
+
call = self.call
|
| 521 |
+
|
| 522 |
+
if call is None:
|
| 523 |
+
raise ValueError("Cannot send on an RPC that has never been opened.")
|
| 524 |
+
|
| 525 |
+
# Don't use self.is_active(), as ResumableBidiRpc will overload it
|
| 526 |
+
# to mean something semantically different.
|
| 527 |
+
if call.is_active():
|
| 528 |
+
self._request_queue.put(request)
|
| 529 |
+
pass
|
| 530 |
+
else:
|
| 531 |
+
# calling next should cause the call to raise.
|
| 532 |
+
next(call)
|
| 533 |
+
|
| 534 |
+
def send(self, request):
|
| 535 |
+
return self._recoverable(self._send, request)
|
| 536 |
+
|
| 537 |
+
def _recv(self):
|
| 538 |
+
with self._operational_lock:
|
| 539 |
+
call = self.call
|
| 540 |
+
|
| 541 |
+
if call is None:
|
| 542 |
+
raise ValueError("Cannot recv on an RPC that has never been opened.")
|
| 543 |
+
|
| 544 |
+
return next(call)
|
| 545 |
+
|
| 546 |
+
def recv(self):
|
| 547 |
+
return self._recoverable(self._recv)
|
| 548 |
+
|
| 549 |
+
def close(self):
|
| 550 |
+
self._finalize(None)
|
| 551 |
+
super(ResumableBidiRpc, self).close()
|
| 552 |
+
|
| 553 |
+
@property
|
| 554 |
+
def is_active(self):
|
| 555 |
+
"""bool: True if this stream is currently open and active."""
|
| 556 |
+
# Use the operational lock. It's entirely possible for something
|
| 557 |
+
# to check the active state *while* the RPC is being retried.
|
| 558 |
+
# Also, use finalized to track the actual terminal state here.
|
| 559 |
+
# This is because if the stream is re-established by the gRPC thread
|
| 560 |
+
# it's technically possible to check this between when gRPC marks the
|
| 561 |
+
# RPC as inactive and when gRPC executes our callback that re-opens
|
| 562 |
+
# the stream.
|
| 563 |
+
with self._operational_lock:
|
| 564 |
+
return self.call is not None and not self._finalized
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
class BackgroundConsumer(object):
|
| 568 |
+
"""A bi-directional stream consumer that runs in a separate thread.
|
| 569 |
+
|
| 570 |
+
This maps the consumption of a stream into a callback-based model. It also
|
| 571 |
+
provides :func:`pause` and :func:`resume` to allow for flow-control.
|
| 572 |
+
|
| 573 |
+
Example::
|
| 574 |
+
|
| 575 |
+
def should_recover(exc):
|
| 576 |
+
return (
|
| 577 |
+
isinstance(exc, grpc.RpcError) and
|
| 578 |
+
exc.code() == grpc.StatusCode.UNAVAILABLE)
|
| 579 |
+
|
| 580 |
+
initial_request = example_pb2.StreamingRpcRequest(
|
| 581 |
+
setting='example')
|
| 582 |
+
|
| 583 |
+
rpc = ResumeableBidiRpc(
|
| 584 |
+
stub.StreamingRpc,
|
| 585 |
+
initial_request=initial_request,
|
| 586 |
+
should_recover=should_recover)
|
| 587 |
+
|
| 588 |
+
def on_response(response):
|
| 589 |
+
print(response)
|
| 590 |
+
|
| 591 |
+
consumer = BackgroundConsumer(rpc, on_response)
|
| 592 |
+
consumer.start()
|
| 593 |
+
|
| 594 |
+
Note that error handling *must* be done by using the provided
|
| 595 |
+
``bidi_rpc``'s ``add_done_callback``. This helper will automatically exit
|
| 596 |
+
whenever the RPC itself exits and will not provide any error details.
|
| 597 |
+
|
| 598 |
+
Args:
|
| 599 |
+
bidi_rpc (BidiRpc): The RPC to consume. Should not have been
|
| 600 |
+
``open()``ed yet.
|
| 601 |
+
on_response (Callable[[protobuf.Message], None]): The callback to
|
| 602 |
+
be called for every response on the stream.
|
| 603 |
+
on_fatal_exception (Callable[[Exception], None]): The callback to
|
| 604 |
+
be called on fatal errors during consumption. Default None.
|
| 605 |
+
"""
|
| 606 |
+
|
| 607 |
+
def __init__(self, bidi_rpc, on_response, on_fatal_exception=None):
|
| 608 |
+
self._bidi_rpc = bidi_rpc
|
| 609 |
+
self._on_response = on_response
|
| 610 |
+
self._paused = False
|
| 611 |
+
self._on_fatal_exception = on_fatal_exception
|
| 612 |
+
self._wake = threading.Condition()
|
| 613 |
+
self._thread = None
|
| 614 |
+
self._operational_lock = threading.Lock()
|
| 615 |
+
|
| 616 |
+
def _on_call_done(self, future):
|
| 617 |
+
# Resume the thread if it's paused, this prevents blocking forever
|
| 618 |
+
# when the RPC has terminated.
|
| 619 |
+
self.resume()
|
| 620 |
+
|
| 621 |
+
def _thread_main(self, ready):
|
| 622 |
+
try:
|
| 623 |
+
ready.set()
|
| 624 |
+
self._bidi_rpc.add_done_callback(self._on_call_done)
|
| 625 |
+
self._bidi_rpc.open()
|
| 626 |
+
|
| 627 |
+
while self._bidi_rpc.is_active:
|
| 628 |
+
# Do not allow the paused status to change at all during this
|
| 629 |
+
# section. There is a condition where we could be resumed
|
| 630 |
+
# between checking if we are paused and calling wake.wait(),
|
| 631 |
+
# which means that we will miss the notification to wake up
|
| 632 |
+
# (oops!) and wait for a notification that will never come.
|
| 633 |
+
# Keeping the lock throughout avoids that.
|
| 634 |
+
# In the future, we could use `Condition.wait_for` if we drop
|
| 635 |
+
# Python 2.7.
|
| 636 |
+
# See: https://github.com/googleapis/python-api-core/issues/211
|
| 637 |
+
with self._wake:
|
| 638 |
+
while self._paused:
|
| 639 |
+
_LOGGER.debug("paused, waiting for waking.")
|
| 640 |
+
self._wake.wait()
|
| 641 |
+
_LOGGER.debug("woken.")
|
| 642 |
+
|
| 643 |
+
_LOGGER.debug("waiting for recv.")
|
| 644 |
+
response = self._bidi_rpc.recv()
|
| 645 |
+
_LOGGER.debug("recved response.")
|
| 646 |
+
if self._on_response is not None:
|
| 647 |
+
self._on_response(response)
|
| 648 |
+
|
| 649 |
+
except exceptions.GoogleAPICallError as exc:
|
| 650 |
+
_LOGGER.debug(
|
| 651 |
+
"%s caught error %s and will exit. Generally this is due to "
|
| 652 |
+
"the RPC itself being cancelled and the error will be "
|
| 653 |
+
"surfaced to the calling code.",
|
| 654 |
+
_BIDIRECTIONAL_CONSUMER_NAME,
|
| 655 |
+
exc,
|
| 656 |
+
exc_info=True,
|
| 657 |
+
)
|
| 658 |
+
if self._on_fatal_exception is not None:
|
| 659 |
+
self._on_fatal_exception(exc)
|
| 660 |
+
|
| 661 |
+
except Exception as exc:
|
| 662 |
+
_LOGGER.exception(
|
| 663 |
+
"%s caught unexpected exception %s and will exit.",
|
| 664 |
+
_BIDIRECTIONAL_CONSUMER_NAME,
|
| 665 |
+
exc,
|
| 666 |
+
)
|
| 667 |
+
if self._on_fatal_exception is not None:
|
| 668 |
+
self._on_fatal_exception(exc)
|
| 669 |
+
|
| 670 |
+
_LOGGER.info("%s exiting", _BIDIRECTIONAL_CONSUMER_NAME)
|
| 671 |
+
|
| 672 |
+
def start(self):
|
| 673 |
+
"""Start the background thread and begin consuming the thread."""
|
| 674 |
+
with self._operational_lock:
|
| 675 |
+
ready = threading.Event()
|
| 676 |
+
thread = threading.Thread(
|
| 677 |
+
name=_BIDIRECTIONAL_CONSUMER_NAME,
|
| 678 |
+
target=self._thread_main,
|
| 679 |
+
args=(ready,),
|
| 680 |
+
daemon=True,
|
| 681 |
+
)
|
| 682 |
+
thread.start()
|
| 683 |
+
# Other parts of the code rely on `thread.is_alive` which
|
| 684 |
+
# isn't sufficient to know if a thread is active, just that it may
|
| 685 |
+
# soon be active. This can cause races. Further protect
|
| 686 |
+
# against races by using a ready event and wait on it to be set.
|
| 687 |
+
ready.wait()
|
| 688 |
+
self._thread = thread
|
| 689 |
+
_LOGGER.debug("Started helper thread %s", thread.name)
|
| 690 |
+
|
| 691 |
+
def stop(self):
|
| 692 |
+
"""Stop consuming the stream and shutdown the background thread.
|
| 693 |
+
|
| 694 |
+
NOTE: Cannot be called within `_thread_main`, since it is not
|
| 695 |
+
possible to join a thread to itself.
|
| 696 |
+
"""
|
| 697 |
+
with self._operational_lock:
|
| 698 |
+
self._bidi_rpc.close()
|
| 699 |
+
|
| 700 |
+
if self._thread is not None:
|
| 701 |
+
# Resume the thread to wake it up in case it is sleeping.
|
| 702 |
+
self.resume()
|
| 703 |
+
# The daemonized thread may itself block, so don't wait
|
| 704 |
+
# for it longer than a second.
|
| 705 |
+
self._thread.join(1.0)
|
| 706 |
+
if self._thread.is_alive(): # pragma: NO COVER
|
| 707 |
+
_LOGGER.warning("Background thread did not exit.")
|
| 708 |
+
|
| 709 |
+
self._thread = None
|
| 710 |
+
self._on_response = None
|
| 711 |
+
self._on_fatal_exception = None
|
| 712 |
+
|
| 713 |
+
@property
|
| 714 |
+
def is_active(self):
|
| 715 |
+
"""bool: True if the background thread is active."""
|
| 716 |
+
return self._thread is not None and self._thread.is_alive()
|
| 717 |
+
|
| 718 |
+
def pause(self):
|
| 719 |
+
"""Pauses the response stream.
|
| 720 |
+
|
| 721 |
+
This does *not* pause the request stream.
|
| 722 |
+
"""
|
| 723 |
+
with self._wake:
|
| 724 |
+
self._paused = True
|
| 725 |
+
|
| 726 |
+
def resume(self):
|
| 727 |
+
"""Resumes the response stream."""
|
| 728 |
+
with self._wake:
|
| 729 |
+
self._paused = False
|
| 730 |
+
self._wake.notify_all()
|
| 731 |
+
|
| 732 |
+
@property
|
| 733 |
+
def is_paused(self):
|
| 734 |
+
"""bool: True if the response stream is paused."""
|
| 735 |
+
return self._paused
|
py311/lib/python3.11/site-packages/google/api_core/bidi_async.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025, Google LLC
|
| 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 |
+
# https://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 |
+
"""Asynchronous bi-directional streaming RPC helpers."""
|
| 16 |
+
|
| 17 |
+
import asyncio
|
| 18 |
+
import logging
|
| 19 |
+
from typing import Callable, Optional, Union
|
| 20 |
+
|
| 21 |
+
from grpc import aio
|
| 22 |
+
|
| 23 |
+
from google.api_core import exceptions
|
| 24 |
+
from google.api_core.bidi_base import BidiRpcBase
|
| 25 |
+
|
| 26 |
+
from google.protobuf.message import Message as ProtobufMessage
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
_LOGGER = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class _AsyncRequestQueueGenerator:
|
| 33 |
+
"""_AsyncRequestQueueGenerator is a helper class for sending asynchronous
|
| 34 |
+
requests to a gRPC stream from a Queue.
|
| 35 |
+
|
| 36 |
+
This generator takes asynchronous requests off a given `asyncio.Queue` and
|
| 37 |
+
yields them to gRPC.
|
| 38 |
+
|
| 39 |
+
It's useful when you have an indeterminate, indefinite, or otherwise
|
| 40 |
+
open-ended set of requests to send through a request-streaming (or
|
| 41 |
+
bidirectional) RPC.
|
| 42 |
+
|
| 43 |
+
Example::
|
| 44 |
+
|
| 45 |
+
requests = _AsyncRequestQueueGenerator(q)
|
| 46 |
+
call = await stub.StreamingRequest(requests)
|
| 47 |
+
requests.call = call
|
| 48 |
+
|
| 49 |
+
async for response in call:
|
| 50 |
+
print(response)
|
| 51 |
+
await q.put(...)
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
queue (asyncio.Queue): The request queue.
|
| 55 |
+
initial_request (Union[ProtobufMessage,
|
| 56 |
+
Callable[[], ProtobufMessage]]): The initial request to
|
| 57 |
+
yield. This is done independently of the request queue to allow for
|
| 58 |
+
easily restarting streams that require some initial configuration
|
| 59 |
+
request.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
def __init__(
|
| 63 |
+
self,
|
| 64 |
+
queue: asyncio.Queue,
|
| 65 |
+
initial_request: Optional[
|
| 66 |
+
Union[ProtobufMessage, Callable[[], ProtobufMessage]]
|
| 67 |
+
] = None,
|
| 68 |
+
) -> None:
|
| 69 |
+
self._queue = queue
|
| 70 |
+
self._initial_request = initial_request
|
| 71 |
+
self.call: Optional[aio.Call] = None
|
| 72 |
+
|
| 73 |
+
def _is_active(self) -> bool:
|
| 74 |
+
"""Returns true if the call is not set or not completed."""
|
| 75 |
+
# Note: there is a possibility that this starts *before* the call
|
| 76 |
+
# property is set. So we have to check if self.call is set before
|
| 77 |
+
# seeing if it's active. We need to return True if self.call is None.
|
| 78 |
+
# See https://github.com/googleapis/python-api-core/issues/560.
|
| 79 |
+
return self.call is None or not self.call.done()
|
| 80 |
+
|
| 81 |
+
async def __aiter__(self):
|
| 82 |
+
# The reason this is necessary is because it lets the user have
|
| 83 |
+
# control on when they would want to send requests proto messages
|
| 84 |
+
# instead of sending all of them initially.
|
| 85 |
+
#
|
| 86 |
+
# This is achieved via asynchronous queue (asyncio.Queue),
|
| 87 |
+
# gRPC awaits until there's a message in the queue.
|
| 88 |
+
#
|
| 89 |
+
# Finally, it allows for retrying without swapping queues because if
|
| 90 |
+
# it does pull an item off the queue when the RPC is inactive, it'll
|
| 91 |
+
# immediately put it back and then exit. This is necessary because
|
| 92 |
+
# yielding the item in this case will cause gRPC to discard it. In
|
| 93 |
+
# practice, this means that the order of messages is not guaranteed.
|
| 94 |
+
# If preserving order is necessary it would be easy to use a priority
|
| 95 |
+
# queue.
|
| 96 |
+
if self._initial_request is not None:
|
| 97 |
+
if callable(self._initial_request):
|
| 98 |
+
yield self._initial_request()
|
| 99 |
+
else:
|
| 100 |
+
yield self._initial_request
|
| 101 |
+
|
| 102 |
+
while True:
|
| 103 |
+
item = await self._queue.get()
|
| 104 |
+
|
| 105 |
+
# The consumer explicitly sent "None", indicating that the request
|
| 106 |
+
# should end.
|
| 107 |
+
if item is None:
|
| 108 |
+
_LOGGER.debug("Cleanly exiting request generator.")
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
if not self._is_active():
|
| 112 |
+
# We have an item, but the call is closed. We should put the
|
| 113 |
+
# item back on the queue so that the next call can consume it.
|
| 114 |
+
await self._queue.put(item)
|
| 115 |
+
_LOGGER.debug(
|
| 116 |
+
"Inactive call, replacing item on queue and exiting "
|
| 117 |
+
"request generator."
|
| 118 |
+
)
|
| 119 |
+
return
|
| 120 |
+
|
| 121 |
+
yield item
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class AsyncBidiRpc(BidiRpcBase):
|
| 125 |
+
"""A helper for consuming a async bi-directional streaming RPC.
|
| 126 |
+
|
| 127 |
+
This maps gRPC's built-in interface which uses a request iterator and a
|
| 128 |
+
response iterator into a socket-like :func:`send` and :func:`recv`. This
|
| 129 |
+
is a more useful pattern for long-running or asymmetric streams (streams
|
| 130 |
+
where there is not a direct correlation between the requests and
|
| 131 |
+
responses).
|
| 132 |
+
|
| 133 |
+
Example::
|
| 134 |
+
|
| 135 |
+
initial_request = example_pb2.StreamingRpcRequest(
|
| 136 |
+
setting='example')
|
| 137 |
+
rpc = AsyncBidiRpc(
|
| 138 |
+
stub.StreamingRpc,
|
| 139 |
+
initial_request=initial_request,
|
| 140 |
+
metadata=[('name', 'value')]
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
await rpc.open()
|
| 144 |
+
|
| 145 |
+
while rpc.is_active:
|
| 146 |
+
print(await rpc.recv())
|
| 147 |
+
await rpc.send(example_pb2.StreamingRpcRequest(
|
| 148 |
+
data='example'))
|
| 149 |
+
|
| 150 |
+
await rpc.close()
|
| 151 |
+
|
| 152 |
+
This does *not* retry the stream on errors.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
start_rpc (grpc.aio.StreamStreamMultiCallable): The gRPC method used to
|
| 156 |
+
start the RPC.
|
| 157 |
+
initial_request (Union[ProtobufMessage,
|
| 158 |
+
Callable[[], ProtobufMessage]]): The initial request to
|
| 159 |
+
yield. This is useful if an initial request is needed to start the
|
| 160 |
+
stream.
|
| 161 |
+
metadata (Sequence[Tuple(str, str)]): RPC metadata to include in
|
| 162 |
+
the request.
|
| 163 |
+
"""
|
| 164 |
+
|
| 165 |
+
def _create_queue(self) -> asyncio.Queue:
|
| 166 |
+
"""Create a queue for requests."""
|
| 167 |
+
return asyncio.Queue()
|
| 168 |
+
|
| 169 |
+
async def open(self) -> None:
|
| 170 |
+
"""Opens the stream."""
|
| 171 |
+
if self.is_active:
|
| 172 |
+
raise ValueError("Cannot open an already open stream.")
|
| 173 |
+
|
| 174 |
+
request_generator = _AsyncRequestQueueGenerator(
|
| 175 |
+
self._request_queue, initial_request=self._initial_request
|
| 176 |
+
)
|
| 177 |
+
try:
|
| 178 |
+
call = await self._start_rpc(request_generator, metadata=self._rpc_metadata)
|
| 179 |
+
except exceptions.GoogleAPICallError as exc:
|
| 180 |
+
# The original `grpc.aio.AioRpcError` (which is usually also a
|
| 181 |
+
# `grpc.aio.Call`) is available from the ``response`` property on
|
| 182 |
+
# the mapped exception.
|
| 183 |
+
self._on_call_done(exc.response)
|
| 184 |
+
raise
|
| 185 |
+
|
| 186 |
+
request_generator.call = call
|
| 187 |
+
|
| 188 |
+
# TODO: api_core should expose the future interface for wrapped
|
| 189 |
+
# callables as well.
|
| 190 |
+
if hasattr(call, "_wrapped"): # pragma: NO COVER
|
| 191 |
+
call._wrapped.add_done_callback(self._on_call_done)
|
| 192 |
+
else:
|
| 193 |
+
call.add_done_callback(self._on_call_done)
|
| 194 |
+
|
| 195 |
+
self._request_generator = request_generator
|
| 196 |
+
self.call = call
|
| 197 |
+
|
| 198 |
+
async def close(self) -> None:
|
| 199 |
+
"""Closes the stream."""
|
| 200 |
+
if self.call is not None:
|
| 201 |
+
self.call.cancel()
|
| 202 |
+
|
| 203 |
+
# Put None in request queue to signal termination.
|
| 204 |
+
await self._request_queue.put(None)
|
| 205 |
+
self._request_generator = None
|
| 206 |
+
self._initial_request = None
|
| 207 |
+
self._callbacks = []
|
| 208 |
+
# Don't set self.call to None. Keep it around so that send/recv can
|
| 209 |
+
# raise the error.
|
| 210 |
+
|
| 211 |
+
async def send(self, request: ProtobufMessage) -> None:
|
| 212 |
+
"""Queue a message to be sent on the stream.
|
| 213 |
+
|
| 214 |
+
If the underlying RPC has been closed, this will raise.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
request (ProtobufMessage): The request to send.
|
| 218 |
+
"""
|
| 219 |
+
if self.call is None:
|
| 220 |
+
raise ValueError("Cannot send on an RPC stream that has never been opened.")
|
| 221 |
+
|
| 222 |
+
if not self.call.done():
|
| 223 |
+
await self._request_queue.put(request)
|
| 224 |
+
else:
|
| 225 |
+
# calling read should cause the call to raise.
|
| 226 |
+
await self.call.read()
|
| 227 |
+
|
| 228 |
+
async def recv(self) -> ProtobufMessage:
|
| 229 |
+
"""Wait for a message to be returned from the stream.
|
| 230 |
+
|
| 231 |
+
If the underlying RPC has been closed, this will raise.
|
| 232 |
+
|
| 233 |
+
Returns:
|
| 234 |
+
ProtobufMessage: The received message.
|
| 235 |
+
"""
|
| 236 |
+
if self.call is None:
|
| 237 |
+
raise ValueError("Cannot recv on an RPC stream that has never been opened.")
|
| 238 |
+
|
| 239 |
+
return await self.call.read()
|
| 240 |
+
|
| 241 |
+
@property
|
| 242 |
+
def is_active(self) -> bool:
|
| 243 |
+
"""Whether the stream is currently open and active."""
|
| 244 |
+
return self.call is not None and not self.call.done()
|
py311/lib/python3.11/site-packages/google/api_core/bidi_base.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025, Google LLC
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# You may obtain a copy of the License at
|
| 5 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 6 |
+
#
|
| 7 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 8 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 9 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 10 |
+
# See the License for the specific language governing permissions and
|
| 11 |
+
# limitations under the License.
|
| 12 |
+
|
| 13 |
+
"""Base class for bi-directional streaming RPC helpers."""
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class BidiRpcBase:
|
| 17 |
+
"""A base class for consuming a bi-directional streaming RPC.
|
| 18 |
+
|
| 19 |
+
This maps gRPC's built-in interface which uses a request iterator and a
|
| 20 |
+
response iterator into a socket-like :func:`send` and :func:`recv`. This
|
| 21 |
+
is a more useful pattern for long-running or asymmetric streams (streams
|
| 22 |
+
where there is not a direct correlation between the requests and
|
| 23 |
+
responses).
|
| 24 |
+
|
| 25 |
+
This does *not* retry the stream on errors.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
start_rpc (Union[grpc.StreamStreamMultiCallable,
|
| 29 |
+
grpc.aio.StreamStreamMultiCallable]): The gRPC method used
|
| 30 |
+
to start the RPC.
|
| 31 |
+
initial_request (Union[protobuf.Message,
|
| 32 |
+
Callable[[], protobuf.Message]]): The initial request to
|
| 33 |
+
yield. This is useful if an initial request is needed to start the
|
| 34 |
+
stream.
|
| 35 |
+
metadata (Sequence[Tuple(str, str)]): RPC metadata to include in
|
| 36 |
+
the request.
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self, start_rpc, initial_request=None, metadata=None):
|
| 40 |
+
self._start_rpc = start_rpc
|
| 41 |
+
self._initial_request = initial_request
|
| 42 |
+
self._rpc_metadata = metadata
|
| 43 |
+
self._request_queue = self._create_queue()
|
| 44 |
+
self._request_generator = None
|
| 45 |
+
self._callbacks = []
|
| 46 |
+
self.call = None
|
| 47 |
+
|
| 48 |
+
def _create_queue(self):
|
| 49 |
+
"""Create a queue for requests."""
|
| 50 |
+
raise NotImplementedError("`_create_queue` is not implemented.")
|
| 51 |
+
|
| 52 |
+
def add_done_callback(self, callback):
|
| 53 |
+
"""Adds a callback that will be called when the RPC terminates.
|
| 54 |
+
|
| 55 |
+
This occurs when the RPC errors or is successfully terminated.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
callback (Union[Callable[[grpc.Future], None], Callable[[Any], None]]):
|
| 59 |
+
The callback to execute after gRPC call completed (success or
|
| 60 |
+
failure).
|
| 61 |
+
|
| 62 |
+
For sync streaming gRPC: Callable[[grpc.Future], None]
|
| 63 |
+
|
| 64 |
+
For async streaming gRPC: Callable[[Any], None]
|
| 65 |
+
"""
|
| 66 |
+
self._callbacks.append(callback)
|
| 67 |
+
|
| 68 |
+
def _on_call_done(self, future):
|
| 69 |
+
# This occurs when the RPC errors or is successfully terminated.
|
| 70 |
+
# Note that grpc's "future" here can also be a grpc.RpcError.
|
| 71 |
+
# See note in https://github.com/grpc/grpc/issues/10885#issuecomment-302651331
|
| 72 |
+
# that `grpc.RpcError` is also `grpc.Call`.
|
| 73 |
+
# for asynchronous gRPC call it would be `grpc.aio.AioRpcError`
|
| 74 |
+
|
| 75 |
+
# Note: sync callbacks can be limiting for async code, because you can't
|
| 76 |
+
# await anything in a sync callback.
|
| 77 |
+
for callback in self._callbacks:
|
| 78 |
+
callback(future)
|
| 79 |
+
|
| 80 |
+
@property
|
| 81 |
+
def is_active(self):
|
| 82 |
+
"""True if the gRPC call is not done yet."""
|
| 83 |
+
raise NotImplementedError("`is_active` is not implemented.")
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def pending_requests(self):
|
| 87 |
+
"""Estimate of the number of queued requests."""
|
| 88 |
+
return self._request_queue.qsize()
|
py311/lib/python3.11/site-packages/google/api_core/client_info.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 providing client information.
|
| 16 |
+
|
| 17 |
+
Client information is used to send information about the calling client,
|
| 18 |
+
such as the library and Python version, to API services.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import platform
|
| 22 |
+
from typing import Union
|
| 23 |
+
|
| 24 |
+
from google.api_core import version as api_core_version
|
| 25 |
+
|
| 26 |
+
_PY_VERSION = platform.python_version()
|
| 27 |
+
_API_CORE_VERSION = api_core_version.__version__
|
| 28 |
+
|
| 29 |
+
_GRPC_VERSION: Union[str, None]
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
import grpc
|
| 33 |
+
|
| 34 |
+
_GRPC_VERSION = grpc.__version__
|
| 35 |
+
except ImportError: # pragma: NO COVER
|
| 36 |
+
_GRPC_VERSION = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ClientInfo(object):
|
| 40 |
+
"""Client information used to generate a user-agent for API calls.
|
| 41 |
+
|
| 42 |
+
This user-agent information is sent along with API calls to allow the
|
| 43 |
+
receiving service to do analytics on which versions of Python and Google
|
| 44 |
+
libraries are being used.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
python_version (str): The Python interpreter version, for example,
|
| 48 |
+
``'3.9.6'``.
|
| 49 |
+
grpc_version (Optional[str]): The gRPC library version.
|
| 50 |
+
api_core_version (str): The google-api-core library version.
|
| 51 |
+
gapic_version (Optional[str]): The version of gapic-generated client
|
| 52 |
+
library, if the library was generated by gapic.
|
| 53 |
+
client_library_version (Optional[str]): The version of the client
|
| 54 |
+
library, generally used if the client library was not generated
|
| 55 |
+
by gapic or if additional functionality was built on top of
|
| 56 |
+
a gapic client library.
|
| 57 |
+
user_agent (Optional[str]): Prefix to the user agent header. This is
|
| 58 |
+
used to supply information such as application name or partner tool.
|
| 59 |
+
Recommended format: ``application-or-tool-ID/major.minor.version``.
|
| 60 |
+
rest_version (Optional[str]): A string with labeled versions of the
|
| 61 |
+
dependencies used for REST transport.
|
| 62 |
+
protobuf_runtime_version (Optional[str]): The protobuf runtime version.
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
python_version=_PY_VERSION,
|
| 68 |
+
grpc_version=_GRPC_VERSION,
|
| 69 |
+
api_core_version=_API_CORE_VERSION,
|
| 70 |
+
gapic_version=None,
|
| 71 |
+
client_library_version=None,
|
| 72 |
+
user_agent=None,
|
| 73 |
+
rest_version=None,
|
| 74 |
+
protobuf_runtime_version=None,
|
| 75 |
+
):
|
| 76 |
+
self.python_version = python_version
|
| 77 |
+
self.grpc_version = grpc_version
|
| 78 |
+
self.api_core_version = api_core_version
|
| 79 |
+
self.gapic_version = gapic_version
|
| 80 |
+
self.client_library_version = client_library_version
|
| 81 |
+
self.user_agent = user_agent
|
| 82 |
+
self.rest_version = rest_version
|
| 83 |
+
self.protobuf_runtime_version = protobuf_runtime_version
|
| 84 |
+
|
| 85 |
+
def to_user_agent(self):
|
| 86 |
+
"""Returns the user-agent string for this client info."""
|
| 87 |
+
|
| 88 |
+
# Note: the order here is important as the internal metrics system
|
| 89 |
+
# expects these items to be in specific locations.
|
| 90 |
+
ua = ""
|
| 91 |
+
|
| 92 |
+
if self.user_agent is not None:
|
| 93 |
+
ua += "{user_agent} "
|
| 94 |
+
|
| 95 |
+
ua += "gl-python/{python_version} "
|
| 96 |
+
|
| 97 |
+
if self.grpc_version is not None:
|
| 98 |
+
ua += "grpc/{grpc_version} "
|
| 99 |
+
|
| 100 |
+
if self.rest_version is not None:
|
| 101 |
+
ua += "rest/{rest_version} "
|
| 102 |
+
|
| 103 |
+
ua += "gax/{api_core_version} "
|
| 104 |
+
|
| 105 |
+
if self.gapic_version is not None:
|
| 106 |
+
ua += "gapic/{gapic_version} "
|
| 107 |
+
|
| 108 |
+
if self.client_library_version is not None:
|
| 109 |
+
ua += "gccl/{client_library_version} "
|
| 110 |
+
|
| 111 |
+
if self.protobuf_runtime_version is not None:
|
| 112 |
+
ua += "pb/{protobuf_runtime_version} "
|
| 113 |
+
|
| 114 |
+
return ua.format(**self.__dict__).strip()
|
py311/lib/python3.11/site-packages/google/api_core/client_logging.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
|
| 7 |
+
_LOGGING_INITIALIZED = False
|
| 8 |
+
_BASE_LOGGER_NAME = "google"
|
| 9 |
+
|
| 10 |
+
# Fields to be included in the StructuredLogFormatter.
|
| 11 |
+
#
|
| 12 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/761): Update this list to support additional logging fields.
|
| 13 |
+
_recognized_logging_fields = [
|
| 14 |
+
"httpRequest",
|
| 15 |
+
"rpcName",
|
| 16 |
+
"serviceName",
|
| 17 |
+
"credentialsType",
|
| 18 |
+
"credentialsInfo",
|
| 19 |
+
"universeDomain",
|
| 20 |
+
"request",
|
| 21 |
+
"response",
|
| 22 |
+
"metadata",
|
| 23 |
+
"retryAttempt",
|
| 24 |
+
"httpResponse",
|
| 25 |
+
] # Additional fields to be Logged.
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def logger_configured(logger) -> bool:
|
| 29 |
+
"""Determines whether `logger` has non-default configuration
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
logger: The logger to check.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
bool: Whether the logger has any non-default configuration.
|
| 36 |
+
"""
|
| 37 |
+
return (
|
| 38 |
+
logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def initialize_logging():
|
| 43 |
+
"""Initializes "google" loggers, partly based on the environment variable
|
| 44 |
+
|
| 45 |
+
Initializes the "google" logger and any loggers (at the "google"
|
| 46 |
+
level or lower) specified by the environment variable
|
| 47 |
+
GOOGLE_SDK_PYTHON_LOGGING_SCOPE, as long as none of these loggers
|
| 48 |
+
were previously configured. If any such loggers (including the
|
| 49 |
+
"google" logger) are initialized, they are set to NOT propagate
|
| 50 |
+
log events up to their parent loggers.
|
| 51 |
+
|
| 52 |
+
This initialization is executed only once, and hence the
|
| 53 |
+
environment variable is only processed the first time this
|
| 54 |
+
function is called.
|
| 55 |
+
"""
|
| 56 |
+
global _LOGGING_INITIALIZED
|
| 57 |
+
if _LOGGING_INITIALIZED:
|
| 58 |
+
return
|
| 59 |
+
scopes = os.getenv("GOOGLE_SDK_PYTHON_LOGGING_SCOPE", "")
|
| 60 |
+
setup_logging(scopes)
|
| 61 |
+
_LOGGING_INITIALIZED = True
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def parse_logging_scopes(scopes: Optional[str] = None) -> List[str]:
|
| 65 |
+
"""Returns a list of logger names.
|
| 66 |
+
|
| 67 |
+
Splits the single string of comma-separated logger names into a list of individual logger name strings.
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.)
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
A list of all the logger names in scopes.
|
| 74 |
+
"""
|
| 75 |
+
if not scopes:
|
| 76 |
+
return []
|
| 77 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/759): check if the namespace is a valid namespace.
|
| 78 |
+
# TODO(b/380481951): Support logging multiple scopes.
|
| 79 |
+
# TODO(b/380483756): Raise or log a warning for an invalid scope.
|
| 80 |
+
namespaces = [scopes]
|
| 81 |
+
return namespaces
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def configure_defaults(logger):
|
| 85 |
+
"""Configures `logger` to emit structured info to stdout."""
|
| 86 |
+
if not logger_configured(logger):
|
| 87 |
+
console_handler = logging.StreamHandler()
|
| 88 |
+
logger.setLevel("DEBUG")
|
| 89 |
+
logger.propagate = False
|
| 90 |
+
formatter = StructuredLogFormatter()
|
| 91 |
+
console_handler.setFormatter(formatter)
|
| 92 |
+
logger.addHandler(console_handler)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def setup_logging(scopes: str = ""):
|
| 96 |
+
"""Sets up logging for the specified `scopes`.
|
| 97 |
+
|
| 98 |
+
If the loggers specified in `scopes` have not been previously
|
| 99 |
+
configured, this will configure them to emit structured log
|
| 100 |
+
entries to stdout, and to not propagate their log events to their
|
| 101 |
+
parent loggers. Additionally, if the "google" logger (whether it
|
| 102 |
+
was specified in `scopes` or not) was not previously configured,
|
| 103 |
+
it will also configure it to not propagate log events to the root
|
| 104 |
+
logger.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.)
|
| 108 |
+
|
| 109 |
+
"""
|
| 110 |
+
|
| 111 |
+
# only returns valid logger scopes (namespaces)
|
| 112 |
+
# this list has at most one element.
|
| 113 |
+
logger_names = parse_logging_scopes(scopes)
|
| 114 |
+
|
| 115 |
+
for namespace in logger_names:
|
| 116 |
+
# This will either create a module level logger or get the reference of the base logger instantiated above.
|
| 117 |
+
logger = logging.getLogger(namespace)
|
| 118 |
+
|
| 119 |
+
# Configure default settings.
|
| 120 |
+
configure_defaults(logger)
|
| 121 |
+
|
| 122 |
+
# disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes.
|
| 123 |
+
base_logger = logging.getLogger(_BASE_LOGGER_NAME)
|
| 124 |
+
if not logger_configured(base_logger):
|
| 125 |
+
base_logger.propagate = False
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/763): Expand documentation.
|
| 129 |
+
class StructuredLogFormatter(logging.Formatter):
|
| 130 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/761): ensure that additional fields such as
|
| 131 |
+
# function name, file name, and line no. appear in a log output.
|
| 132 |
+
def format(self, record: logging.LogRecord):
|
| 133 |
+
log_obj = {
|
| 134 |
+
"timestamp": self.formatTime(record),
|
| 135 |
+
"severity": record.levelname,
|
| 136 |
+
"name": record.name,
|
| 137 |
+
"message": record.getMessage(),
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
for field_name in _recognized_logging_fields:
|
| 141 |
+
value = getattr(record, field_name, None)
|
| 142 |
+
if value is not None:
|
| 143 |
+
log_obj[field_name] = value
|
| 144 |
+
return json.dumps(log_obj)
|
py311/lib/python3.11/site-packages/google/api_core/client_options.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2019 Google LLC
|
| 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 options class.
|
| 16 |
+
|
| 17 |
+
Client options provide a consistent interface for user options to be defined
|
| 18 |
+
across clients.
|
| 19 |
+
|
| 20 |
+
You can pass a client options object to a client.
|
| 21 |
+
|
| 22 |
+
.. code-block:: python
|
| 23 |
+
|
| 24 |
+
from google.api_core.client_options import ClientOptions
|
| 25 |
+
from google.cloud.vision_v1 import ImageAnnotatorClient
|
| 26 |
+
|
| 27 |
+
def get_client_cert():
|
| 28 |
+
# code to load client certificate and private key.
|
| 29 |
+
return client_cert_bytes, client_private_key_bytes
|
| 30 |
+
|
| 31 |
+
options = ClientOptions(api_endpoint="foo.googleapis.com",
|
| 32 |
+
client_cert_source=get_client_cert)
|
| 33 |
+
|
| 34 |
+
client = ImageAnnotatorClient(client_options=options)
|
| 35 |
+
|
| 36 |
+
You can also pass a mapping object.
|
| 37 |
+
|
| 38 |
+
.. code-block:: python
|
| 39 |
+
|
| 40 |
+
from google.cloud.vision_v1 import ImageAnnotatorClient
|
| 41 |
+
|
| 42 |
+
client = ImageAnnotatorClient(
|
| 43 |
+
client_options={
|
| 44 |
+
"api_endpoint": "foo.googleapis.com",
|
| 45 |
+
"client_cert_source" : get_client_cert
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
from typing import Callable, Mapping, Optional, Sequence, Tuple
|
| 52 |
+
import warnings
|
| 53 |
+
|
| 54 |
+
from google.api_core import general_helpers
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ClientOptions(object):
|
| 58 |
+
"""Client Options used to set options on clients.
|
| 59 |
+
|
| 60 |
+
Args:
|
| 61 |
+
api_endpoint (Optional[str]): The desired API endpoint, e.g.,
|
| 62 |
+
compute.googleapis.com
|
| 63 |
+
client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A callback
|
| 64 |
+
which returns client certificate bytes and private key bytes both in
|
| 65 |
+
PEM format. ``client_cert_source`` and ``client_encrypted_cert_source``
|
| 66 |
+
are mutually exclusive.
|
| 67 |
+
client_encrypted_cert_source (Optional[Callable[[], Tuple[str, str, bytes]]]):
|
| 68 |
+
A callback which returns client certificate file path, encrypted
|
| 69 |
+
private key file path, and the passphrase bytes.``client_cert_source``
|
| 70 |
+
and ``client_encrypted_cert_source`` are mutually exclusive.
|
| 71 |
+
quota_project_id (Optional[str]): A project name that a client's
|
| 72 |
+
quota belongs to.
|
| 73 |
+
credentials_file (Optional[str]): Deprecated. A path to a file storing credentials.
|
| 74 |
+
``credentials_file` and ``api_key`` are mutually exclusive. This argument will be
|
| 75 |
+
removed in the next major version of `google-api-core`.
|
| 76 |
+
|
| 77 |
+
.. warning::
|
| 78 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 79 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 80 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 81 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 82 |
+
the security of your systems and data. For more information, refer to
|
| 83 |
+
`Validate credential configurations from external sources`_.
|
| 84 |
+
|
| 85 |
+
.. _Validate credential configurations from external sources:
|
| 86 |
+
|
| 87 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 88 |
+
scopes (Optional[Sequence[str]]): OAuth access token override scopes.
|
| 89 |
+
api_key (Optional[str]): Google API key. ``credentials_file`` and
|
| 90 |
+
``api_key`` are mutually exclusive.
|
| 91 |
+
api_audience (Optional[str]): The intended audience for the API calls
|
| 92 |
+
to the service that will be set when using certain 3rd party
|
| 93 |
+
authentication flows. Audience is typically a resource identifier.
|
| 94 |
+
If not set, the service endpoint value will be used as a default.
|
| 95 |
+
An example of a valid ``api_audience`` is: "https://language.googleapis.com".
|
| 96 |
+
universe_domain (Optional[str]): The desired universe domain. This must match
|
| 97 |
+
the one in credentials. If not set, the default universe domain is
|
| 98 |
+
`googleapis.com`. If both `api_endpoint` and `universe_domain` are set,
|
| 99 |
+
then `api_endpoint` is used as the service endpoint. If `api_endpoint` is
|
| 100 |
+
not specified, the format will be `{service}.{universe_domain}`.
|
| 101 |
+
|
| 102 |
+
Raises:
|
| 103 |
+
ValueError: If both ``client_cert_source`` and ``client_encrypted_cert_source``
|
| 104 |
+
are provided, or both ``credentials_file`` and ``api_key`` are provided.
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(
|
| 108 |
+
self,
|
| 109 |
+
api_endpoint: Optional[str] = None,
|
| 110 |
+
client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None,
|
| 111 |
+
client_encrypted_cert_source: Optional[
|
| 112 |
+
Callable[[], Tuple[str, str, bytes]]
|
| 113 |
+
] = None,
|
| 114 |
+
quota_project_id: Optional[str] = None,
|
| 115 |
+
credentials_file: Optional[str] = None,
|
| 116 |
+
scopes: Optional[Sequence[str]] = None,
|
| 117 |
+
api_key: Optional[str] = None,
|
| 118 |
+
api_audience: Optional[str] = None,
|
| 119 |
+
universe_domain: Optional[str] = None,
|
| 120 |
+
):
|
| 121 |
+
if credentials_file is not None:
|
| 122 |
+
warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning)
|
| 123 |
+
|
| 124 |
+
if client_cert_source and client_encrypted_cert_source:
|
| 125 |
+
raise ValueError(
|
| 126 |
+
"client_cert_source and client_encrypted_cert_source are mutually exclusive"
|
| 127 |
+
)
|
| 128 |
+
if api_key and credentials_file:
|
| 129 |
+
raise ValueError("api_key and credentials_file are mutually exclusive")
|
| 130 |
+
self.api_endpoint = api_endpoint
|
| 131 |
+
self.client_cert_source = client_cert_source
|
| 132 |
+
self.client_encrypted_cert_source = client_encrypted_cert_source
|
| 133 |
+
self.quota_project_id = quota_project_id
|
| 134 |
+
self.credentials_file = credentials_file
|
| 135 |
+
self.scopes = scopes
|
| 136 |
+
self.api_key = api_key
|
| 137 |
+
self.api_audience = api_audience
|
| 138 |
+
self.universe_domain = universe_domain
|
| 139 |
+
|
| 140 |
+
def __repr__(self) -> str:
|
| 141 |
+
return "ClientOptions: " + repr(self.__dict__)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def from_dict(options: Mapping[str, object]) -> ClientOptions:
|
| 145 |
+
"""Construct a client options object from a mapping object.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
options (collections.abc.Mapping): A mapping object with client options.
|
| 149 |
+
See the docstring for ClientOptions for details on valid arguments.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
client_options = ClientOptions()
|
| 153 |
+
|
| 154 |
+
for key, value in options.items():
|
| 155 |
+
if hasattr(client_options, key):
|
| 156 |
+
setattr(client_options, key, value)
|
| 157 |
+
else:
|
| 158 |
+
raise ValueError("ClientOptions does not accept an option '" + key + "'")
|
| 159 |
+
|
| 160 |
+
return client_options
|
py311/lib/python3.11/site-packages/google/api_core/datetime_helpers.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 :mod:`datetime`."""
|
| 16 |
+
|
| 17 |
+
import calendar
|
| 18 |
+
import datetime
|
| 19 |
+
import re
|
| 20 |
+
|
| 21 |
+
from google.protobuf import timestamp_pb2
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_UTC_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
| 25 |
+
_RFC3339_MICROS = "%Y-%m-%dT%H:%M:%S.%fZ"
|
| 26 |
+
_RFC3339_NO_FRACTION = "%Y-%m-%dT%H:%M:%S"
|
| 27 |
+
# datetime.strptime cannot handle nanosecond precision: parse w/ regex
|
| 28 |
+
_RFC3339_NANOS = re.compile(
|
| 29 |
+
r"""
|
| 30 |
+
(?P<no_fraction>
|
| 31 |
+
\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2} # YYYY-MM-DDTHH:MM:SS
|
| 32 |
+
)
|
| 33 |
+
( # Optional decimal part
|
| 34 |
+
\. # decimal point
|
| 35 |
+
(?P<nanos>\d{1,9}) # nanoseconds, maybe truncated
|
| 36 |
+
)?
|
| 37 |
+
Z # Zulu
|
| 38 |
+
""",
|
| 39 |
+
re.VERBOSE,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def utcnow():
|
| 44 |
+
"""A :meth:`datetime.datetime.utcnow()` alias to allow mocking in tests."""
|
| 45 |
+
return datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def to_milliseconds(value):
|
| 49 |
+
"""Convert a zone-aware datetime to milliseconds since the unix epoch.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
value (datetime.datetime): The datetime to covert.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
int: Milliseconds since the unix epoch.
|
| 56 |
+
"""
|
| 57 |
+
micros = to_microseconds(value)
|
| 58 |
+
return micros // 1000
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def from_microseconds(value):
|
| 62 |
+
"""Convert timestamp in microseconds since the unix epoch to datetime.
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
value (float): The timestamp to convert, in microseconds.
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
datetime.datetime: The datetime object equivalent to the timestamp in
|
| 69 |
+
UTC.
|
| 70 |
+
"""
|
| 71 |
+
return _UTC_EPOCH + datetime.timedelta(microseconds=value)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def to_microseconds(value):
|
| 75 |
+
"""Convert a datetime to microseconds since the unix epoch.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
value (datetime.datetime): The datetime to covert.
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
int: Microseconds since the unix epoch.
|
| 82 |
+
"""
|
| 83 |
+
if not value.tzinfo:
|
| 84 |
+
value = value.replace(tzinfo=datetime.timezone.utc)
|
| 85 |
+
# Regardless of what timezone is on the value, convert it to UTC.
|
| 86 |
+
value = value.astimezone(datetime.timezone.utc)
|
| 87 |
+
# Convert the datetime to a microsecond timestamp.
|
| 88 |
+
return int(calendar.timegm(value.timetuple()) * 1e6) + value.microsecond
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def from_iso8601_date(value):
|
| 92 |
+
"""Convert a ISO8601 date string to a date.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
value (str): The ISO8601 date string.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
datetime.date: A date equivalent to the date string.
|
| 99 |
+
"""
|
| 100 |
+
return datetime.datetime.strptime(value, "%Y-%m-%d").date()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def from_iso8601_time(value):
|
| 104 |
+
"""Convert a zoneless ISO8601 time string to a time.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
value (str): The ISO8601 time string.
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
datetime.time: A time equivalent to the time string.
|
| 111 |
+
"""
|
| 112 |
+
return datetime.datetime.strptime(value, "%H:%M:%S").time()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def from_rfc3339(value):
|
| 116 |
+
"""Convert an RFC3339-format timestamp to a native datetime.
|
| 117 |
+
|
| 118 |
+
Supported formats include those without fractional seconds, or with
|
| 119 |
+
any fraction up to nanosecond precision.
|
| 120 |
+
|
| 121 |
+
.. note::
|
| 122 |
+
Python datetimes do not support nanosecond precision; this function
|
| 123 |
+
therefore truncates such values to microseconds.
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
value (str): The RFC3339 string to convert.
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
datetime.datetime: The datetime object equivalent to the timestamp
|
| 130 |
+
in UTC.
|
| 131 |
+
|
| 132 |
+
Raises:
|
| 133 |
+
ValueError: If the timestamp does not match the RFC3339
|
| 134 |
+
regular expression.
|
| 135 |
+
"""
|
| 136 |
+
with_nanos = _RFC3339_NANOS.match(value)
|
| 137 |
+
|
| 138 |
+
if with_nanos is None:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
"Timestamp: {!r}, does not match pattern: {!r}".format(
|
| 141 |
+
value, _RFC3339_NANOS.pattern
|
| 142 |
+
)
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
bare_seconds = datetime.datetime.strptime(
|
| 146 |
+
with_nanos.group("no_fraction"), _RFC3339_NO_FRACTION
|
| 147 |
+
)
|
| 148 |
+
fraction = with_nanos.group("nanos")
|
| 149 |
+
|
| 150 |
+
if fraction is None:
|
| 151 |
+
micros = 0
|
| 152 |
+
else:
|
| 153 |
+
scale = 9 - len(fraction)
|
| 154 |
+
nanos = int(fraction) * (10**scale)
|
| 155 |
+
micros = nanos // 1000
|
| 156 |
+
|
| 157 |
+
return bare_seconds.replace(microsecond=micros, tzinfo=datetime.timezone.utc)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
from_rfc3339_nanos = from_rfc3339 # from_rfc3339_nanos method was deprecated.
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def to_rfc3339(value, ignore_zone=True):
|
| 164 |
+
"""Convert a datetime to an RFC3339 timestamp string.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
value (datetime.datetime):
|
| 168 |
+
The datetime object to be converted to a string.
|
| 169 |
+
ignore_zone (bool): If True, then the timezone (if any) of the
|
| 170 |
+
datetime object is ignored and the datetime is treated as UTC.
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
str: The RFC3339 formatted string representing the datetime.
|
| 174 |
+
"""
|
| 175 |
+
if not ignore_zone and value.tzinfo is not None:
|
| 176 |
+
# Convert to UTC and remove the time zone info.
|
| 177 |
+
value = value.replace(tzinfo=None) - value.utcoffset()
|
| 178 |
+
|
| 179 |
+
return value.strftime(_RFC3339_MICROS)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class DatetimeWithNanoseconds(datetime.datetime):
|
| 183 |
+
"""Track nanosecond in addition to normal datetime attrs.
|
| 184 |
+
|
| 185 |
+
Nanosecond can be passed only as a keyword argument.
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
__slots__ = ("_nanosecond",)
|
| 189 |
+
|
| 190 |
+
# pylint: disable=arguments-differ
|
| 191 |
+
def __new__(cls, *args, **kw):
|
| 192 |
+
nanos = kw.pop("nanosecond", 0)
|
| 193 |
+
if nanos > 0:
|
| 194 |
+
if "microsecond" in kw:
|
| 195 |
+
raise TypeError("Specify only one of 'microsecond' or 'nanosecond'")
|
| 196 |
+
kw["microsecond"] = nanos // 1000
|
| 197 |
+
inst = datetime.datetime.__new__(cls, *args, **kw)
|
| 198 |
+
inst._nanosecond = nanos or 0
|
| 199 |
+
return inst
|
| 200 |
+
|
| 201 |
+
# pylint: disable=arguments-differ
|
| 202 |
+
|
| 203 |
+
@property
|
| 204 |
+
def nanosecond(self):
|
| 205 |
+
"""Read-only: nanosecond precision."""
|
| 206 |
+
return self._nanosecond
|
| 207 |
+
|
| 208 |
+
def rfc3339(self):
|
| 209 |
+
"""Return an RFC3339-compliant timestamp.
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
(str): Timestamp string according to RFC3339 spec.
|
| 213 |
+
"""
|
| 214 |
+
if self._nanosecond == 0:
|
| 215 |
+
return to_rfc3339(self)
|
| 216 |
+
nanos = str(self._nanosecond).rjust(9, "0").rstrip("0")
|
| 217 |
+
return "{}.{}Z".format(self.strftime(_RFC3339_NO_FRACTION), nanos)
|
| 218 |
+
|
| 219 |
+
@classmethod
|
| 220 |
+
def from_rfc3339(cls, stamp):
|
| 221 |
+
"""Parse RFC3339-compliant timestamp, preserving nanoseconds.
|
| 222 |
+
|
| 223 |
+
Args:
|
| 224 |
+
stamp (str): RFC3339 stamp, with up to nanosecond precision
|
| 225 |
+
|
| 226 |
+
Returns:
|
| 227 |
+
:class:`DatetimeWithNanoseconds`:
|
| 228 |
+
an instance matching the timestamp string
|
| 229 |
+
|
| 230 |
+
Raises:
|
| 231 |
+
ValueError: if `stamp` does not match the expected format
|
| 232 |
+
"""
|
| 233 |
+
with_nanos = _RFC3339_NANOS.match(stamp)
|
| 234 |
+
if with_nanos is None:
|
| 235 |
+
raise ValueError(
|
| 236 |
+
"Timestamp: {}, does not match pattern: {}".format(
|
| 237 |
+
stamp, _RFC3339_NANOS.pattern
|
| 238 |
+
)
|
| 239 |
+
)
|
| 240 |
+
bare = datetime.datetime.strptime(
|
| 241 |
+
with_nanos.group("no_fraction"), _RFC3339_NO_FRACTION
|
| 242 |
+
)
|
| 243 |
+
fraction = with_nanos.group("nanos")
|
| 244 |
+
if fraction is None:
|
| 245 |
+
nanos = 0
|
| 246 |
+
else:
|
| 247 |
+
scale = 9 - len(fraction)
|
| 248 |
+
nanos = int(fraction) * (10**scale)
|
| 249 |
+
return cls(
|
| 250 |
+
bare.year,
|
| 251 |
+
bare.month,
|
| 252 |
+
bare.day,
|
| 253 |
+
bare.hour,
|
| 254 |
+
bare.minute,
|
| 255 |
+
bare.second,
|
| 256 |
+
nanosecond=nanos,
|
| 257 |
+
tzinfo=datetime.timezone.utc,
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
def timestamp_pb(self):
|
| 261 |
+
"""Return a timestamp message.
|
| 262 |
+
|
| 263 |
+
Returns:
|
| 264 |
+
(:class:`~google.protobuf.timestamp_pb2.Timestamp`): Timestamp message
|
| 265 |
+
"""
|
| 266 |
+
inst = (
|
| 267 |
+
self
|
| 268 |
+
if self.tzinfo is not None
|
| 269 |
+
else self.replace(tzinfo=datetime.timezone.utc)
|
| 270 |
+
)
|
| 271 |
+
delta = inst - _UTC_EPOCH
|
| 272 |
+
seconds = int(delta.total_seconds())
|
| 273 |
+
nanos = self._nanosecond or self.microsecond * 1000
|
| 274 |
+
return timestamp_pb2.Timestamp(seconds=seconds, nanos=nanos)
|
| 275 |
+
|
| 276 |
+
@classmethod
|
| 277 |
+
def from_timestamp_pb(cls, stamp):
|
| 278 |
+
"""Parse RFC3339-compliant timestamp, preserving nanoseconds.
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
stamp (:class:`~google.protobuf.timestamp_pb2.Timestamp`): timestamp message
|
| 282 |
+
|
| 283 |
+
Returns:
|
| 284 |
+
:class:`DatetimeWithNanoseconds`:
|
| 285 |
+
an instance matching the timestamp message
|
| 286 |
+
"""
|
| 287 |
+
microseconds = int(stamp.seconds * 1e6)
|
| 288 |
+
bare = from_microseconds(microseconds)
|
| 289 |
+
return cls(
|
| 290 |
+
bare.year,
|
| 291 |
+
bare.month,
|
| 292 |
+
bare.day,
|
| 293 |
+
bare.hour,
|
| 294 |
+
bare.minute,
|
| 295 |
+
bare.second,
|
| 296 |
+
nanosecond=stamp.nanos,
|
| 297 |
+
tzinfo=datetime.timezone.utc,
|
| 298 |
+
)
|
py311/lib/python3.11/site-packages/google/api_core/exceptions.py
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google LLC
|
| 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 |
+
"""Exceptions raised by Google API core & clients.
|
| 16 |
+
|
| 17 |
+
This module provides base classes for all errors raised by libraries based
|
| 18 |
+
on :mod:`google.api_core`, including both HTTP and gRPC clients.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import absolute_import
|
| 22 |
+
from __future__ import unicode_literals
|
| 23 |
+
|
| 24 |
+
import http.client
|
| 25 |
+
from typing import Optional, Dict
|
| 26 |
+
from typing import Union
|
| 27 |
+
import warnings
|
| 28 |
+
|
| 29 |
+
from google.rpc import error_details_pb2
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _warn_could_not_import_grpcio_status():
|
| 33 |
+
warnings.warn(
|
| 34 |
+
"Please install grpcio-status to obtain helpful grpc error messages.",
|
| 35 |
+
ImportWarning,
|
| 36 |
+
) # pragma: NO COVER
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
import grpc
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
from grpc_status import rpc_status
|
| 44 |
+
except ImportError: # pragma: NO COVER
|
| 45 |
+
_warn_could_not_import_grpcio_status()
|
| 46 |
+
rpc_status = None
|
| 47 |
+
except ImportError: # pragma: NO COVER
|
| 48 |
+
grpc = None
|
| 49 |
+
|
| 50 |
+
# Lookup tables for mapping exceptions from HTTP and gRPC transports.
|
| 51 |
+
# Populated by _GoogleAPICallErrorMeta
|
| 52 |
+
_HTTP_CODE_TO_EXCEPTION: Dict[int, Exception] = {}
|
| 53 |
+
_GRPC_CODE_TO_EXCEPTION: Dict[int, Exception] = {}
|
| 54 |
+
|
| 55 |
+
# Additional lookup table to map integer status codes to grpc status code
|
| 56 |
+
# grpc does not currently support initializing enums from ints
|
| 57 |
+
# i.e., grpc.StatusCode(5) raises an error
|
| 58 |
+
_INT_TO_GRPC_CODE = {}
|
| 59 |
+
if grpc is not None: # pragma: no branch
|
| 60 |
+
for x in grpc.StatusCode:
|
| 61 |
+
_INT_TO_GRPC_CODE[x.value[0]] = x
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class GoogleAPIError(Exception):
|
| 65 |
+
"""Base class for all exceptions raised by Google API Clients."""
|
| 66 |
+
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class DuplicateCredentialArgs(GoogleAPIError):
|
| 71 |
+
"""Raised when multiple credentials are passed."""
|
| 72 |
+
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class RetryError(GoogleAPIError):
|
| 77 |
+
"""Raised when a function has exhausted all of its available retries.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
message (str): The exception message.
|
| 81 |
+
cause (Exception): The last exception raised when retrying the
|
| 82 |
+
function.
|
| 83 |
+
"""
|
| 84 |
+
|
| 85 |
+
def __init__(self, message, cause):
|
| 86 |
+
super(RetryError, self).__init__(message)
|
| 87 |
+
self.message = message
|
| 88 |
+
self._cause = cause
|
| 89 |
+
|
| 90 |
+
@property
|
| 91 |
+
def cause(self):
|
| 92 |
+
"""The last exception raised when retrying the function."""
|
| 93 |
+
return self._cause
|
| 94 |
+
|
| 95 |
+
def __str__(self):
|
| 96 |
+
return "{}, last exception: {}".format(self.message, self.cause)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class _GoogleAPICallErrorMeta(type):
|
| 100 |
+
"""Metaclass for registering GoogleAPICallError subclasses."""
|
| 101 |
+
|
| 102 |
+
def __new__(mcs, name, bases, class_dict):
|
| 103 |
+
cls = type.__new__(mcs, name, bases, class_dict)
|
| 104 |
+
if cls.code is not None:
|
| 105 |
+
_HTTP_CODE_TO_EXCEPTION.setdefault(cls.code, cls)
|
| 106 |
+
if cls.grpc_status_code is not None:
|
| 107 |
+
_GRPC_CODE_TO_EXCEPTION.setdefault(cls.grpc_status_code, cls)
|
| 108 |
+
return cls
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class GoogleAPICallError(GoogleAPIError, metaclass=_GoogleAPICallErrorMeta):
|
| 112 |
+
"""Base class for exceptions raised by calling API methods.
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
message (str): The exception message.
|
| 116 |
+
errors (Sequence[Any]): An optional list of error details.
|
| 117 |
+
details (Sequence[Any]): An optional list of objects defined in google.rpc.error_details.
|
| 118 |
+
response (Union[requests.Request, grpc.Call]): The response or
|
| 119 |
+
gRPC call metadata.
|
| 120 |
+
error_info (Union[error_details_pb2.ErrorInfo, None]): An optional object containing error info
|
| 121 |
+
(google.rpc.error_details.ErrorInfo).
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
code: Union[int, None] = None
|
| 125 |
+
"""Optional[int]: The HTTP status code associated with this error.
|
| 126 |
+
|
| 127 |
+
This may be ``None`` if the exception does not have a direct mapping
|
| 128 |
+
to an HTTP error.
|
| 129 |
+
|
| 130 |
+
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
grpc_status_code = None
|
| 134 |
+
"""Optional[grpc.StatusCode]: The gRPC status code associated with this
|
| 135 |
+
error.
|
| 136 |
+
|
| 137 |
+
This may be ``None`` if the exception does not match up to a gRPC error.
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(self, message, errors=(), details=(), response=None, error_info=None):
|
| 141 |
+
super(GoogleAPICallError, self).__init__(message)
|
| 142 |
+
self.message = message
|
| 143 |
+
"""str: The exception message."""
|
| 144 |
+
self._errors = errors
|
| 145 |
+
self._details = details
|
| 146 |
+
self._response = response
|
| 147 |
+
self._error_info = error_info
|
| 148 |
+
|
| 149 |
+
def __str__(self):
|
| 150 |
+
error_msg = "{} {}".format(self.code, self.message)
|
| 151 |
+
if self.details:
|
| 152 |
+
error_msg = "{} {}".format(error_msg, self.details)
|
| 153 |
+
# Note: This else condition can be removed once proposal A from
|
| 154 |
+
# b/284179390 is implemented.
|
| 155 |
+
else:
|
| 156 |
+
if self.errors:
|
| 157 |
+
errors = [
|
| 158 |
+
f"{error.code}: {error.message}"
|
| 159 |
+
for error in self.errors
|
| 160 |
+
if hasattr(error, "code") and hasattr(error, "message")
|
| 161 |
+
]
|
| 162 |
+
if errors:
|
| 163 |
+
error_msg = "{} {}".format(error_msg, "\n".join(errors))
|
| 164 |
+
return error_msg
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def reason(self):
|
| 168 |
+
"""The reason of the error.
|
| 169 |
+
|
| 170 |
+
Reference:
|
| 171 |
+
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
Union[str, None]: An optional string containing reason of the error.
|
| 175 |
+
"""
|
| 176 |
+
return self._error_info.reason if self._error_info else None
|
| 177 |
+
|
| 178 |
+
@property
|
| 179 |
+
def domain(self):
|
| 180 |
+
"""The logical grouping to which the "reason" belongs.
|
| 181 |
+
|
| 182 |
+
Reference:
|
| 183 |
+
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Union[str, None]: An optional string containing a logical grouping to which the "reason" belongs.
|
| 187 |
+
"""
|
| 188 |
+
return self._error_info.domain if self._error_info else None
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def metadata(self):
|
| 192 |
+
"""Additional structured details about this error.
|
| 193 |
+
|
| 194 |
+
Reference:
|
| 195 |
+
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto#L112
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
Union[Dict[str, str], None]: An optional object containing structured details about the error.
|
| 199 |
+
"""
|
| 200 |
+
return self._error_info.metadata if self._error_info else None
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def errors(self):
|
| 204 |
+
"""Detailed error information.
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
Sequence[Any]: A list of additional error details.
|
| 208 |
+
"""
|
| 209 |
+
return list(self._errors)
|
| 210 |
+
|
| 211 |
+
@property
|
| 212 |
+
def details(self):
|
| 213 |
+
"""Information contained in google.rpc.status.details.
|
| 214 |
+
|
| 215 |
+
Reference:
|
| 216 |
+
https://github.com/googleapis/googleapis/blob/master/google/rpc/status.proto
|
| 217 |
+
https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
Sequence[Any]: A list of structured objects from error_details.proto
|
| 221 |
+
"""
|
| 222 |
+
return list(self._details)
|
| 223 |
+
|
| 224 |
+
@property
|
| 225 |
+
def response(self):
|
| 226 |
+
"""Optional[Union[requests.Request, grpc.Call]]: The response or
|
| 227 |
+
gRPC call metadata."""
|
| 228 |
+
return self._response
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class Redirection(GoogleAPICallError):
|
| 232 |
+
"""Base class for for all redirection (HTTP 3xx) responses."""
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class MovedPermanently(Redirection):
|
| 236 |
+
"""Exception mapping a ``301 Moved Permanently`` response."""
|
| 237 |
+
|
| 238 |
+
code = http.client.MOVED_PERMANENTLY
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class NotModified(Redirection):
|
| 242 |
+
"""Exception mapping a ``304 Not Modified`` response."""
|
| 243 |
+
|
| 244 |
+
code = http.client.NOT_MODIFIED
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class TemporaryRedirect(Redirection):
|
| 248 |
+
"""Exception mapping a ``307 Temporary Redirect`` response."""
|
| 249 |
+
|
| 250 |
+
code = http.client.TEMPORARY_REDIRECT
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
class ResumeIncomplete(Redirection):
|
| 254 |
+
"""Exception mapping a ``308 Resume Incomplete`` response.
|
| 255 |
+
|
| 256 |
+
.. note:: :attr:`http.client.PERMANENT_REDIRECT` is ``308``, but Google
|
| 257 |
+
APIs differ in their use of this status code.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
code = 308
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class ClientError(GoogleAPICallError):
|
| 264 |
+
"""Base class for all client error (HTTP 4xx) responses."""
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class BadRequest(ClientError):
|
| 268 |
+
"""Exception mapping a ``400 Bad Request`` response."""
|
| 269 |
+
|
| 270 |
+
code = http.client.BAD_REQUEST
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class InvalidArgument(BadRequest):
|
| 274 |
+
"""Exception mapping a :attr:`grpc.StatusCode.INVALID_ARGUMENT` error."""
|
| 275 |
+
|
| 276 |
+
grpc_status_code = grpc.StatusCode.INVALID_ARGUMENT if grpc is not None else None
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class FailedPrecondition(BadRequest):
|
| 280 |
+
"""Exception mapping a :attr:`grpc.StatusCode.FAILED_PRECONDITION`
|
| 281 |
+
error."""
|
| 282 |
+
|
| 283 |
+
grpc_status_code = grpc.StatusCode.FAILED_PRECONDITION if grpc is not None else None
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
class OutOfRange(BadRequest):
|
| 287 |
+
"""Exception mapping a :attr:`grpc.StatusCode.OUT_OF_RANGE` error."""
|
| 288 |
+
|
| 289 |
+
grpc_status_code = grpc.StatusCode.OUT_OF_RANGE if grpc is not None else None
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class Unauthorized(ClientError):
|
| 293 |
+
"""Exception mapping a ``401 Unauthorized`` response."""
|
| 294 |
+
|
| 295 |
+
code = http.client.UNAUTHORIZED
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
class Unauthenticated(Unauthorized):
|
| 299 |
+
"""Exception mapping a :attr:`grpc.StatusCode.UNAUTHENTICATED` error."""
|
| 300 |
+
|
| 301 |
+
grpc_status_code = grpc.StatusCode.UNAUTHENTICATED if grpc is not None else None
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class Forbidden(ClientError):
|
| 305 |
+
"""Exception mapping a ``403 Forbidden`` response."""
|
| 306 |
+
|
| 307 |
+
code = http.client.FORBIDDEN
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
class PermissionDenied(Forbidden):
|
| 311 |
+
"""Exception mapping a :attr:`grpc.StatusCode.PERMISSION_DENIED` error."""
|
| 312 |
+
|
| 313 |
+
grpc_status_code = grpc.StatusCode.PERMISSION_DENIED if grpc is not None else None
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
class NotFound(ClientError):
|
| 317 |
+
"""Exception mapping a ``404 Not Found`` response or a
|
| 318 |
+
:attr:`grpc.StatusCode.NOT_FOUND` error."""
|
| 319 |
+
|
| 320 |
+
code = http.client.NOT_FOUND
|
| 321 |
+
grpc_status_code = grpc.StatusCode.NOT_FOUND if grpc is not None else None
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class MethodNotAllowed(ClientError):
|
| 325 |
+
"""Exception mapping a ``405 Method Not Allowed`` response."""
|
| 326 |
+
|
| 327 |
+
code = http.client.METHOD_NOT_ALLOWED
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class Conflict(ClientError):
|
| 331 |
+
"""Exception mapping a ``409 Conflict`` response."""
|
| 332 |
+
|
| 333 |
+
code = http.client.CONFLICT
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class AlreadyExists(Conflict):
|
| 337 |
+
"""Exception mapping a :attr:`grpc.StatusCode.ALREADY_EXISTS` error."""
|
| 338 |
+
|
| 339 |
+
grpc_status_code = grpc.StatusCode.ALREADY_EXISTS if grpc is not None else None
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
class Aborted(Conflict):
|
| 343 |
+
"""Exception mapping a :attr:`grpc.StatusCode.ABORTED` error."""
|
| 344 |
+
|
| 345 |
+
grpc_status_code = grpc.StatusCode.ABORTED if grpc is not None else None
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
class LengthRequired(ClientError):
|
| 349 |
+
"""Exception mapping a ``411 Length Required`` response."""
|
| 350 |
+
|
| 351 |
+
code = http.client.LENGTH_REQUIRED
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
class PreconditionFailed(ClientError):
|
| 355 |
+
"""Exception mapping a ``412 Precondition Failed`` response."""
|
| 356 |
+
|
| 357 |
+
code = http.client.PRECONDITION_FAILED
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class RequestRangeNotSatisfiable(ClientError):
|
| 361 |
+
"""Exception mapping a ``416 Request Range Not Satisfiable`` response."""
|
| 362 |
+
|
| 363 |
+
code = http.client.REQUESTED_RANGE_NOT_SATISFIABLE
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class TooManyRequests(ClientError):
|
| 367 |
+
"""Exception mapping a ``429 Too Many Requests`` response."""
|
| 368 |
+
|
| 369 |
+
code = http.client.TOO_MANY_REQUESTS
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
class ResourceExhausted(TooManyRequests):
|
| 373 |
+
"""Exception mapping a :attr:`grpc.StatusCode.RESOURCE_EXHAUSTED` error."""
|
| 374 |
+
|
| 375 |
+
grpc_status_code = grpc.StatusCode.RESOURCE_EXHAUSTED if grpc is not None else None
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
class Cancelled(ClientError):
|
| 379 |
+
"""Exception mapping a :attr:`grpc.StatusCode.CANCELLED` error."""
|
| 380 |
+
|
| 381 |
+
# This maps to HTTP status code 499. See
|
| 382 |
+
# https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
|
| 383 |
+
code = 499
|
| 384 |
+
grpc_status_code = grpc.StatusCode.CANCELLED if grpc is not None else None
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class ServerError(GoogleAPICallError):
|
| 388 |
+
"""Base for 5xx responses."""
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
class InternalServerError(ServerError):
|
| 392 |
+
"""Exception mapping a ``500 Internal Server Error`` response. or a
|
| 393 |
+
:attr:`grpc.StatusCode.INTERNAL` error."""
|
| 394 |
+
|
| 395 |
+
code = http.client.INTERNAL_SERVER_ERROR
|
| 396 |
+
grpc_status_code = grpc.StatusCode.INTERNAL if grpc is not None else None
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
class Unknown(ServerError):
|
| 400 |
+
"""Exception mapping a :attr:`grpc.StatusCode.UNKNOWN` error."""
|
| 401 |
+
|
| 402 |
+
grpc_status_code = grpc.StatusCode.UNKNOWN if grpc is not None else None
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
class DataLoss(ServerError):
|
| 406 |
+
"""Exception mapping a :attr:`grpc.StatusCode.DATA_LOSS` error."""
|
| 407 |
+
|
| 408 |
+
grpc_status_code = grpc.StatusCode.DATA_LOSS if grpc is not None else None
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class MethodNotImplemented(ServerError):
|
| 412 |
+
"""Exception mapping a ``501 Not Implemented`` response or a
|
| 413 |
+
:attr:`grpc.StatusCode.UNIMPLEMENTED` error."""
|
| 414 |
+
|
| 415 |
+
code = http.client.NOT_IMPLEMENTED
|
| 416 |
+
grpc_status_code = grpc.StatusCode.UNIMPLEMENTED if grpc is not None else None
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
class BadGateway(ServerError):
|
| 420 |
+
"""Exception mapping a ``502 Bad Gateway`` response."""
|
| 421 |
+
|
| 422 |
+
code = http.client.BAD_GATEWAY
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
class ServiceUnavailable(ServerError):
|
| 426 |
+
"""Exception mapping a ``503 Service Unavailable`` response or a
|
| 427 |
+
:attr:`grpc.StatusCode.UNAVAILABLE` error."""
|
| 428 |
+
|
| 429 |
+
code = http.client.SERVICE_UNAVAILABLE
|
| 430 |
+
grpc_status_code = grpc.StatusCode.UNAVAILABLE if grpc is not None else None
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class GatewayTimeout(ServerError):
|
| 434 |
+
"""Exception mapping a ``504 Gateway Timeout`` response."""
|
| 435 |
+
|
| 436 |
+
code = http.client.GATEWAY_TIMEOUT
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
class DeadlineExceeded(GatewayTimeout):
|
| 440 |
+
"""Exception mapping a :attr:`grpc.StatusCode.DEADLINE_EXCEEDED` error."""
|
| 441 |
+
|
| 442 |
+
grpc_status_code = grpc.StatusCode.DEADLINE_EXCEEDED if grpc is not None else None
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
class AsyncRestUnsupportedParameterError(NotImplementedError):
|
| 446 |
+
"""Raised when an unsupported parameter is configured against async rest transport."""
|
| 447 |
+
|
| 448 |
+
pass
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def exception_class_for_http_status(status_code):
|
| 452 |
+
"""Return the exception class for a specific HTTP status code.
|
| 453 |
+
|
| 454 |
+
Args:
|
| 455 |
+
status_code (int): The HTTP status code.
|
| 456 |
+
|
| 457 |
+
Returns:
|
| 458 |
+
:func:`type`: the appropriate subclass of :class:`GoogleAPICallError`.
|
| 459 |
+
"""
|
| 460 |
+
return _HTTP_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def from_http_status(status_code, message, **kwargs):
|
| 464 |
+
"""Create a :class:`GoogleAPICallError` from an HTTP status code.
|
| 465 |
+
|
| 466 |
+
Args:
|
| 467 |
+
status_code (int): The HTTP status code.
|
| 468 |
+
message (str): The exception message.
|
| 469 |
+
kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
|
| 470 |
+
constructor.
|
| 471 |
+
|
| 472 |
+
Returns:
|
| 473 |
+
GoogleAPICallError: An instance of the appropriate subclass of
|
| 474 |
+
:class:`GoogleAPICallError`.
|
| 475 |
+
"""
|
| 476 |
+
error_class = exception_class_for_http_status(status_code)
|
| 477 |
+
error = error_class(message, **kwargs)
|
| 478 |
+
|
| 479 |
+
if error.code is None:
|
| 480 |
+
error.code = status_code
|
| 481 |
+
|
| 482 |
+
return error
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def _format_rest_error_message(error, method, url):
|
| 486 |
+
method = method.upper() if method else None
|
| 487 |
+
message = "{method} {url}: {error}".format(
|
| 488 |
+
method=method,
|
| 489 |
+
url=url,
|
| 490 |
+
error=error,
|
| 491 |
+
)
|
| 492 |
+
return message
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
# NOTE: We're moving away from `from_http_status` because it expects an aiohttp response compared
|
| 496 |
+
# to `format_http_response_error` which expects a more abstract response from google.auth and is
|
| 497 |
+
# compatible with both sync and async response types.
|
| 498 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/691): Add type hint for response.
|
| 499 |
+
def format_http_response_error(
|
| 500 |
+
response, method: str, url: str, payload: Optional[Dict] = None
|
| 501 |
+
):
|
| 502 |
+
"""Create a :class:`GoogleAPICallError` from a google auth rest response.
|
| 503 |
+
|
| 504 |
+
Args:
|
| 505 |
+
response Union[google.auth.transport.Response, google.auth.aio.transport.Response]: The HTTP response.
|
| 506 |
+
method Optional(str): The HTTP request method.
|
| 507 |
+
url Optional(str): The HTTP request url.
|
| 508 |
+
payload Optional(dict): The HTTP response payload. If not passed in, it is read from response for a response type of google.auth.transport.Response.
|
| 509 |
+
|
| 510 |
+
Returns:
|
| 511 |
+
GoogleAPICallError: An instance of the appropriate subclass of
|
| 512 |
+
:class:`GoogleAPICallError`, with the message and errors populated
|
| 513 |
+
from the response.
|
| 514 |
+
"""
|
| 515 |
+
payload = {} if not payload else payload
|
| 516 |
+
error_message = payload.get("error", {}).get("message", "unknown error")
|
| 517 |
+
errors = payload.get("error", {}).get("errors", ())
|
| 518 |
+
# In JSON, details are already formatted in developer-friendly way.
|
| 519 |
+
details = payload.get("error", {}).get("details", ())
|
| 520 |
+
error_info_list = list(
|
| 521 |
+
filter(
|
| 522 |
+
lambda detail: detail.get("@type", "")
|
| 523 |
+
== "type.googleapis.com/google.rpc.ErrorInfo",
|
| 524 |
+
details,
|
| 525 |
+
)
|
| 526 |
+
)
|
| 527 |
+
error_info = error_info_list[0] if error_info_list else None
|
| 528 |
+
message = _format_rest_error_message(error_message, method, url)
|
| 529 |
+
|
| 530 |
+
exception = from_http_status(
|
| 531 |
+
response.status_code,
|
| 532 |
+
message,
|
| 533 |
+
errors=errors,
|
| 534 |
+
details=details,
|
| 535 |
+
response=response,
|
| 536 |
+
error_info=error_info,
|
| 537 |
+
)
|
| 538 |
+
return exception
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def from_http_response(response):
|
| 542 |
+
"""Create a :class:`GoogleAPICallError` from a :class:`requests.Response`.
|
| 543 |
+
|
| 544 |
+
Args:
|
| 545 |
+
response (requests.Response): The HTTP response.
|
| 546 |
+
|
| 547 |
+
Returns:
|
| 548 |
+
GoogleAPICallError: An instance of the appropriate subclass of
|
| 549 |
+
:class:`GoogleAPICallError`, with the message and errors populated
|
| 550 |
+
from the response.
|
| 551 |
+
"""
|
| 552 |
+
try:
|
| 553 |
+
payload = response.json()
|
| 554 |
+
except ValueError:
|
| 555 |
+
payload = {"error": {"message": response.text or "unknown error"}}
|
| 556 |
+
return format_http_response_error(
|
| 557 |
+
response, response.request.method, response.request.url, payload
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def exception_class_for_grpc_status(status_code):
|
| 562 |
+
"""Return the exception class for a specific :class:`grpc.StatusCode`.
|
| 563 |
+
|
| 564 |
+
Args:
|
| 565 |
+
status_code (grpc.StatusCode): The gRPC status code.
|
| 566 |
+
|
| 567 |
+
Returns:
|
| 568 |
+
:func:`type`: the appropriate subclass of :class:`GoogleAPICallError`.
|
| 569 |
+
"""
|
| 570 |
+
return _GRPC_CODE_TO_EXCEPTION.get(status_code, GoogleAPICallError)
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
def from_grpc_status(status_code, message, **kwargs):
|
| 574 |
+
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.StatusCode`.
|
| 575 |
+
|
| 576 |
+
Args:
|
| 577 |
+
status_code (Union[grpc.StatusCode, int]): The gRPC status code.
|
| 578 |
+
message (str): The exception message.
|
| 579 |
+
kwargs: Additional arguments passed to the :class:`GoogleAPICallError`
|
| 580 |
+
constructor.
|
| 581 |
+
|
| 582 |
+
Returns:
|
| 583 |
+
GoogleAPICallError: An instance of the appropriate subclass of
|
| 584 |
+
:class:`GoogleAPICallError`.
|
| 585 |
+
"""
|
| 586 |
+
|
| 587 |
+
if isinstance(status_code, int):
|
| 588 |
+
status_code = _INT_TO_GRPC_CODE.get(status_code, status_code)
|
| 589 |
+
|
| 590 |
+
error_class = exception_class_for_grpc_status(status_code)
|
| 591 |
+
error = error_class(message, **kwargs)
|
| 592 |
+
|
| 593 |
+
if error.grpc_status_code is None:
|
| 594 |
+
error.grpc_status_code = status_code
|
| 595 |
+
|
| 596 |
+
return error
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
def _is_informative_grpc_error(rpc_exc):
|
| 600 |
+
return hasattr(rpc_exc, "code") and hasattr(rpc_exc, "details")
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
def _parse_grpc_error_details(rpc_exc):
|
| 604 |
+
if not rpc_status: # pragma: NO COVER
|
| 605 |
+
_warn_could_not_import_grpcio_status()
|
| 606 |
+
return [], None
|
| 607 |
+
try:
|
| 608 |
+
status = rpc_status.from_call(rpc_exc)
|
| 609 |
+
except NotImplementedError: # workaround
|
| 610 |
+
return [], None
|
| 611 |
+
|
| 612 |
+
if not status:
|
| 613 |
+
return [], None
|
| 614 |
+
|
| 615 |
+
possible_errors = [
|
| 616 |
+
error_details_pb2.BadRequest,
|
| 617 |
+
error_details_pb2.PreconditionFailure,
|
| 618 |
+
error_details_pb2.QuotaFailure,
|
| 619 |
+
error_details_pb2.ErrorInfo,
|
| 620 |
+
error_details_pb2.RetryInfo,
|
| 621 |
+
error_details_pb2.ResourceInfo,
|
| 622 |
+
error_details_pb2.RequestInfo,
|
| 623 |
+
error_details_pb2.DebugInfo,
|
| 624 |
+
error_details_pb2.Help,
|
| 625 |
+
error_details_pb2.LocalizedMessage,
|
| 626 |
+
]
|
| 627 |
+
error_info = None
|
| 628 |
+
error_details = []
|
| 629 |
+
for detail in status.details:
|
| 630 |
+
matched_detail_cls = list(
|
| 631 |
+
filter(lambda x: detail.Is(x.DESCRIPTOR), possible_errors)
|
| 632 |
+
)
|
| 633 |
+
# If nothing matched, use detail directly.
|
| 634 |
+
if len(matched_detail_cls) == 0:
|
| 635 |
+
info = detail
|
| 636 |
+
else:
|
| 637 |
+
info = matched_detail_cls[0]()
|
| 638 |
+
detail.Unpack(info)
|
| 639 |
+
error_details.append(info)
|
| 640 |
+
if isinstance(info, error_details_pb2.ErrorInfo):
|
| 641 |
+
error_info = info
|
| 642 |
+
return error_details, error_info
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
def from_grpc_error(rpc_exc):
|
| 646 |
+
"""Create a :class:`GoogleAPICallError` from a :class:`grpc.RpcError`.
|
| 647 |
+
|
| 648 |
+
Args:
|
| 649 |
+
rpc_exc (grpc.RpcError): The gRPC error.
|
| 650 |
+
|
| 651 |
+
Returns:
|
| 652 |
+
GoogleAPICallError: An instance of the appropriate subclass of
|
| 653 |
+
:class:`GoogleAPICallError`.
|
| 654 |
+
"""
|
| 655 |
+
# NOTE(lidiz) All gRPC error shares the parent class grpc.RpcError.
|
| 656 |
+
# However, check for grpc.RpcError breaks backward compatibility.
|
| 657 |
+
if (
|
| 658 |
+
grpc is not None and isinstance(rpc_exc, grpc.Call)
|
| 659 |
+
) or _is_informative_grpc_error(rpc_exc):
|
| 660 |
+
details, err_info = _parse_grpc_error_details(rpc_exc)
|
| 661 |
+
return from_grpc_status(
|
| 662 |
+
rpc_exc.code(),
|
| 663 |
+
rpc_exc.details(),
|
| 664 |
+
errors=(rpc_exc,),
|
| 665 |
+
details=details,
|
| 666 |
+
response=rpc_exc,
|
| 667 |
+
error_info=err_info,
|
| 668 |
+
)
|
| 669 |
+
else:
|
| 670 |
+
return GoogleAPICallError(str(rpc_exc), errors=(rpc_exc,), response=rpc_exc)
|
py311/lib/python3.11/site-packages/google/api_core/extended_operation.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC
|
| 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 |
+
"""Futures for extended long-running operations returned from Google Cloud APIs.
|
| 16 |
+
|
| 17 |
+
These futures can be used to synchronously wait for the result of a
|
| 18 |
+
long-running operations using :meth:`ExtendedOperation.result`:
|
| 19 |
+
|
| 20 |
+
.. code-block:: python
|
| 21 |
+
|
| 22 |
+
extended_operation = my_api_client.long_running_method()
|
| 23 |
+
|
| 24 |
+
extended_operation.result()
|
| 25 |
+
|
| 26 |
+
Or asynchronously using callbacks and :meth:`Operation.add_done_callback`:
|
| 27 |
+
|
| 28 |
+
.. code-block:: python
|
| 29 |
+
|
| 30 |
+
extended_operation = my_api_client.long_running_method()
|
| 31 |
+
|
| 32 |
+
def my_callback(ex_op):
|
| 33 |
+
print(f"Operation {ex_op.name} completed")
|
| 34 |
+
|
| 35 |
+
extended_operation.add_done_callback(my_callback)
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
import threading
|
| 40 |
+
|
| 41 |
+
from google.api_core import exceptions
|
| 42 |
+
from google.api_core.future import polling
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class ExtendedOperation(polling.PollingFuture):
|
| 46 |
+
"""An ExtendedOperation future for interacting with a Google API Long-Running Operation.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
extended_operation (proto.Message): The initial operation.
|
| 50 |
+
refresh (Callable[[], type(extended_operation)]): A callable that returns
|
| 51 |
+
the latest state of the operation.
|
| 52 |
+
cancel (Callable[[], None]): A callable that tries to cancel the operation.
|
| 53 |
+
polling Optional(google.api_core.retry.Retry): The configuration used
|
| 54 |
+
for polling. This can be used to control how often :meth:`done`
|
| 55 |
+
is polled. If the ``timeout`` argument to :meth:`result` is
|
| 56 |
+
specified it will override the ``polling.timeout`` property.
|
| 57 |
+
retry Optional(google.api_core.retry.Retry): DEPRECATED use ``polling``
|
| 58 |
+
instead. If specified it will override ``polling`` parameter to
|
| 59 |
+
maintain backward compatibility.
|
| 60 |
+
|
| 61 |
+
Note: Most long-running API methods use google.api_core.operation.Operation
|
| 62 |
+
This class is a wrapper for a subset of methods that use alternative
|
| 63 |
+
Long-Running Operation (LRO) semantics.
|
| 64 |
+
|
| 65 |
+
Note: there is not a concrete type the extended operation must be.
|
| 66 |
+
It MUST have fields that correspond to the following, POSSIBLY WITH DIFFERENT NAMES:
|
| 67 |
+
* name: str
|
| 68 |
+
* status: Union[str, bool, enum.Enum]
|
| 69 |
+
* error_code: int
|
| 70 |
+
* error_message: str
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
extended_operation,
|
| 76 |
+
refresh,
|
| 77 |
+
cancel,
|
| 78 |
+
polling=polling.DEFAULT_POLLING,
|
| 79 |
+
**kwargs,
|
| 80 |
+
):
|
| 81 |
+
super().__init__(polling=polling, **kwargs)
|
| 82 |
+
self._extended_operation = extended_operation
|
| 83 |
+
self._refresh = refresh
|
| 84 |
+
self._cancel = cancel
|
| 85 |
+
# Note: the extended operation does not give a good way to indicate cancellation.
|
| 86 |
+
# We make do with manually tracking cancellation and checking for doneness.
|
| 87 |
+
self._cancelled = False
|
| 88 |
+
self._completion_lock = threading.Lock()
|
| 89 |
+
# Invoke in case the operation came back already complete.
|
| 90 |
+
self._handle_refreshed_operation()
|
| 91 |
+
|
| 92 |
+
# Note: the following four properties MUST be overridden in a subclass
|
| 93 |
+
# if, and only if, the fields in the corresponding extended operation message
|
| 94 |
+
# have different names.
|
| 95 |
+
#
|
| 96 |
+
# E.g. we have an extended operation class that looks like
|
| 97 |
+
#
|
| 98 |
+
# class MyOperation(proto.Message):
|
| 99 |
+
# moniker = proto.Field(proto.STRING, number=1)
|
| 100 |
+
# status_msg = proto.Field(proto.STRING, number=2)
|
| 101 |
+
# optional http_error_code = proto.Field(proto.INT32, number=3)
|
| 102 |
+
# optional http_error_msg = proto.Field(proto.STRING, number=4)
|
| 103 |
+
#
|
| 104 |
+
# the ExtendedOperation subclass would provide property overrides that map
|
| 105 |
+
# to these (poorly named) fields.
|
| 106 |
+
@property
|
| 107 |
+
def name(self):
|
| 108 |
+
return self._extended_operation.name
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def status(self):
|
| 112 |
+
return self._extended_operation.status
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def error_code(self):
|
| 116 |
+
return self._extended_operation.error_code
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def error_message(self):
|
| 120 |
+
return self._extended_operation.error_message
|
| 121 |
+
|
| 122 |
+
def __getattr__(self, name):
|
| 123 |
+
return getattr(self._extended_operation, name)
|
| 124 |
+
|
| 125 |
+
def done(self, retry=None):
|
| 126 |
+
self._refresh_and_update(retry)
|
| 127 |
+
return self._extended_operation.done
|
| 128 |
+
|
| 129 |
+
def cancel(self):
|
| 130 |
+
if self.done():
|
| 131 |
+
return False
|
| 132 |
+
|
| 133 |
+
self._cancel()
|
| 134 |
+
self._cancelled = True
|
| 135 |
+
return True
|
| 136 |
+
|
| 137 |
+
def cancelled(self):
|
| 138 |
+
# TODO(dovs): there is not currently a good way to determine whether the
|
| 139 |
+
# operation has been cancelled.
|
| 140 |
+
# The best we can do is manually keep track of cancellation
|
| 141 |
+
# and check for doneness.
|
| 142 |
+
if not self._cancelled:
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
self._refresh_and_update()
|
| 146 |
+
return self._extended_operation.done
|
| 147 |
+
|
| 148 |
+
def _refresh_and_update(self, retry=None):
|
| 149 |
+
if not self._extended_operation.done:
|
| 150 |
+
self._extended_operation = (
|
| 151 |
+
self._refresh(retry=retry) if retry else self._refresh()
|
| 152 |
+
)
|
| 153 |
+
self._handle_refreshed_operation()
|
| 154 |
+
|
| 155 |
+
def _handle_refreshed_operation(self):
|
| 156 |
+
with self._completion_lock:
|
| 157 |
+
if not self._extended_operation.done:
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
if self.error_code and self.error_message:
|
| 161 |
+
# Note: `errors` can be removed once proposal A from
|
| 162 |
+
# b/284179390 is implemented.
|
| 163 |
+
errors = []
|
| 164 |
+
if hasattr(self, "error") and hasattr(self.error, "errors"):
|
| 165 |
+
errors = self.error.errors
|
| 166 |
+
exception = exceptions.from_http_status(
|
| 167 |
+
status_code=self.error_code,
|
| 168 |
+
message=self.error_message,
|
| 169 |
+
response=self._extended_operation,
|
| 170 |
+
errors=errors,
|
| 171 |
+
)
|
| 172 |
+
self.set_exception(exception)
|
| 173 |
+
elif self.error_code or self.error_message:
|
| 174 |
+
exception = exceptions.GoogleAPICallError(
|
| 175 |
+
f"Unexpected error {self.error_code}: {self.error_message}"
|
| 176 |
+
)
|
| 177 |
+
self.set_exception(exception)
|
| 178 |
+
else:
|
| 179 |
+
# Extended operations have no payload.
|
| 180 |
+
self.set_result(None)
|
| 181 |
+
|
| 182 |
+
@classmethod
|
| 183 |
+
def make(cls, refresh, cancel, extended_operation, **kwargs):
|
| 184 |
+
"""
|
| 185 |
+
Return an instantiated ExtendedOperation (or child) that wraps
|
| 186 |
+
* a refresh callable
|
| 187 |
+
* a cancel callable (can be a no-op)
|
| 188 |
+
* an initial result
|
| 189 |
+
|
| 190 |
+
.. note::
|
| 191 |
+
It is the caller's responsibility to set up refresh and cancel
|
| 192 |
+
with their correct request argument.
|
| 193 |
+
The reason for this is that the services that use Extended Operations
|
| 194 |
+
have rpcs that look something like the following:
|
| 195 |
+
|
| 196 |
+
// service.proto
|
| 197 |
+
service MyLongService {
|
| 198 |
+
rpc StartLongTask(StartLongTaskRequest) returns (ExtendedOperation) {
|
| 199 |
+
option (google.cloud.operation_service) = "CustomOperationService";
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
service CustomOperationService {
|
| 204 |
+
rpc Get(GetOperationRequest) returns (ExtendedOperation) {
|
| 205 |
+
option (google.cloud.operation_polling_method) = true;
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
Any info needed for the poll, e.g. a name, path params, etc.
|
| 210 |
+
is held in the request, which the initial client method is in a much
|
| 211 |
+
better position to make made because the caller made the initial request.
|
| 212 |
+
|
| 213 |
+
TL;DR: the caller sets up closures for refresh and cancel that carry
|
| 214 |
+
the properly configured requests.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
refresh (Callable[Optional[Retry]][type(extended_operation)]): A callable that
|
| 218 |
+
returns the latest state of the operation.
|
| 219 |
+
cancel (Callable[][Any]): A callable that tries to cancel the operation
|
| 220 |
+
on a best effort basis.
|
| 221 |
+
extended_operation (Any): The initial response of the long running method.
|
| 222 |
+
See the docstring for ExtendedOperation.__init__ for requirements on
|
| 223 |
+
the type and fields of extended_operation
|
| 224 |
+
"""
|
| 225 |
+
return cls(extended_operation, refresh, cancel, **kwargs)
|
py311/lib/python3.11/site-packages/google/api_core/general_helpers.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 |
+
# This import for backward compatibility only.
|
| 16 |
+
from functools import wraps # noqa: F401 pragma: NO COVER
|
| 17 |
+
|
| 18 |
+
_CREDENTIALS_FILE_WARNING = """\
|
| 19 |
+
The `credentials_file` argument is deprecated because of a potential security risk.
|
| 20 |
+
|
| 21 |
+
The `google.auth.load_credentials_from_file` method does not validate the credential
|
| 22 |
+
configuration. The security risk occurs when a credential configuration is accepted
|
| 23 |
+
from a source that is not under your control and used without validation on your side.
|
| 24 |
+
|
| 25 |
+
If you know that you will be loading credential configurations of a
|
| 26 |
+
specific type, it is recommended to use a credential-type-specific
|
| 27 |
+
load method.
|
| 28 |
+
|
| 29 |
+
This will ensure that an unexpected credential type with potential for
|
| 30 |
+
malicious intent is not loaded unintentionally. You might still have to do
|
| 31 |
+
validation for certain credential types. Please follow the recommendations
|
| 32 |
+
for that method. For example, if you want to load only service accounts,
|
| 33 |
+
you can create the service account credentials explicitly:
|
| 34 |
+
|
| 35 |
+
```
|
| 36 |
+
from google.cloud.vision_v1 import ImageAnnotatorClient
|
| 37 |
+
from google.oauth2 import service_account
|
| 38 |
+
|
| 39 |
+
credentials = service_account.Credentials.from_service_account_file(filename)
|
| 40 |
+
client = ImageAnnotatorClient(credentials=credentials)
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
If you are loading your credential configuration from an untrusted source and have
|
| 44 |
+
not mitigated the risks (e.g. by validating the configuration yourself), make
|
| 45 |
+
these changes as soon as possible to prevent security risks to your environment.
|
| 46 |
+
|
| 47 |
+
Regardless of the method used, it is always your responsibility to validate
|
| 48 |
+
configurations received from external sources.
|
| 49 |
+
|
| 50 |
+
Refer to https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 51 |
+
for more details.
|
| 52 |
+
"""
|
py311/lib/python3.11/site-packages/google/api_core/grpc_helpers.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 :mod:`grpc`."""
|
| 16 |
+
import collections
|
| 17 |
+
import functools
|
| 18 |
+
from typing import Generic, Iterator, Optional, TypeVar
|
| 19 |
+
import warnings
|
| 20 |
+
|
| 21 |
+
import google.auth
|
| 22 |
+
import google.auth.credentials
|
| 23 |
+
import google.auth.transport.grpc
|
| 24 |
+
import google.auth.transport.requests
|
| 25 |
+
import google.protobuf
|
| 26 |
+
import grpc
|
| 27 |
+
|
| 28 |
+
from google.api_core import exceptions, general_helpers
|
| 29 |
+
|
| 30 |
+
PROTOBUF_VERSION = google.protobuf.__version__
|
| 31 |
+
|
| 32 |
+
# The grpcio-gcp package only has support for protobuf < 4
|
| 33 |
+
if PROTOBUF_VERSION[0:2] == "3.": # pragma: NO COVER
|
| 34 |
+
try:
|
| 35 |
+
import grpc_gcp
|
| 36 |
+
|
| 37 |
+
warnings.warn(
|
| 38 |
+
"""Support for grpcio-gcp is deprecated. This feature will be
|
| 39 |
+
removed from `google-api-core` after January 1, 2024. If you need to
|
| 40 |
+
continue to use this feature, please pin to a specific version of
|
| 41 |
+
`google-api-core`.""",
|
| 42 |
+
DeprecationWarning,
|
| 43 |
+
)
|
| 44 |
+
HAS_GRPC_GCP = True
|
| 45 |
+
except ImportError:
|
| 46 |
+
HAS_GRPC_GCP = False
|
| 47 |
+
else:
|
| 48 |
+
HAS_GRPC_GCP = False
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# The list of gRPC Callable interfaces that return iterators.
|
| 52 |
+
_STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable)
|
| 53 |
+
|
| 54 |
+
# denotes the proto response type for grpc calls
|
| 55 |
+
P = TypeVar("P")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _patch_callable_name(callable_):
|
| 59 |
+
"""Fix-up gRPC callable attributes.
|
| 60 |
+
|
| 61 |
+
gRPC callable lack the ``__name__`` attribute which causes
|
| 62 |
+
:func:`functools.wraps` to error. This adds the attribute if needed.
|
| 63 |
+
"""
|
| 64 |
+
if not hasattr(callable_, "__name__"):
|
| 65 |
+
callable_.__name__ = callable_.__class__.__name__
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _wrap_unary_errors(callable_):
|
| 69 |
+
"""Map errors for Unary-Unary and Stream-Unary gRPC callables."""
|
| 70 |
+
_patch_callable_name(callable_)
|
| 71 |
+
|
| 72 |
+
@functools.wraps(callable_)
|
| 73 |
+
def error_remapped_callable(*args, **kwargs):
|
| 74 |
+
try:
|
| 75 |
+
return callable_(*args, **kwargs)
|
| 76 |
+
except grpc.RpcError as exc:
|
| 77 |
+
raise exceptions.from_grpc_error(exc) from exc
|
| 78 |
+
|
| 79 |
+
return error_remapped_callable
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class _StreamingResponseIterator(Generic[P], grpc.Call):
|
| 83 |
+
def __init__(self, wrapped, prefetch_first_result=True):
|
| 84 |
+
self._wrapped = wrapped
|
| 85 |
+
|
| 86 |
+
# This iterator is used in a retry context, and returned outside after init.
|
| 87 |
+
# gRPC will not throw an exception until the stream is consumed, so we need
|
| 88 |
+
# to retrieve the first result, in order to fail, in order to trigger a retry.
|
| 89 |
+
try:
|
| 90 |
+
if prefetch_first_result:
|
| 91 |
+
self._stored_first_result = next(self._wrapped)
|
| 92 |
+
except TypeError:
|
| 93 |
+
# It is possible the wrapped method isn't an iterable (a grpc.Call
|
| 94 |
+
# for instance). If this happens don't store the first result.
|
| 95 |
+
pass
|
| 96 |
+
except StopIteration:
|
| 97 |
+
# ignore stop iteration at this time. This should be handled outside of retry.
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
+
def __iter__(self) -> Iterator[P]:
|
| 101 |
+
"""This iterator is also an iterable that returns itself."""
|
| 102 |
+
return self
|
| 103 |
+
|
| 104 |
+
def __next__(self) -> P:
|
| 105 |
+
"""Get the next response from the stream.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
protobuf.Message: A single response from the stream.
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
if hasattr(self, "_stored_first_result"):
|
| 112 |
+
result = self._stored_first_result
|
| 113 |
+
del self._stored_first_result
|
| 114 |
+
return result
|
| 115 |
+
return next(self._wrapped)
|
| 116 |
+
except grpc.RpcError as exc:
|
| 117 |
+
# If the stream has already returned data, we cannot recover here.
|
| 118 |
+
raise exceptions.from_grpc_error(exc) from exc
|
| 119 |
+
|
| 120 |
+
# grpc.Call & grpc.RpcContext interface
|
| 121 |
+
|
| 122 |
+
def add_callback(self, callback):
|
| 123 |
+
return self._wrapped.add_callback(callback)
|
| 124 |
+
|
| 125 |
+
def cancel(self):
|
| 126 |
+
return self._wrapped.cancel()
|
| 127 |
+
|
| 128 |
+
def code(self):
|
| 129 |
+
return self._wrapped.code()
|
| 130 |
+
|
| 131 |
+
def details(self):
|
| 132 |
+
return self._wrapped.details()
|
| 133 |
+
|
| 134 |
+
def initial_metadata(self):
|
| 135 |
+
return self._wrapped.initial_metadata()
|
| 136 |
+
|
| 137 |
+
def is_active(self):
|
| 138 |
+
return self._wrapped.is_active()
|
| 139 |
+
|
| 140 |
+
def time_remaining(self):
|
| 141 |
+
return self._wrapped.time_remaining()
|
| 142 |
+
|
| 143 |
+
def trailing_metadata(self):
|
| 144 |
+
return self._wrapped.trailing_metadata()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# public type alias denoting the return type of streaming gapic calls
|
| 148 |
+
GrpcStream = _StreamingResponseIterator[P]
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _wrap_stream_errors(callable_):
|
| 152 |
+
"""Wrap errors for Unary-Stream and Stream-Stream gRPC callables.
|
| 153 |
+
|
| 154 |
+
The callables that return iterators require a bit more logic to re-map
|
| 155 |
+
errors when iterating. This wraps both the initial invocation and the
|
| 156 |
+
iterator of the return value to re-map errors.
|
| 157 |
+
"""
|
| 158 |
+
_patch_callable_name(callable_)
|
| 159 |
+
|
| 160 |
+
@functools.wraps(callable_)
|
| 161 |
+
def error_remapped_callable(*args, **kwargs):
|
| 162 |
+
try:
|
| 163 |
+
result = callable_(*args, **kwargs)
|
| 164 |
+
# Auto-fetching the first result causes PubSub client's streaming pull
|
| 165 |
+
# to hang when re-opening the stream, thus we need examine the hacky
|
| 166 |
+
# hidden flag to see if pre-fetching is disabled.
|
| 167 |
+
# https://github.com/googleapis/python-pubsub/issues/93#issuecomment-630762257
|
| 168 |
+
prefetch_first = getattr(callable_, "_prefetch_first_result_", True)
|
| 169 |
+
return _StreamingResponseIterator(
|
| 170 |
+
result, prefetch_first_result=prefetch_first
|
| 171 |
+
)
|
| 172 |
+
except grpc.RpcError as exc:
|
| 173 |
+
raise exceptions.from_grpc_error(exc) from exc
|
| 174 |
+
|
| 175 |
+
return error_remapped_callable
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def wrap_errors(callable_):
|
| 179 |
+
"""Wrap a gRPC callable and map :class:`grpc.RpcErrors` to friendly error
|
| 180 |
+
classes.
|
| 181 |
+
|
| 182 |
+
Errors raised by the gRPC callable are mapped to the appropriate
|
| 183 |
+
:class:`google.api_core.exceptions.GoogleAPICallError` subclasses.
|
| 184 |
+
The original `grpc.RpcError` (which is usually also a `grpc.Call`) is
|
| 185 |
+
available from the ``response`` property on the mapped exception. This
|
| 186 |
+
is useful for extracting metadata from the original error.
|
| 187 |
+
|
| 188 |
+
Args:
|
| 189 |
+
callable_ (Callable): A gRPC callable.
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
Callable: The wrapped gRPC callable.
|
| 193 |
+
"""
|
| 194 |
+
if isinstance(callable_, _STREAM_WRAP_CLASSES):
|
| 195 |
+
return _wrap_stream_errors(callable_)
|
| 196 |
+
else:
|
| 197 |
+
return _wrap_unary_errors(callable_)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _create_composite_credentials(
|
| 201 |
+
credentials=None,
|
| 202 |
+
credentials_file=None,
|
| 203 |
+
default_scopes=None,
|
| 204 |
+
scopes=None,
|
| 205 |
+
ssl_credentials=None,
|
| 206 |
+
quota_project_id=None,
|
| 207 |
+
default_host=None,
|
| 208 |
+
):
|
| 209 |
+
"""Create the composite credentials for secure channels.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
credentials (google.auth.credentials.Credentials): The credentials. If
|
| 213 |
+
not specified, then this function will attempt to ascertain the
|
| 214 |
+
credentials from the environment using :func:`google.auth.default`.
|
| 215 |
+
credentials_file (str): Deprecated. A file with credentials that can be loaded with
|
| 216 |
+
:func:`google.auth.load_credentials_from_file`. This argument is
|
| 217 |
+
mutually exclusive with credentials. This argument will be
|
| 218 |
+
removed in the next major version of `google-api-core`.
|
| 219 |
+
|
| 220 |
+
.. warning::
|
| 221 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 222 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 223 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 224 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 225 |
+
the security of your systems and data. For more information, refer to
|
| 226 |
+
`Validate credential configurations from external sources`_.
|
| 227 |
+
|
| 228 |
+
.. _Validate credential configurations from external sources:
|
| 229 |
+
|
| 230 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 231 |
+
default_scopes (Sequence[str]): A optional list of scopes needed for this
|
| 232 |
+
service. These are only used when credentials are not specified and
|
| 233 |
+
are passed to :func:`google.auth.default`.
|
| 234 |
+
scopes (Sequence[str]): A optional list of scopes needed for this
|
| 235 |
+
service. These are only used when credentials are not specified and
|
| 236 |
+
are passed to :func:`google.auth.default`.
|
| 237 |
+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
| 238 |
+
credentials. This can be used to specify different certificates.
|
| 239 |
+
quota_project_id (str): An optional project to use for billing and quota.
|
| 240 |
+
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
grpc.ChannelCredentials: The composed channel credentials object.
|
| 244 |
+
|
| 245 |
+
Raises:
|
| 246 |
+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
|
| 247 |
+
"""
|
| 248 |
+
if credentials_file is not None:
|
| 249 |
+
warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning)
|
| 250 |
+
|
| 251 |
+
if credentials and credentials_file:
|
| 252 |
+
raise exceptions.DuplicateCredentialArgs(
|
| 253 |
+
"'credentials' and 'credentials_file' are mutually exclusive."
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
if credentials_file:
|
| 257 |
+
credentials, _ = google.auth.load_credentials_from_file(
|
| 258 |
+
credentials_file, scopes=scopes, default_scopes=default_scopes
|
| 259 |
+
)
|
| 260 |
+
elif credentials:
|
| 261 |
+
credentials = google.auth.credentials.with_scopes_if_required(
|
| 262 |
+
credentials, scopes=scopes, default_scopes=default_scopes
|
| 263 |
+
)
|
| 264 |
+
else:
|
| 265 |
+
credentials, _ = google.auth.default(
|
| 266 |
+
scopes=scopes, default_scopes=default_scopes
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
if quota_project_id and isinstance(
|
| 270 |
+
credentials, google.auth.credentials.CredentialsWithQuotaProject
|
| 271 |
+
):
|
| 272 |
+
credentials = credentials.with_quota_project(quota_project_id)
|
| 273 |
+
|
| 274 |
+
request = google.auth.transport.requests.Request()
|
| 275 |
+
|
| 276 |
+
# Create the metadata plugin for inserting the authorization header.
|
| 277 |
+
metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
|
| 278 |
+
credentials,
|
| 279 |
+
request,
|
| 280 |
+
default_host=default_host,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Create a set of grpc.CallCredentials using the metadata plugin.
|
| 284 |
+
google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
|
| 285 |
+
|
| 286 |
+
# if `ssl_credentials` is set, use `grpc.composite_channel_credentials` instead of
|
| 287 |
+
# `grpc.compute_engine_channel_credentials` as the former supports passing
|
| 288 |
+
# `ssl_credentials` via `channel_credentials` which is needed for mTLS.
|
| 289 |
+
if ssl_credentials:
|
| 290 |
+
# Combine the ssl credentials and the authorization credentials.
|
| 291 |
+
# See https://grpc.github.io/grpc/python/grpc.html#grpc.composite_channel_credentials
|
| 292 |
+
return grpc.composite_channel_credentials(
|
| 293 |
+
ssl_credentials, google_auth_credentials
|
| 294 |
+
)
|
| 295 |
+
else:
|
| 296 |
+
# Use grpc.compute_engine_channel_credentials in order to support Direct Path.
|
| 297 |
+
# See https://grpc.github.io/grpc/python/grpc.html#grpc.compute_engine_channel_credentials
|
| 298 |
+
# TODO(https://github.com/googleapis/python-api-core/issues/598):
|
| 299 |
+
# Although `grpc.compute_engine_channel_credentials` returns channel credentials
|
| 300 |
+
# outside of a Google Compute Engine environment (GCE), we should determine if
|
| 301 |
+
# there is a way to reliably detect a GCE environment so that
|
| 302 |
+
# `grpc.compute_engine_channel_credentials` is not called outside of GCE.
|
| 303 |
+
return grpc.compute_engine_channel_credentials(google_auth_credentials)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def create_channel(
|
| 307 |
+
target,
|
| 308 |
+
credentials=None,
|
| 309 |
+
scopes=None,
|
| 310 |
+
ssl_credentials=None,
|
| 311 |
+
credentials_file=None,
|
| 312 |
+
quota_project_id=None,
|
| 313 |
+
default_scopes=None,
|
| 314 |
+
default_host=None,
|
| 315 |
+
compression=None,
|
| 316 |
+
attempt_direct_path: Optional[bool] = False,
|
| 317 |
+
**kwargs,
|
| 318 |
+
):
|
| 319 |
+
"""Create a secure channel with credentials.
|
| 320 |
+
|
| 321 |
+
Args:
|
| 322 |
+
target (str): The target service address in the format 'hostname:port'.
|
| 323 |
+
credentials (google.auth.credentials.Credentials): The credentials. If
|
| 324 |
+
not specified, then this function will attempt to ascertain the
|
| 325 |
+
credentials from the environment using :func:`google.auth.default`.
|
| 326 |
+
scopes (Sequence[str]): A optional list of scopes needed for this
|
| 327 |
+
service. These are only used when credentials are not specified and
|
| 328 |
+
are passed to :func:`google.auth.default`.
|
| 329 |
+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
| 330 |
+
credentials. This can be used to specify different certificates.
|
| 331 |
+
credentials_file (str): A file with credentials that can be loaded with
|
| 332 |
+
:func:`google.auth.load_credentials_from_file`. This argument is
|
| 333 |
+
mutually exclusive with credentials.
|
| 334 |
+
|
| 335 |
+
.. warning::
|
| 336 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 337 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 338 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 339 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 340 |
+
the security of your systems and data. For more information, refer to
|
| 341 |
+
`Validate credential configurations from external sources`_.
|
| 342 |
+
|
| 343 |
+
.. _Validate credential configurations from external sources:
|
| 344 |
+
|
| 345 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 346 |
+
quota_project_id (str): An optional project to use for billing and quota.
|
| 347 |
+
default_scopes (Sequence[str]): Default scopes passed by a Google client
|
| 348 |
+
library. Use 'scopes' for user-defined scopes.
|
| 349 |
+
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
|
| 350 |
+
compression (grpc.Compression): An optional value indicating the
|
| 351 |
+
compression method to be used over the lifetime of the channel.
|
| 352 |
+
attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted
|
| 353 |
+
when the request is made. Direct Path is only available within a Google
|
| 354 |
+
Compute Engine (GCE) environment and provides a proxyless connection
|
| 355 |
+
which increases the available throughput, reduces latency, and increases
|
| 356 |
+
reliability. Note:
|
| 357 |
+
|
| 358 |
+
- This argument should only be set in a GCE environment and for Services
|
| 359 |
+
that are known to support Direct Path.
|
| 360 |
+
- If this argument is set outside of GCE, then this request will fail
|
| 361 |
+
unless the back-end service happens to have configured fall-back to DNS.
|
| 362 |
+
- If the request causes a `ServiceUnavailable` response, it is recommended
|
| 363 |
+
that the client repeat the request with `attempt_direct_path` set to
|
| 364 |
+
`False` as the Service may not support Direct Path.
|
| 365 |
+
- Using `ssl_credentials` with `attempt_direct_path` set to `True` will
|
| 366 |
+
result in `ValueError` as this combination is not yet supported.
|
| 367 |
+
|
| 368 |
+
kwargs: Additional key-word args passed to
|
| 369 |
+
:func:`grpc_gcp.secure_channel` or :func:`grpc.secure_channel`.
|
| 370 |
+
Note: `grpc_gcp` is only supported in environments with protobuf < 4.0.0.
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
grpc.Channel: The created channel.
|
| 374 |
+
|
| 375 |
+
Raises:
|
| 376 |
+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
|
| 377 |
+
ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
|
| 378 |
+
"""
|
| 379 |
+
|
| 380 |
+
# If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
|
| 381 |
+
# raise ValueError as this is not yet supported.
|
| 382 |
+
# See https://github.com/googleapis/python-api-core/issues/590
|
| 383 |
+
if ssl_credentials and attempt_direct_path:
|
| 384 |
+
raise ValueError("Using ssl_credentials with Direct Path is not supported")
|
| 385 |
+
|
| 386 |
+
composite_credentials = _create_composite_credentials(
|
| 387 |
+
credentials=credentials,
|
| 388 |
+
credentials_file=credentials_file,
|
| 389 |
+
default_scopes=default_scopes,
|
| 390 |
+
scopes=scopes,
|
| 391 |
+
ssl_credentials=ssl_credentials,
|
| 392 |
+
quota_project_id=quota_project_id,
|
| 393 |
+
default_host=default_host,
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
# Note that grpcio-gcp is deprecated
|
| 397 |
+
if HAS_GRPC_GCP: # pragma: NO COVER
|
| 398 |
+
if compression is not None and compression != grpc.Compression.NoCompression:
|
| 399 |
+
warnings.warn(
|
| 400 |
+
"The `compression` argument is ignored for grpc_gcp.secure_channel creation.",
|
| 401 |
+
DeprecationWarning,
|
| 402 |
+
)
|
| 403 |
+
if attempt_direct_path:
|
| 404 |
+
warnings.warn(
|
| 405 |
+
"""The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation.""",
|
| 406 |
+
DeprecationWarning,
|
| 407 |
+
)
|
| 408 |
+
return grpc_gcp.secure_channel(target, composite_credentials, **kwargs)
|
| 409 |
+
|
| 410 |
+
if attempt_direct_path:
|
| 411 |
+
target = _modify_target_for_direct_path(target)
|
| 412 |
+
|
| 413 |
+
return grpc.secure_channel(
|
| 414 |
+
target, composite_credentials, compression=compression, **kwargs
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def _modify_target_for_direct_path(target: str) -> str:
|
| 419 |
+
"""
|
| 420 |
+
Given a target, return a modified version which is compatible with Direct Path.
|
| 421 |
+
|
| 422 |
+
Args:
|
| 423 |
+
target (str): The target service address in the format 'hostname[:port]' or
|
| 424 |
+
'dns://hostname[:port]'.
|
| 425 |
+
|
| 426 |
+
Returns:
|
| 427 |
+
target (str): The target service address which is converted into a format compatible with Direct Path.
|
| 428 |
+
If the target contains `dns:///` or does not contain `:///`, the target will be converted in
|
| 429 |
+
a format compatible with Direct Path; otherwise the original target will be returned as the
|
| 430 |
+
original target may already denote Direct Path.
|
| 431 |
+
"""
|
| 432 |
+
|
| 433 |
+
# A DNS prefix may be included with the target to indicate the endpoint is living in the Internet,
|
| 434 |
+
# outside of Google Cloud Platform.
|
| 435 |
+
dns_prefix = "dns:///"
|
| 436 |
+
# Remove "dns:///" if `attempt_direct_path` is set to True as
|
| 437 |
+
# the Direct Path prefix `google-c2p:///` will be used instead.
|
| 438 |
+
target = target.replace(dns_prefix, "")
|
| 439 |
+
|
| 440 |
+
direct_path_separator = ":///"
|
| 441 |
+
if direct_path_separator not in target:
|
| 442 |
+
target_without_port = target.split(":")[0]
|
| 443 |
+
# Modify the target to use Direct Path by adding the `google-c2p:///` prefix
|
| 444 |
+
target = f"google-c2p{direct_path_separator}{target_without_port}"
|
| 445 |
+
return target
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
_MethodCall = collections.namedtuple(
|
| 449 |
+
"_MethodCall", ("request", "timeout", "metadata", "credentials", "compression")
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
_ChannelRequest = collections.namedtuple("_ChannelRequest", ("method", "request"))
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
class _CallableStub(object):
|
| 456 |
+
"""Stub for the grpc.*MultiCallable interfaces."""
|
| 457 |
+
|
| 458 |
+
def __init__(self, method, channel):
|
| 459 |
+
self._method = method
|
| 460 |
+
self._channel = channel
|
| 461 |
+
self.response = None
|
| 462 |
+
"""Union[protobuf.Message, Callable[protobuf.Message], exception]:
|
| 463 |
+
The response to give when invoking this callable. If this is a
|
| 464 |
+
callable, it will be invoked with the request protobuf. If it's an
|
| 465 |
+
exception, the exception will be raised when this is invoked.
|
| 466 |
+
"""
|
| 467 |
+
self.responses = None
|
| 468 |
+
"""Iterator[
|
| 469 |
+
Union[protobuf.Message, Callable[protobuf.Message], exception]]:
|
| 470 |
+
An iterator of responses. If specified, self.response will be populated
|
| 471 |
+
on each invocation by calling ``next(self.responses)``."""
|
| 472 |
+
self.requests = []
|
| 473 |
+
"""List[protobuf.Message]: All requests sent to this callable."""
|
| 474 |
+
self.calls = []
|
| 475 |
+
"""List[Tuple]: All invocations of this callable. Each tuple is the
|
| 476 |
+
request, timeout, metadata, compression, and credentials."""
|
| 477 |
+
|
| 478 |
+
def __call__(
|
| 479 |
+
self, request, timeout=None, metadata=None, credentials=None, compression=None
|
| 480 |
+
):
|
| 481 |
+
self._channel.requests.append(_ChannelRequest(self._method, request))
|
| 482 |
+
self.calls.append(
|
| 483 |
+
_MethodCall(request, timeout, metadata, credentials, compression)
|
| 484 |
+
)
|
| 485 |
+
self.requests.append(request)
|
| 486 |
+
|
| 487 |
+
response = self.response
|
| 488 |
+
if self.responses is not None:
|
| 489 |
+
if response is None:
|
| 490 |
+
response = next(self.responses)
|
| 491 |
+
else:
|
| 492 |
+
raise ValueError(
|
| 493 |
+
"{method}.response and {method}.responses are mutually "
|
| 494 |
+
"exclusive.".format(method=self._method)
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
if callable(response):
|
| 498 |
+
return response(request)
|
| 499 |
+
|
| 500 |
+
if isinstance(response, Exception):
|
| 501 |
+
raise response
|
| 502 |
+
|
| 503 |
+
if response is not None:
|
| 504 |
+
return response
|
| 505 |
+
|
| 506 |
+
raise ValueError('Method stub for "{}" has no response.'.format(self._method))
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def _simplify_method_name(method):
|
| 510 |
+
"""Simplifies a gRPC method name.
|
| 511 |
+
|
| 512 |
+
When gRPC invokes the channel to create a callable, it gives a full
|
| 513 |
+
method name like "/google.pubsub.v1.Publisher/CreateTopic". This
|
| 514 |
+
returns just the name of the method, in this case "CreateTopic".
|
| 515 |
+
|
| 516 |
+
Args:
|
| 517 |
+
method (str): The name of the method.
|
| 518 |
+
|
| 519 |
+
Returns:
|
| 520 |
+
str: The simplified name of the method.
|
| 521 |
+
"""
|
| 522 |
+
return method.rsplit("/", 1).pop()
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
class ChannelStub(grpc.Channel):
|
| 526 |
+
"""A testing stub for the grpc.Channel interface.
|
| 527 |
+
|
| 528 |
+
This can be used to test any client that eventually uses a gRPC channel
|
| 529 |
+
to communicate. By passing in a channel stub, you can configure which
|
| 530 |
+
responses are returned and track which requests are made.
|
| 531 |
+
|
| 532 |
+
For example:
|
| 533 |
+
|
| 534 |
+
.. code-block:: python
|
| 535 |
+
|
| 536 |
+
channel_stub = grpc_helpers.ChannelStub()
|
| 537 |
+
client = FooClient(channel=channel_stub)
|
| 538 |
+
|
| 539 |
+
channel_stub.GetFoo.response = foo_pb2.Foo(name='bar')
|
| 540 |
+
|
| 541 |
+
foo = client.get_foo(labels=['baz'])
|
| 542 |
+
|
| 543 |
+
assert foo.name == 'bar'
|
| 544 |
+
assert channel_stub.GetFoo.requests[0].labels = ['baz']
|
| 545 |
+
|
| 546 |
+
Each method on the stub can be accessed and configured on the channel.
|
| 547 |
+
Here's some examples of various configurations:
|
| 548 |
+
|
| 549 |
+
.. code-block:: python
|
| 550 |
+
|
| 551 |
+
# Return a basic response:
|
| 552 |
+
|
| 553 |
+
channel_stub.GetFoo.response = foo_pb2.Foo(name='bar')
|
| 554 |
+
assert client.get_foo().name == 'bar'
|
| 555 |
+
|
| 556 |
+
# Raise an exception:
|
| 557 |
+
channel_stub.GetFoo.response = NotFound('...')
|
| 558 |
+
|
| 559 |
+
with pytest.raises(NotFound):
|
| 560 |
+
client.get_foo()
|
| 561 |
+
|
| 562 |
+
# Use a sequence of responses:
|
| 563 |
+
channel_stub.GetFoo.responses = iter([
|
| 564 |
+
foo_pb2.Foo(name='bar'),
|
| 565 |
+
foo_pb2.Foo(name='baz'),
|
| 566 |
+
])
|
| 567 |
+
|
| 568 |
+
assert client.get_foo().name == 'bar'
|
| 569 |
+
assert client.get_foo().name == 'baz'
|
| 570 |
+
|
| 571 |
+
# Use a callable
|
| 572 |
+
|
| 573 |
+
def on_get_foo(request):
|
| 574 |
+
return foo_pb2.Foo(name='bar' + request.id)
|
| 575 |
+
|
| 576 |
+
channel_stub.GetFoo.response = on_get_foo
|
| 577 |
+
|
| 578 |
+
assert client.get_foo(id='123').name == 'bar123'
|
| 579 |
+
"""
|
| 580 |
+
|
| 581 |
+
def __init__(self, responses=[]):
|
| 582 |
+
self.requests = []
|
| 583 |
+
"""Sequence[Tuple[str, protobuf.Message]]: A list of all requests made
|
| 584 |
+
on this channel in order. The tuple is of method name, request
|
| 585 |
+
message."""
|
| 586 |
+
self._method_stubs = {}
|
| 587 |
+
|
| 588 |
+
def _stub_for_method(self, method):
|
| 589 |
+
method = _simplify_method_name(method)
|
| 590 |
+
self._method_stubs[method] = _CallableStub(method, self)
|
| 591 |
+
return self._method_stubs[method]
|
| 592 |
+
|
| 593 |
+
def __getattr__(self, key):
|
| 594 |
+
try:
|
| 595 |
+
return self._method_stubs[key]
|
| 596 |
+
except KeyError:
|
| 597 |
+
raise AttributeError
|
| 598 |
+
|
| 599 |
+
def unary_unary(
|
| 600 |
+
self,
|
| 601 |
+
method,
|
| 602 |
+
request_serializer=None,
|
| 603 |
+
response_deserializer=None,
|
| 604 |
+
_registered_method=False,
|
| 605 |
+
):
|
| 606 |
+
"""grpc.Channel.unary_unary implementation."""
|
| 607 |
+
return self._stub_for_method(method)
|
| 608 |
+
|
| 609 |
+
def unary_stream(
|
| 610 |
+
self,
|
| 611 |
+
method,
|
| 612 |
+
request_serializer=None,
|
| 613 |
+
response_deserializer=None,
|
| 614 |
+
_registered_method=False,
|
| 615 |
+
):
|
| 616 |
+
"""grpc.Channel.unary_stream implementation."""
|
| 617 |
+
return self._stub_for_method(method)
|
| 618 |
+
|
| 619 |
+
def stream_unary(
|
| 620 |
+
self,
|
| 621 |
+
method,
|
| 622 |
+
request_serializer=None,
|
| 623 |
+
response_deserializer=None,
|
| 624 |
+
_registered_method=False,
|
| 625 |
+
):
|
| 626 |
+
"""grpc.Channel.stream_unary implementation."""
|
| 627 |
+
return self._stub_for_method(method)
|
| 628 |
+
|
| 629 |
+
def stream_stream(
|
| 630 |
+
self,
|
| 631 |
+
method,
|
| 632 |
+
request_serializer=None,
|
| 633 |
+
response_deserializer=None,
|
| 634 |
+
_registered_method=False,
|
| 635 |
+
):
|
| 636 |
+
"""grpc.Channel.stream_stream implementation."""
|
| 637 |
+
return self._stub_for_method(method)
|
| 638 |
+
|
| 639 |
+
def subscribe(self, callback, try_to_connect=False):
|
| 640 |
+
"""grpc.Channel.subscribe implementation."""
|
| 641 |
+
pass
|
| 642 |
+
|
| 643 |
+
def unsubscribe(self, callback):
|
| 644 |
+
"""grpc.Channel.unsubscribe implementation."""
|
| 645 |
+
pass
|
| 646 |
+
|
| 647 |
+
def close(self):
|
| 648 |
+
"""grpc.Channel.close implementation."""
|
| 649 |
+
pass
|
py311/lib/python3.11/site-packages/google/api_core/grpc_helpers_async.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""AsyncIO helpers for :mod:`grpc` supporting 3.7+.
|
| 16 |
+
|
| 17 |
+
Please combine more detailed docstring in grpc_helpers.py to use following
|
| 18 |
+
functions. This module is implementing the same surface with AsyncIO semantics.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import asyncio
|
| 22 |
+
import functools
|
| 23 |
+
import warnings
|
| 24 |
+
|
| 25 |
+
from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar
|
| 26 |
+
|
| 27 |
+
import grpc
|
| 28 |
+
from grpc import aio
|
| 29 |
+
|
| 30 |
+
from google.api_core import exceptions, general_helpers, grpc_helpers
|
| 31 |
+
|
| 32 |
+
# denotes the proto response type for grpc calls
|
| 33 |
+
P = TypeVar("P")
|
| 34 |
+
|
| 35 |
+
# NOTE(lidiz) Alternatively, we can hack "__getattribute__" to perform
|
| 36 |
+
# automatic patching for us. But that means the overhead of creating an
|
| 37 |
+
# extra Python function spreads to every single send and receive.
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _WrappedCall(aio.Call):
|
| 41 |
+
def __init__(self):
|
| 42 |
+
self._call = None
|
| 43 |
+
|
| 44 |
+
def with_call(self, call):
|
| 45 |
+
"""Supplies the call object separately to keep __init__ clean."""
|
| 46 |
+
self._call = call
|
| 47 |
+
return self
|
| 48 |
+
|
| 49 |
+
async def initial_metadata(self):
|
| 50 |
+
return await self._call.initial_metadata()
|
| 51 |
+
|
| 52 |
+
async def trailing_metadata(self):
|
| 53 |
+
return await self._call.trailing_metadata()
|
| 54 |
+
|
| 55 |
+
async def code(self):
|
| 56 |
+
return await self._call.code()
|
| 57 |
+
|
| 58 |
+
async def details(self):
|
| 59 |
+
return await self._call.details()
|
| 60 |
+
|
| 61 |
+
def cancelled(self):
|
| 62 |
+
return self._call.cancelled()
|
| 63 |
+
|
| 64 |
+
def done(self):
|
| 65 |
+
return self._call.done()
|
| 66 |
+
|
| 67 |
+
def time_remaining(self):
|
| 68 |
+
return self._call.time_remaining()
|
| 69 |
+
|
| 70 |
+
def cancel(self):
|
| 71 |
+
return self._call.cancel()
|
| 72 |
+
|
| 73 |
+
def add_done_callback(self, callback):
|
| 74 |
+
self._call.add_done_callback(callback)
|
| 75 |
+
|
| 76 |
+
async def wait_for_connection(self):
|
| 77 |
+
try:
|
| 78 |
+
await self._call.wait_for_connection()
|
| 79 |
+
except grpc.RpcError as rpc_error:
|
| 80 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class _WrappedUnaryResponseMixin(Generic[P], _WrappedCall):
|
| 84 |
+
def __await__(self) -> Iterator[P]:
|
| 85 |
+
try:
|
| 86 |
+
response = yield from self._call.__await__()
|
| 87 |
+
return response
|
| 88 |
+
except grpc.RpcError as rpc_error:
|
| 89 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class _WrappedStreamResponseMixin(Generic[P], _WrappedCall):
|
| 93 |
+
def __init__(self):
|
| 94 |
+
self._wrapped_async_generator = None
|
| 95 |
+
|
| 96 |
+
async def read(self) -> P:
|
| 97 |
+
try:
|
| 98 |
+
return await self._call.read()
|
| 99 |
+
except grpc.RpcError as rpc_error:
|
| 100 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 101 |
+
|
| 102 |
+
async def _wrapped_aiter(self) -> AsyncGenerator[P, None]:
|
| 103 |
+
try:
|
| 104 |
+
# NOTE(lidiz) coverage doesn't understand the exception raised from
|
| 105 |
+
# __anext__ method. It is covered by test case:
|
| 106 |
+
# test_wrap_stream_errors_aiter_non_rpc_error
|
| 107 |
+
async for response in self._call: # pragma: no branch
|
| 108 |
+
yield response
|
| 109 |
+
except grpc.RpcError as rpc_error:
|
| 110 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 111 |
+
|
| 112 |
+
def __aiter__(self) -> AsyncGenerator[P, None]:
|
| 113 |
+
if not self._wrapped_async_generator:
|
| 114 |
+
self._wrapped_async_generator = self._wrapped_aiter()
|
| 115 |
+
return self._wrapped_async_generator
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class _WrappedStreamRequestMixin(_WrappedCall):
|
| 119 |
+
async def write(self, request):
|
| 120 |
+
try:
|
| 121 |
+
await self._call.write(request)
|
| 122 |
+
except grpc.RpcError as rpc_error:
|
| 123 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 124 |
+
|
| 125 |
+
async def done_writing(self):
|
| 126 |
+
try:
|
| 127 |
+
await self._call.done_writing()
|
| 128 |
+
except grpc.RpcError as rpc_error:
|
| 129 |
+
raise exceptions.from_grpc_error(rpc_error) from rpc_error
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# NOTE(lidiz) Implementing each individual class separately, so we don't
|
| 133 |
+
# expose any API that should not be seen. E.g., __aiter__ in unary-unary
|
| 134 |
+
# RPC, or __await__ in stream-stream RPC.
|
| 135 |
+
class _WrappedUnaryUnaryCall(_WrappedUnaryResponseMixin[P], aio.UnaryUnaryCall):
|
| 136 |
+
"""Wrapped UnaryUnaryCall to map exceptions."""
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class _WrappedUnaryStreamCall(_WrappedStreamResponseMixin[P], aio.UnaryStreamCall):
|
| 140 |
+
"""Wrapped UnaryStreamCall to map exceptions."""
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class _WrappedStreamUnaryCall(
|
| 144 |
+
_WrappedUnaryResponseMixin[P], _WrappedStreamRequestMixin, aio.StreamUnaryCall
|
| 145 |
+
):
|
| 146 |
+
"""Wrapped StreamUnaryCall to map exceptions."""
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class _WrappedStreamStreamCall(
|
| 150 |
+
_WrappedStreamRequestMixin, _WrappedStreamResponseMixin[P], aio.StreamStreamCall
|
| 151 |
+
):
|
| 152 |
+
"""Wrapped StreamStreamCall to map exceptions."""
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# public type alias denoting the return type of async streaming gapic calls
|
| 156 |
+
GrpcAsyncStream = _WrappedStreamResponseMixin
|
| 157 |
+
# public type alias denoting the return type of unary gapic calls
|
| 158 |
+
AwaitableGrpcCall = _WrappedUnaryResponseMixin
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _wrap_unary_errors(callable_):
|
| 162 |
+
"""Map errors for Unary-Unary async callables."""
|
| 163 |
+
|
| 164 |
+
@functools.wraps(callable_)
|
| 165 |
+
def error_remapped_callable(*args, **kwargs):
|
| 166 |
+
call = callable_(*args, **kwargs)
|
| 167 |
+
return _WrappedUnaryUnaryCall().with_call(call)
|
| 168 |
+
|
| 169 |
+
return error_remapped_callable
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _wrap_stream_errors(callable_, wrapper_type):
|
| 173 |
+
"""Map errors for streaming RPC async callables."""
|
| 174 |
+
|
| 175 |
+
@functools.wraps(callable_)
|
| 176 |
+
async def error_remapped_callable(*args, **kwargs):
|
| 177 |
+
call = callable_(*args, **kwargs)
|
| 178 |
+
call = wrapper_type().with_call(call)
|
| 179 |
+
await call.wait_for_connection()
|
| 180 |
+
return call
|
| 181 |
+
|
| 182 |
+
return error_remapped_callable
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def wrap_errors(callable_):
|
| 186 |
+
"""Wrap a gRPC async callable and map :class:`grpc.RpcErrors` to
|
| 187 |
+
friendly error classes.
|
| 188 |
+
|
| 189 |
+
Errors raised by the gRPC callable are mapped to the appropriate
|
| 190 |
+
:class:`google.api_core.exceptions.GoogleAPICallError` subclasses. The
|
| 191 |
+
original `grpc.RpcError` (which is usually also a `grpc.Call`) is
|
| 192 |
+
available from the ``response`` property on the mapped exception. This
|
| 193 |
+
is useful for extracting metadata from the original error.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
callable_ (Callable): A gRPC callable.
|
| 197 |
+
|
| 198 |
+
Returns: Callable: The wrapped gRPC callable.
|
| 199 |
+
"""
|
| 200 |
+
grpc_helpers._patch_callable_name(callable_)
|
| 201 |
+
|
| 202 |
+
if isinstance(callable_, aio.UnaryStreamMultiCallable):
|
| 203 |
+
return _wrap_stream_errors(callable_, _WrappedUnaryStreamCall)
|
| 204 |
+
elif isinstance(callable_, aio.StreamUnaryMultiCallable):
|
| 205 |
+
return _wrap_stream_errors(callable_, _WrappedStreamUnaryCall)
|
| 206 |
+
elif isinstance(callable_, aio.StreamStreamMultiCallable):
|
| 207 |
+
return _wrap_stream_errors(callable_, _WrappedStreamStreamCall)
|
| 208 |
+
else:
|
| 209 |
+
return _wrap_unary_errors(callable_)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def create_channel(
|
| 213 |
+
target,
|
| 214 |
+
credentials=None,
|
| 215 |
+
scopes=None,
|
| 216 |
+
ssl_credentials=None,
|
| 217 |
+
credentials_file=None,
|
| 218 |
+
quota_project_id=None,
|
| 219 |
+
default_scopes=None,
|
| 220 |
+
default_host=None,
|
| 221 |
+
compression=None,
|
| 222 |
+
attempt_direct_path: Optional[bool] = False,
|
| 223 |
+
**kwargs,
|
| 224 |
+
):
|
| 225 |
+
"""Create an AsyncIO secure channel with credentials.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
target (str): The target service address in the format 'hostname:port'.
|
| 229 |
+
credentials (google.auth.credentials.Credentials): The credentials. If
|
| 230 |
+
not specified, then this function will attempt to ascertain the
|
| 231 |
+
credentials from the environment using :func:`google.auth.default`.
|
| 232 |
+
scopes (Sequence[str]): A optional list of scopes needed for this
|
| 233 |
+
service. These are only used when credentials are not specified and
|
| 234 |
+
are passed to :func:`google.auth.default`.
|
| 235 |
+
ssl_credentials (grpc.ChannelCredentials): Optional SSL channel
|
| 236 |
+
credentials. This can be used to specify different certificates.
|
| 237 |
+
credentials_file (str): Deprecated. A file with credentials that can be loaded with
|
| 238 |
+
:func:`google.auth.load_credentials_from_file`. This argument is
|
| 239 |
+
mutually exclusive with credentials. This argument will be
|
| 240 |
+
removed in the next major version of `google-api-core`.
|
| 241 |
+
|
| 242 |
+
.. warning::
|
| 243 |
+
Important: If you accept a credential configuration (credential JSON/File/Stream)
|
| 244 |
+
from an external source for authentication to Google Cloud Platform, you must
|
| 245 |
+
validate it before providing it to any Google API or client library. Providing an
|
| 246 |
+
unvalidated credential configuration to Google APIs or libraries can compromise
|
| 247 |
+
the security of your systems and data. For more information, refer to
|
| 248 |
+
`Validate credential configurations from external sources`_.
|
| 249 |
+
|
| 250 |
+
.. _Validate credential configurations from external sources:
|
| 251 |
+
|
| 252 |
+
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
|
| 253 |
+
quota_project_id (str): An optional project to use for billing and quota.
|
| 254 |
+
default_scopes (Sequence[str]): Default scopes passed by a Google client
|
| 255 |
+
library. Use 'scopes' for user-defined scopes.
|
| 256 |
+
default_host (str): The default endpoint. e.g., "pubsub.googleapis.com".
|
| 257 |
+
compression (grpc.Compression): An optional value indicating the
|
| 258 |
+
compression method to be used over the lifetime of the channel.
|
| 259 |
+
attempt_direct_path (Optional[bool]): If set, Direct Path will be attempted
|
| 260 |
+
when the request is made. Direct Path is only available within a Google
|
| 261 |
+
Compute Engine (GCE) environment and provides a proxyless connection
|
| 262 |
+
which increases the available throughput, reduces latency, and increases
|
| 263 |
+
reliability. Note:
|
| 264 |
+
|
| 265 |
+
- This argument should only be set in a GCE environment and for Services
|
| 266 |
+
that are known to support Direct Path.
|
| 267 |
+
- If this argument is set outside of GCE, then this request will fail
|
| 268 |
+
unless the back-end service happens to have configured fall-back to DNS.
|
| 269 |
+
- If the request causes a `ServiceUnavailable` response, it is recommended
|
| 270 |
+
that the client repeat the request with `attempt_direct_path` set to
|
| 271 |
+
`False` as the Service may not support Direct Path.
|
| 272 |
+
- Using `ssl_credentials` with `attempt_direct_path` set to `True` will
|
| 273 |
+
result in `ValueError` as this combination is not yet supported.
|
| 274 |
+
|
| 275 |
+
kwargs: Additional key-word args passed to :func:`aio.secure_channel`.
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
aio.Channel: The created channel.
|
| 279 |
+
|
| 280 |
+
Raises:
|
| 281 |
+
google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed.
|
| 282 |
+
ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`.
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
if credentials_file is not None:
|
| 286 |
+
warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning)
|
| 287 |
+
|
| 288 |
+
# If `ssl_credentials` is set and `attempt_direct_path` is set to `True`,
|
| 289 |
+
# raise ValueError as this is not yet supported.
|
| 290 |
+
# See https://github.com/googleapis/python-api-core/issues/590
|
| 291 |
+
if ssl_credentials and attempt_direct_path:
|
| 292 |
+
raise ValueError("Using ssl_credentials with Direct Path is not supported")
|
| 293 |
+
|
| 294 |
+
composite_credentials = grpc_helpers._create_composite_credentials(
|
| 295 |
+
credentials=credentials,
|
| 296 |
+
credentials_file=credentials_file,
|
| 297 |
+
scopes=scopes,
|
| 298 |
+
default_scopes=default_scopes,
|
| 299 |
+
ssl_credentials=ssl_credentials,
|
| 300 |
+
quota_project_id=quota_project_id,
|
| 301 |
+
default_host=default_host,
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
if attempt_direct_path:
|
| 305 |
+
target = grpc_helpers._modify_target_for_direct_path(target)
|
| 306 |
+
|
| 307 |
+
return aio.secure_channel(
|
| 308 |
+
target, composite_credentials, compression=compression, **kwargs
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class FakeUnaryUnaryCall(_WrappedUnaryUnaryCall):
|
| 313 |
+
"""Fake implementation for unary-unary RPCs.
|
| 314 |
+
|
| 315 |
+
It is a dummy object for response message. Supply the intended response
|
| 316 |
+
upon the initialization, and the coroutine will return the exact response
|
| 317 |
+
message.
|
| 318 |
+
"""
|
| 319 |
+
|
| 320 |
+
def __init__(self, response=object()):
|
| 321 |
+
self.response = response
|
| 322 |
+
self._future = asyncio.get_event_loop().create_future()
|
| 323 |
+
self._future.set_result(self.response)
|
| 324 |
+
|
| 325 |
+
def __await__(self):
|
| 326 |
+
response = yield from self._future.__await__()
|
| 327 |
+
return response
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class FakeStreamUnaryCall(_WrappedStreamUnaryCall):
|
| 331 |
+
"""Fake implementation for stream-unary RPCs.
|
| 332 |
+
|
| 333 |
+
It is a dummy object for response message. Supply the intended response
|
| 334 |
+
upon the initialization, and the coroutine will return the exact response
|
| 335 |
+
message.
|
| 336 |
+
"""
|
| 337 |
+
|
| 338 |
+
def __init__(self, response=object()):
|
| 339 |
+
self.response = response
|
| 340 |
+
self._future = asyncio.get_event_loop().create_future()
|
| 341 |
+
self._future.set_result(self.response)
|
| 342 |
+
|
| 343 |
+
def __await__(self):
|
| 344 |
+
response = yield from self._future.__await__()
|
| 345 |
+
return response
|
| 346 |
+
|
| 347 |
+
async def wait_for_connection(self):
|
| 348 |
+
pass
|
py311/lib/python3.11/site-packages/google/api_core/iam.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 |
+
"""Non-API-specific IAM policy definitions
|
| 15 |
+
|
| 16 |
+
For allowed roles / permissions, see:
|
| 17 |
+
https://cloud.google.com/iam/docs/understanding-roles
|
| 18 |
+
|
| 19 |
+
Example usage:
|
| 20 |
+
|
| 21 |
+
.. code-block:: python
|
| 22 |
+
|
| 23 |
+
# ``get_iam_policy`` returns a :class:'~google.api_core.iam.Policy`.
|
| 24 |
+
policy = resource.get_iam_policy(requested_policy_version=3)
|
| 25 |
+
|
| 26 |
+
phred = "user:phred@example.com"
|
| 27 |
+
admin_group = "group:admins@groups.example.com"
|
| 28 |
+
account = "serviceAccount:account-1234@accounts.example.com"
|
| 29 |
+
|
| 30 |
+
policy.version = 3
|
| 31 |
+
policy.bindings = [
|
| 32 |
+
{
|
| 33 |
+
"role": "roles/owner",
|
| 34 |
+
"members": {phred, admin_group, account}
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"role": "roles/editor",
|
| 38 |
+
"members": {"allAuthenticatedUsers"}
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"role": "roles/viewer",
|
| 42 |
+
"members": {"allUsers"}
|
| 43 |
+
"condition": {
|
| 44 |
+
"title": "request_time",
|
| 45 |
+
"description": "Requests made before 2021-01-01T00:00:00Z",
|
| 46 |
+
"expression": "request.time < timestamp(\"2021-01-01T00:00:00Z\")"
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
resource.set_iam_policy(policy)
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
import collections
|
| 55 |
+
import collections.abc
|
| 56 |
+
import operator
|
| 57 |
+
import warnings
|
| 58 |
+
|
| 59 |
+
# Generic IAM roles
|
| 60 |
+
|
| 61 |
+
OWNER_ROLE = "roles/owner"
|
| 62 |
+
"""Generic role implying all rights to an object."""
|
| 63 |
+
|
| 64 |
+
EDITOR_ROLE = "roles/editor"
|
| 65 |
+
"""Generic role implying rights to modify an object."""
|
| 66 |
+
|
| 67 |
+
VIEWER_ROLE = "roles/viewer"
|
| 68 |
+
"""Generic role implying rights to access an object."""
|
| 69 |
+
|
| 70 |
+
_ASSIGNMENT_DEPRECATED_MSG = """\
|
| 71 |
+
Assigning to '{}' is deprecated. Use the `policy.bindings` property to modify bindings instead."""
|
| 72 |
+
|
| 73 |
+
_DICT_ACCESS_MSG = """\
|
| 74 |
+
Dict access is not supported on policies with version > 1 or with conditional bindings."""
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class InvalidOperationException(Exception):
|
| 78 |
+
"""Raised when trying to use Policy class as a dict."""
|
| 79 |
+
|
| 80 |
+
pass
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class Policy(collections.abc.MutableMapping):
|
| 84 |
+
"""IAM Policy
|
| 85 |
+
|
| 86 |
+
Args:
|
| 87 |
+
etag (Optional[str]): ETag used to identify a unique of the policy
|
| 88 |
+
version (Optional[int]): The syntax schema version of the policy.
|
| 89 |
+
|
| 90 |
+
Note:
|
| 91 |
+
Using conditions in bindings requires the policy's version to be set
|
| 92 |
+
to `3` or greater, depending on the versions that are currently supported.
|
| 93 |
+
|
| 94 |
+
Accessing the policy using dict operations will raise InvalidOperationException
|
| 95 |
+
when the policy's version is set to 3.
|
| 96 |
+
|
| 97 |
+
Use the policy.bindings getter/setter to retrieve and modify the policy's bindings.
|
| 98 |
+
|
| 99 |
+
See:
|
| 100 |
+
IAM Policy https://cloud.google.com/iam/reference/rest/v1/Policy
|
| 101 |
+
Policy versions https://cloud.google.com/iam/docs/policies#versions
|
| 102 |
+
Conditions overview https://cloud.google.com/iam/docs/conditions-overview.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
_OWNER_ROLES = (OWNER_ROLE,)
|
| 106 |
+
"""Roles mapped onto our ``owners`` attribute."""
|
| 107 |
+
|
| 108 |
+
_EDITOR_ROLES = (EDITOR_ROLE,)
|
| 109 |
+
"""Roles mapped onto our ``editors`` attribute."""
|
| 110 |
+
|
| 111 |
+
_VIEWER_ROLES = (VIEWER_ROLE,)
|
| 112 |
+
"""Roles mapped onto our ``viewers`` attribute."""
|
| 113 |
+
|
| 114 |
+
def __init__(self, etag=None, version=None):
|
| 115 |
+
self.etag = etag
|
| 116 |
+
self.version = version
|
| 117 |
+
self._bindings = []
|
| 118 |
+
|
| 119 |
+
def __iter__(self):
|
| 120 |
+
self.__check_version__()
|
| 121 |
+
# Exclude bindings with no members
|
| 122 |
+
return (binding["role"] for binding in self._bindings if binding["members"])
|
| 123 |
+
|
| 124 |
+
def __len__(self):
|
| 125 |
+
self.__check_version__()
|
| 126 |
+
# Exclude bindings with no members
|
| 127 |
+
return len(list(self.__iter__()))
|
| 128 |
+
|
| 129 |
+
def __getitem__(self, key):
|
| 130 |
+
self.__check_version__()
|
| 131 |
+
for b in self._bindings:
|
| 132 |
+
if b["role"] == key:
|
| 133 |
+
return b["members"]
|
| 134 |
+
# If the binding does not yet exist, create one
|
| 135 |
+
# NOTE: This will create bindings with no members
|
| 136 |
+
# which are ignored by __iter__ and __len__
|
| 137 |
+
new_binding = {"role": key, "members": set()}
|
| 138 |
+
self._bindings.append(new_binding)
|
| 139 |
+
return new_binding["members"]
|
| 140 |
+
|
| 141 |
+
def __setitem__(self, key, value):
|
| 142 |
+
self.__check_version__()
|
| 143 |
+
value = set(value)
|
| 144 |
+
for binding in self._bindings:
|
| 145 |
+
if binding["role"] == key:
|
| 146 |
+
binding["members"] = value
|
| 147 |
+
return
|
| 148 |
+
self._bindings.append({"role": key, "members": value})
|
| 149 |
+
|
| 150 |
+
def __delitem__(self, key):
|
| 151 |
+
self.__check_version__()
|
| 152 |
+
for b in self._bindings:
|
| 153 |
+
if b["role"] == key:
|
| 154 |
+
self._bindings.remove(b)
|
| 155 |
+
return
|
| 156 |
+
raise KeyError(key)
|
| 157 |
+
|
| 158 |
+
def __check_version__(self):
|
| 159 |
+
"""Raise InvalidOperationException if version is greater than 1 or policy contains conditions."""
|
| 160 |
+
raise_version = self.version is not None and self.version > 1
|
| 161 |
+
|
| 162 |
+
if raise_version or self._contains_conditions():
|
| 163 |
+
raise InvalidOperationException(_DICT_ACCESS_MSG)
|
| 164 |
+
|
| 165 |
+
def _contains_conditions(self):
|
| 166 |
+
for b in self._bindings:
|
| 167 |
+
if b.get("condition") is not None:
|
| 168 |
+
return True
|
| 169 |
+
return False
|
| 170 |
+
|
| 171 |
+
@property
|
| 172 |
+
def bindings(self):
|
| 173 |
+
"""The policy's list of bindings.
|
| 174 |
+
|
| 175 |
+
A binding is specified by a dictionary with keys:
|
| 176 |
+
|
| 177 |
+
* role (str): Role that is assigned to `members`.
|
| 178 |
+
|
| 179 |
+
* members (:obj:`set` of str): Specifies the identities associated to this binding.
|
| 180 |
+
|
| 181 |
+
* condition (:obj:`dict` of str:str): Specifies a condition under which this binding will apply.
|
| 182 |
+
|
| 183 |
+
* title (str): Title for the condition.
|
| 184 |
+
|
| 185 |
+
* description (:obj:str, optional): Description of the condition.
|
| 186 |
+
|
| 187 |
+
* expression: A CEL expression.
|
| 188 |
+
|
| 189 |
+
Type:
|
| 190 |
+
:obj:`list` of :obj:`dict`
|
| 191 |
+
|
| 192 |
+
See:
|
| 193 |
+
Policy versions https://cloud.google.com/iam/docs/policies#versions
|
| 194 |
+
Conditions overview https://cloud.google.com/iam/docs/conditions-overview.
|
| 195 |
+
|
| 196 |
+
Example:
|
| 197 |
+
|
| 198 |
+
.. code-block:: python
|
| 199 |
+
|
| 200 |
+
USER = "user:phred@example.com"
|
| 201 |
+
ADMIN_GROUP = "group:admins@groups.example.com"
|
| 202 |
+
SERVICE_ACCOUNT = "serviceAccount:account-1234@accounts.example.com"
|
| 203 |
+
CONDITION = {
|
| 204 |
+
"title": "request_time",
|
| 205 |
+
"description": "Requests made before 2021-01-01T00:00:00Z", # Optional
|
| 206 |
+
"expression": "request.time < timestamp(\"2021-01-01T00:00:00Z\")"
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
# Set policy's version to 3 before setting bindings containing conditions.
|
| 210 |
+
policy.version = 3
|
| 211 |
+
|
| 212 |
+
policy.bindings = [
|
| 213 |
+
{
|
| 214 |
+
"role": "roles/viewer",
|
| 215 |
+
"members": {USER, ADMIN_GROUP, SERVICE_ACCOUNT},
|
| 216 |
+
"condition": CONDITION
|
| 217 |
+
},
|
| 218 |
+
...
|
| 219 |
+
]
|
| 220 |
+
"""
|
| 221 |
+
return self._bindings
|
| 222 |
+
|
| 223 |
+
@bindings.setter
|
| 224 |
+
def bindings(self, bindings):
|
| 225 |
+
self._bindings = bindings
|
| 226 |
+
|
| 227 |
+
@property
|
| 228 |
+
def owners(self):
|
| 229 |
+
"""Legacy access to owner role.
|
| 230 |
+
|
| 231 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 232 |
+
|
| 233 |
+
DEPRECATED: use `policy.bindings` to access bindings instead.
|
| 234 |
+
"""
|
| 235 |
+
result = set()
|
| 236 |
+
for role in self._OWNER_ROLES:
|
| 237 |
+
for member in self.get(role, ()):
|
| 238 |
+
result.add(member)
|
| 239 |
+
return frozenset(result)
|
| 240 |
+
|
| 241 |
+
@owners.setter
|
| 242 |
+
def owners(self, value):
|
| 243 |
+
"""Update owners.
|
| 244 |
+
|
| 245 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 246 |
+
|
| 247 |
+
DEPRECATED: use `policy.bindings` to access bindings instead.
|
| 248 |
+
"""
|
| 249 |
+
warnings.warn(
|
| 250 |
+
_ASSIGNMENT_DEPRECATED_MSG.format("owners", OWNER_ROLE), DeprecationWarning
|
| 251 |
+
)
|
| 252 |
+
self[OWNER_ROLE] = value
|
| 253 |
+
|
| 254 |
+
@property
|
| 255 |
+
def editors(self):
|
| 256 |
+
"""Legacy access to editor role.
|
| 257 |
+
|
| 258 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 259 |
+
|
| 260 |
+
DEPRECATED: use `policy.bindings` to access bindings instead.
|
| 261 |
+
"""
|
| 262 |
+
result = set()
|
| 263 |
+
for role in self._EDITOR_ROLES:
|
| 264 |
+
for member in self.get(role, ()):
|
| 265 |
+
result.add(member)
|
| 266 |
+
return frozenset(result)
|
| 267 |
+
|
| 268 |
+
@editors.setter
|
| 269 |
+
def editors(self, value):
|
| 270 |
+
"""Update editors.
|
| 271 |
+
|
| 272 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 273 |
+
|
| 274 |
+
DEPRECATED: use `policy.bindings` to modify bindings instead.
|
| 275 |
+
"""
|
| 276 |
+
warnings.warn(
|
| 277 |
+
_ASSIGNMENT_DEPRECATED_MSG.format("editors", EDITOR_ROLE),
|
| 278 |
+
DeprecationWarning,
|
| 279 |
+
)
|
| 280 |
+
self[EDITOR_ROLE] = value
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def viewers(self):
|
| 284 |
+
"""Legacy access to viewer role.
|
| 285 |
+
|
| 286 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 287 |
+
|
| 288 |
+
DEPRECATED: use `policy.bindings` to modify bindings instead.
|
| 289 |
+
"""
|
| 290 |
+
result = set()
|
| 291 |
+
for role in self._VIEWER_ROLES:
|
| 292 |
+
for member in self.get(role, ()):
|
| 293 |
+
result.add(member)
|
| 294 |
+
return frozenset(result)
|
| 295 |
+
|
| 296 |
+
@viewers.setter
|
| 297 |
+
def viewers(self, value):
|
| 298 |
+
"""Update viewers.
|
| 299 |
+
|
| 300 |
+
Raise InvalidOperationException if version is greater than 1 or policy contains conditions.
|
| 301 |
+
|
| 302 |
+
DEPRECATED: use `policy.bindings` to modify bindings instead.
|
| 303 |
+
"""
|
| 304 |
+
warnings.warn(
|
| 305 |
+
_ASSIGNMENT_DEPRECATED_MSG.format("viewers", VIEWER_ROLE),
|
| 306 |
+
DeprecationWarning,
|
| 307 |
+
)
|
| 308 |
+
self[VIEWER_ROLE] = value
|
| 309 |
+
|
| 310 |
+
@staticmethod
|
| 311 |
+
def user(email):
|
| 312 |
+
"""Factory method for a user member.
|
| 313 |
+
|
| 314 |
+
Args:
|
| 315 |
+
email (str): E-mail for this particular user.
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
str: A member string corresponding to the given user.
|
| 319 |
+
"""
|
| 320 |
+
return "user:%s" % (email,)
|
| 321 |
+
|
| 322 |
+
@staticmethod
|
| 323 |
+
def service_account(email):
|
| 324 |
+
"""Factory method for a service account member.
|
| 325 |
+
|
| 326 |
+
Args:
|
| 327 |
+
email (str): E-mail for this particular service account.
|
| 328 |
+
|
| 329 |
+
Returns:
|
| 330 |
+
str: A member string corresponding to the given service account.
|
| 331 |
+
|
| 332 |
+
"""
|
| 333 |
+
return "serviceAccount:%s" % (email,)
|
| 334 |
+
|
| 335 |
+
@staticmethod
|
| 336 |
+
def group(email):
|
| 337 |
+
"""Factory method for a group member.
|
| 338 |
+
|
| 339 |
+
Args:
|
| 340 |
+
email (str): An id or e-mail for this particular group.
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
str: A member string corresponding to the given group.
|
| 344 |
+
"""
|
| 345 |
+
return "group:%s" % (email,)
|
| 346 |
+
|
| 347 |
+
@staticmethod
|
| 348 |
+
def domain(domain):
|
| 349 |
+
"""Factory method for a domain member.
|
| 350 |
+
|
| 351 |
+
Args:
|
| 352 |
+
domain (str): The domain for this member.
|
| 353 |
+
|
| 354 |
+
Returns:
|
| 355 |
+
str: A member string corresponding to the given domain.
|
| 356 |
+
"""
|
| 357 |
+
return "domain:%s" % (domain,)
|
| 358 |
+
|
| 359 |
+
@staticmethod
|
| 360 |
+
def all_users():
|
| 361 |
+
"""Factory method for a member representing all users.
|
| 362 |
+
|
| 363 |
+
Returns:
|
| 364 |
+
str: A member string representing all users.
|
| 365 |
+
"""
|
| 366 |
+
return "allUsers"
|
| 367 |
+
|
| 368 |
+
@staticmethod
|
| 369 |
+
def authenticated_users():
|
| 370 |
+
"""Factory method for a member representing all authenticated users.
|
| 371 |
+
|
| 372 |
+
Returns:
|
| 373 |
+
str: A member string representing all authenticated users.
|
| 374 |
+
"""
|
| 375 |
+
return "allAuthenticatedUsers"
|
| 376 |
+
|
| 377 |
+
@classmethod
|
| 378 |
+
def from_api_repr(cls, resource):
|
| 379 |
+
"""Factory: create a policy from a JSON resource.
|
| 380 |
+
|
| 381 |
+
Args:
|
| 382 |
+
resource (dict): policy resource returned by ``getIamPolicy`` API.
|
| 383 |
+
|
| 384 |
+
Returns:
|
| 385 |
+
:class:`Policy`: the parsed policy
|
| 386 |
+
"""
|
| 387 |
+
version = resource.get("version")
|
| 388 |
+
etag = resource.get("etag")
|
| 389 |
+
policy = cls(etag, version)
|
| 390 |
+
policy.bindings = resource.get("bindings", [])
|
| 391 |
+
|
| 392 |
+
for binding in policy.bindings:
|
| 393 |
+
binding["members"] = set(binding.get("members", ()))
|
| 394 |
+
|
| 395 |
+
return policy
|
| 396 |
+
|
| 397 |
+
def to_api_repr(self):
|
| 398 |
+
"""Render a JSON policy resource.
|
| 399 |
+
|
| 400 |
+
Returns:
|
| 401 |
+
dict: a resource to be passed to the ``setIamPolicy`` API.
|
| 402 |
+
"""
|
| 403 |
+
resource = {}
|
| 404 |
+
|
| 405 |
+
if self.etag is not None:
|
| 406 |
+
resource["etag"] = self.etag
|
| 407 |
+
|
| 408 |
+
if self.version is not None:
|
| 409 |
+
resource["version"] = self.version
|
| 410 |
+
|
| 411 |
+
if self._bindings and len(self._bindings) > 0:
|
| 412 |
+
bindings = []
|
| 413 |
+
for binding in self._bindings:
|
| 414 |
+
members = binding.get("members")
|
| 415 |
+
if members:
|
| 416 |
+
new_binding = {"role": binding["role"], "members": sorted(members)}
|
| 417 |
+
condition = binding.get("condition")
|
| 418 |
+
if condition:
|
| 419 |
+
new_binding["condition"] = condition
|
| 420 |
+
bindings.append(new_binding)
|
| 421 |
+
|
| 422 |
+
if bindings:
|
| 423 |
+
# Sort bindings by role
|
| 424 |
+
key = operator.itemgetter("role")
|
| 425 |
+
resource["bindings"] = sorted(bindings, key=key)
|
| 426 |
+
|
| 427 |
+
return resource
|
py311/lib/python3.11/site-packages/google/api_core/operation.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google LLC
|
| 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 |
+
"""Futures for long-running operations returned from Google Cloud APIs.
|
| 16 |
+
|
| 17 |
+
These futures can be used to synchronously wait for the result of a
|
| 18 |
+
long-running operation using :meth:`Operation.result`:
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
.. code-block:: python
|
| 22 |
+
|
| 23 |
+
operation = my_api_client.long_running_method()
|
| 24 |
+
result = operation.result()
|
| 25 |
+
|
| 26 |
+
Or asynchronously using callbacks and :meth:`Operation.add_done_callback`:
|
| 27 |
+
|
| 28 |
+
.. code-block:: python
|
| 29 |
+
|
| 30 |
+
operation = my_api_client.long_running_method()
|
| 31 |
+
|
| 32 |
+
def my_callback(future):
|
| 33 |
+
result = future.result()
|
| 34 |
+
|
| 35 |
+
operation.add_done_callback(my_callback)
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
import functools
|
| 40 |
+
import threading
|
| 41 |
+
|
| 42 |
+
from google.api_core import exceptions
|
| 43 |
+
from google.api_core import protobuf_helpers
|
| 44 |
+
from google.api_core.future import polling
|
| 45 |
+
from google.longrunning import operations_pb2
|
| 46 |
+
from google.protobuf import json_format
|
| 47 |
+
from google.rpc import code_pb2
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Operation(polling.PollingFuture):
|
| 51 |
+
"""A Future for interacting with a Google API Long-Running Operation.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
operation (google.longrunning.operations_pb2.Operation): The
|
| 55 |
+
initial operation.
|
| 56 |
+
refresh (Callable[[], ~.api_core.operation.Operation]): A callable that
|
| 57 |
+
returns the latest state of the operation.
|
| 58 |
+
cancel (Callable[[], None]): A callable that tries to cancel
|
| 59 |
+
the operation.
|
| 60 |
+
result_type (func:`type`): The protobuf type for the operation's
|
| 61 |
+
result.
|
| 62 |
+
metadata_type (func:`type`): The protobuf type for the operation's
|
| 63 |
+
metadata.
|
| 64 |
+
polling (google.api_core.retry.Retry): The configuration used for polling.
|
| 65 |
+
This parameter controls how often :meth:`done` is polled. If the
|
| 66 |
+
``timeout`` argument is specified in the :meth:`result` method, it will
|
| 67 |
+
override the ``polling.timeout`` property.
|
| 68 |
+
retry (google.api_core.retry.Retry): DEPRECATED: use ``polling`` instead.
|
| 69 |
+
If specified it will override ``polling`` parameter to maintain
|
| 70 |
+
backward compatibility.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
operation,
|
| 76 |
+
refresh,
|
| 77 |
+
cancel,
|
| 78 |
+
result_type,
|
| 79 |
+
metadata_type=None,
|
| 80 |
+
polling=polling.DEFAULT_POLLING,
|
| 81 |
+
**kwargs,
|
| 82 |
+
):
|
| 83 |
+
super(Operation, self).__init__(polling=polling, **kwargs)
|
| 84 |
+
self._operation = operation
|
| 85 |
+
self._refresh = refresh
|
| 86 |
+
self._cancel = cancel
|
| 87 |
+
self._result_type = result_type
|
| 88 |
+
self._metadata_type = metadata_type
|
| 89 |
+
self._completion_lock = threading.Lock()
|
| 90 |
+
# Invoke this in case the operation came back already complete.
|
| 91 |
+
self._set_result_from_operation()
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def operation(self):
|
| 95 |
+
"""google.longrunning.Operation: The current long-running operation."""
|
| 96 |
+
return self._operation
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def metadata(self):
|
| 100 |
+
"""google.protobuf.Message: the current operation metadata."""
|
| 101 |
+
if not self._operation.HasField("metadata"):
|
| 102 |
+
return None
|
| 103 |
+
|
| 104 |
+
return protobuf_helpers.from_any_pb(
|
| 105 |
+
self._metadata_type, self._operation.metadata
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
@classmethod
|
| 109 |
+
def deserialize(self, payload):
|
| 110 |
+
"""Deserialize a ``google.longrunning.Operation`` protocol buffer.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
payload (bytes): A serialized operation protocol buffer.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
~.operations_pb2.Operation: An Operation protobuf object.
|
| 117 |
+
"""
|
| 118 |
+
return operations_pb2.Operation.FromString(payload)
|
| 119 |
+
|
| 120 |
+
def _set_result_from_operation(self):
|
| 121 |
+
"""Set the result or exception from the operation if it is complete."""
|
| 122 |
+
# This must be done in a lock to prevent the polling thread
|
| 123 |
+
# and main thread from both executing the completion logic
|
| 124 |
+
# at the same time.
|
| 125 |
+
with self._completion_lock:
|
| 126 |
+
# If the operation isn't complete or if the result has already been
|
| 127 |
+
# set, do not call set_result/set_exception again.
|
| 128 |
+
# Note: self._result_set is set to True in set_result and
|
| 129 |
+
# set_exception, in case those methods are invoked directly.
|
| 130 |
+
if not self._operation.done or self._result_set:
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
if self._operation.HasField("response"):
|
| 134 |
+
response = protobuf_helpers.from_any_pb(
|
| 135 |
+
self._result_type, self._operation.response
|
| 136 |
+
)
|
| 137 |
+
self.set_result(response)
|
| 138 |
+
elif self._operation.HasField("error"):
|
| 139 |
+
exception = exceptions.from_grpc_status(
|
| 140 |
+
status_code=self._operation.error.code,
|
| 141 |
+
message=self._operation.error.message,
|
| 142 |
+
errors=(self._operation.error,),
|
| 143 |
+
response=self._operation,
|
| 144 |
+
)
|
| 145 |
+
self.set_exception(exception)
|
| 146 |
+
else:
|
| 147 |
+
exception = exceptions.GoogleAPICallError(
|
| 148 |
+
"Unexpected state: Long-running operation had neither "
|
| 149 |
+
"response nor error set."
|
| 150 |
+
)
|
| 151 |
+
self.set_exception(exception)
|
| 152 |
+
|
| 153 |
+
def _refresh_and_update(self, retry=None):
|
| 154 |
+
"""Refresh the operation and update the result if needed.
|
| 155 |
+
|
| 156 |
+
Args:
|
| 157 |
+
retry (google.api_core.retry.Retry): (Optional) How to retry the RPC.
|
| 158 |
+
"""
|
| 159 |
+
# If the currently cached operation is done, no need to make another
|
| 160 |
+
# RPC as it will not change once done.
|
| 161 |
+
if not self._operation.done:
|
| 162 |
+
self._operation = self._refresh(retry=retry) if retry else self._refresh()
|
| 163 |
+
self._set_result_from_operation()
|
| 164 |
+
|
| 165 |
+
def done(self, retry=None):
|
| 166 |
+
"""Checks to see if the operation is complete.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
retry (google.api_core.retry.Retry): (Optional) How to retry the RPC.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
bool: True if the operation is complete, False otherwise.
|
| 173 |
+
"""
|
| 174 |
+
self._refresh_and_update(retry)
|
| 175 |
+
return self._operation.done
|
| 176 |
+
|
| 177 |
+
def cancel(self):
|
| 178 |
+
"""Attempt to cancel the operation.
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
bool: True if the cancel RPC was made, False if the operation is
|
| 182 |
+
already complete.
|
| 183 |
+
"""
|
| 184 |
+
if self.done():
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
self._cancel()
|
| 188 |
+
return True
|
| 189 |
+
|
| 190 |
+
def cancelled(self):
|
| 191 |
+
"""True if the operation was cancelled."""
|
| 192 |
+
self._refresh_and_update()
|
| 193 |
+
return (
|
| 194 |
+
self._operation.HasField("error")
|
| 195 |
+
and self._operation.error.code == code_pb2.CANCELLED
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def _refresh_http(api_request, operation_name, retry=None):
|
| 200 |
+
"""Refresh an operation using a JSON/HTTP client.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
api_request (Callable): A callable used to make an API request. This
|
| 204 |
+
should generally be
|
| 205 |
+
:meth:`google.cloud._http.Connection.api_request`.
|
| 206 |
+
operation_name (str): The name of the operation.
|
| 207 |
+
retry (google.api_core.retry.Retry): (Optional) retry policy
|
| 208 |
+
|
| 209 |
+
Returns:
|
| 210 |
+
google.longrunning.operations_pb2.Operation: The operation.
|
| 211 |
+
"""
|
| 212 |
+
path = "operations/{}".format(operation_name)
|
| 213 |
+
|
| 214 |
+
if retry is not None:
|
| 215 |
+
api_request = retry(api_request)
|
| 216 |
+
|
| 217 |
+
api_response = api_request(method="GET", path=path)
|
| 218 |
+
return json_format.ParseDict(api_response, operations_pb2.Operation())
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _cancel_http(api_request, operation_name):
|
| 222 |
+
"""Cancel an operation using a JSON/HTTP client.
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
api_request (Callable): A callable used to make an API request. This
|
| 226 |
+
should generally be
|
| 227 |
+
:meth:`google.cloud._http.Connection.api_request`.
|
| 228 |
+
operation_name (str): The name of the operation.
|
| 229 |
+
"""
|
| 230 |
+
path = "operations/{}:cancel".format(operation_name)
|
| 231 |
+
api_request(method="POST", path=path)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def from_http_json(operation, api_request, result_type, **kwargs):
|
| 235 |
+
"""Create an operation future using a HTTP/JSON client.
|
| 236 |
+
|
| 237 |
+
This interacts with the long-running operations `service`_ (specific
|
| 238 |
+
to a given API) via `HTTP/JSON`_.
|
| 239 |
+
|
| 240 |
+
.. _HTTP/JSON: https://cloud.google.com/speech/reference/rest/\
|
| 241 |
+
v1beta1/operations#Operation
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
operation (dict): Operation as a dictionary.
|
| 245 |
+
api_request (Callable): A callable used to make an API request. This
|
| 246 |
+
should generally be
|
| 247 |
+
:meth:`google.cloud._http.Connection.api_request`.
|
| 248 |
+
result_type (:func:`type`): The protobuf result type.
|
| 249 |
+
kwargs: Keyword args passed into the :class:`Operation` constructor.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
~.api_core.operation.Operation: The operation future to track the given
|
| 253 |
+
operation.
|
| 254 |
+
"""
|
| 255 |
+
operation_proto = json_format.ParseDict(operation, operations_pb2.Operation())
|
| 256 |
+
refresh = functools.partial(_refresh_http, api_request, operation_proto.name)
|
| 257 |
+
cancel = functools.partial(_cancel_http, api_request, operation_proto.name)
|
| 258 |
+
return Operation(operation_proto, refresh, cancel, result_type, **kwargs)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def _refresh_grpc(operations_stub, operation_name, retry=None):
|
| 262 |
+
"""Refresh an operation using a gRPC client.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
operations_stub (google.longrunning.operations_pb2.OperationsStub):
|
| 266 |
+
The gRPC operations stub.
|
| 267 |
+
operation_name (str): The name of the operation.
|
| 268 |
+
retry (google.api_core.retry.Retry): (Optional) retry policy
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
google.longrunning.operations_pb2.Operation: The operation.
|
| 272 |
+
"""
|
| 273 |
+
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
|
| 274 |
+
|
| 275 |
+
rpc = operations_stub.GetOperation
|
| 276 |
+
if retry is not None:
|
| 277 |
+
rpc = retry(rpc)
|
| 278 |
+
|
| 279 |
+
return rpc(request_pb)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def _cancel_grpc(operations_stub, operation_name):
|
| 283 |
+
"""Cancel an operation using a gRPC client.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
operations_stub (google.longrunning.operations_pb2.OperationsStub):
|
| 287 |
+
The gRPC operations stub.
|
| 288 |
+
operation_name (str): The name of the operation.
|
| 289 |
+
"""
|
| 290 |
+
request_pb = operations_pb2.CancelOperationRequest(name=operation_name)
|
| 291 |
+
operations_stub.CancelOperation(request_pb)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def from_grpc(operation, operations_stub, result_type, grpc_metadata=None, **kwargs):
|
| 295 |
+
"""Create an operation future using a gRPC client.
|
| 296 |
+
|
| 297 |
+
This interacts with the long-running operations `service`_ (specific
|
| 298 |
+
to a given API) via gRPC.
|
| 299 |
+
|
| 300 |
+
.. _service: https://github.com/googleapis/googleapis/blob/\
|
| 301 |
+
050400df0fdb16f63b63e9dee53819044bffc857/\
|
| 302 |
+
google/longrunning/operations.proto#L38
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
operation (google.longrunning.operations_pb2.Operation): The operation.
|
| 306 |
+
operations_stub (google.longrunning.operations_pb2.OperationsStub):
|
| 307 |
+
The operations stub.
|
| 308 |
+
result_type (:func:`type`): The protobuf result type.
|
| 309 |
+
grpc_metadata (Optional[List[Tuple[str, str]]]): Additional metadata to pass
|
| 310 |
+
to the rpc.
|
| 311 |
+
kwargs: Keyword args passed into the :class:`Operation` constructor.
|
| 312 |
+
|
| 313 |
+
Returns:
|
| 314 |
+
~.api_core.operation.Operation: The operation future to track the given
|
| 315 |
+
operation.
|
| 316 |
+
"""
|
| 317 |
+
refresh = functools.partial(
|
| 318 |
+
_refresh_grpc,
|
| 319 |
+
operations_stub,
|
| 320 |
+
operation.name,
|
| 321 |
+
metadata=grpc_metadata,
|
| 322 |
+
)
|
| 323 |
+
cancel = functools.partial(
|
| 324 |
+
_cancel_grpc,
|
| 325 |
+
operations_stub,
|
| 326 |
+
operation.name,
|
| 327 |
+
metadata=grpc_metadata,
|
| 328 |
+
)
|
| 329 |
+
return Operation(operation, refresh, cancel, result_type, **kwargs)
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def from_gapic(operation, operations_client, result_type, grpc_metadata=None, **kwargs):
|
| 333 |
+
"""Create an operation future from a gapic client.
|
| 334 |
+
|
| 335 |
+
This interacts with the long-running operations `service`_ (specific
|
| 336 |
+
to a given API) via a gapic client.
|
| 337 |
+
|
| 338 |
+
.. _service: https://github.com/googleapis/googleapis/blob/\
|
| 339 |
+
050400df0fdb16f63b63e9dee53819044bffc857/\
|
| 340 |
+
google/longrunning/operations.proto#L38
|
| 341 |
+
|
| 342 |
+
Args:
|
| 343 |
+
operation (google.longrunning.operations_pb2.Operation): The operation.
|
| 344 |
+
operations_client (google.api_core.operations_v1.OperationsClient):
|
| 345 |
+
The operations client.
|
| 346 |
+
result_type (:func:`type`): The protobuf result type.
|
| 347 |
+
grpc_metadata (Optional[List[Tuple[str, str]]]): Additional metadata to pass
|
| 348 |
+
to the rpc.
|
| 349 |
+
kwargs: Keyword args passed into the :class:`Operation` constructor.
|
| 350 |
+
|
| 351 |
+
Returns:
|
| 352 |
+
~.api_core.operation.Operation: The operation future to track the given
|
| 353 |
+
operation.
|
| 354 |
+
"""
|
| 355 |
+
refresh = functools.partial(
|
| 356 |
+
operations_client.get_operation,
|
| 357 |
+
operation.name,
|
| 358 |
+
metadata=grpc_metadata,
|
| 359 |
+
)
|
| 360 |
+
cancel = functools.partial(
|
| 361 |
+
operations_client.cancel_operation,
|
| 362 |
+
operation.name,
|
| 363 |
+
metadata=grpc_metadata,
|
| 364 |
+
)
|
| 365 |
+
return Operation(operation, refresh, cancel, result_type, **kwargs)
|
py311/lib/python3.11/site-packages/google/api_core/operation_async.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""AsyncIO futures for long-running operations returned from Google Cloud APIs.
|
| 16 |
+
|
| 17 |
+
These futures can be used to await for the result of a long-running operation
|
| 18 |
+
using :meth:`AsyncOperation.result`:
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
.. code-block:: python
|
| 22 |
+
|
| 23 |
+
operation = my_api_client.long_running_method()
|
| 24 |
+
result = await operation.result()
|
| 25 |
+
|
| 26 |
+
Or asynchronously using callbacks and :meth:`Operation.add_done_callback`:
|
| 27 |
+
|
| 28 |
+
.. code-block:: python
|
| 29 |
+
|
| 30 |
+
operation = my_api_client.long_running_method()
|
| 31 |
+
|
| 32 |
+
def my_callback(future):
|
| 33 |
+
result = await future.result()
|
| 34 |
+
|
| 35 |
+
operation.add_done_callback(my_callback)
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
import functools
|
| 40 |
+
import threading
|
| 41 |
+
|
| 42 |
+
from google.api_core import exceptions
|
| 43 |
+
from google.api_core import protobuf_helpers
|
| 44 |
+
from google.api_core.future import async_future
|
| 45 |
+
from google.longrunning import operations_pb2
|
| 46 |
+
from google.rpc import code_pb2
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class AsyncOperation(async_future.AsyncFuture):
|
| 50 |
+
"""A Future for interacting with a Google API Long-Running Operation.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
operation (google.longrunning.operations_pb2.Operation): The
|
| 54 |
+
initial operation.
|
| 55 |
+
refresh (Callable[[], ~.api_core.operation.Operation]): A callable that
|
| 56 |
+
returns the latest state of the operation.
|
| 57 |
+
cancel (Callable[[], None]): A callable that tries to cancel
|
| 58 |
+
the operation.
|
| 59 |
+
result_type (func:`type`): The protobuf type for the operation's
|
| 60 |
+
result.
|
| 61 |
+
metadata_type (func:`type`): The protobuf type for the operation's
|
| 62 |
+
metadata.
|
| 63 |
+
retry (google.api_core.retry.Retry): The retry configuration used
|
| 64 |
+
when polling. This can be used to control how often :meth:`done`
|
| 65 |
+
is polled. Regardless of the retry's ``deadline``, it will be
|
| 66 |
+
overridden by the ``timeout`` argument to :meth:`result`.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
operation,
|
| 72 |
+
refresh,
|
| 73 |
+
cancel,
|
| 74 |
+
result_type,
|
| 75 |
+
metadata_type=None,
|
| 76 |
+
retry=async_future.DEFAULT_RETRY,
|
| 77 |
+
):
|
| 78 |
+
super().__init__(retry=retry)
|
| 79 |
+
self._operation = operation
|
| 80 |
+
self._refresh = refresh
|
| 81 |
+
self._cancel = cancel
|
| 82 |
+
self._result_type = result_type
|
| 83 |
+
self._metadata_type = metadata_type
|
| 84 |
+
self._completion_lock = threading.Lock()
|
| 85 |
+
# Invoke this in case the operation came back already complete.
|
| 86 |
+
self._set_result_from_operation()
|
| 87 |
+
|
| 88 |
+
@property
|
| 89 |
+
def operation(self):
|
| 90 |
+
"""google.longrunning.Operation: The current long-running operation."""
|
| 91 |
+
return self._operation
|
| 92 |
+
|
| 93 |
+
@property
|
| 94 |
+
def metadata(self):
|
| 95 |
+
"""google.protobuf.Message: the current operation metadata."""
|
| 96 |
+
if not self._operation.HasField("metadata"):
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
return protobuf_helpers.from_any_pb(
|
| 100 |
+
self._metadata_type, self._operation.metadata
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
@classmethod
|
| 104 |
+
def deserialize(cls, payload):
|
| 105 |
+
"""Deserialize a ``google.longrunning.Operation`` protocol buffer.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
payload (bytes): A serialized operation protocol buffer.
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
~.operations_pb2.Operation: An Operation protobuf object.
|
| 112 |
+
"""
|
| 113 |
+
return operations_pb2.Operation.FromString(payload)
|
| 114 |
+
|
| 115 |
+
def _set_result_from_operation(self):
|
| 116 |
+
"""Set the result or exception from the operation if it is complete."""
|
| 117 |
+
# This must be done in a lock to prevent the async_future thread
|
| 118 |
+
# and main thread from both executing the completion logic
|
| 119 |
+
# at the same time.
|
| 120 |
+
with self._completion_lock:
|
| 121 |
+
# If the operation isn't complete or if the result has already been
|
| 122 |
+
# set, do not call set_result/set_exception again.
|
| 123 |
+
if not self._operation.done or self._future.done():
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
if self._operation.HasField("response"):
|
| 127 |
+
response = protobuf_helpers.from_any_pb(
|
| 128 |
+
self._result_type, self._operation.response
|
| 129 |
+
)
|
| 130 |
+
self.set_result(response)
|
| 131 |
+
elif self._operation.HasField("error"):
|
| 132 |
+
exception = exceptions.GoogleAPICallError(
|
| 133 |
+
self._operation.error.message,
|
| 134 |
+
errors=(self._operation.error,),
|
| 135 |
+
response=self._operation,
|
| 136 |
+
)
|
| 137 |
+
self.set_exception(exception)
|
| 138 |
+
else:
|
| 139 |
+
exception = exceptions.GoogleAPICallError(
|
| 140 |
+
"Unexpected state: Long-running operation had neither "
|
| 141 |
+
"response nor error set."
|
| 142 |
+
)
|
| 143 |
+
self.set_exception(exception)
|
| 144 |
+
|
| 145 |
+
async def _refresh_and_update(self, retry=async_future.DEFAULT_RETRY):
|
| 146 |
+
"""Refresh the operation and update the result if needed.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
retry (google.api_core.retry.Retry): (Optional) How to retry the RPC.
|
| 150 |
+
"""
|
| 151 |
+
# If the currently cached operation is done, no need to make another
|
| 152 |
+
# RPC as it will not change once done.
|
| 153 |
+
if not self._operation.done:
|
| 154 |
+
self._operation = await self._refresh(retry=retry)
|
| 155 |
+
self._set_result_from_operation()
|
| 156 |
+
|
| 157 |
+
async def done(self, retry=async_future.DEFAULT_RETRY):
|
| 158 |
+
"""Checks to see if the operation is complete.
|
| 159 |
+
|
| 160 |
+
Args:
|
| 161 |
+
retry (google.api_core.retry.Retry): (Optional) How to retry the RPC.
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
bool: True if the operation is complete, False otherwise.
|
| 165 |
+
"""
|
| 166 |
+
await self._refresh_and_update(retry)
|
| 167 |
+
return self._operation.done
|
| 168 |
+
|
| 169 |
+
async def cancel(self):
|
| 170 |
+
"""Attempt to cancel the operation.
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
bool: True if the cancel RPC was made, False if the operation is
|
| 174 |
+
already complete.
|
| 175 |
+
"""
|
| 176 |
+
result = await self.done()
|
| 177 |
+
if result:
|
| 178 |
+
return False
|
| 179 |
+
else:
|
| 180 |
+
await self._cancel()
|
| 181 |
+
return True
|
| 182 |
+
|
| 183 |
+
async def cancelled(self):
|
| 184 |
+
"""True if the operation was cancelled."""
|
| 185 |
+
await self._refresh_and_update()
|
| 186 |
+
return (
|
| 187 |
+
self._operation.HasField("error")
|
| 188 |
+
and self._operation.error.code == code_pb2.CANCELLED
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def from_gapic(operation, operations_client, result_type, grpc_metadata=None, **kwargs):
|
| 193 |
+
"""Create an operation future from a gapic client.
|
| 194 |
+
|
| 195 |
+
This interacts with the long-running operations `service`_ (specific
|
| 196 |
+
to a given API) via a gapic client.
|
| 197 |
+
|
| 198 |
+
.. _service: https://github.com/googleapis/googleapis/blob/\
|
| 199 |
+
050400df0fdb16f63b63e9dee53819044bffc857/\
|
| 200 |
+
google/longrunning/operations.proto#L38
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
operation (google.longrunning.operations_pb2.Operation): The operation.
|
| 204 |
+
operations_client (google.api_core.operations_v1.OperationsClient):
|
| 205 |
+
The operations client.
|
| 206 |
+
result_type (:func:`type`): The protobuf result type.
|
| 207 |
+
grpc_metadata (Optional[List[Tuple[str, str]]]): Additional metadata to pass
|
| 208 |
+
to the rpc.
|
| 209 |
+
kwargs: Keyword args passed into the :class:`Operation` constructor.
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
~.api_core.operation.Operation: The operation future to track the given
|
| 213 |
+
operation.
|
| 214 |
+
"""
|
| 215 |
+
refresh = functools.partial(
|
| 216 |
+
operations_client.get_operation,
|
| 217 |
+
operation.name,
|
| 218 |
+
metadata=grpc_metadata,
|
| 219 |
+
)
|
| 220 |
+
cancel = functools.partial(
|
| 221 |
+
operations_client.cancel_operation,
|
| 222 |
+
operation.name,
|
| 223 |
+
metadata=grpc_metadata,
|
| 224 |
+
)
|
| 225 |
+
return AsyncOperation(operation, refresh, cancel, result_type, **kwargs)
|
py311/lib/python3.11/site-packages/google/api_core/page_iterator.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google LLC
|
| 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 |
+
"""Iterators for paging through paged API methods.
|
| 16 |
+
|
| 17 |
+
These iterators simplify the process of paging through API responses
|
| 18 |
+
where the request takes a page token and the response is a list of results with
|
| 19 |
+
a token for the next page. See `list pagination`_ in the Google API Style Guide
|
| 20 |
+
for more details.
|
| 21 |
+
|
| 22 |
+
.. _list pagination:
|
| 23 |
+
https://cloud.google.com/apis/design/design_patterns#list_pagination
|
| 24 |
+
|
| 25 |
+
API clients that have methods that follow the list pagination pattern can
|
| 26 |
+
return an :class:`.Iterator`. You can use this iterator to get **all** of
|
| 27 |
+
the results across all pages::
|
| 28 |
+
|
| 29 |
+
>>> results_iterator = client.list_resources()
|
| 30 |
+
>>> list(results_iterator) # Convert to a list (consumes all values).
|
| 31 |
+
|
| 32 |
+
Or you can walk your way through items and call off the search early if
|
| 33 |
+
you find what you're looking for (resulting in possibly fewer requests)::
|
| 34 |
+
|
| 35 |
+
>>> for resource in results_iterator:
|
| 36 |
+
... print(resource.name)
|
| 37 |
+
... if not resource.is_valid:
|
| 38 |
+
... break
|
| 39 |
+
|
| 40 |
+
At any point, you may check the number of items consumed by referencing the
|
| 41 |
+
``num_results`` property of the iterator::
|
| 42 |
+
|
| 43 |
+
>>> for my_item in results_iterator:
|
| 44 |
+
... if results_iterator.num_results >= 10:
|
| 45 |
+
... break
|
| 46 |
+
|
| 47 |
+
When iterating, not every new item will send a request to the server.
|
| 48 |
+
To iterate based on each page of items (where a page corresponds to
|
| 49 |
+
a request)::
|
| 50 |
+
|
| 51 |
+
>>> for page in results_iterator.pages:
|
| 52 |
+
... print('=' * 20)
|
| 53 |
+
... print(' Page number: {:d}'.format(iterator.page_number))
|
| 54 |
+
... print(' Items in page: {:d}'.format(page.num_items))
|
| 55 |
+
... print(' First item: {!r}'.format(next(page)))
|
| 56 |
+
... print('Items remaining: {:d}'.format(page.remaining))
|
| 57 |
+
... print('Next page token: {}'.format(iterator.next_page_token))
|
| 58 |
+
====================
|
| 59 |
+
Page number: 1
|
| 60 |
+
Items in page: 1
|
| 61 |
+
First item: <MyItemClass at 0x7f1d3cccf690>
|
| 62 |
+
Items remaining: 0
|
| 63 |
+
Next page token: eav1OzQB0OM8rLdGXOEsyQWSG
|
| 64 |
+
====================
|
| 65 |
+
Page number: 2
|
| 66 |
+
Items in page: 19
|
| 67 |
+
First item: <MyItemClass at 0x7f1d3cccffd0>
|
| 68 |
+
Items remaining: 18
|
| 69 |
+
Next page token: None
|
| 70 |
+
|
| 71 |
+
Then, for each page you can get all the resources on that page by iterating
|
| 72 |
+
through it or using :func:`list`::
|
| 73 |
+
|
| 74 |
+
>>> list(page)
|
| 75 |
+
[
|
| 76 |
+
<MyItemClass at 0x7fd64a098ad0>,
|
| 77 |
+
<MyItemClass at 0x7fd64a098ed0>,
|
| 78 |
+
<MyItemClass at 0x7fd64a098e90>,
|
| 79 |
+
]
|
| 80 |
+
"""
|
| 81 |
+
|
| 82 |
+
import abc
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class Page(object):
|
| 86 |
+
"""Single page of results in an iterator.
|
| 87 |
+
|
| 88 |
+
Args:
|
| 89 |
+
parent (google.api_core.page_iterator.Iterator): The iterator that owns
|
| 90 |
+
the current page.
|
| 91 |
+
items (Sequence[Any]): An iterable (that also defines __len__) of items
|
| 92 |
+
from a raw API response.
|
| 93 |
+
item_to_value (Callable[google.api_core.page_iterator.Iterator, Any]):
|
| 94 |
+
Callable to convert an item from the type in the raw API response
|
| 95 |
+
into the native object. Will be called with the iterator and a
|
| 96 |
+
single item.
|
| 97 |
+
raw_page Optional[google.protobuf.message.Message]:
|
| 98 |
+
The raw page response.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __init__(self, parent, items, item_to_value, raw_page=None):
|
| 102 |
+
self._parent = parent
|
| 103 |
+
self._num_items = len(items)
|
| 104 |
+
self._remaining = self._num_items
|
| 105 |
+
self._item_iter = iter(items)
|
| 106 |
+
self._item_to_value = item_to_value
|
| 107 |
+
self._raw_page = raw_page
|
| 108 |
+
|
| 109 |
+
@property
|
| 110 |
+
def raw_page(self):
|
| 111 |
+
"""google.protobuf.message.Message"""
|
| 112 |
+
return self._raw_page
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def num_items(self):
|
| 116 |
+
"""int: Total items in the page."""
|
| 117 |
+
return self._num_items
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def remaining(self):
|
| 121 |
+
"""int: Remaining items in the page."""
|
| 122 |
+
return self._remaining
|
| 123 |
+
|
| 124 |
+
def __iter__(self):
|
| 125 |
+
"""The :class:`Page` is an iterator of items."""
|
| 126 |
+
return self
|
| 127 |
+
|
| 128 |
+
def __next__(self):
|
| 129 |
+
"""Get the next value in the page."""
|
| 130 |
+
item = next(self._item_iter)
|
| 131 |
+
result = self._item_to_value(self._parent, item)
|
| 132 |
+
# Since we've successfully got the next value from the
|
| 133 |
+
# iterator, we update the number of remaining.
|
| 134 |
+
self._remaining -= 1
|
| 135 |
+
return result
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def _item_to_value_identity(iterator, item):
|
| 139 |
+
"""An item to value transformer that returns the item un-changed."""
|
| 140 |
+
# pylint: disable=unused-argument
|
| 141 |
+
# We are conforming to the interface defined by Iterator.
|
| 142 |
+
return item
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class Iterator(object, metaclass=abc.ABCMeta):
|
| 146 |
+
"""A generic class for iterating through API list responses.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
client(google.cloud.client.Client): The API client.
|
| 150 |
+
item_to_value (Callable[google.api_core.page_iterator.Iterator, Any]):
|
| 151 |
+
Callable to convert an item from the type in the raw API response
|
| 152 |
+
into the native object. Will be called with the iterator and a
|
| 153 |
+
single item.
|
| 154 |
+
page_token (str): A token identifying a page in a result set to start
|
| 155 |
+
fetching results from.
|
| 156 |
+
max_results (int): The maximum number of results to fetch.
|
| 157 |
+
"""
|
| 158 |
+
|
| 159 |
+
def __init__(
|
| 160 |
+
self,
|
| 161 |
+
client,
|
| 162 |
+
item_to_value=_item_to_value_identity,
|
| 163 |
+
page_token=None,
|
| 164 |
+
max_results=None,
|
| 165 |
+
):
|
| 166 |
+
self._started = False
|
| 167 |
+
self.__active_iterator = None
|
| 168 |
+
|
| 169 |
+
self.client = client
|
| 170 |
+
"""Optional[Any]: The client that created this iterator."""
|
| 171 |
+
self.item_to_value = item_to_value
|
| 172 |
+
"""Callable[Iterator, Any]: Callable to convert an item from the type
|
| 173 |
+
in the raw API response into the native object. Will be called with
|
| 174 |
+
the iterator and a
|
| 175 |
+
single item.
|
| 176 |
+
"""
|
| 177 |
+
self.max_results = max_results
|
| 178 |
+
"""int: The maximum number of results to fetch"""
|
| 179 |
+
|
| 180 |
+
# The attributes below will change over the life of the iterator.
|
| 181 |
+
self.page_number = 0
|
| 182 |
+
"""int: The current page of results."""
|
| 183 |
+
self.next_page_token = page_token
|
| 184 |
+
"""str: The token for the next page of results. If this is set before
|
| 185 |
+
the iterator starts, it effectively offsets the iterator to a
|
| 186 |
+
specific starting point."""
|
| 187 |
+
self.num_results = 0
|
| 188 |
+
"""int: The total number of results fetched so far."""
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def pages(self):
|
| 192 |
+
"""Iterator of pages in the response.
|
| 193 |
+
|
| 194 |
+
returns:
|
| 195 |
+
types.GeneratorType[google.api_core.page_iterator.Page]: A
|
| 196 |
+
generator of page instances.
|
| 197 |
+
|
| 198 |
+
raises:
|
| 199 |
+
ValueError: If the iterator has already been started.
|
| 200 |
+
"""
|
| 201 |
+
if self._started:
|
| 202 |
+
raise ValueError("Iterator has already started", self)
|
| 203 |
+
self._started = True
|
| 204 |
+
return self._page_iter(increment=True)
|
| 205 |
+
|
| 206 |
+
def _items_iter(self):
|
| 207 |
+
"""Iterator for each item returned."""
|
| 208 |
+
for page in self._page_iter(increment=False):
|
| 209 |
+
for item in page:
|
| 210 |
+
self.num_results += 1
|
| 211 |
+
yield item
|
| 212 |
+
|
| 213 |
+
def __iter__(self):
|
| 214 |
+
"""Iterator for each item returned.
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
types.GeneratorType[Any]: A generator of items from the API.
|
| 218 |
+
|
| 219 |
+
Raises:
|
| 220 |
+
ValueError: If the iterator has already been started.
|
| 221 |
+
"""
|
| 222 |
+
if self._started:
|
| 223 |
+
raise ValueError("Iterator has already started", self)
|
| 224 |
+
self._started = True
|
| 225 |
+
return self._items_iter()
|
| 226 |
+
|
| 227 |
+
def __next__(self):
|
| 228 |
+
if self.__active_iterator is None:
|
| 229 |
+
self.__active_iterator = iter(self)
|
| 230 |
+
return next(self.__active_iterator)
|
| 231 |
+
|
| 232 |
+
def _page_iter(self, increment):
|
| 233 |
+
"""Generator of pages of API responses.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
increment (bool): Flag indicating if the total number of results
|
| 237 |
+
should be incremented on each page. This is useful since a page
|
| 238 |
+
iterator will want to increment by results per page while an
|
| 239 |
+
items iterator will want to increment per item.
|
| 240 |
+
|
| 241 |
+
Yields:
|
| 242 |
+
Page: each page of items from the API.
|
| 243 |
+
"""
|
| 244 |
+
page = self._next_page()
|
| 245 |
+
while page is not None:
|
| 246 |
+
self.page_number += 1
|
| 247 |
+
if increment:
|
| 248 |
+
self.num_results += page.num_items
|
| 249 |
+
yield page
|
| 250 |
+
page = self._next_page()
|
| 251 |
+
|
| 252 |
+
@abc.abstractmethod
|
| 253 |
+
def _next_page(self):
|
| 254 |
+
"""Get the next page in the iterator.
|
| 255 |
+
|
| 256 |
+
This does nothing and is intended to be over-ridden by subclasses
|
| 257 |
+
to return the next :class:`Page`.
|
| 258 |
+
|
| 259 |
+
Raises:
|
| 260 |
+
NotImplementedError: Always, this method is abstract.
|
| 261 |
+
"""
|
| 262 |
+
raise NotImplementedError
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _do_nothing_page_start(iterator, page, response):
|
| 266 |
+
"""Helper to provide custom behavior after a :class:`Page` is started.
|
| 267 |
+
|
| 268 |
+
This is a do-nothing stand-in as the default value.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
iterator (Iterator): An iterator that holds some request info.
|
| 272 |
+
page (Page): The page that was just created.
|
| 273 |
+
response (Any): The API response for a page.
|
| 274 |
+
"""
|
| 275 |
+
# pylint: disable=unused-argument
|
| 276 |
+
pass
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class HTTPIterator(Iterator):
|
| 280 |
+
"""A generic class for iterating through HTTP/JSON API list responses.
|
| 281 |
+
|
| 282 |
+
To make an iterator work, you'll need to provide a way to convert a JSON
|
| 283 |
+
item returned from the API into the object of your choice (via
|
| 284 |
+
``item_to_value``). You also may need to specify a custom ``items_key`` so
|
| 285 |
+
that a given response (containing a page of results) can be parsed into an
|
| 286 |
+
iterable page of the actual objects you want.
|
| 287 |
+
|
| 288 |
+
Args:
|
| 289 |
+
client (google.cloud.client.Client): The API client.
|
| 290 |
+
api_request (Callable): The function to use to make API requests.
|
| 291 |
+
Generally, this will be
|
| 292 |
+
:meth:`google.cloud._http.JSONConnection.api_request`.
|
| 293 |
+
path (str): The method path to query for the list of items.
|
| 294 |
+
item_to_value (Callable[google.api_core.page_iterator.Iterator, Any]):
|
| 295 |
+
Callable to convert an item from the type in the JSON response into
|
| 296 |
+
a native object. Will be called with the iterator and a single
|
| 297 |
+
item.
|
| 298 |
+
items_key (str): The key in the API response where the list of items
|
| 299 |
+
can be found.
|
| 300 |
+
page_token (str): A token identifying a page in a result set to start
|
| 301 |
+
fetching results from.
|
| 302 |
+
page_size (int): The maximum number of results to fetch per page
|
| 303 |
+
max_results (int): The maximum number of results to fetch
|
| 304 |
+
extra_params (dict): Extra query string parameters for the
|
| 305 |
+
API call.
|
| 306 |
+
page_start (Callable[
|
| 307 |
+
google.api_core.page_iterator.Iterator,
|
| 308 |
+
google.api_core.page_iterator.Page, dict]): Callable to provide
|
| 309 |
+
any special behavior after a new page has been created. Assumed
|
| 310 |
+
signature takes the :class:`.Iterator` that started the page,
|
| 311 |
+
the :class:`.Page` that was started and the dictionary containing
|
| 312 |
+
the page response.
|
| 313 |
+
next_token (str): The name of the field used in the response for page
|
| 314 |
+
tokens.
|
| 315 |
+
|
| 316 |
+
.. autoattribute:: pages
|
| 317 |
+
"""
|
| 318 |
+
|
| 319 |
+
_DEFAULT_ITEMS_KEY = "items"
|
| 320 |
+
_PAGE_TOKEN = "pageToken"
|
| 321 |
+
_MAX_RESULTS = "maxResults"
|
| 322 |
+
_NEXT_TOKEN = "nextPageToken"
|
| 323 |
+
_RESERVED_PARAMS = frozenset([_PAGE_TOKEN])
|
| 324 |
+
_HTTP_METHOD = "GET"
|
| 325 |
+
|
| 326 |
+
def __init__(
|
| 327 |
+
self,
|
| 328 |
+
client,
|
| 329 |
+
api_request,
|
| 330 |
+
path,
|
| 331 |
+
item_to_value,
|
| 332 |
+
items_key=_DEFAULT_ITEMS_KEY,
|
| 333 |
+
page_token=None,
|
| 334 |
+
page_size=None,
|
| 335 |
+
max_results=None,
|
| 336 |
+
extra_params=None,
|
| 337 |
+
page_start=_do_nothing_page_start,
|
| 338 |
+
next_token=_NEXT_TOKEN,
|
| 339 |
+
):
|
| 340 |
+
super(HTTPIterator, self).__init__(
|
| 341 |
+
client, item_to_value, page_token=page_token, max_results=max_results
|
| 342 |
+
)
|
| 343 |
+
self.api_request = api_request
|
| 344 |
+
self.path = path
|
| 345 |
+
self._items_key = items_key
|
| 346 |
+
self.extra_params = extra_params
|
| 347 |
+
self._page_size = page_size
|
| 348 |
+
self._page_start = page_start
|
| 349 |
+
self._next_token = next_token
|
| 350 |
+
# Verify inputs / provide defaults.
|
| 351 |
+
if self.extra_params is None:
|
| 352 |
+
self.extra_params = {}
|
| 353 |
+
self._verify_params()
|
| 354 |
+
|
| 355 |
+
def _verify_params(self):
|
| 356 |
+
"""Verifies the parameters don't use any reserved parameter.
|
| 357 |
+
|
| 358 |
+
Raises:
|
| 359 |
+
ValueError: If a reserved parameter is used.
|
| 360 |
+
"""
|
| 361 |
+
reserved_in_use = self._RESERVED_PARAMS.intersection(self.extra_params)
|
| 362 |
+
if reserved_in_use:
|
| 363 |
+
raise ValueError("Using a reserved parameter", reserved_in_use)
|
| 364 |
+
|
| 365 |
+
def _next_page(self):
|
| 366 |
+
"""Get the next page in the iterator.
|
| 367 |
+
|
| 368 |
+
Returns:
|
| 369 |
+
Optional[Page]: The next page in the iterator or :data:`None` if
|
| 370 |
+
there are no pages left.
|
| 371 |
+
"""
|
| 372 |
+
if self._has_next_page():
|
| 373 |
+
response = self._get_next_page_response()
|
| 374 |
+
items = response.get(self._items_key, ())
|
| 375 |
+
page = Page(self, items, self.item_to_value, raw_page=response)
|
| 376 |
+
self._page_start(self, page, response)
|
| 377 |
+
self.next_page_token = response.get(self._next_token)
|
| 378 |
+
return page
|
| 379 |
+
else:
|
| 380 |
+
return None
|
| 381 |
+
|
| 382 |
+
def _has_next_page(self):
|
| 383 |
+
"""Determines whether or not there are more pages with results.
|
| 384 |
+
|
| 385 |
+
Returns:
|
| 386 |
+
bool: Whether the iterator has more pages.
|
| 387 |
+
"""
|
| 388 |
+
if self.page_number == 0:
|
| 389 |
+
return True
|
| 390 |
+
|
| 391 |
+
if self.max_results is not None:
|
| 392 |
+
if self.num_results >= self.max_results:
|
| 393 |
+
return False
|
| 394 |
+
|
| 395 |
+
return self.next_page_token is not None
|
| 396 |
+
|
| 397 |
+
def _get_query_params(self):
|
| 398 |
+
"""Getter for query parameters for the next request.
|
| 399 |
+
|
| 400 |
+
Returns:
|
| 401 |
+
dict: A dictionary of query parameters.
|
| 402 |
+
"""
|
| 403 |
+
result = {}
|
| 404 |
+
if self.next_page_token is not None:
|
| 405 |
+
result[self._PAGE_TOKEN] = self.next_page_token
|
| 406 |
+
|
| 407 |
+
page_size = None
|
| 408 |
+
if self.max_results is not None:
|
| 409 |
+
page_size = self.max_results - self.num_results
|
| 410 |
+
if self._page_size is not None:
|
| 411 |
+
page_size = min(page_size, self._page_size)
|
| 412 |
+
elif self._page_size is not None:
|
| 413 |
+
page_size = self._page_size
|
| 414 |
+
|
| 415 |
+
if page_size is not None:
|
| 416 |
+
result[self._MAX_RESULTS] = page_size
|
| 417 |
+
|
| 418 |
+
result.update(self.extra_params)
|
| 419 |
+
return result
|
| 420 |
+
|
| 421 |
+
def _get_next_page_response(self):
|
| 422 |
+
"""Requests the next page from the path provided.
|
| 423 |
+
|
| 424 |
+
Returns:
|
| 425 |
+
dict: The parsed JSON response of the next page's contents.
|
| 426 |
+
|
| 427 |
+
Raises:
|
| 428 |
+
ValueError: If the HTTP method is not ``GET`` or ``POST``.
|
| 429 |
+
"""
|
| 430 |
+
params = self._get_query_params()
|
| 431 |
+
if self._HTTP_METHOD == "GET":
|
| 432 |
+
return self.api_request(
|
| 433 |
+
method=self._HTTP_METHOD, path=self.path, query_params=params
|
| 434 |
+
)
|
| 435 |
+
elif self._HTTP_METHOD == "POST":
|
| 436 |
+
return self.api_request(
|
| 437 |
+
method=self._HTTP_METHOD, path=self.path, data=params
|
| 438 |
+
)
|
| 439 |
+
else:
|
| 440 |
+
raise ValueError("Unexpected HTTP method", self._HTTP_METHOD)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
class _GAXIterator(Iterator):
|
| 444 |
+
"""A generic class for iterating through Cloud gRPC APIs list responses.
|
| 445 |
+
|
| 446 |
+
Any:
|
| 447 |
+
client (google.cloud.client.Client): The API client.
|
| 448 |
+
page_iter (google.gax.PageIterator): A GAX page iterator to be wrapped
|
| 449 |
+
to conform to the :class:`Iterator` interface.
|
| 450 |
+
item_to_value (Callable[Iterator, Any]): Callable to convert an item
|
| 451 |
+
from the protobuf response into a native object. Will
|
| 452 |
+
be called with the iterator and a single item.
|
| 453 |
+
max_results (int): The maximum number of results to fetch.
|
| 454 |
+
|
| 455 |
+
.. autoattribute:: pages
|
| 456 |
+
"""
|
| 457 |
+
|
| 458 |
+
def __init__(self, client, page_iter, item_to_value, max_results=None):
|
| 459 |
+
super(_GAXIterator, self).__init__(
|
| 460 |
+
client,
|
| 461 |
+
item_to_value,
|
| 462 |
+
page_token=page_iter.page_token,
|
| 463 |
+
max_results=max_results,
|
| 464 |
+
)
|
| 465 |
+
self._gax_page_iter = page_iter
|
| 466 |
+
|
| 467 |
+
def _next_page(self):
|
| 468 |
+
"""Get the next page in the iterator.
|
| 469 |
+
|
| 470 |
+
Wraps the response from the :class:`~google.gax.PageIterator` in a
|
| 471 |
+
:class:`Page` instance and captures some state at each page.
|
| 472 |
+
|
| 473 |
+
Returns:
|
| 474 |
+
Optional[Page]: The next page in the iterator or :data:`None` if
|
| 475 |
+
there are no pages left.
|
| 476 |
+
"""
|
| 477 |
+
try:
|
| 478 |
+
items = next(self._gax_page_iter)
|
| 479 |
+
page = Page(self, items, self.item_to_value)
|
| 480 |
+
self.next_page_token = self._gax_page_iter.page_token or None
|
| 481 |
+
return page
|
| 482 |
+
except StopIteration:
|
| 483 |
+
return None
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
class GRPCIterator(Iterator):
|
| 487 |
+
"""A generic class for iterating through gRPC list responses.
|
| 488 |
+
|
| 489 |
+
.. note:: The class does not take a ``page_token`` argument because it can
|
| 490 |
+
just be specified in the ``request``.
|
| 491 |
+
|
| 492 |
+
Args:
|
| 493 |
+
client (google.cloud.client.Client): The API client. This unused by
|
| 494 |
+
this class, but kept to satisfy the :class:`Iterator` interface.
|
| 495 |
+
method (Callable[protobuf.Message]): A bound gRPC method that should
|
| 496 |
+
take a single message for the request.
|
| 497 |
+
request (protobuf.Message): The request message.
|
| 498 |
+
items_field (str): The field in the response message that has the
|
| 499 |
+
items for the page.
|
| 500 |
+
item_to_value (Callable[GRPCIterator, Any]): Callable to convert an
|
| 501 |
+
item from the type in the JSON response into a native object. Will
|
| 502 |
+
be called with the iterator and a single item.
|
| 503 |
+
request_token_field (str): The field in the request message used to
|
| 504 |
+
specify the page token.
|
| 505 |
+
response_token_field (str): The field in the response message that has
|
| 506 |
+
the token for the next page.
|
| 507 |
+
max_results (int): The maximum number of results to fetch.
|
| 508 |
+
|
| 509 |
+
.. autoattribute:: pages
|
| 510 |
+
"""
|
| 511 |
+
|
| 512 |
+
_DEFAULT_REQUEST_TOKEN_FIELD = "page_token"
|
| 513 |
+
_DEFAULT_RESPONSE_TOKEN_FIELD = "next_page_token"
|
| 514 |
+
|
| 515 |
+
def __init__(
|
| 516 |
+
self,
|
| 517 |
+
client,
|
| 518 |
+
method,
|
| 519 |
+
request,
|
| 520 |
+
items_field,
|
| 521 |
+
item_to_value=_item_to_value_identity,
|
| 522 |
+
request_token_field=_DEFAULT_REQUEST_TOKEN_FIELD,
|
| 523 |
+
response_token_field=_DEFAULT_RESPONSE_TOKEN_FIELD,
|
| 524 |
+
max_results=None,
|
| 525 |
+
):
|
| 526 |
+
super(GRPCIterator, self).__init__(
|
| 527 |
+
client, item_to_value, max_results=max_results
|
| 528 |
+
)
|
| 529 |
+
self._method = method
|
| 530 |
+
self._request = request
|
| 531 |
+
self._items_field = items_field
|
| 532 |
+
self._request_token_field = request_token_field
|
| 533 |
+
self._response_token_field = response_token_field
|
| 534 |
+
|
| 535 |
+
def _next_page(self):
|
| 536 |
+
"""Get the next page in the iterator.
|
| 537 |
+
|
| 538 |
+
Returns:
|
| 539 |
+
Page: The next page in the iterator or :data:`None` if
|
| 540 |
+
there are no pages left.
|
| 541 |
+
"""
|
| 542 |
+
if not self._has_next_page():
|
| 543 |
+
return None
|
| 544 |
+
|
| 545 |
+
if self.next_page_token is not None:
|
| 546 |
+
setattr(self._request, self._request_token_field, self.next_page_token)
|
| 547 |
+
|
| 548 |
+
response = self._method(self._request)
|
| 549 |
+
|
| 550 |
+
self.next_page_token = getattr(response, self._response_token_field)
|
| 551 |
+
items = getattr(response, self._items_field)
|
| 552 |
+
page = Page(self, items, self.item_to_value, raw_page=response)
|
| 553 |
+
|
| 554 |
+
return page
|
| 555 |
+
|
| 556 |
+
def _has_next_page(self):
|
| 557 |
+
"""Determines whether or not there are more pages with results.
|
| 558 |
+
|
| 559 |
+
Returns:
|
| 560 |
+
bool: Whether the iterator has more pages.
|
| 561 |
+
"""
|
| 562 |
+
if self.page_number == 0:
|
| 563 |
+
return True
|
| 564 |
+
|
| 565 |
+
if self.max_results is not None:
|
| 566 |
+
if self.num_results >= self.max_results:
|
| 567 |
+
return False
|
| 568 |
+
|
| 569 |
+
# Note: intentionally a falsy check instead of a None check. The RPC
|
| 570 |
+
# can return an empty string indicating no more pages.
|
| 571 |
+
return True if self.next_page_token else False
|
py311/lib/python3.11/site-packages/google/api_core/page_iterator_async.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020 Google LLC
|
| 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 |
+
"""AsyncIO iterators for paging through paged API methods.
|
| 16 |
+
|
| 17 |
+
These iterators simplify the process of paging through API responses
|
| 18 |
+
where the request takes a page token and the response is a list of results with
|
| 19 |
+
a token for the next page. See `list pagination`_ in the Google API Style Guide
|
| 20 |
+
for more details.
|
| 21 |
+
|
| 22 |
+
.. _list pagination:
|
| 23 |
+
https://cloud.google.com/apis/design/design_patterns#list_pagination
|
| 24 |
+
|
| 25 |
+
API clients that have methods that follow the list pagination pattern can
|
| 26 |
+
return an :class:`.AsyncIterator`:
|
| 27 |
+
|
| 28 |
+
>>> results_iterator = await client.list_resources()
|
| 29 |
+
|
| 30 |
+
Or you can walk your way through items and call off the search early if
|
| 31 |
+
you find what you're looking for (resulting in possibly fewer requests)::
|
| 32 |
+
|
| 33 |
+
>>> async for resource in results_iterator:
|
| 34 |
+
... print(resource.name)
|
| 35 |
+
... if not resource.is_valid:
|
| 36 |
+
... break
|
| 37 |
+
|
| 38 |
+
At any point, you may check the number of items consumed by referencing the
|
| 39 |
+
``num_results`` property of the iterator::
|
| 40 |
+
|
| 41 |
+
>>> async for my_item in results_iterator:
|
| 42 |
+
... if results_iterator.num_results >= 10:
|
| 43 |
+
... break
|
| 44 |
+
|
| 45 |
+
When iterating, not every new item will send a request to the server.
|
| 46 |
+
To iterate based on each page of items (where a page corresponds to
|
| 47 |
+
a request)::
|
| 48 |
+
|
| 49 |
+
>>> async for page in results_iterator.pages:
|
| 50 |
+
... print('=' * 20)
|
| 51 |
+
... print(' Page number: {:d}'.format(iterator.page_number))
|
| 52 |
+
... print(' Items in page: {:d}'.format(page.num_items))
|
| 53 |
+
... print(' First item: {!r}'.format(next(page)))
|
| 54 |
+
... print('Items remaining: {:d}'.format(page.remaining))
|
| 55 |
+
... print('Next page token: {}'.format(iterator.next_page_token))
|
| 56 |
+
====================
|
| 57 |
+
Page number: 1
|
| 58 |
+
Items in page: 1
|
| 59 |
+
First item: <MyItemClass at 0x7f1d3cccf690>
|
| 60 |
+
Items remaining: 0
|
| 61 |
+
Next page token: eav1OzQB0OM8rLdGXOEsyQWSG
|
| 62 |
+
====================
|
| 63 |
+
Page number: 2
|
| 64 |
+
Items in page: 19
|
| 65 |
+
First item: <MyItemClass at 0x7f1d3cccffd0>
|
| 66 |
+
Items remaining: 18
|
| 67 |
+
Next page token: None
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
import abc
|
| 71 |
+
|
| 72 |
+
from google.api_core.page_iterator import Page
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _item_to_value_identity(iterator, item):
|
| 76 |
+
"""An item to value transformer that returns the item un-changed."""
|
| 77 |
+
# pylint: disable=unused-argument
|
| 78 |
+
# We are conforming to the interface defined by Iterator.
|
| 79 |
+
return item
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class AsyncIterator(abc.ABC):
|
| 83 |
+
"""A generic class for iterating through API list responses.
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
client(google.cloud.client.Client): The API client.
|
| 87 |
+
item_to_value (Callable[google.api_core.page_iterator_async.AsyncIterator, Any]):
|
| 88 |
+
Callable to convert an item from the type in the raw API response
|
| 89 |
+
into the native object. Will be called with the iterator and a
|
| 90 |
+
single item.
|
| 91 |
+
page_token (str): A token identifying a page in a result set to start
|
| 92 |
+
fetching results from.
|
| 93 |
+
max_results (int): The maximum number of results to fetch.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def __init__(
|
| 97 |
+
self,
|
| 98 |
+
client,
|
| 99 |
+
item_to_value=_item_to_value_identity,
|
| 100 |
+
page_token=None,
|
| 101 |
+
max_results=None,
|
| 102 |
+
):
|
| 103 |
+
self._started = False
|
| 104 |
+
self.__active_aiterator = None
|
| 105 |
+
|
| 106 |
+
self.client = client
|
| 107 |
+
"""Optional[Any]: The client that created this iterator."""
|
| 108 |
+
self.item_to_value = item_to_value
|
| 109 |
+
"""Callable[Iterator, Any]: Callable to convert an item from the type
|
| 110 |
+
in the raw API response into the native object. Will be called with
|
| 111 |
+
the iterator and a
|
| 112 |
+
single item.
|
| 113 |
+
"""
|
| 114 |
+
self.max_results = max_results
|
| 115 |
+
"""int: The maximum number of results to fetch."""
|
| 116 |
+
|
| 117 |
+
# The attributes below will change over the life of the iterator.
|
| 118 |
+
self.page_number = 0
|
| 119 |
+
"""int: The current page of results."""
|
| 120 |
+
self.next_page_token = page_token
|
| 121 |
+
"""str: The token for the next page of results. If this is set before
|
| 122 |
+
the iterator starts, it effectively offsets the iterator to a
|
| 123 |
+
specific starting point."""
|
| 124 |
+
self.num_results = 0
|
| 125 |
+
"""int: The total number of results fetched so far."""
|
| 126 |
+
|
| 127 |
+
@property
|
| 128 |
+
def pages(self):
|
| 129 |
+
"""Iterator of pages in the response.
|
| 130 |
+
|
| 131 |
+
returns:
|
| 132 |
+
types.GeneratorType[google.api_core.page_iterator.Page]: A
|
| 133 |
+
generator of page instances.
|
| 134 |
+
|
| 135 |
+
raises:
|
| 136 |
+
ValueError: If the iterator has already been started.
|
| 137 |
+
"""
|
| 138 |
+
if self._started:
|
| 139 |
+
raise ValueError("Iterator has already started", self)
|
| 140 |
+
self._started = True
|
| 141 |
+
return self._page_aiter(increment=True)
|
| 142 |
+
|
| 143 |
+
async def _items_aiter(self):
|
| 144 |
+
"""Iterator for each item returned."""
|
| 145 |
+
async for page in self._page_aiter(increment=False):
|
| 146 |
+
for item in page:
|
| 147 |
+
self.num_results += 1
|
| 148 |
+
yield item
|
| 149 |
+
|
| 150 |
+
def __aiter__(self):
|
| 151 |
+
"""Iterator for each item returned.
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
types.GeneratorType[Any]: A generator of items from the API.
|
| 155 |
+
|
| 156 |
+
Raises:
|
| 157 |
+
ValueError: If the iterator has already been started.
|
| 158 |
+
"""
|
| 159 |
+
if self._started:
|
| 160 |
+
raise ValueError("Iterator has already started", self)
|
| 161 |
+
self._started = True
|
| 162 |
+
return self._items_aiter()
|
| 163 |
+
|
| 164 |
+
async def __anext__(self):
|
| 165 |
+
if self.__active_aiterator is None:
|
| 166 |
+
self.__active_aiterator = self.__aiter__()
|
| 167 |
+
return await self.__active_aiterator.__anext__()
|
| 168 |
+
|
| 169 |
+
async def _page_aiter(self, increment):
|
| 170 |
+
"""Generator of pages of API responses.
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
increment (bool): Flag indicating if the total number of results
|
| 174 |
+
should be incremented on each page. This is useful since a page
|
| 175 |
+
iterator will want to increment by results per page while an
|
| 176 |
+
items iterator will want to increment per item.
|
| 177 |
+
|
| 178 |
+
Yields:
|
| 179 |
+
Page: each page of items from the API.
|
| 180 |
+
"""
|
| 181 |
+
page = await self._next_page()
|
| 182 |
+
while page is not None:
|
| 183 |
+
self.page_number += 1
|
| 184 |
+
if increment:
|
| 185 |
+
self.num_results += page.num_items
|
| 186 |
+
yield page
|
| 187 |
+
page = await self._next_page()
|
| 188 |
+
|
| 189 |
+
@abc.abstractmethod
|
| 190 |
+
async def _next_page(self):
|
| 191 |
+
"""Get the next page in the iterator.
|
| 192 |
+
|
| 193 |
+
This does nothing and is intended to be over-ridden by subclasses
|
| 194 |
+
to return the next :class:`Page`.
|
| 195 |
+
|
| 196 |
+
Raises:
|
| 197 |
+
NotImplementedError: Always, this method is abstract.
|
| 198 |
+
"""
|
| 199 |
+
raise NotImplementedError
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class AsyncGRPCIterator(AsyncIterator):
|
| 203 |
+
"""A generic class for iterating through gRPC list responses.
|
| 204 |
+
|
| 205 |
+
.. note:: The class does not take a ``page_token`` argument because it can
|
| 206 |
+
just be specified in the ``request``.
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
client (google.cloud.client.Client): The API client. This unused by
|
| 210 |
+
this class, but kept to satisfy the :class:`Iterator` interface.
|
| 211 |
+
method (Callable[protobuf.Message]): A bound gRPC method that should
|
| 212 |
+
take a single message for the request.
|
| 213 |
+
request (protobuf.Message): The request message.
|
| 214 |
+
items_field (str): The field in the response message that has the
|
| 215 |
+
items for the page.
|
| 216 |
+
item_to_value (Callable[GRPCIterator, Any]): Callable to convert an
|
| 217 |
+
item from the type in the JSON response into a native object. Will
|
| 218 |
+
be called with the iterator and a single item.
|
| 219 |
+
request_token_field (str): The field in the request message used to
|
| 220 |
+
specify the page token.
|
| 221 |
+
response_token_field (str): The field in the response message that has
|
| 222 |
+
the token for the next page.
|
| 223 |
+
max_results (int): The maximum number of results to fetch.
|
| 224 |
+
|
| 225 |
+
.. autoattribute:: pages
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
_DEFAULT_REQUEST_TOKEN_FIELD = "page_token"
|
| 229 |
+
_DEFAULT_RESPONSE_TOKEN_FIELD = "next_page_token"
|
| 230 |
+
|
| 231 |
+
def __init__(
|
| 232 |
+
self,
|
| 233 |
+
client,
|
| 234 |
+
method,
|
| 235 |
+
request,
|
| 236 |
+
items_field,
|
| 237 |
+
item_to_value=_item_to_value_identity,
|
| 238 |
+
request_token_field=_DEFAULT_REQUEST_TOKEN_FIELD,
|
| 239 |
+
response_token_field=_DEFAULT_RESPONSE_TOKEN_FIELD,
|
| 240 |
+
max_results=None,
|
| 241 |
+
):
|
| 242 |
+
super().__init__(client, item_to_value, max_results=max_results)
|
| 243 |
+
self._method = method
|
| 244 |
+
self._request = request
|
| 245 |
+
self._items_field = items_field
|
| 246 |
+
self._request_token_field = request_token_field
|
| 247 |
+
self._response_token_field = response_token_field
|
| 248 |
+
|
| 249 |
+
async def _next_page(self):
|
| 250 |
+
"""Get the next page in the iterator.
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
Page: The next page in the iterator or :data:`None` if
|
| 254 |
+
there are no pages left.
|
| 255 |
+
"""
|
| 256 |
+
if not self._has_next_page():
|
| 257 |
+
return None
|
| 258 |
+
|
| 259 |
+
if self.next_page_token is not None:
|
| 260 |
+
setattr(self._request, self._request_token_field, self.next_page_token)
|
| 261 |
+
|
| 262 |
+
response = await self._method(self._request)
|
| 263 |
+
|
| 264 |
+
self.next_page_token = getattr(response, self._response_token_field)
|
| 265 |
+
items = getattr(response, self._items_field)
|
| 266 |
+
page = Page(self, items, self.item_to_value, raw_page=response)
|
| 267 |
+
|
| 268 |
+
return page
|
| 269 |
+
|
| 270 |
+
def _has_next_page(self):
|
| 271 |
+
"""Determines whether or not there are more pages with results.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
bool: Whether the iterator has more pages.
|
| 275 |
+
"""
|
| 276 |
+
if self.page_number == 0:
|
| 277 |
+
return True
|
| 278 |
+
|
| 279 |
+
# Note: intentionally a falsy check instead of a None check. The RPC
|
| 280 |
+
# can return an empty string indicating no more pages.
|
| 281 |
+
if self.max_results is not None:
|
| 282 |
+
if self.num_results >= self.max_results:
|
| 283 |
+
return False
|
| 284 |
+
|
| 285 |
+
return True if self.next_page_token else False
|
py311/lib/python3.11/site-packages/google/api_core/path_template.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 |
+
"""Expand and validate URL path templates.
|
| 16 |
+
|
| 17 |
+
This module provides the :func:`expand` and :func:`validate` functions for
|
| 18 |
+
interacting with Google-style URL `path templates`_ which are commonly used
|
| 19 |
+
in Google APIs for `resource names`_.
|
| 20 |
+
|
| 21 |
+
.. _path templates: https://github.com/googleapis/googleapis/blob
|
| 22 |
+
/57e2d376ac7ef48681554204a3ba78a414f2c533/google/api/http.proto#L212
|
| 23 |
+
.. _resource names: https://cloud.google.com/apis/design/resource_names
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from __future__ import unicode_literals
|
| 27 |
+
|
| 28 |
+
from collections import deque
|
| 29 |
+
import copy
|
| 30 |
+
import functools
|
| 31 |
+
import re
|
| 32 |
+
|
| 33 |
+
# Regular expression for extracting variable parts from a path template.
|
| 34 |
+
# The variables can be expressed as:
|
| 35 |
+
#
|
| 36 |
+
# - "*": a single-segment positional variable, for example: "books/*"
|
| 37 |
+
# - "**": a multi-segment positional variable, for example: "shelf/**/book/*"
|
| 38 |
+
# - "{name}": a single-segment wildcard named variable, for example
|
| 39 |
+
# "books/{name}"
|
| 40 |
+
# - "{name=*}: same as above.
|
| 41 |
+
# - "{name=**}": a multi-segment wildcard named variable, for example
|
| 42 |
+
# "shelf/{name=**}"
|
| 43 |
+
# - "{name=/path/*/**}": a multi-segment named variable with a sub-template.
|
| 44 |
+
_VARIABLE_RE = re.compile(
|
| 45 |
+
r"""
|
| 46 |
+
( # Capture the entire variable expression
|
| 47 |
+
(?P<positional>\*\*?) # Match & capture * and ** positional variables.
|
| 48 |
+
|
|
| 49 |
+
# Match & capture named variables {name}
|
| 50 |
+
{
|
| 51 |
+
(?P<name>[^/]+?)
|
| 52 |
+
# Optionally match and capture the named variable's template.
|
| 53 |
+
(?:=(?P<template>.+?))?
|
| 54 |
+
}
|
| 55 |
+
)
|
| 56 |
+
""",
|
| 57 |
+
re.VERBOSE,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Segment expressions used for validating paths against a template.
|
| 61 |
+
_SINGLE_SEGMENT_PATTERN = r"([^/]+)"
|
| 62 |
+
_MULTI_SEGMENT_PATTERN = r"(.+)"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _expand_variable_match(positional_vars, named_vars, match):
|
| 66 |
+
"""Expand a matched variable with its value.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
positional_vars (list): A list of positional variables. This list will
|
| 70 |
+
be modified.
|
| 71 |
+
named_vars (dict): A dictionary of named variables.
|
| 72 |
+
match (re.Match): A regular expression match.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
str: The expanded variable to replace the match.
|
| 76 |
+
|
| 77 |
+
Raises:
|
| 78 |
+
ValueError: If a positional or named variable is required by the
|
| 79 |
+
template but not specified or if an unexpected template expression
|
| 80 |
+
is encountered.
|
| 81 |
+
"""
|
| 82 |
+
positional = match.group("positional")
|
| 83 |
+
name = match.group("name")
|
| 84 |
+
if name is not None:
|
| 85 |
+
try:
|
| 86 |
+
return str(named_vars[name])
|
| 87 |
+
except KeyError:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
"Named variable '{}' not specified and needed by template "
|
| 90 |
+
"`{}` at position {}".format(name, match.string, match.start())
|
| 91 |
+
)
|
| 92 |
+
elif positional is not None:
|
| 93 |
+
try:
|
| 94 |
+
return str(positional_vars.pop(0))
|
| 95 |
+
except IndexError:
|
| 96 |
+
raise ValueError(
|
| 97 |
+
"Positional variable not specified and needed by template "
|
| 98 |
+
"`{}` at position {}".format(match.string, match.start())
|
| 99 |
+
)
|
| 100 |
+
else:
|
| 101 |
+
raise ValueError("Unknown template expression {}".format(match.group(0)))
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def expand(tmpl, *args, **kwargs):
|
| 105 |
+
"""Expand a path template with the given variables.
|
| 106 |
+
|
| 107 |
+
.. code-block:: python
|
| 108 |
+
|
| 109 |
+
>>> expand('users/*/messages/*', 'me', '123')
|
| 110 |
+
users/me/messages/123
|
| 111 |
+
>>> expand('/v1/{name=shelves/*/books/*}', name='shelves/1/books/3')
|
| 112 |
+
/v1/shelves/1/books/3
|
| 113 |
+
|
| 114 |
+
Args:
|
| 115 |
+
tmpl (str): The path template.
|
| 116 |
+
args: The positional variables for the path.
|
| 117 |
+
kwargs: The named variables for the path.
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
str: The expanded path
|
| 121 |
+
|
| 122 |
+
Raises:
|
| 123 |
+
ValueError: If a positional or named variable is required by the
|
| 124 |
+
template but not specified or if an unexpected template expression
|
| 125 |
+
is encountered.
|
| 126 |
+
"""
|
| 127 |
+
replacer = functools.partial(_expand_variable_match, list(args), kwargs)
|
| 128 |
+
return _VARIABLE_RE.sub(replacer, tmpl)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _replace_variable_with_pattern(match):
|
| 132 |
+
"""Replace a variable match with a pattern that can be used to validate it.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
match (re.Match): A regular expression match
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
str: A regular expression pattern that can be used to validate the
|
| 139 |
+
variable in an expanded path.
|
| 140 |
+
|
| 141 |
+
Raises:
|
| 142 |
+
ValueError: If an unexpected template expression is encountered.
|
| 143 |
+
"""
|
| 144 |
+
positional = match.group("positional")
|
| 145 |
+
name = match.group("name")
|
| 146 |
+
template = match.group("template")
|
| 147 |
+
if name is not None:
|
| 148 |
+
if not template:
|
| 149 |
+
return _SINGLE_SEGMENT_PATTERN.format(name)
|
| 150 |
+
elif template == "**":
|
| 151 |
+
return _MULTI_SEGMENT_PATTERN.format(name)
|
| 152 |
+
else:
|
| 153 |
+
return _generate_pattern_for_template(template)
|
| 154 |
+
elif positional == "*":
|
| 155 |
+
return _SINGLE_SEGMENT_PATTERN
|
| 156 |
+
elif positional == "**":
|
| 157 |
+
return _MULTI_SEGMENT_PATTERN
|
| 158 |
+
else:
|
| 159 |
+
raise ValueError("Unknown template expression {}".format(match.group(0)))
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _generate_pattern_for_template(tmpl):
|
| 163 |
+
"""Generate a pattern that can validate a path template.
|
| 164 |
+
|
| 165 |
+
Args:
|
| 166 |
+
tmpl (str): The path template
|
| 167 |
+
|
| 168 |
+
Returns:
|
| 169 |
+
str: A regular expression pattern that can be used to validate an
|
| 170 |
+
expanded path template.
|
| 171 |
+
"""
|
| 172 |
+
return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def get_field(request, field):
|
| 176 |
+
"""Get the value of a field from a given dictionary.
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
request (dict | Message): A dictionary or a Message object.
|
| 180 |
+
field (str): The key to the request in dot notation.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
The value of the field.
|
| 184 |
+
"""
|
| 185 |
+
parts = field.split(".")
|
| 186 |
+
value = request
|
| 187 |
+
|
| 188 |
+
for part in parts:
|
| 189 |
+
if not isinstance(value, dict):
|
| 190 |
+
value = getattr(value, part, None)
|
| 191 |
+
else:
|
| 192 |
+
value = value.get(part)
|
| 193 |
+
if isinstance(value, dict):
|
| 194 |
+
return
|
| 195 |
+
return value
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def delete_field(request, field):
|
| 199 |
+
"""Delete the value of a field from a given dictionary.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
request (dict | Message): A dictionary object or a Message.
|
| 203 |
+
field (str): The key to the request in dot notation.
|
| 204 |
+
"""
|
| 205 |
+
parts = deque(field.split("."))
|
| 206 |
+
while len(parts) > 1:
|
| 207 |
+
part = parts.popleft()
|
| 208 |
+
if not isinstance(request, dict):
|
| 209 |
+
if hasattr(request, part):
|
| 210 |
+
request = getattr(request, part, None)
|
| 211 |
+
else:
|
| 212 |
+
return
|
| 213 |
+
else:
|
| 214 |
+
request = request.get(part)
|
| 215 |
+
part = parts.popleft()
|
| 216 |
+
if not isinstance(request, dict):
|
| 217 |
+
if hasattr(request, part):
|
| 218 |
+
request.ClearField(part)
|
| 219 |
+
else:
|
| 220 |
+
return
|
| 221 |
+
else:
|
| 222 |
+
request.pop(part, None)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def validate(tmpl, path):
|
| 226 |
+
"""Validate a path against the path template.
|
| 227 |
+
|
| 228 |
+
.. code-block:: python
|
| 229 |
+
|
| 230 |
+
>>> validate('users/*/messages/*', 'users/me/messages/123')
|
| 231 |
+
True
|
| 232 |
+
>>> validate('users/*/messages/*', 'users/me/drafts/123')
|
| 233 |
+
False
|
| 234 |
+
>>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/books/3)
|
| 235 |
+
True
|
| 236 |
+
>>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/tapes/3)
|
| 237 |
+
False
|
| 238 |
+
|
| 239 |
+
Args:
|
| 240 |
+
tmpl (str): The path template.
|
| 241 |
+
path (str): The expanded path.
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
bool: True if the path matches.
|
| 245 |
+
"""
|
| 246 |
+
pattern = _generate_pattern_for_template(tmpl) + "$"
|
| 247 |
+
return True if re.match(pattern, path) is not None else False
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def transcode(http_options, message=None, **request_kwargs):
|
| 251 |
+
"""Transcodes a grpc request pattern into a proper HTTP request following the rules outlined here,
|
| 252 |
+
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
http_options (list(dict)): A list of dicts which consist of these keys,
|
| 256 |
+
'method' (str): The http method
|
| 257 |
+
'uri' (str): The path template
|
| 258 |
+
'body' (str): The body field name (optional)
|
| 259 |
+
(This is a simplified representation of the proto option `google.api.http`)
|
| 260 |
+
|
| 261 |
+
message (Message) : A request object (optional)
|
| 262 |
+
request_kwargs (dict) : A dict representing the request object
|
| 263 |
+
|
| 264 |
+
Returns:
|
| 265 |
+
dict: The transcoded request with these keys,
|
| 266 |
+
'method' (str) : The http method
|
| 267 |
+
'uri' (str) : The expanded uri
|
| 268 |
+
'body' (dict | Message) : A dict or a Message representing the body (optional)
|
| 269 |
+
'query_params' (dict | Message) : A dict or Message mapping query parameter variables and values
|
| 270 |
+
|
| 271 |
+
Raises:
|
| 272 |
+
ValueError: If the request does not match the given template.
|
| 273 |
+
"""
|
| 274 |
+
transcoded_value = message or request_kwargs
|
| 275 |
+
bindings = []
|
| 276 |
+
for http_option in http_options:
|
| 277 |
+
request = {}
|
| 278 |
+
|
| 279 |
+
# Assign path
|
| 280 |
+
uri_template = http_option["uri"]
|
| 281 |
+
fields = [
|
| 282 |
+
(m.group("name"), m.group("template"))
|
| 283 |
+
for m in _VARIABLE_RE.finditer(uri_template)
|
| 284 |
+
]
|
| 285 |
+
bindings.append((uri_template, fields))
|
| 286 |
+
|
| 287 |
+
path_args = {field: get_field(transcoded_value, field) for field, _ in fields}
|
| 288 |
+
request["uri"] = expand(uri_template, **path_args)
|
| 289 |
+
|
| 290 |
+
if not validate(uri_template, request["uri"]) or not all(path_args.values()):
|
| 291 |
+
continue
|
| 292 |
+
|
| 293 |
+
# Remove fields used in uri path from request
|
| 294 |
+
leftovers = copy.deepcopy(transcoded_value)
|
| 295 |
+
for path_field, _ in fields:
|
| 296 |
+
delete_field(leftovers, path_field)
|
| 297 |
+
|
| 298 |
+
# Assign body and query params
|
| 299 |
+
body = http_option.get("body")
|
| 300 |
+
|
| 301 |
+
if body:
|
| 302 |
+
if body == "*":
|
| 303 |
+
request["body"] = leftovers
|
| 304 |
+
if message:
|
| 305 |
+
request["query_params"] = message.__class__()
|
| 306 |
+
else:
|
| 307 |
+
request["query_params"] = {}
|
| 308 |
+
else:
|
| 309 |
+
try:
|
| 310 |
+
if message:
|
| 311 |
+
request["body"] = getattr(leftovers, body)
|
| 312 |
+
delete_field(leftovers, body)
|
| 313 |
+
else:
|
| 314 |
+
request["body"] = leftovers.pop(body)
|
| 315 |
+
except (KeyError, AttributeError):
|
| 316 |
+
continue
|
| 317 |
+
request["query_params"] = leftovers
|
| 318 |
+
else:
|
| 319 |
+
request["query_params"] = leftovers
|
| 320 |
+
request["method"] = http_option["method"]
|
| 321 |
+
return request
|
| 322 |
+
|
| 323 |
+
bindings_description = [
|
| 324 |
+
'\n\tURI: "{}"'
|
| 325 |
+
"\n\tRequired request fields:\n\t\t{}".format(
|
| 326 |
+
uri,
|
| 327 |
+
"\n\t\t".join(
|
| 328 |
+
[
|
| 329 |
+
'field: "{}", pattern: "{}"'.format(n, p if p else "*")
|
| 330 |
+
for n, p in fields
|
| 331 |
+
]
|
| 332 |
+
),
|
| 333 |
+
)
|
| 334 |
+
for uri, fields in bindings
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
raise ValueError(
|
| 338 |
+
"Invalid request."
|
| 339 |
+
"\nSome of the fields of the request message are either not initialized or "
|
| 340 |
+
"initialized with an invalid value."
|
| 341 |
+
"\nPlease make sure your request matches at least one accepted HTTP binding."
|
| 342 |
+
"\nTo match a binding the request message must have all the required fields "
|
| 343 |
+
"initialized with values matching their patterns as listed below:{}".format(
|
| 344 |
+
"\n".join(bindings_description)
|
| 345 |
+
)
|
| 346 |
+
)
|
py311/lib/python3.11/site-packages/google/api_core/protobuf_helpers.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2017 Google LLC
|
| 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 :mod:`protobuf`."""
|
| 16 |
+
|
| 17 |
+
import collections
|
| 18 |
+
import collections.abc
|
| 19 |
+
import copy
|
| 20 |
+
import inspect
|
| 21 |
+
|
| 22 |
+
from google.protobuf import field_mask_pb2
|
| 23 |
+
from google.protobuf import message
|
| 24 |
+
from google.protobuf import wrappers_pb2
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
_SENTINEL = object()
|
| 28 |
+
_WRAPPER_TYPES = (
|
| 29 |
+
wrappers_pb2.BoolValue,
|
| 30 |
+
wrappers_pb2.BytesValue,
|
| 31 |
+
wrappers_pb2.DoubleValue,
|
| 32 |
+
wrappers_pb2.FloatValue,
|
| 33 |
+
wrappers_pb2.Int32Value,
|
| 34 |
+
wrappers_pb2.Int64Value,
|
| 35 |
+
wrappers_pb2.StringValue,
|
| 36 |
+
wrappers_pb2.UInt32Value,
|
| 37 |
+
wrappers_pb2.UInt64Value,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def from_any_pb(pb_type, any_pb):
|
| 42 |
+
"""Converts an ``Any`` protobuf to the specified message type.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
pb_type (type): the type of the message that any_pb stores an instance
|
| 46 |
+
of.
|
| 47 |
+
any_pb (google.protobuf.any_pb2.Any): the object to be converted.
|
| 48 |
+
|
| 49 |
+
Returns:
|
| 50 |
+
pb_type: An instance of the pb_type message.
|
| 51 |
+
|
| 52 |
+
Raises:
|
| 53 |
+
TypeError: if the message could not be converted.
|
| 54 |
+
"""
|
| 55 |
+
msg = pb_type()
|
| 56 |
+
|
| 57 |
+
# Unwrap proto-plus wrapped messages.
|
| 58 |
+
if callable(getattr(pb_type, "pb", None)):
|
| 59 |
+
msg_pb = pb_type.pb(msg)
|
| 60 |
+
else:
|
| 61 |
+
msg_pb = msg
|
| 62 |
+
|
| 63 |
+
# Unpack the Any object and populate the protobuf message instance.
|
| 64 |
+
if not any_pb.Unpack(msg_pb):
|
| 65 |
+
raise TypeError(
|
| 66 |
+
f"Could not convert `{any_pb.TypeName()}` with underlying type `google.protobuf.any_pb2.Any` to `{msg_pb.DESCRIPTOR.full_name}`"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Done; return the message.
|
| 70 |
+
return msg
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def check_oneof(**kwargs):
|
| 74 |
+
"""Raise ValueError if more than one keyword argument is not ``None``.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
kwargs (dict): The keyword arguments sent to the function.
|
| 78 |
+
|
| 79 |
+
Raises:
|
| 80 |
+
ValueError: If more than one entry in ``kwargs`` is not ``None``.
|
| 81 |
+
"""
|
| 82 |
+
# Sanity check: If no keyword arguments were sent, this is fine.
|
| 83 |
+
if not kwargs:
|
| 84 |
+
return
|
| 85 |
+
|
| 86 |
+
not_nones = [val for val in kwargs.values() if val is not None]
|
| 87 |
+
if len(not_nones) > 1:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
"Only one of {fields} should be set.".format(
|
| 90 |
+
fields=", ".join(sorted(kwargs.keys()))
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def get_messages(module):
|
| 96 |
+
"""Discovers all protobuf Message classes in a given import module.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
module (module): A Python module; :func:`dir` will be run against this
|
| 100 |
+
module to find Message subclasses.
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
dict[str, google.protobuf.message.Message]: A dictionary with the
|
| 104 |
+
Message class names as keys, and the Message subclasses themselves
|
| 105 |
+
as values.
|
| 106 |
+
"""
|
| 107 |
+
answer = collections.OrderedDict()
|
| 108 |
+
for name in dir(module):
|
| 109 |
+
candidate = getattr(module, name)
|
| 110 |
+
if inspect.isclass(candidate) and issubclass(candidate, message.Message):
|
| 111 |
+
answer[name] = candidate
|
| 112 |
+
return answer
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _resolve_subkeys(key, separator="."):
|
| 116 |
+
"""Resolve a potentially nested key.
|
| 117 |
+
|
| 118 |
+
If the key contains the ``separator`` (e.g. ``.``) then the key will be
|
| 119 |
+
split on the first instance of the subkey::
|
| 120 |
+
|
| 121 |
+
>>> _resolve_subkeys('a.b.c')
|
| 122 |
+
('a', 'b.c')
|
| 123 |
+
>>> _resolve_subkeys('d|e|f', separator='|')
|
| 124 |
+
('d', 'e|f')
|
| 125 |
+
|
| 126 |
+
If not, the subkey will be :data:`None`::
|
| 127 |
+
|
| 128 |
+
>>> _resolve_subkeys('foo')
|
| 129 |
+
('foo', None)
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
key (str): A string that may or may not contain the separator.
|
| 133 |
+
separator (str): The namespace separator. Defaults to `.`.
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
Tuple[str, str]: The key and subkey(s).
|
| 137 |
+
"""
|
| 138 |
+
parts = key.split(separator, 1)
|
| 139 |
+
|
| 140 |
+
if len(parts) > 1:
|
| 141 |
+
return parts
|
| 142 |
+
else:
|
| 143 |
+
return parts[0], None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def get(msg_or_dict, key, default=_SENTINEL):
|
| 147 |
+
"""Retrieve a key's value from a protobuf Message or dictionary.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
mdg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
|
| 151 |
+
object.
|
| 152 |
+
key (str): The key to retrieve from the object.
|
| 153 |
+
default (Any): If the key is not present on the object, and a default
|
| 154 |
+
is set, returns that default instead. A type-appropriate falsy
|
| 155 |
+
default is generally recommended, as protobuf messages almost
|
| 156 |
+
always have default values for unset values and it is not always
|
| 157 |
+
possible to tell the difference between a falsy value and an
|
| 158 |
+
unset one. If no default is set then :class:`KeyError` will be
|
| 159 |
+
raised if the key is not present in the object.
|
| 160 |
+
|
| 161 |
+
Returns:
|
| 162 |
+
Any: The return value from the underlying Message or dict.
|
| 163 |
+
|
| 164 |
+
Raises:
|
| 165 |
+
KeyError: If the key is not found. Note that, for unset values,
|
| 166 |
+
messages and dictionaries may not have consistent behavior.
|
| 167 |
+
TypeError: If ``msg_or_dict`` is not a Message or Mapping.
|
| 168 |
+
"""
|
| 169 |
+
# We may need to get a nested key. Resolve this.
|
| 170 |
+
key, subkey = _resolve_subkeys(key)
|
| 171 |
+
|
| 172 |
+
# Attempt to get the value from the two types of objects we know about.
|
| 173 |
+
# If we get something else, complain.
|
| 174 |
+
if isinstance(msg_or_dict, message.Message):
|
| 175 |
+
answer = getattr(msg_or_dict, key, default)
|
| 176 |
+
elif isinstance(msg_or_dict, collections.abc.Mapping):
|
| 177 |
+
answer = msg_or_dict.get(key, default)
|
| 178 |
+
else:
|
| 179 |
+
raise TypeError(
|
| 180 |
+
"get() expected a dict or protobuf message, got {!r}.".format(
|
| 181 |
+
type(msg_or_dict)
|
| 182 |
+
)
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# If the object we got back is our sentinel, raise KeyError; this is
|
| 186 |
+
# a "not found" case.
|
| 187 |
+
if answer is _SENTINEL:
|
| 188 |
+
raise KeyError(key)
|
| 189 |
+
|
| 190 |
+
# If a subkey exists, call this method recursively against the answer.
|
| 191 |
+
if subkey is not None and answer is not default:
|
| 192 |
+
return get(answer, subkey, default=default)
|
| 193 |
+
|
| 194 |
+
return answer
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def _set_field_on_message(msg, key, value):
|
| 198 |
+
"""Set helper for protobuf Messages."""
|
| 199 |
+
# Attempt to set the value on the types of objects we know how to deal
|
| 200 |
+
# with.
|
| 201 |
+
if isinstance(value, (collections.abc.MutableSequence, tuple)):
|
| 202 |
+
# Clear the existing repeated protobuf message of any elements
|
| 203 |
+
# currently inside it.
|
| 204 |
+
while getattr(msg, key):
|
| 205 |
+
getattr(msg, key).pop()
|
| 206 |
+
|
| 207 |
+
# Write our new elements to the repeated field.
|
| 208 |
+
for item in value:
|
| 209 |
+
if isinstance(item, collections.abc.Mapping):
|
| 210 |
+
getattr(msg, key).add(**item)
|
| 211 |
+
else:
|
| 212 |
+
# protobuf's RepeatedCompositeContainer doesn't support
|
| 213 |
+
# append.
|
| 214 |
+
getattr(msg, key).extend([item])
|
| 215 |
+
elif isinstance(value, collections.abc.Mapping):
|
| 216 |
+
# Assign the dictionary values to the protobuf message.
|
| 217 |
+
for item_key, item_value in value.items():
|
| 218 |
+
set(getattr(msg, key), item_key, item_value)
|
| 219 |
+
elif isinstance(value, message.Message):
|
| 220 |
+
getattr(msg, key).CopyFrom(value)
|
| 221 |
+
else:
|
| 222 |
+
setattr(msg, key, value)
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def set(msg_or_dict, key, value):
|
| 226 |
+
"""Set a key's value on a protobuf Message or dictionary.
|
| 227 |
+
|
| 228 |
+
Args:
|
| 229 |
+
msg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
|
| 230 |
+
object.
|
| 231 |
+
key (str): The key to set.
|
| 232 |
+
value (Any): The value to set.
|
| 233 |
+
|
| 234 |
+
Raises:
|
| 235 |
+
TypeError: If ``msg_or_dict`` is not a Message or dictionary.
|
| 236 |
+
"""
|
| 237 |
+
# Sanity check: Is our target object valid?
|
| 238 |
+
if not isinstance(msg_or_dict, (collections.abc.MutableMapping, message.Message)):
|
| 239 |
+
raise TypeError(
|
| 240 |
+
"set() expected a dict or protobuf message, got {!r}.".format(
|
| 241 |
+
type(msg_or_dict)
|
| 242 |
+
)
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# We may be setting a nested key. Resolve this.
|
| 246 |
+
basekey, subkey = _resolve_subkeys(key)
|
| 247 |
+
|
| 248 |
+
# If a subkey exists, then get that object and call this method
|
| 249 |
+
# recursively against it using the subkey.
|
| 250 |
+
if subkey is not None:
|
| 251 |
+
if isinstance(msg_or_dict, collections.abc.MutableMapping):
|
| 252 |
+
msg_or_dict.setdefault(basekey, {})
|
| 253 |
+
set(get(msg_or_dict, basekey), subkey, value)
|
| 254 |
+
return
|
| 255 |
+
|
| 256 |
+
if isinstance(msg_or_dict, collections.abc.MutableMapping):
|
| 257 |
+
msg_or_dict[key] = value
|
| 258 |
+
else:
|
| 259 |
+
_set_field_on_message(msg_or_dict, key, value)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def setdefault(msg_or_dict, key, value):
|
| 263 |
+
"""Set the key on a protobuf Message or dictionary to a given value if the
|
| 264 |
+
current value is falsy.
|
| 265 |
+
|
| 266 |
+
Because protobuf Messages do not distinguish between unset values and
|
| 267 |
+
falsy ones particularly well (by design), this method treats any falsy
|
| 268 |
+
value (e.g. 0, empty list) as a target to be overwritten, on both Messages
|
| 269 |
+
and dictionaries.
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
msg_or_dict (Union[~google.protobuf.message.Message, Mapping]): the
|
| 273 |
+
object.
|
| 274 |
+
key (str): The key on the object in question.
|
| 275 |
+
value (Any): The value to set.
|
| 276 |
+
|
| 277 |
+
Raises:
|
| 278 |
+
TypeError: If ``msg_or_dict`` is not a Message or dictionary.
|
| 279 |
+
"""
|
| 280 |
+
if not get(msg_or_dict, key, default=None):
|
| 281 |
+
set(msg_or_dict, key, value)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def field_mask(original, modified):
|
| 285 |
+
"""Create a field mask by comparing two messages.
|
| 286 |
+
|
| 287 |
+
Args:
|
| 288 |
+
original (~google.protobuf.message.Message): the original message.
|
| 289 |
+
If set to None, this field will be interpreted as an empty
|
| 290 |
+
message.
|
| 291 |
+
modified (~google.protobuf.message.Message): the modified message.
|
| 292 |
+
If set to None, this field will be interpreted as an empty
|
| 293 |
+
message.
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
google.protobuf.field_mask_pb2.FieldMask: field mask that contains
|
| 297 |
+
the list of field names that have different values between the two
|
| 298 |
+
messages. If the messages are equivalent, then the field mask is empty.
|
| 299 |
+
|
| 300 |
+
Raises:
|
| 301 |
+
ValueError: If the ``original`` or ``modified`` are not the same type.
|
| 302 |
+
"""
|
| 303 |
+
if original is None and modified is None:
|
| 304 |
+
return field_mask_pb2.FieldMask()
|
| 305 |
+
|
| 306 |
+
if original is None and modified is not None:
|
| 307 |
+
original = copy.deepcopy(modified)
|
| 308 |
+
original.Clear()
|
| 309 |
+
|
| 310 |
+
if modified is None and original is not None:
|
| 311 |
+
modified = copy.deepcopy(original)
|
| 312 |
+
modified.Clear()
|
| 313 |
+
|
| 314 |
+
if not isinstance(original, type(modified)):
|
| 315 |
+
raise ValueError(
|
| 316 |
+
"expected that both original and modified should be of the "
|
| 317 |
+
'same type, received "{!r}" and "{!r}".'.format(
|
| 318 |
+
type(original), type(modified)
|
| 319 |
+
)
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
return field_mask_pb2.FieldMask(paths=_field_mask_helper(original, modified))
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def _field_mask_helper(original, modified, current=""):
|
| 326 |
+
answer = []
|
| 327 |
+
|
| 328 |
+
for name in original.DESCRIPTOR.fields_by_name:
|
| 329 |
+
field_path = _get_path(current, name)
|
| 330 |
+
|
| 331 |
+
original_val = getattr(original, name)
|
| 332 |
+
modified_val = getattr(modified, name)
|
| 333 |
+
|
| 334 |
+
if _is_message(original_val) or _is_message(modified_val):
|
| 335 |
+
if original_val != modified_val:
|
| 336 |
+
# Wrapper types do not need to include the .value part of the
|
| 337 |
+
# path.
|
| 338 |
+
if _is_wrapper(original_val) or _is_wrapper(modified_val):
|
| 339 |
+
answer.append(field_path)
|
| 340 |
+
elif not modified_val.ListFields():
|
| 341 |
+
answer.append(field_path)
|
| 342 |
+
else:
|
| 343 |
+
answer.extend(
|
| 344 |
+
_field_mask_helper(original_val, modified_val, field_path)
|
| 345 |
+
)
|
| 346 |
+
else:
|
| 347 |
+
if original_val != modified_val:
|
| 348 |
+
answer.append(field_path)
|
| 349 |
+
|
| 350 |
+
return answer
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def _get_path(current, name):
|
| 354 |
+
# gapic-generator-python appends underscores to field names
|
| 355 |
+
# that collide with python keywords.
|
| 356 |
+
# `_` is stripped away as it is not possible to
|
| 357 |
+
# natively define a field with a trailing underscore in protobuf.
|
| 358 |
+
# APIs will reject field masks if fields have trailing underscores.
|
| 359 |
+
# See https://github.com/googleapis/python-api-core/issues/227
|
| 360 |
+
name = name.rstrip("_")
|
| 361 |
+
if not current:
|
| 362 |
+
return name
|
| 363 |
+
return "%s.%s" % (current, name)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _is_message(value):
|
| 367 |
+
return isinstance(value, message.Message)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _is_wrapper(value):
|
| 371 |
+
return type(value) in _WRAPPER_TYPES
|
py311/lib/python3.11/site-packages/google/api_core/py.typed
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Marker file for PEP 561.
|
| 2 |
+
# The google-api-core package uses inline types.
|