Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .venv/lib/python3.11/site-packages/httptools/__init__.py +6 -0
- .venv/lib/python3.11/site-packages/httptools/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httptools/__pycache__/_version.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httptools/_version.py +13 -0
- .venv/lib/python3.11/site-packages/httptools/parser/__init__.py +5 -0
- .venv/lib/python3.11/site-packages/httptools/parser/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httptools/parser/__pycache__/errors.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httptools/parser/cparser.pxd +167 -0
- .venv/lib/python3.11/site-packages/httptools/parser/errors.py +30 -0
- .venv/lib/python3.11/site-packages/httptools/parser/parser.pyx +436 -0
- .venv/lib/python3.11/site-packages/httptools/parser/python.pxd +6 -0
- .venv/lib/python3.11/site-packages/httptools/parser/url_cparser.pxd +31 -0
- .venv/lib/python3.11/site-packages/httptools/parser/url_parser.pyx +108 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__init__.py +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test__init.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_config.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_decorators.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_heaps.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_mapped_queue.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_misc.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_random_sequence.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_rcm.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_unionfind.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/test_config.py +231 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/test_mapped_queue.py +268 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/test_misc.py +268 -0
- .venv/lib/python3.11/site-packages/networkx/utils/tests/test_random_sequence.py +38 -0
- .venv/lib/python3.11/site-packages/oauth2client/__init__.py +24 -0
- .venv/lib/python3.11/site-packages/oauth2client/_helpers.py +341 -0
- .venv/lib/python3.11/site-packages/oauth2client/_openssl_crypt.py +136 -0
- .venv/lib/python3.11/site-packages/oauth2client/_pkce.py +67 -0
- .venv/lib/python3.11/site-packages/oauth2client/_pure_python_crypt.py +184 -0
- .venv/lib/python3.11/site-packages/oauth2client/_pycrypto_crypt.py +124 -0
- .venv/lib/python3.11/site-packages/oauth2client/client.py +2170 -0
- .venv/lib/python3.11/site-packages/oauth2client/clientsecrets.py +173 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__init__.py +6 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/_appengine_ndb.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/_metadata.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/appengine.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/devshell.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/dictionary_storage.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/flask_util.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/gce.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/keyring_storage.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/multiprocess_file_storage.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/sqlalchemy.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/xsrfutil.cpython-311.pyc +0 -0
.venv/lib/python3.11/site-packages/httptools/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import parser
|
| 2 |
+
from .parser import * # NOQA
|
| 3 |
+
|
| 4 |
+
from ._version import __version__ # NOQA
|
| 5 |
+
|
| 6 |
+
__all__ = parser.__all__ + ('__version__',) # NOQA
|
.venv/lib/python3.11/site-packages/httptools/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (352 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/httptools/__pycache__/_version.cpython-311.pyc
ADDED
|
Binary file (204 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/httptools/_version.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file MUST NOT contain anything but the __version__ assignment.
|
| 2 |
+
#
|
| 3 |
+
# When making a release, change the value of __version__
|
| 4 |
+
# to an appropriate value, and open a pull request against
|
| 5 |
+
# the correct branch (master if making a new feature release).
|
| 6 |
+
# The commit message MUST contain a properly formatted release
|
| 7 |
+
# log, and the commit must be signed.
|
| 8 |
+
#
|
| 9 |
+
# The release automation will: build and test the packages for the
|
| 10 |
+
# supported platforms, publish the packages on PyPI, merge the PR
|
| 11 |
+
# to the target branch, create a Git tag pointing to the commit.
|
| 12 |
+
|
| 13 |
+
__version__ = '0.6.4'
|
.venv/lib/python3.11/site-packages/httptools/parser/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .parser import * # NoQA
|
| 2 |
+
from .errors import * # NoQA
|
| 3 |
+
from .url_parser import * # NoQA
|
| 4 |
+
|
| 5 |
+
__all__ = parser.__all__ + errors.__all__ + url_parser.__all__ # NoQA
|
.venv/lib/python3.11/site-packages/httptools/parser/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (355 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/httptools/parser/__pycache__/errors.cpython-311.pyc
ADDED
|
Binary file (1.45 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httptools/parser/cparser.pxd
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
cdef extern from "llhttp.h":
|
| 5 |
+
struct llhttp__internal_s:
|
| 6 |
+
int32_t _index
|
| 7 |
+
void *_span_pos0
|
| 8 |
+
void *_span_cb0
|
| 9 |
+
int32_t error
|
| 10 |
+
const char *reason
|
| 11 |
+
const char *error_pos
|
| 12 |
+
void *data
|
| 13 |
+
void *_current
|
| 14 |
+
uint64_t content_length
|
| 15 |
+
uint8_t type
|
| 16 |
+
uint8_t method
|
| 17 |
+
uint8_t http_major
|
| 18 |
+
uint8_t http_minor
|
| 19 |
+
uint8_t header_state
|
| 20 |
+
uint16_t flags
|
| 21 |
+
uint8_t upgrade
|
| 22 |
+
uint16_t status_code
|
| 23 |
+
uint8_t finish
|
| 24 |
+
void *settings
|
| 25 |
+
ctypedef llhttp__internal_s llhttp__internal_t
|
| 26 |
+
ctypedef llhttp__internal_t llhttp_t
|
| 27 |
+
|
| 28 |
+
ctypedef int (*llhttp_data_cb) (llhttp_t*,
|
| 29 |
+
const char *at,
|
| 30 |
+
size_t length) except -1
|
| 31 |
+
|
| 32 |
+
ctypedef int (*llhttp_cb) (llhttp_t*) except -1
|
| 33 |
+
|
| 34 |
+
struct llhttp_settings_s:
|
| 35 |
+
llhttp_cb on_message_begin
|
| 36 |
+
llhttp_data_cb on_url
|
| 37 |
+
llhttp_data_cb on_status
|
| 38 |
+
llhttp_data_cb on_header_field
|
| 39 |
+
llhttp_data_cb on_header_value
|
| 40 |
+
llhttp_cb on_headers_complete
|
| 41 |
+
llhttp_data_cb on_body
|
| 42 |
+
llhttp_cb on_message_complete
|
| 43 |
+
llhttp_cb on_chunk_header
|
| 44 |
+
llhttp_cb on_chunk_complete
|
| 45 |
+
ctypedef llhttp_settings_s llhttp_settings_t
|
| 46 |
+
|
| 47 |
+
enum llhttp_type:
|
| 48 |
+
HTTP_BOTH,
|
| 49 |
+
HTTP_REQUEST,
|
| 50 |
+
HTTP_RESPONSE
|
| 51 |
+
ctypedef llhttp_type llhttp_type_t
|
| 52 |
+
|
| 53 |
+
enum llhttp_errno:
|
| 54 |
+
HPE_OK,
|
| 55 |
+
HPE_INTERNAL,
|
| 56 |
+
HPE_STRICT,
|
| 57 |
+
HPE_LF_EXPECTED,
|
| 58 |
+
HPE_UNEXPECTED_CONTENT_LENGTH,
|
| 59 |
+
HPE_CLOSED_CONNECTION,
|
| 60 |
+
HPE_INVALID_METHOD,
|
| 61 |
+
HPE_INVALID_URL,
|
| 62 |
+
HPE_INVALID_CONSTANT,
|
| 63 |
+
HPE_INVALID_VERSION,
|
| 64 |
+
HPE_INVALID_HEADER_TOKEN,
|
| 65 |
+
HPE_INVALID_CONTENT_LENGTH,
|
| 66 |
+
HPE_INVALID_CHUNK_SIZE,
|
| 67 |
+
HPE_INVALID_STATUS,
|
| 68 |
+
HPE_INVALID_EOF_STATE,
|
| 69 |
+
HPE_INVALID_TRANSFER_ENCODING,
|
| 70 |
+
HPE_CB_MESSAGE_BEGIN,
|
| 71 |
+
HPE_CB_HEADERS_COMPLETE,
|
| 72 |
+
HPE_CB_MESSAGE_COMPLETE,
|
| 73 |
+
HPE_CB_CHUNK_HEADER,
|
| 74 |
+
HPE_CB_CHUNK_COMPLETE,
|
| 75 |
+
HPE_PAUSED,
|
| 76 |
+
HPE_PAUSED_UPGRADE,
|
| 77 |
+
HPE_USER
|
| 78 |
+
ctypedef llhttp_errno llhttp_errno_t
|
| 79 |
+
|
| 80 |
+
enum llhttp_flags:
|
| 81 |
+
F_CONNECTION_KEEP_ALIVE,
|
| 82 |
+
F_CONNECTION_CLOSE,
|
| 83 |
+
F_CONNECTION_UPGRADE,
|
| 84 |
+
F_CHUNKED,
|
| 85 |
+
F_UPGRADE,
|
| 86 |
+
F_CONTENT_LENGTH,
|
| 87 |
+
F_SKIPBODY,
|
| 88 |
+
F_TRAILING,
|
| 89 |
+
F_LENIENT,
|
| 90 |
+
F_TRANSFER_ENCODING
|
| 91 |
+
ctypedef llhttp_flags llhttp_flags_t
|
| 92 |
+
|
| 93 |
+
enum llhttp_method:
|
| 94 |
+
HTTP_DELETE,
|
| 95 |
+
HTTP_GET,
|
| 96 |
+
HTTP_HEAD,
|
| 97 |
+
HTTP_POST,
|
| 98 |
+
HTTP_PUT,
|
| 99 |
+
HTTP_CONNECT,
|
| 100 |
+
HTTP_OPTIONS,
|
| 101 |
+
HTTP_TRACE,
|
| 102 |
+
HTTP_COPY,
|
| 103 |
+
HTTP_LOCK,
|
| 104 |
+
HTTP_MKCOL,
|
| 105 |
+
HTTP_MOVE,
|
| 106 |
+
HTTP_PROPFIND,
|
| 107 |
+
HTTP_PROPPATCH,
|
| 108 |
+
HTTP_SEARCH,
|
| 109 |
+
HTTP_UNLOCK,
|
| 110 |
+
HTTP_BIND,
|
| 111 |
+
HTTP_REBIND,
|
| 112 |
+
HTTP_UNBIND,
|
| 113 |
+
HTTP_ACL,
|
| 114 |
+
HTTP_REPORT,
|
| 115 |
+
HTTP_MKACTIVITY,
|
| 116 |
+
HTTP_CHECKOUT,
|
| 117 |
+
HTTP_MERGE,
|
| 118 |
+
HTTP_MSEARCH,
|
| 119 |
+
HTTP_NOTIFY,
|
| 120 |
+
HTTP_SUBSCRIBE,
|
| 121 |
+
HTTP_UNSUBSCRIBE,
|
| 122 |
+
HTTP_PATCH,
|
| 123 |
+
HTTP_PURGE,
|
| 124 |
+
HTTP_MKCALENDAR,
|
| 125 |
+
HTTP_LINK,
|
| 126 |
+
HTTP_UNLINK,
|
| 127 |
+
HTTP_SOURCE,
|
| 128 |
+
HTTP_PRI,
|
| 129 |
+
HTTP_DESCRIBE,
|
| 130 |
+
HTTP_ANNOUNCE,
|
| 131 |
+
HTTP_SETUP,
|
| 132 |
+
HTTP_PLAY,
|
| 133 |
+
HTTP_PAUSE,
|
| 134 |
+
HTTP_TEARDOWN,
|
| 135 |
+
HTTP_GET_PARAMETER,
|
| 136 |
+
HTTP_SET_PARAMETER,
|
| 137 |
+
HTTP_REDIRECT,
|
| 138 |
+
HTTP_RECORD,
|
| 139 |
+
HTTP_FLUSH
|
| 140 |
+
ctypedef llhttp_method llhttp_method_t
|
| 141 |
+
|
| 142 |
+
void llhttp_init(llhttp_t* parser, llhttp_type_t type, const llhttp_settings_t* settings)
|
| 143 |
+
|
| 144 |
+
void llhttp_settings_init(llhttp_settings_t* settings)
|
| 145 |
+
|
| 146 |
+
llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len)
|
| 147 |
+
|
| 148 |
+
void llhttp_resume_after_upgrade(llhttp_t* parser)
|
| 149 |
+
|
| 150 |
+
int llhttp_should_keep_alive(const llhttp_t* parser)
|
| 151 |
+
|
| 152 |
+
const char* llhttp_get_error_pos(const llhttp_t* parser)
|
| 153 |
+
const char* llhttp_get_error_reason(const llhttp_t* parser)
|
| 154 |
+
const char* llhttp_method_name(llhttp_method_t method)
|
| 155 |
+
|
| 156 |
+
void llhttp_set_error_reason(llhttp_t* parser, const char* reason);
|
| 157 |
+
|
| 158 |
+
void llhttp_set_lenient_headers(llhttp_t* parser, bint enabled);
|
| 159 |
+
void llhttp_set_lenient_chunked_length(llhttp_t* parser, bint enabled);
|
| 160 |
+
void llhttp_set_lenient_keep_alive(llhttp_t* parser, bint enabled);
|
| 161 |
+
void llhttp_set_lenient_transfer_encoding(llhttp_t* parser, bint enabled);
|
| 162 |
+
void llhttp_set_lenient_version(llhttp_t* parser, bint enabled);
|
| 163 |
+
void llhttp_set_lenient_data_after_close(llhttp_t* parser, bint enabled);
|
| 164 |
+
void llhttp_set_lenient_optional_lf_after_cr(llhttp_t* parser, bint enabled);
|
| 165 |
+
void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, bint enabled);
|
| 166 |
+
void llhttp_set_lenient_optional_crlf_after_chunk(llhttp_t* parser, bint enabled);
|
| 167 |
+
void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, bint enabled);
|
.venv/lib/python3.11/site-packages/httptools/parser/errors.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = ('HttpParserError',
|
| 2 |
+
'HttpParserCallbackError',
|
| 3 |
+
'HttpParserInvalidStatusError',
|
| 4 |
+
'HttpParserInvalidMethodError',
|
| 5 |
+
'HttpParserInvalidURLError',
|
| 6 |
+
'HttpParserUpgrade')
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class HttpParserError(Exception):
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class HttpParserCallbackError(HttpParserError):
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class HttpParserInvalidStatusError(HttpParserError):
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class HttpParserInvalidMethodError(HttpParserError):
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class HttpParserInvalidURLError(HttpParserError):
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class HttpParserUpgrade(Exception):
|
| 30 |
+
pass
|
.venv/lib/python3.11/site-packages/httptools/parser/parser.pyx
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#cython: language_level=3
|
| 2 |
+
|
| 3 |
+
from __future__ import print_function
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
| 7 |
+
from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \
|
| 8 |
+
Py_buffer, PyBytes_AsString
|
| 9 |
+
|
| 10 |
+
from .python cimport PyMemoryView_Check, PyMemoryView_GET_BUFFER
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
from .errors import (HttpParserError,
|
| 14 |
+
HttpParserCallbackError,
|
| 15 |
+
HttpParserInvalidStatusError,
|
| 16 |
+
HttpParserInvalidMethodError,
|
| 17 |
+
HttpParserInvalidURLError,
|
| 18 |
+
HttpParserUpgrade)
|
| 19 |
+
|
| 20 |
+
cimport cython
|
| 21 |
+
from . cimport cparser
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
__all__ = ('HttpRequestParser', 'HttpResponseParser')
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@cython.internal
|
| 28 |
+
cdef class HttpParser:
|
| 29 |
+
|
| 30 |
+
cdef:
|
| 31 |
+
cparser.llhttp_t* _cparser
|
| 32 |
+
cparser.llhttp_settings_t* _csettings
|
| 33 |
+
|
| 34 |
+
bytes _current_header_name
|
| 35 |
+
bytes _current_header_value
|
| 36 |
+
|
| 37 |
+
_proto_on_url, _proto_on_status, _proto_on_body, \
|
| 38 |
+
_proto_on_header, _proto_on_headers_complete, \
|
| 39 |
+
_proto_on_message_complete, _proto_on_chunk_header, \
|
| 40 |
+
_proto_on_chunk_complete, _proto_on_message_begin
|
| 41 |
+
|
| 42 |
+
object _last_error
|
| 43 |
+
|
| 44 |
+
Py_buffer py_buf
|
| 45 |
+
|
| 46 |
+
def __cinit__(self):
|
| 47 |
+
self._cparser = <cparser.llhttp_t*> \
|
| 48 |
+
PyMem_Malloc(sizeof(cparser.llhttp_t))
|
| 49 |
+
if self._cparser is NULL:
|
| 50 |
+
raise MemoryError()
|
| 51 |
+
|
| 52 |
+
self._csettings = <cparser.llhttp_settings_t*> \
|
| 53 |
+
PyMem_Malloc(sizeof(cparser.llhttp_settings_t))
|
| 54 |
+
if self._csettings is NULL:
|
| 55 |
+
raise MemoryError()
|
| 56 |
+
|
| 57 |
+
def __dealloc__(self):
|
| 58 |
+
PyMem_Free(self._cparser)
|
| 59 |
+
PyMem_Free(self._csettings)
|
| 60 |
+
|
| 61 |
+
cdef _init(self, protocol, cparser.llhttp_type_t mode):
|
| 62 |
+
cparser.llhttp_settings_init(self._csettings)
|
| 63 |
+
|
| 64 |
+
cparser.llhttp_init(self._cparser, mode, self._csettings)
|
| 65 |
+
self._cparser.data = <void*>self
|
| 66 |
+
|
| 67 |
+
self._current_header_name = None
|
| 68 |
+
self._current_header_value = None
|
| 69 |
+
|
| 70 |
+
self._proto_on_header = getattr(protocol, 'on_header', None)
|
| 71 |
+
if self._proto_on_header is not None:
|
| 72 |
+
self._csettings.on_header_field = cb_on_header_field
|
| 73 |
+
self._csettings.on_header_value = cb_on_header_value
|
| 74 |
+
self._proto_on_headers_complete = getattr(
|
| 75 |
+
protocol, 'on_headers_complete', None)
|
| 76 |
+
self._csettings.on_headers_complete = cb_on_headers_complete
|
| 77 |
+
|
| 78 |
+
self._proto_on_body = getattr(protocol, 'on_body', None)
|
| 79 |
+
if self._proto_on_body is not None:
|
| 80 |
+
self._csettings.on_body = cb_on_body
|
| 81 |
+
|
| 82 |
+
self._proto_on_message_begin = getattr(
|
| 83 |
+
protocol, 'on_message_begin', None)
|
| 84 |
+
if self._proto_on_message_begin is not None:
|
| 85 |
+
self._csettings.on_message_begin = cb_on_message_begin
|
| 86 |
+
|
| 87 |
+
self._proto_on_message_complete = getattr(
|
| 88 |
+
protocol, 'on_message_complete', None)
|
| 89 |
+
if self._proto_on_message_complete is not None:
|
| 90 |
+
self._csettings.on_message_complete = cb_on_message_complete
|
| 91 |
+
|
| 92 |
+
self._proto_on_chunk_header = getattr(
|
| 93 |
+
protocol, 'on_chunk_header', None)
|
| 94 |
+
self._csettings.on_chunk_header = cb_on_chunk_header
|
| 95 |
+
|
| 96 |
+
self._proto_on_chunk_complete = getattr(
|
| 97 |
+
protocol, 'on_chunk_complete', None)
|
| 98 |
+
self._csettings.on_chunk_complete = cb_on_chunk_complete
|
| 99 |
+
|
| 100 |
+
self._last_error = None
|
| 101 |
+
|
| 102 |
+
cdef _maybe_call_on_header(self):
|
| 103 |
+
if self._current_header_value is not None:
|
| 104 |
+
current_header_name = self._current_header_name
|
| 105 |
+
current_header_value = self._current_header_value
|
| 106 |
+
|
| 107 |
+
self._current_header_name = self._current_header_value = None
|
| 108 |
+
|
| 109 |
+
if self._proto_on_header is not None:
|
| 110 |
+
self._proto_on_header(current_header_name,
|
| 111 |
+
current_header_value)
|
| 112 |
+
|
| 113 |
+
cdef _on_header_field(self, bytes field):
|
| 114 |
+
self._maybe_call_on_header()
|
| 115 |
+
if self._current_header_name is None:
|
| 116 |
+
self._current_header_name = field
|
| 117 |
+
else:
|
| 118 |
+
self._current_header_name += field
|
| 119 |
+
|
| 120 |
+
cdef _on_header_value(self, bytes val):
|
| 121 |
+
if self._current_header_value is None:
|
| 122 |
+
self._current_header_value = val
|
| 123 |
+
else:
|
| 124 |
+
# This is unlikely, as mostly HTTP headers are one-line
|
| 125 |
+
self._current_header_value += val
|
| 126 |
+
|
| 127 |
+
cdef _on_headers_complete(self):
|
| 128 |
+
self._maybe_call_on_header()
|
| 129 |
+
|
| 130 |
+
if self._proto_on_headers_complete is not None:
|
| 131 |
+
self._proto_on_headers_complete()
|
| 132 |
+
|
| 133 |
+
cdef _on_chunk_header(self):
|
| 134 |
+
if (self._current_header_value is not None or
|
| 135 |
+
self._current_header_name is not None):
|
| 136 |
+
raise HttpParserError('invalid headers state')
|
| 137 |
+
|
| 138 |
+
if self._proto_on_chunk_header is not None:
|
| 139 |
+
self._proto_on_chunk_header()
|
| 140 |
+
|
| 141 |
+
cdef _on_chunk_complete(self):
|
| 142 |
+
self._maybe_call_on_header()
|
| 143 |
+
|
| 144 |
+
if self._proto_on_chunk_complete is not None:
|
| 145 |
+
self._proto_on_chunk_complete()
|
| 146 |
+
|
| 147 |
+
### Public API ###
|
| 148 |
+
|
| 149 |
+
def set_dangerous_leniencies(
|
| 150 |
+
self,
|
| 151 |
+
lenient_headers: Optional[bool] = None,
|
| 152 |
+
lenient_chunked_length: Optional[bool] = None,
|
| 153 |
+
lenient_keep_alive: Optional[bool] = None,
|
| 154 |
+
lenient_transfer_encoding: Optional[bool] = None,
|
| 155 |
+
lenient_version: Optional[bool] = None,
|
| 156 |
+
lenient_data_after_close: Optional[bool] = None,
|
| 157 |
+
lenient_optional_lf_after_cr: Optional[bool] = None,
|
| 158 |
+
lenient_optional_cr_before_lf: Optional[bool] = None,
|
| 159 |
+
lenient_optional_crlf_after_chunk: Optional[bool] = None,
|
| 160 |
+
lenient_spaces_after_chunk_size: Optional[bool] = None,
|
| 161 |
+
):
|
| 162 |
+
cdef cparser.llhttp_t* parser = self._cparser
|
| 163 |
+
if lenient_headers is not None:
|
| 164 |
+
cparser.llhttp_set_lenient_headers(
|
| 165 |
+
parser, lenient_headers)
|
| 166 |
+
if lenient_chunked_length is not None:
|
| 167 |
+
cparser.llhttp_set_lenient_chunked_length(
|
| 168 |
+
parser, lenient_chunked_length)
|
| 169 |
+
if lenient_keep_alive is not None:
|
| 170 |
+
cparser.llhttp_set_lenient_keep_alive(
|
| 171 |
+
parser, lenient_keep_alive)
|
| 172 |
+
if lenient_transfer_encoding is not None:
|
| 173 |
+
cparser.llhttp_set_lenient_transfer_encoding(
|
| 174 |
+
parser, lenient_transfer_encoding)
|
| 175 |
+
if lenient_version is not None:
|
| 176 |
+
cparser.llhttp_set_lenient_version(
|
| 177 |
+
parser, lenient_version)
|
| 178 |
+
if lenient_data_after_close is not None:
|
| 179 |
+
cparser.llhttp_set_lenient_data_after_close(
|
| 180 |
+
parser, lenient_data_after_close)
|
| 181 |
+
if lenient_optional_lf_after_cr is not None:
|
| 182 |
+
cparser.llhttp_set_lenient_optional_lf_after_cr(
|
| 183 |
+
parser, lenient_optional_lf_after_cr)
|
| 184 |
+
if lenient_optional_cr_before_lf is not None:
|
| 185 |
+
cparser.llhttp_set_lenient_optional_cr_before_lf(
|
| 186 |
+
parser, lenient_optional_cr_before_lf)
|
| 187 |
+
if lenient_optional_crlf_after_chunk is not None:
|
| 188 |
+
cparser.llhttp_set_lenient_optional_crlf_after_chunk(
|
| 189 |
+
parser, lenient_optional_crlf_after_chunk)
|
| 190 |
+
if lenient_spaces_after_chunk_size is not None:
|
| 191 |
+
cparser.llhttp_set_lenient_spaces_after_chunk_size(
|
| 192 |
+
parser, lenient_spaces_after_chunk_size)
|
| 193 |
+
|
| 194 |
+
def get_http_version(self):
|
| 195 |
+
cdef cparser.llhttp_t* parser = self._cparser
|
| 196 |
+
return '{}.{}'.format(parser.http_major, parser.http_minor)
|
| 197 |
+
|
| 198 |
+
def should_keep_alive(self):
|
| 199 |
+
return bool(cparser.llhttp_should_keep_alive(self._cparser))
|
| 200 |
+
|
| 201 |
+
def should_upgrade(self):
|
| 202 |
+
cdef cparser.llhttp_t* parser = self._cparser
|
| 203 |
+
return bool(parser.upgrade)
|
| 204 |
+
|
| 205 |
+
def feed_data(self, data):
|
| 206 |
+
cdef:
|
| 207 |
+
size_t data_len
|
| 208 |
+
cparser.llhttp_errno_t err
|
| 209 |
+
Py_buffer *buf
|
| 210 |
+
bint owning_buf = False
|
| 211 |
+
const char* err_pos
|
| 212 |
+
|
| 213 |
+
if PyMemoryView_Check(data):
|
| 214 |
+
buf = PyMemoryView_GET_BUFFER(data)
|
| 215 |
+
data_len = <size_t>buf.len
|
| 216 |
+
err = cparser.llhttp_execute(
|
| 217 |
+
self._cparser,
|
| 218 |
+
<char*>buf.buf,
|
| 219 |
+
data_len)
|
| 220 |
+
|
| 221 |
+
else:
|
| 222 |
+
buf = &self.py_buf
|
| 223 |
+
PyObject_GetBuffer(data, buf, PyBUF_SIMPLE)
|
| 224 |
+
owning_buf = True
|
| 225 |
+
data_len = <size_t>buf.len
|
| 226 |
+
|
| 227 |
+
err = cparser.llhttp_execute(
|
| 228 |
+
self._cparser,
|
| 229 |
+
<char*>buf.buf,
|
| 230 |
+
data_len)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
if self._cparser.upgrade == 1 and err == cparser.HPE_PAUSED_UPGRADE:
|
| 234 |
+
err_pos = cparser.llhttp_get_error_pos(self._cparser)
|
| 235 |
+
|
| 236 |
+
# Immediately free the parser from "error" state, simulating
|
| 237 |
+
# http-parser behavior here because 1) we never had the API to
|
| 238 |
+
# allow users manually "resume after upgrade", and 2) the use
|
| 239 |
+
# case for resuming parsing is very rare.
|
| 240 |
+
cparser.llhttp_resume_after_upgrade(self._cparser)
|
| 241 |
+
|
| 242 |
+
# The err_pos here is specific for the input buf. So if we ever
|
| 243 |
+
# switch to the llhttp behavior (re-raise HttpParserUpgrade for
|
| 244 |
+
# successive calls to feed_data() until resume_after_upgrade is
|
| 245 |
+
# called), we have to store the result and keep our own state.
|
| 246 |
+
raise HttpParserUpgrade(err_pos - <char*>buf.buf)
|
| 247 |
+
finally:
|
| 248 |
+
if owning_buf:
|
| 249 |
+
PyBuffer_Release(buf)
|
| 250 |
+
|
| 251 |
+
if err != cparser.HPE_OK:
|
| 252 |
+
ex = parser_error_from_errno(
|
| 253 |
+
self._cparser,
|
| 254 |
+
<cparser.llhttp_errno_t> self._cparser.error)
|
| 255 |
+
if isinstance(ex, HttpParserCallbackError):
|
| 256 |
+
if self._last_error is not None:
|
| 257 |
+
ex.__context__ = self._last_error
|
| 258 |
+
self._last_error = None
|
| 259 |
+
raise ex
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
cdef class HttpRequestParser(HttpParser):
|
| 263 |
+
|
| 264 |
+
def __init__(self, protocol):
|
| 265 |
+
self._init(protocol, cparser.HTTP_REQUEST)
|
| 266 |
+
|
| 267 |
+
self._proto_on_url = getattr(protocol, 'on_url', None)
|
| 268 |
+
if self._proto_on_url is not None:
|
| 269 |
+
self._csettings.on_url = cb_on_url
|
| 270 |
+
|
| 271 |
+
def get_method(self):
|
| 272 |
+
cdef cparser.llhttp_t* parser = self._cparser
|
| 273 |
+
return cparser.llhttp_method_name(<cparser.llhttp_method_t> parser.method)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
cdef class HttpResponseParser(HttpParser):
|
| 277 |
+
|
| 278 |
+
def __init__(self, protocol):
|
| 279 |
+
self._init(protocol, cparser.HTTP_RESPONSE)
|
| 280 |
+
|
| 281 |
+
self._proto_on_status = getattr(protocol, 'on_status', None)
|
| 282 |
+
if self._proto_on_status is not None:
|
| 283 |
+
self._csettings.on_status = cb_on_status
|
| 284 |
+
|
| 285 |
+
def get_status_code(self):
|
| 286 |
+
cdef cparser.llhttp_t* parser = self._cparser
|
| 287 |
+
return parser.status_code
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1:
|
| 291 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 292 |
+
try:
|
| 293 |
+
pyparser._proto_on_message_begin()
|
| 294 |
+
except BaseException as ex:
|
| 295 |
+
pyparser._last_error = ex
|
| 296 |
+
return -1
|
| 297 |
+
else:
|
| 298 |
+
return 0
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
cdef int cb_on_url(cparser.llhttp_t* parser,
|
| 302 |
+
const char *at, size_t length) except -1:
|
| 303 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 304 |
+
try:
|
| 305 |
+
pyparser._proto_on_url(at[:length])
|
| 306 |
+
except BaseException as ex:
|
| 307 |
+
cparser.llhttp_set_error_reason(parser, "`on_url` callback error")
|
| 308 |
+
pyparser._last_error = ex
|
| 309 |
+
return cparser.HPE_USER
|
| 310 |
+
else:
|
| 311 |
+
return 0
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
cdef int cb_on_status(cparser.llhttp_t* parser,
|
| 315 |
+
const char *at, size_t length) except -1:
|
| 316 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 317 |
+
try:
|
| 318 |
+
pyparser._proto_on_status(at[:length])
|
| 319 |
+
except BaseException as ex:
|
| 320 |
+
cparser.llhttp_set_error_reason(parser, "`on_status` callback error")
|
| 321 |
+
pyparser._last_error = ex
|
| 322 |
+
return cparser.HPE_USER
|
| 323 |
+
else:
|
| 324 |
+
return 0
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
cdef int cb_on_header_field(cparser.llhttp_t* parser,
|
| 328 |
+
const char *at, size_t length) except -1:
|
| 329 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 330 |
+
try:
|
| 331 |
+
pyparser._on_header_field(at[:length])
|
| 332 |
+
except BaseException as ex:
|
| 333 |
+
cparser.llhttp_set_error_reason(parser, "`on_header_field` callback error")
|
| 334 |
+
pyparser._last_error = ex
|
| 335 |
+
return cparser.HPE_USER
|
| 336 |
+
else:
|
| 337 |
+
return 0
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
cdef int cb_on_header_value(cparser.llhttp_t* parser,
|
| 341 |
+
const char *at, size_t length) except -1:
|
| 342 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 343 |
+
try:
|
| 344 |
+
pyparser._on_header_value(at[:length])
|
| 345 |
+
except BaseException as ex:
|
| 346 |
+
cparser.llhttp_set_error_reason(parser, "`on_header_value` callback error")
|
| 347 |
+
pyparser._last_error = ex
|
| 348 |
+
return cparser.HPE_USER
|
| 349 |
+
else:
|
| 350 |
+
return 0
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1:
|
| 354 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 355 |
+
try:
|
| 356 |
+
pyparser._on_headers_complete()
|
| 357 |
+
except BaseException as ex:
|
| 358 |
+
pyparser._last_error = ex
|
| 359 |
+
return -1
|
| 360 |
+
else:
|
| 361 |
+
if pyparser._cparser.upgrade:
|
| 362 |
+
return 1
|
| 363 |
+
else:
|
| 364 |
+
return 0
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
cdef int cb_on_body(cparser.llhttp_t* parser,
|
| 368 |
+
const char *at, size_t length) except -1:
|
| 369 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 370 |
+
try:
|
| 371 |
+
pyparser._proto_on_body(at[:length])
|
| 372 |
+
except BaseException as ex:
|
| 373 |
+
cparser.llhttp_set_error_reason(parser, "`on_body` callback error")
|
| 374 |
+
pyparser._last_error = ex
|
| 375 |
+
return cparser.HPE_USER
|
| 376 |
+
else:
|
| 377 |
+
return 0
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1:
|
| 381 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 382 |
+
try:
|
| 383 |
+
pyparser._proto_on_message_complete()
|
| 384 |
+
except BaseException as ex:
|
| 385 |
+
pyparser._last_error = ex
|
| 386 |
+
return -1
|
| 387 |
+
else:
|
| 388 |
+
return 0
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
cdef int cb_on_chunk_header(cparser.llhttp_t* parser) except -1:
|
| 392 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 393 |
+
try:
|
| 394 |
+
pyparser._on_chunk_header()
|
| 395 |
+
except BaseException as ex:
|
| 396 |
+
pyparser._last_error = ex
|
| 397 |
+
return -1
|
| 398 |
+
else:
|
| 399 |
+
return 0
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
cdef int cb_on_chunk_complete(cparser.llhttp_t* parser) except -1:
|
| 403 |
+
cdef HttpParser pyparser = <HttpParser>parser.data
|
| 404 |
+
try:
|
| 405 |
+
pyparser._on_chunk_complete()
|
| 406 |
+
except BaseException as ex:
|
| 407 |
+
pyparser._last_error = ex
|
| 408 |
+
return -1
|
| 409 |
+
else:
|
| 410 |
+
return 0
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
cdef parser_error_from_errno(cparser.llhttp_t* parser, cparser.llhttp_errno_t errno):
|
| 414 |
+
cdef bytes reason = cparser.llhttp_get_error_reason(parser)
|
| 415 |
+
|
| 416 |
+
if errno in (cparser.HPE_CB_MESSAGE_BEGIN,
|
| 417 |
+
cparser.HPE_CB_HEADERS_COMPLETE,
|
| 418 |
+
cparser.HPE_CB_MESSAGE_COMPLETE,
|
| 419 |
+
cparser.HPE_CB_CHUNK_HEADER,
|
| 420 |
+
cparser.HPE_CB_CHUNK_COMPLETE,
|
| 421 |
+
cparser.HPE_USER):
|
| 422 |
+
cls = HttpParserCallbackError
|
| 423 |
+
|
| 424 |
+
elif errno == cparser.HPE_INVALID_STATUS:
|
| 425 |
+
cls = HttpParserInvalidStatusError
|
| 426 |
+
|
| 427 |
+
elif errno == cparser.HPE_INVALID_METHOD:
|
| 428 |
+
cls = HttpParserInvalidMethodError
|
| 429 |
+
|
| 430 |
+
elif errno == cparser.HPE_INVALID_URL:
|
| 431 |
+
cls = HttpParserInvalidURLError
|
| 432 |
+
|
| 433 |
+
else:
|
| 434 |
+
cls = HttpParserError
|
| 435 |
+
|
| 436 |
+
return cls(reason.decode('latin-1'))
|
.venv/lib/python3.11/site-packages/httptools/parser/python.pxd
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cimport cpython
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
cdef extern from "Python.h":
|
| 5 |
+
cpython.Py_buffer* PyMemoryView_GET_BUFFER(object)
|
| 6 |
+
bint PyMemoryView_Check(object)
|
.venv/lib/python3.11/site-packages/httptools/parser/url_cparser.pxd
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from libc.stdint cimport uint16_t
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
cdef extern from "http_parser.h":
|
| 5 |
+
# URL Parser
|
| 6 |
+
|
| 7 |
+
enum http_parser_url_fields:
|
| 8 |
+
UF_SCHEMA = 0,
|
| 9 |
+
UF_HOST = 1,
|
| 10 |
+
UF_PORT = 2,
|
| 11 |
+
UF_PATH = 3,
|
| 12 |
+
UF_QUERY = 4,
|
| 13 |
+
UF_FRAGMENT = 5,
|
| 14 |
+
UF_USERINFO = 6,
|
| 15 |
+
UF_MAX = 7
|
| 16 |
+
|
| 17 |
+
struct http_parser_url_field_data:
|
| 18 |
+
uint16_t off
|
| 19 |
+
uint16_t len
|
| 20 |
+
|
| 21 |
+
struct http_parser_url:
|
| 22 |
+
uint16_t field_set
|
| 23 |
+
uint16_t port
|
| 24 |
+
http_parser_url_field_data[<int>UF_MAX] field_data
|
| 25 |
+
|
| 26 |
+
void http_parser_url_init(http_parser_url *u)
|
| 27 |
+
|
| 28 |
+
int http_parser_parse_url(const char *buf,
|
| 29 |
+
size_t buflen,
|
| 30 |
+
int is_connect,
|
| 31 |
+
http_parser_url *u)
|
.venv/lib/python3.11/site-packages/httptools/parser/url_parser.pyx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#cython: language_level=3
|
| 2 |
+
|
| 3 |
+
from __future__ import print_function
|
| 4 |
+
from cpython.mem cimport PyMem_Malloc, PyMem_Free
|
| 5 |
+
from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \
|
| 6 |
+
Py_buffer
|
| 7 |
+
|
| 8 |
+
from .errors import HttpParserInvalidURLError
|
| 9 |
+
|
| 10 |
+
cimport cython
|
| 11 |
+
from . cimport url_cparser as uparser
|
| 12 |
+
|
| 13 |
+
__all__ = ('parse_url',)
|
| 14 |
+
|
| 15 |
+
@cython.freelist(250)
|
| 16 |
+
cdef class URL:
|
| 17 |
+
cdef readonly bytes schema
|
| 18 |
+
cdef readonly bytes host
|
| 19 |
+
cdef readonly object port
|
| 20 |
+
cdef readonly bytes path
|
| 21 |
+
cdef readonly bytes query
|
| 22 |
+
cdef readonly bytes fragment
|
| 23 |
+
cdef readonly bytes userinfo
|
| 24 |
+
|
| 25 |
+
def __cinit__(self, bytes schema, bytes host, object port, bytes path,
|
| 26 |
+
bytes query, bytes fragment, bytes userinfo):
|
| 27 |
+
|
| 28 |
+
self.schema = schema
|
| 29 |
+
self.host = host
|
| 30 |
+
self.port = port
|
| 31 |
+
self.path = path
|
| 32 |
+
self.query = query
|
| 33 |
+
self.fragment = fragment
|
| 34 |
+
self.userinfo = userinfo
|
| 35 |
+
|
| 36 |
+
def __repr__(self):
|
| 37 |
+
return ('<URL schema: {!r}, host: {!r}, port: {!r}, path: {!r}, '
|
| 38 |
+
'query: {!r}, fragment: {!r}, userinfo: {!r}>'
|
| 39 |
+
.format(self.schema, self.host, self.port, self.path,
|
| 40 |
+
self.query, self.fragment, self.userinfo))
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def parse_url(url):
|
| 44 |
+
cdef:
|
| 45 |
+
Py_buffer py_buf
|
| 46 |
+
char* buf_data
|
| 47 |
+
uparser.http_parser_url* parsed
|
| 48 |
+
int res
|
| 49 |
+
bytes schema = None
|
| 50 |
+
bytes host = None
|
| 51 |
+
object port = None
|
| 52 |
+
bytes path = None
|
| 53 |
+
bytes query = None
|
| 54 |
+
bytes fragment = None
|
| 55 |
+
bytes userinfo = None
|
| 56 |
+
object result = None
|
| 57 |
+
int off
|
| 58 |
+
int ln
|
| 59 |
+
|
| 60 |
+
parsed = <uparser.http_parser_url*> \
|
| 61 |
+
PyMem_Malloc(sizeof(uparser.http_parser_url))
|
| 62 |
+
uparser.http_parser_url_init(parsed)
|
| 63 |
+
|
| 64 |
+
PyObject_GetBuffer(url, &py_buf, PyBUF_SIMPLE)
|
| 65 |
+
try:
|
| 66 |
+
buf_data = <char*>py_buf.buf
|
| 67 |
+
res = uparser.http_parser_parse_url(buf_data, py_buf.len, 0, parsed)
|
| 68 |
+
|
| 69 |
+
if res == 0:
|
| 70 |
+
if parsed.field_set & (1 << uparser.UF_SCHEMA):
|
| 71 |
+
off = parsed.field_data[<int>uparser.UF_SCHEMA].off
|
| 72 |
+
ln = parsed.field_data[<int>uparser.UF_SCHEMA].len
|
| 73 |
+
schema = buf_data[off:off+ln]
|
| 74 |
+
|
| 75 |
+
if parsed.field_set & (1 << uparser.UF_HOST):
|
| 76 |
+
off = parsed.field_data[<int>uparser.UF_HOST].off
|
| 77 |
+
ln = parsed.field_data[<int>uparser.UF_HOST].len
|
| 78 |
+
host = buf_data[off:off+ln]
|
| 79 |
+
|
| 80 |
+
if parsed.field_set & (1 << uparser.UF_PORT):
|
| 81 |
+
port = parsed.port
|
| 82 |
+
|
| 83 |
+
if parsed.field_set & (1 << uparser.UF_PATH):
|
| 84 |
+
off = parsed.field_data[<int>uparser.UF_PATH].off
|
| 85 |
+
ln = parsed.field_data[<int>uparser.UF_PATH].len
|
| 86 |
+
path = buf_data[off:off+ln]
|
| 87 |
+
|
| 88 |
+
if parsed.field_set & (1 << uparser.UF_QUERY):
|
| 89 |
+
off = parsed.field_data[<int>uparser.UF_QUERY].off
|
| 90 |
+
ln = parsed.field_data[<int>uparser.UF_QUERY].len
|
| 91 |
+
query = buf_data[off:off+ln]
|
| 92 |
+
|
| 93 |
+
if parsed.field_set & (1 << uparser.UF_FRAGMENT):
|
| 94 |
+
off = parsed.field_data[<int>uparser.UF_FRAGMENT].off
|
| 95 |
+
ln = parsed.field_data[<int>uparser.UF_FRAGMENT].len
|
| 96 |
+
fragment = buf_data[off:off+ln]
|
| 97 |
+
|
| 98 |
+
if parsed.field_set & (1 << uparser.UF_USERINFO):
|
| 99 |
+
off = parsed.field_data[<int>uparser.UF_USERINFO].off
|
| 100 |
+
ln = parsed.field_data[<int>uparser.UF_USERINFO].len
|
| 101 |
+
userinfo = buf_data[off:off+ln]
|
| 102 |
+
|
| 103 |
+
return URL(schema, host, port, path, query, fragment, userinfo)
|
| 104 |
+
else:
|
| 105 |
+
raise HttpParserInvalidURLError("invalid url {!r}".format(url))
|
| 106 |
+
finally:
|
| 107 |
+
PyBuffer_Release(&py_buf)
|
| 108 |
+
PyMem_Free(parsed)
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__init__.py
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (193 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test__init.cpython-311.pyc
ADDED
|
Binary file (1.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_backends.cpython-311.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_config.cpython-311.pyc
ADDED
|
Binary file (19.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_decorators.cpython-311.pyc
ADDED
|
Binary file (36.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_heaps.cpython-311.pyc
ADDED
|
Binary file (6.07 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_mapped_queue.cpython-311.pyc
ADDED
|
Binary file (17.1 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_misc.cpython-311.pyc
ADDED
|
Binary file (19.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_random_sequence.cpython-311.pyc
ADDED
|
Binary file (2.26 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_rcm.cpython-311.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/__pycache__/test_unionfind.cpython-311.pyc
ADDED
|
Binary file (3.01 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/test_config.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils.configs import BackendPriorities, Config
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Define this at module level so we can test pickling
|
| 11 |
+
class ExampleConfig(Config):
|
| 12 |
+
"""Example configuration."""
|
| 13 |
+
|
| 14 |
+
x: int
|
| 15 |
+
y: str
|
| 16 |
+
|
| 17 |
+
def _on_setattr(self, key, value):
|
| 18 |
+
if key == "x" and value <= 0:
|
| 19 |
+
raise ValueError("x must be positive")
|
| 20 |
+
if key == "y" and not isinstance(value, str):
|
| 21 |
+
raise TypeError("y must be a str")
|
| 22 |
+
return value
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class EmptyConfig(Config):
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.mark.parametrize("cfg", [EmptyConfig(), Config()])
|
| 30 |
+
def test_config_empty(cfg):
|
| 31 |
+
assert dir(cfg) == []
|
| 32 |
+
with pytest.raises(AttributeError):
|
| 33 |
+
cfg.x = 1
|
| 34 |
+
with pytest.raises(KeyError):
|
| 35 |
+
cfg["x"] = 1
|
| 36 |
+
with pytest.raises(AttributeError):
|
| 37 |
+
cfg.x
|
| 38 |
+
with pytest.raises(KeyError):
|
| 39 |
+
cfg["x"]
|
| 40 |
+
assert len(cfg) == 0
|
| 41 |
+
assert "x" not in cfg
|
| 42 |
+
assert cfg == cfg
|
| 43 |
+
assert cfg.get("x", 2) == 2
|
| 44 |
+
assert set(cfg.keys()) == set()
|
| 45 |
+
assert set(cfg.values()) == set()
|
| 46 |
+
assert set(cfg.items()) == set()
|
| 47 |
+
cfg2 = pickle.loads(pickle.dumps(cfg))
|
| 48 |
+
assert cfg == cfg2
|
| 49 |
+
assert isinstance(cfg, collections.abc.Collection)
|
| 50 |
+
assert isinstance(cfg, collections.abc.Mapping)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_config_subclass():
|
| 54 |
+
with pytest.raises(TypeError, match="missing 2 required keyword-only"):
|
| 55 |
+
ExampleConfig()
|
| 56 |
+
with pytest.raises(ValueError, match="x must be positive"):
|
| 57 |
+
ExampleConfig(x=0, y="foo")
|
| 58 |
+
with pytest.raises(TypeError, match="unexpected keyword"):
|
| 59 |
+
ExampleConfig(x=1, y="foo", z="bad config")
|
| 60 |
+
with pytest.raises(TypeError, match="unexpected keyword"):
|
| 61 |
+
EmptyConfig(z="bad config")
|
| 62 |
+
cfg = ExampleConfig(x=1, y="foo")
|
| 63 |
+
assert cfg.x == 1
|
| 64 |
+
assert cfg["x"] == 1
|
| 65 |
+
assert cfg["y"] == "foo"
|
| 66 |
+
assert cfg.y == "foo"
|
| 67 |
+
assert "x" in cfg
|
| 68 |
+
assert "y" in cfg
|
| 69 |
+
assert "z" not in cfg
|
| 70 |
+
assert len(cfg) == 2
|
| 71 |
+
assert set(iter(cfg)) == {"x", "y"}
|
| 72 |
+
assert set(cfg.keys()) == {"x", "y"}
|
| 73 |
+
assert set(cfg.values()) == {1, "foo"}
|
| 74 |
+
assert set(cfg.items()) == {("x", 1), ("y", "foo")}
|
| 75 |
+
assert dir(cfg) == ["x", "y"]
|
| 76 |
+
cfg.x = 2
|
| 77 |
+
cfg["y"] = "bar"
|
| 78 |
+
assert cfg["x"] == 2
|
| 79 |
+
assert cfg.y == "bar"
|
| 80 |
+
with pytest.raises(TypeError, match="can't be deleted"):
|
| 81 |
+
del cfg.x
|
| 82 |
+
with pytest.raises(TypeError, match="can't be deleted"):
|
| 83 |
+
del cfg["y"]
|
| 84 |
+
assert cfg.x == 2
|
| 85 |
+
assert cfg == cfg
|
| 86 |
+
assert cfg == ExampleConfig(x=2, y="bar")
|
| 87 |
+
assert cfg != ExampleConfig(x=3, y="baz")
|
| 88 |
+
assert cfg != Config(x=2, y="bar")
|
| 89 |
+
with pytest.raises(TypeError, match="y must be a str"):
|
| 90 |
+
cfg["y"] = 5
|
| 91 |
+
with pytest.raises(ValueError, match="x must be positive"):
|
| 92 |
+
cfg.x = -5
|
| 93 |
+
assert cfg.get("x", 10) == 2
|
| 94 |
+
with pytest.raises(AttributeError):
|
| 95 |
+
cfg.z = 5
|
| 96 |
+
with pytest.raises(KeyError):
|
| 97 |
+
cfg["z"] = 5
|
| 98 |
+
with pytest.raises(AttributeError):
|
| 99 |
+
cfg.z
|
| 100 |
+
with pytest.raises(KeyError):
|
| 101 |
+
cfg["z"]
|
| 102 |
+
cfg2 = pickle.loads(pickle.dumps(cfg))
|
| 103 |
+
assert cfg == cfg2
|
| 104 |
+
assert cfg.__doc__ == "Example configuration."
|
| 105 |
+
assert cfg2.__doc__ == "Example configuration."
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def test_config_defaults():
|
| 109 |
+
class DefaultConfig(Config):
|
| 110 |
+
x: int = 0
|
| 111 |
+
y: int
|
| 112 |
+
|
| 113 |
+
cfg = DefaultConfig(y=1)
|
| 114 |
+
assert cfg.x == 0
|
| 115 |
+
cfg = DefaultConfig(x=2, y=1)
|
| 116 |
+
assert cfg.x == 2
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_nxconfig():
|
| 120 |
+
assert isinstance(nx.config.backend_priority, BackendPriorities)
|
| 121 |
+
assert isinstance(nx.config.backend_priority.algos, list)
|
| 122 |
+
assert isinstance(nx.config.backends, Config)
|
| 123 |
+
with pytest.raises(TypeError, match="must be a list of backend names"):
|
| 124 |
+
nx.config.backend_priority.algos = "nx_loopback"
|
| 125 |
+
with pytest.raises(ValueError, match="Unknown backend when setting"):
|
| 126 |
+
nx.config.backend_priority.algos = ["this_almost_certainly_is_not_a_backend"]
|
| 127 |
+
with pytest.raises(TypeError, match="must be a Config of backend configs"):
|
| 128 |
+
nx.config.backends = {}
|
| 129 |
+
with pytest.raises(TypeError, match="must be a Config of backend configs"):
|
| 130 |
+
nx.config.backends = Config(plausible_backend_name={})
|
| 131 |
+
with pytest.raises(ValueError, match="Unknown backend when setting"):
|
| 132 |
+
nx.config.backends = Config(this_almost_certainly_is_not_a_backend=Config())
|
| 133 |
+
with pytest.raises(TypeError, match="must be True or False"):
|
| 134 |
+
nx.config.cache_converted_graphs = "bad value"
|
| 135 |
+
with pytest.raises(TypeError, match="must be a set of "):
|
| 136 |
+
nx.config.warnings_to_ignore = 7
|
| 137 |
+
with pytest.raises(ValueError, match="Unknown warning "):
|
| 138 |
+
nx.config.warnings_to_ignore = {"bad value"}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def test_not_strict():
|
| 142 |
+
class FlexibleConfig(Config, strict=False):
|
| 143 |
+
x: int
|
| 144 |
+
|
| 145 |
+
cfg = FlexibleConfig(x=1)
|
| 146 |
+
assert "_strict" not in cfg
|
| 147 |
+
assert len(cfg) == 1
|
| 148 |
+
assert list(cfg) == ["x"]
|
| 149 |
+
assert list(cfg.keys()) == ["x"]
|
| 150 |
+
assert list(cfg.values()) == [1]
|
| 151 |
+
assert list(cfg.items()) == [("x", 1)]
|
| 152 |
+
assert cfg.x == 1
|
| 153 |
+
assert cfg["x"] == 1
|
| 154 |
+
assert "x" in cfg
|
| 155 |
+
assert hasattr(cfg, "x")
|
| 156 |
+
assert "FlexibleConfig(x=1)" in repr(cfg)
|
| 157 |
+
assert cfg == FlexibleConfig(x=1)
|
| 158 |
+
del cfg.x
|
| 159 |
+
assert "FlexibleConfig()" in repr(cfg)
|
| 160 |
+
assert len(cfg) == 0
|
| 161 |
+
assert not hasattr(cfg, "x")
|
| 162 |
+
assert "x" not in cfg
|
| 163 |
+
assert not hasattr(cfg, "y")
|
| 164 |
+
assert "y" not in cfg
|
| 165 |
+
cfg.y = 2
|
| 166 |
+
assert len(cfg) == 1
|
| 167 |
+
assert list(cfg) == ["y"]
|
| 168 |
+
assert list(cfg.keys()) == ["y"]
|
| 169 |
+
assert list(cfg.values()) == [2]
|
| 170 |
+
assert list(cfg.items()) == [("y", 2)]
|
| 171 |
+
assert cfg.y == 2
|
| 172 |
+
assert cfg["y"] == 2
|
| 173 |
+
assert hasattr(cfg, "y")
|
| 174 |
+
assert "y" in cfg
|
| 175 |
+
del cfg["y"]
|
| 176 |
+
assert len(cfg) == 0
|
| 177 |
+
assert list(cfg) == []
|
| 178 |
+
with pytest.raises(AttributeError, match="y"):
|
| 179 |
+
del cfg.y
|
| 180 |
+
with pytest.raises(KeyError, match="y"):
|
| 181 |
+
del cfg["y"]
|
| 182 |
+
with pytest.raises(TypeError, match="missing 1 required keyword-only"):
|
| 183 |
+
FlexibleConfig()
|
| 184 |
+
# Be strict when first creating the config object
|
| 185 |
+
with pytest.raises(TypeError, match="unexpected keyword argument 'y'"):
|
| 186 |
+
FlexibleConfig(x=1, y=2)
|
| 187 |
+
|
| 188 |
+
class FlexibleConfigWithDefault(Config, strict=False):
|
| 189 |
+
x: int = 0
|
| 190 |
+
|
| 191 |
+
assert FlexibleConfigWithDefault().x == 0
|
| 192 |
+
assert FlexibleConfigWithDefault(x=1)["x"] == 1
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def test_context():
|
| 196 |
+
cfg = Config(x=1)
|
| 197 |
+
with cfg(x=2) as c:
|
| 198 |
+
assert c.x == 2
|
| 199 |
+
c.x = 3
|
| 200 |
+
assert cfg.x == 3
|
| 201 |
+
assert cfg.x == 1
|
| 202 |
+
|
| 203 |
+
with cfg(x=2) as c:
|
| 204 |
+
assert c == cfg
|
| 205 |
+
assert cfg.x == 2
|
| 206 |
+
with cfg(x=3) as c2:
|
| 207 |
+
assert c2 == cfg
|
| 208 |
+
assert cfg.x == 3
|
| 209 |
+
with pytest.raises(RuntimeError, match="context manager without"):
|
| 210 |
+
with cfg as c3: # Forgot to call `cfg(...)`
|
| 211 |
+
pass
|
| 212 |
+
assert cfg.x == 3
|
| 213 |
+
assert cfg.x == 2
|
| 214 |
+
assert cfg.x == 1
|
| 215 |
+
|
| 216 |
+
c = cfg(x=4) # Not yet as context (not recommended, but possible)
|
| 217 |
+
assert c == cfg
|
| 218 |
+
assert cfg.x == 4
|
| 219 |
+
# Cheat by looking at internal data; context stack should only grow with __enter__
|
| 220 |
+
assert cfg._prev is not None
|
| 221 |
+
assert cfg._context_stack == []
|
| 222 |
+
with c:
|
| 223 |
+
assert c == cfg
|
| 224 |
+
assert cfg.x == 4
|
| 225 |
+
assert cfg.x == 1
|
| 226 |
+
# Cheat again; there was no preceding `cfg(...)` call this time
|
| 227 |
+
assert cfg._prev is None
|
| 228 |
+
with pytest.raises(RuntimeError, match="context manager without"):
|
| 229 |
+
with cfg:
|
| 230 |
+
pass
|
| 231 |
+
assert cfg.x == 1
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/test_mapped_queue.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from networkx.utils.mapped_queue import MappedQueue, _HeapElement
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_HeapElement_gtlt():
|
| 7 |
+
bar = _HeapElement(1.1, "a")
|
| 8 |
+
foo = _HeapElement(1, "b")
|
| 9 |
+
assert foo < bar
|
| 10 |
+
assert bar > foo
|
| 11 |
+
assert foo < 1.1
|
| 12 |
+
assert 1 < bar
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_HeapElement_gtlt_tied_priority():
|
| 16 |
+
bar = _HeapElement(1, "a")
|
| 17 |
+
foo = _HeapElement(1, "b")
|
| 18 |
+
assert foo > bar
|
| 19 |
+
assert bar < foo
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def test_HeapElement_eq():
|
| 23 |
+
bar = _HeapElement(1.1, "a")
|
| 24 |
+
foo = _HeapElement(1, "a")
|
| 25 |
+
assert foo == bar
|
| 26 |
+
assert bar == foo
|
| 27 |
+
assert foo == "a"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_HeapElement_iter():
|
| 31 |
+
foo = _HeapElement(1, "a")
|
| 32 |
+
bar = _HeapElement(1.1, (3, 2, 1))
|
| 33 |
+
assert list(foo) == [1, "a"]
|
| 34 |
+
assert list(bar) == [1.1, 3, 2, 1]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_HeapElement_getitem():
|
| 38 |
+
foo = _HeapElement(1, "a")
|
| 39 |
+
bar = _HeapElement(1.1, (3, 2, 1))
|
| 40 |
+
assert foo[1] == "a"
|
| 41 |
+
assert foo[0] == 1
|
| 42 |
+
assert bar[0] == 1.1
|
| 43 |
+
assert bar[2] == 2
|
| 44 |
+
assert bar[3] == 1
|
| 45 |
+
pytest.raises(IndexError, bar.__getitem__, 4)
|
| 46 |
+
pytest.raises(IndexError, foo.__getitem__, 2)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class TestMappedQueue:
|
| 50 |
+
def setup_method(self):
|
| 51 |
+
pass
|
| 52 |
+
|
| 53 |
+
def _check_map(self, q):
|
| 54 |
+
assert q.position == {elt: pos for pos, elt in enumerate(q.heap)}
|
| 55 |
+
|
| 56 |
+
def _make_mapped_queue(self, h):
|
| 57 |
+
q = MappedQueue()
|
| 58 |
+
q.heap = h
|
| 59 |
+
q.position = {elt: pos for pos, elt in enumerate(h)}
|
| 60 |
+
return q
|
| 61 |
+
|
| 62 |
+
def test_heapify(self):
|
| 63 |
+
h = [5, 4, 3, 2, 1, 0]
|
| 64 |
+
q = self._make_mapped_queue(h)
|
| 65 |
+
q._heapify()
|
| 66 |
+
self._check_map(q)
|
| 67 |
+
|
| 68 |
+
def test_init(self):
|
| 69 |
+
h = [5, 4, 3, 2, 1, 0]
|
| 70 |
+
q = MappedQueue(h)
|
| 71 |
+
self._check_map(q)
|
| 72 |
+
|
| 73 |
+
def test_incomparable(self):
|
| 74 |
+
h = [5, 4, "a", 2, 1, 0]
|
| 75 |
+
pytest.raises(TypeError, MappedQueue, h)
|
| 76 |
+
|
| 77 |
+
def test_len(self):
|
| 78 |
+
h = [5, 4, 3, 2, 1, 0]
|
| 79 |
+
q = MappedQueue(h)
|
| 80 |
+
self._check_map(q)
|
| 81 |
+
assert len(q) == 6
|
| 82 |
+
|
| 83 |
+
def test_siftup_leaf(self):
|
| 84 |
+
h = [2]
|
| 85 |
+
h_sifted = [2]
|
| 86 |
+
q = self._make_mapped_queue(h)
|
| 87 |
+
q._siftup(0)
|
| 88 |
+
assert q.heap == h_sifted
|
| 89 |
+
self._check_map(q)
|
| 90 |
+
|
| 91 |
+
def test_siftup_one_child(self):
|
| 92 |
+
h = [2, 0]
|
| 93 |
+
h_sifted = [0, 2]
|
| 94 |
+
q = self._make_mapped_queue(h)
|
| 95 |
+
q._siftup(0)
|
| 96 |
+
assert q.heap == h_sifted
|
| 97 |
+
self._check_map(q)
|
| 98 |
+
|
| 99 |
+
def test_siftup_left_child(self):
|
| 100 |
+
h = [2, 0, 1]
|
| 101 |
+
h_sifted = [0, 2, 1]
|
| 102 |
+
q = self._make_mapped_queue(h)
|
| 103 |
+
q._siftup(0)
|
| 104 |
+
assert q.heap == h_sifted
|
| 105 |
+
self._check_map(q)
|
| 106 |
+
|
| 107 |
+
def test_siftup_right_child(self):
|
| 108 |
+
h = [2, 1, 0]
|
| 109 |
+
h_sifted = [0, 1, 2]
|
| 110 |
+
q = self._make_mapped_queue(h)
|
| 111 |
+
q._siftup(0)
|
| 112 |
+
assert q.heap == h_sifted
|
| 113 |
+
self._check_map(q)
|
| 114 |
+
|
| 115 |
+
def test_siftup_multiple(self):
|
| 116 |
+
h = [0, 1, 2, 4, 3, 5, 6]
|
| 117 |
+
h_sifted = [0, 1, 2, 4, 3, 5, 6]
|
| 118 |
+
q = self._make_mapped_queue(h)
|
| 119 |
+
q._siftup(0)
|
| 120 |
+
assert q.heap == h_sifted
|
| 121 |
+
self._check_map(q)
|
| 122 |
+
|
| 123 |
+
def test_siftdown_leaf(self):
|
| 124 |
+
h = [2]
|
| 125 |
+
h_sifted = [2]
|
| 126 |
+
q = self._make_mapped_queue(h)
|
| 127 |
+
q._siftdown(0, 0)
|
| 128 |
+
assert q.heap == h_sifted
|
| 129 |
+
self._check_map(q)
|
| 130 |
+
|
| 131 |
+
def test_siftdown_single(self):
|
| 132 |
+
h = [1, 0]
|
| 133 |
+
h_sifted = [0, 1]
|
| 134 |
+
q = self._make_mapped_queue(h)
|
| 135 |
+
q._siftdown(0, len(h) - 1)
|
| 136 |
+
assert q.heap == h_sifted
|
| 137 |
+
self._check_map(q)
|
| 138 |
+
|
| 139 |
+
def test_siftdown_multiple(self):
|
| 140 |
+
h = [1, 2, 3, 4, 5, 6, 7, 0]
|
| 141 |
+
h_sifted = [0, 1, 3, 2, 5, 6, 7, 4]
|
| 142 |
+
q = self._make_mapped_queue(h)
|
| 143 |
+
q._siftdown(0, len(h) - 1)
|
| 144 |
+
assert q.heap == h_sifted
|
| 145 |
+
self._check_map(q)
|
| 146 |
+
|
| 147 |
+
def test_push(self):
|
| 148 |
+
to_push = [6, 1, 4, 3, 2, 5, 0]
|
| 149 |
+
h_sifted = [0, 2, 1, 6, 3, 5, 4]
|
| 150 |
+
q = MappedQueue()
|
| 151 |
+
for elt in to_push:
|
| 152 |
+
q.push(elt)
|
| 153 |
+
assert q.heap == h_sifted
|
| 154 |
+
self._check_map(q)
|
| 155 |
+
|
| 156 |
+
def test_push_duplicate(self):
|
| 157 |
+
to_push = [2, 1, 0]
|
| 158 |
+
h_sifted = [0, 2, 1]
|
| 159 |
+
q = MappedQueue()
|
| 160 |
+
for elt in to_push:
|
| 161 |
+
inserted = q.push(elt)
|
| 162 |
+
assert inserted
|
| 163 |
+
assert q.heap == h_sifted
|
| 164 |
+
self._check_map(q)
|
| 165 |
+
inserted = q.push(1)
|
| 166 |
+
assert not inserted
|
| 167 |
+
|
| 168 |
+
def test_pop(self):
|
| 169 |
+
h = [3, 4, 6, 0, 1, 2, 5]
|
| 170 |
+
h_sorted = sorted(h)
|
| 171 |
+
q = self._make_mapped_queue(h)
|
| 172 |
+
q._heapify()
|
| 173 |
+
popped = [q.pop() for _ in range(len(h))]
|
| 174 |
+
assert popped == h_sorted
|
| 175 |
+
self._check_map(q)
|
| 176 |
+
|
| 177 |
+
def test_remove_leaf(self):
|
| 178 |
+
h = [0, 2, 1, 6, 3, 5, 4]
|
| 179 |
+
h_removed = [0, 2, 1, 6, 4, 5]
|
| 180 |
+
q = self._make_mapped_queue(h)
|
| 181 |
+
removed = q.remove(3)
|
| 182 |
+
assert q.heap == h_removed
|
| 183 |
+
|
| 184 |
+
def test_remove_root(self):
|
| 185 |
+
h = [0, 2, 1, 6, 3, 5, 4]
|
| 186 |
+
h_removed = [1, 2, 4, 6, 3, 5]
|
| 187 |
+
q = self._make_mapped_queue(h)
|
| 188 |
+
removed = q.remove(0)
|
| 189 |
+
assert q.heap == h_removed
|
| 190 |
+
|
| 191 |
+
def test_update_leaf(self):
|
| 192 |
+
h = [0, 20, 10, 60, 30, 50, 40]
|
| 193 |
+
h_updated = [0, 15, 10, 60, 20, 50, 40]
|
| 194 |
+
q = self._make_mapped_queue(h)
|
| 195 |
+
removed = q.update(30, 15)
|
| 196 |
+
assert q.heap == h_updated
|
| 197 |
+
|
| 198 |
+
def test_update_root(self):
|
| 199 |
+
h = [0, 20, 10, 60, 30, 50, 40]
|
| 200 |
+
h_updated = [10, 20, 35, 60, 30, 50, 40]
|
| 201 |
+
q = self._make_mapped_queue(h)
|
| 202 |
+
removed = q.update(0, 35)
|
| 203 |
+
assert q.heap == h_updated
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class TestMappedDict(TestMappedQueue):
|
| 207 |
+
def _make_mapped_queue(self, h):
|
| 208 |
+
priority_dict = {elt: elt for elt in h}
|
| 209 |
+
return MappedQueue(priority_dict)
|
| 210 |
+
|
| 211 |
+
def test_init(self):
|
| 212 |
+
d = {5: 0, 4: 1, "a": 2, 2: 3, 1: 4}
|
| 213 |
+
q = MappedQueue(d)
|
| 214 |
+
assert q.position == d
|
| 215 |
+
|
| 216 |
+
def test_ties(self):
|
| 217 |
+
d = {5: 0, 4: 1, 3: 2, 2: 3, 1: 4}
|
| 218 |
+
q = MappedQueue(d)
|
| 219 |
+
assert q.position == {elt: pos for pos, elt in enumerate(q.heap)}
|
| 220 |
+
|
| 221 |
+
def test_pop(self):
|
| 222 |
+
d = {5: 0, 4: 1, 3: 2, 2: 3, 1: 4}
|
| 223 |
+
q = MappedQueue(d)
|
| 224 |
+
assert q.pop() == _HeapElement(0, 5)
|
| 225 |
+
assert q.position == {elt: pos for pos, elt in enumerate(q.heap)}
|
| 226 |
+
|
| 227 |
+
def test_empty_pop(self):
|
| 228 |
+
q = MappedQueue()
|
| 229 |
+
pytest.raises(IndexError, q.pop)
|
| 230 |
+
|
| 231 |
+
def test_incomparable_ties(self):
|
| 232 |
+
d = {5: 0, 4: 0, "a": 0, 2: 0, 1: 0}
|
| 233 |
+
pytest.raises(TypeError, MappedQueue, d)
|
| 234 |
+
|
| 235 |
+
def test_push(self):
|
| 236 |
+
to_push = [6, 1, 4, 3, 2, 5, 0]
|
| 237 |
+
h_sifted = [0, 2, 1, 6, 3, 5, 4]
|
| 238 |
+
q = MappedQueue()
|
| 239 |
+
for elt in to_push:
|
| 240 |
+
q.push(elt, priority=elt)
|
| 241 |
+
assert q.heap == h_sifted
|
| 242 |
+
self._check_map(q)
|
| 243 |
+
|
| 244 |
+
def test_push_duplicate(self):
|
| 245 |
+
to_push = [2, 1, 0]
|
| 246 |
+
h_sifted = [0, 2, 1]
|
| 247 |
+
q = MappedQueue()
|
| 248 |
+
for elt in to_push:
|
| 249 |
+
inserted = q.push(elt, priority=elt)
|
| 250 |
+
assert inserted
|
| 251 |
+
assert q.heap == h_sifted
|
| 252 |
+
self._check_map(q)
|
| 253 |
+
inserted = q.push(1, priority=1)
|
| 254 |
+
assert not inserted
|
| 255 |
+
|
| 256 |
+
def test_update_leaf(self):
|
| 257 |
+
h = [0, 20, 10, 60, 30, 50, 40]
|
| 258 |
+
h_updated = [0, 15, 10, 60, 20, 50, 40]
|
| 259 |
+
q = self._make_mapped_queue(h)
|
| 260 |
+
removed = q.update(30, 15, priority=15)
|
| 261 |
+
assert q.heap == h_updated
|
| 262 |
+
|
| 263 |
+
def test_update_root(self):
|
| 264 |
+
h = [0, 20, 10, 60, 30, 50, 40]
|
| 265 |
+
h_updated = [10, 20, 35, 60, 30, 50, 40]
|
| 266 |
+
q = self._make_mapped_queue(h)
|
| 267 |
+
removed = q.update(0, 35, priority=35)
|
| 268 |
+
assert q.heap == h_updated
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/test_misc.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from copy import copy
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
import networkx as nx
|
| 7 |
+
from networkx.utils import (
|
| 8 |
+
PythonRandomInterface,
|
| 9 |
+
PythonRandomViaNumpyBits,
|
| 10 |
+
arbitrary_element,
|
| 11 |
+
create_py_random_state,
|
| 12 |
+
create_random_state,
|
| 13 |
+
dict_to_numpy_array,
|
| 14 |
+
discrete_sequence,
|
| 15 |
+
flatten,
|
| 16 |
+
groups,
|
| 17 |
+
make_list_of_ints,
|
| 18 |
+
pairwise,
|
| 19 |
+
powerlaw_sequence,
|
| 20 |
+
)
|
| 21 |
+
from networkx.utils.misc import _dict_to_numpy_array1, _dict_to_numpy_array2
|
| 22 |
+
|
| 23 |
+
nested_depth = (
|
| 24 |
+
1,
|
| 25 |
+
2,
|
| 26 |
+
(3, 4, ((5, 6, (7,), (8, (9, 10), 11), (12, 13, (14, 15)), 16), 17), 18, 19),
|
| 27 |
+
20,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
nested_set = {
|
| 31 |
+
(1, 2, 3, 4),
|
| 32 |
+
(5, 6, 7, 8, 9),
|
| 33 |
+
(10, 11, (12, 13, 14), (15, 16, 17, 18)),
|
| 34 |
+
19,
|
| 35 |
+
20,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
nested_mixed = [
|
| 39 |
+
1,
|
| 40 |
+
(2, 3, {4, (5, 6), 7}, [8, 9]),
|
| 41 |
+
{10: "foo", 11: "bar", (12, 13): "baz"},
|
| 42 |
+
{(14, 15): "qwe", 16: "asd"},
|
| 43 |
+
(17, (18, "19"), 20),
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@pytest.mark.parametrize("result", [None, [], ["existing"], ["existing1", "existing2"]])
|
| 48 |
+
@pytest.mark.parametrize("nested", [nested_depth, nested_mixed, nested_set])
|
| 49 |
+
def test_flatten(nested, result):
|
| 50 |
+
if result is None:
|
| 51 |
+
val = flatten(nested, result)
|
| 52 |
+
assert len(val) == 20
|
| 53 |
+
else:
|
| 54 |
+
_result = copy(result) # because pytest passes parameters as is
|
| 55 |
+
nexisting = len(_result)
|
| 56 |
+
val = flatten(nested, _result)
|
| 57 |
+
assert len(val) == len(_result) == 20 + nexisting
|
| 58 |
+
|
| 59 |
+
assert issubclass(type(val), tuple)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_make_list_of_ints():
|
| 63 |
+
mylist = [1, 2, 3.0, 42, -2]
|
| 64 |
+
assert make_list_of_ints(mylist) is mylist
|
| 65 |
+
assert make_list_of_ints(mylist) == mylist
|
| 66 |
+
assert type(make_list_of_ints(mylist)[2]) is int
|
| 67 |
+
pytest.raises(nx.NetworkXError, make_list_of_ints, [1, 2, 3, "kermit"])
|
| 68 |
+
pytest.raises(nx.NetworkXError, make_list_of_ints, [1, 2, 3.1])
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_random_number_distribution():
|
| 72 |
+
# smoke test only
|
| 73 |
+
z = powerlaw_sequence(20, exponent=2.5)
|
| 74 |
+
z = discrete_sequence(20, distribution=[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3])
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class TestNumpyArray:
|
| 78 |
+
@classmethod
|
| 79 |
+
def setup_class(cls):
|
| 80 |
+
global np
|
| 81 |
+
np = pytest.importorskip("numpy")
|
| 82 |
+
|
| 83 |
+
def test_numpy_to_list_of_ints(self):
|
| 84 |
+
a = np.array([1, 2, 3], dtype=np.int64)
|
| 85 |
+
b = np.array([1.0, 2, 3])
|
| 86 |
+
c = np.array([1.1, 2, 3])
|
| 87 |
+
assert type(make_list_of_ints(a)) == list
|
| 88 |
+
assert make_list_of_ints(b) == list(b)
|
| 89 |
+
B = make_list_of_ints(b)
|
| 90 |
+
assert type(B[0]) == int
|
| 91 |
+
pytest.raises(nx.NetworkXError, make_list_of_ints, c)
|
| 92 |
+
|
| 93 |
+
def test__dict_to_numpy_array1(self):
|
| 94 |
+
d = {"a": 1, "b": 2}
|
| 95 |
+
a = _dict_to_numpy_array1(d, mapping={"a": 0, "b": 1})
|
| 96 |
+
np.testing.assert_allclose(a, np.array([1, 2]))
|
| 97 |
+
a = _dict_to_numpy_array1(d, mapping={"b": 0, "a": 1})
|
| 98 |
+
np.testing.assert_allclose(a, np.array([2, 1]))
|
| 99 |
+
|
| 100 |
+
a = _dict_to_numpy_array1(d)
|
| 101 |
+
np.testing.assert_allclose(a.sum(), 3)
|
| 102 |
+
|
| 103 |
+
def test__dict_to_numpy_array2(self):
|
| 104 |
+
d = {"a": {"a": 1, "b": 2}, "b": {"a": 10, "b": 20}}
|
| 105 |
+
|
| 106 |
+
mapping = {"a": 1, "b": 0}
|
| 107 |
+
a = _dict_to_numpy_array2(d, mapping=mapping)
|
| 108 |
+
np.testing.assert_allclose(a, np.array([[20, 10], [2, 1]]))
|
| 109 |
+
|
| 110 |
+
a = _dict_to_numpy_array2(d)
|
| 111 |
+
np.testing.assert_allclose(a.sum(), 33)
|
| 112 |
+
|
| 113 |
+
def test_dict_to_numpy_array_a(self):
|
| 114 |
+
d = {"a": {"a": 1, "b": 2}, "b": {"a": 10, "b": 20}}
|
| 115 |
+
|
| 116 |
+
mapping = {"a": 0, "b": 1}
|
| 117 |
+
a = dict_to_numpy_array(d, mapping=mapping)
|
| 118 |
+
np.testing.assert_allclose(a, np.array([[1, 2], [10, 20]]))
|
| 119 |
+
|
| 120 |
+
mapping = {"a": 1, "b": 0}
|
| 121 |
+
a = dict_to_numpy_array(d, mapping=mapping)
|
| 122 |
+
np.testing.assert_allclose(a, np.array([[20, 10], [2, 1]]))
|
| 123 |
+
|
| 124 |
+
a = _dict_to_numpy_array2(d)
|
| 125 |
+
np.testing.assert_allclose(a.sum(), 33)
|
| 126 |
+
|
| 127 |
+
def test_dict_to_numpy_array_b(self):
|
| 128 |
+
d = {"a": 1, "b": 2}
|
| 129 |
+
|
| 130 |
+
mapping = {"a": 0, "b": 1}
|
| 131 |
+
a = dict_to_numpy_array(d, mapping=mapping)
|
| 132 |
+
np.testing.assert_allclose(a, np.array([1, 2]))
|
| 133 |
+
|
| 134 |
+
a = _dict_to_numpy_array1(d)
|
| 135 |
+
np.testing.assert_allclose(a.sum(), 3)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def test_pairwise():
|
| 139 |
+
nodes = range(4)
|
| 140 |
+
node_pairs = [(0, 1), (1, 2), (2, 3)]
|
| 141 |
+
node_pairs_cycle = node_pairs + [(3, 0)]
|
| 142 |
+
assert list(pairwise(nodes)) == node_pairs
|
| 143 |
+
assert list(pairwise(iter(nodes))) == node_pairs
|
| 144 |
+
assert list(pairwise(nodes, cyclic=True)) == node_pairs_cycle
|
| 145 |
+
empty_iter = iter(())
|
| 146 |
+
assert list(pairwise(empty_iter)) == []
|
| 147 |
+
empty_iter = iter(())
|
| 148 |
+
assert list(pairwise(empty_iter, cyclic=True)) == []
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_groups():
|
| 152 |
+
many_to_one = dict(zip("abcde", [0, 0, 1, 1, 2]))
|
| 153 |
+
actual = groups(many_to_one)
|
| 154 |
+
expected = {0: {"a", "b"}, 1: {"c", "d"}, 2: {"e"}}
|
| 155 |
+
assert actual == expected
|
| 156 |
+
assert {} == groups({})
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_create_random_state():
|
| 160 |
+
np = pytest.importorskip("numpy")
|
| 161 |
+
rs = np.random.RandomState
|
| 162 |
+
|
| 163 |
+
assert isinstance(create_random_state(1), rs)
|
| 164 |
+
assert isinstance(create_random_state(None), rs)
|
| 165 |
+
assert isinstance(create_random_state(np.random), rs)
|
| 166 |
+
assert isinstance(create_random_state(rs(1)), rs)
|
| 167 |
+
# Support for numpy.random.Generator
|
| 168 |
+
rng = np.random.default_rng()
|
| 169 |
+
assert isinstance(create_random_state(rng), np.random.Generator)
|
| 170 |
+
pytest.raises(ValueError, create_random_state, "a")
|
| 171 |
+
|
| 172 |
+
assert np.all(rs(1).rand(10) == create_random_state(1).rand(10))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def test_create_py_random_state():
|
| 176 |
+
pyrs = random.Random
|
| 177 |
+
|
| 178 |
+
assert isinstance(create_py_random_state(1), pyrs)
|
| 179 |
+
assert isinstance(create_py_random_state(None), pyrs)
|
| 180 |
+
assert isinstance(create_py_random_state(pyrs(1)), pyrs)
|
| 181 |
+
pytest.raises(ValueError, create_py_random_state, "a")
|
| 182 |
+
|
| 183 |
+
np = pytest.importorskip("numpy")
|
| 184 |
+
|
| 185 |
+
rs = np.random.RandomState
|
| 186 |
+
rng = np.random.default_rng(1000)
|
| 187 |
+
rng_explicit = np.random.Generator(np.random.SFC64())
|
| 188 |
+
old_nprs = PythonRandomInterface
|
| 189 |
+
nprs = PythonRandomViaNumpyBits
|
| 190 |
+
assert isinstance(create_py_random_state(np.random), nprs)
|
| 191 |
+
assert isinstance(create_py_random_state(rs(1)), old_nprs)
|
| 192 |
+
assert isinstance(create_py_random_state(rng), nprs)
|
| 193 |
+
assert isinstance(create_py_random_state(rng_explicit), nprs)
|
| 194 |
+
# test default rng input
|
| 195 |
+
assert isinstance(PythonRandomInterface(), old_nprs)
|
| 196 |
+
assert isinstance(PythonRandomViaNumpyBits(), nprs)
|
| 197 |
+
|
| 198 |
+
# VeryLargeIntegers Smoke test (they raise error for np.random)
|
| 199 |
+
int64max = 9223372036854775807 # from np.iinfo(np.int64).max
|
| 200 |
+
for r in (rng, rs(1)):
|
| 201 |
+
prs = create_py_random_state(r)
|
| 202 |
+
prs.randrange(3, int64max + 5)
|
| 203 |
+
prs.randint(3, int64max + 5)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_PythonRandomInterface_RandomState():
|
| 207 |
+
np = pytest.importorskip("numpy")
|
| 208 |
+
|
| 209 |
+
seed = 42
|
| 210 |
+
rs = np.random.RandomState
|
| 211 |
+
rng = PythonRandomInterface(rs(seed))
|
| 212 |
+
rs42 = rs(seed)
|
| 213 |
+
|
| 214 |
+
# make sure these functions are same as expected outcome
|
| 215 |
+
assert rng.randrange(3, 5) == rs42.randint(3, 5)
|
| 216 |
+
assert rng.choice([1, 2, 3]) == rs42.choice([1, 2, 3])
|
| 217 |
+
assert rng.gauss(0, 1) == rs42.normal(0, 1)
|
| 218 |
+
assert rng.expovariate(1.5) == rs42.exponential(1 / 1.5)
|
| 219 |
+
assert np.all(rng.shuffle([1, 2, 3]) == rs42.shuffle([1, 2, 3]))
|
| 220 |
+
assert np.all(
|
| 221 |
+
rng.sample([1, 2, 3], 2) == rs42.choice([1, 2, 3], (2,), replace=False)
|
| 222 |
+
)
|
| 223 |
+
assert np.all(
|
| 224 |
+
[rng.randint(3, 5) for _ in range(100)]
|
| 225 |
+
== [rs42.randint(3, 6) for _ in range(100)]
|
| 226 |
+
)
|
| 227 |
+
assert rng.random() == rs42.random_sample()
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def test_PythonRandomInterface_Generator():
|
| 231 |
+
np = pytest.importorskip("numpy")
|
| 232 |
+
|
| 233 |
+
seed = 42
|
| 234 |
+
rng = np.random.default_rng(seed)
|
| 235 |
+
pri = PythonRandomInterface(np.random.default_rng(seed))
|
| 236 |
+
|
| 237 |
+
# make sure these functions are same as expected outcome
|
| 238 |
+
assert pri.randrange(3, 5) == rng.integers(3, 5)
|
| 239 |
+
assert pri.choice([1, 2, 3]) == rng.choice([1, 2, 3])
|
| 240 |
+
assert pri.gauss(0, 1) == rng.normal(0, 1)
|
| 241 |
+
assert pri.expovariate(1.5) == rng.exponential(1 / 1.5)
|
| 242 |
+
assert np.all(pri.shuffle([1, 2, 3]) == rng.shuffle([1, 2, 3]))
|
| 243 |
+
assert np.all(
|
| 244 |
+
pri.sample([1, 2, 3], 2) == rng.choice([1, 2, 3], (2,), replace=False)
|
| 245 |
+
)
|
| 246 |
+
assert np.all(
|
| 247 |
+
[pri.randint(3, 5) for _ in range(100)]
|
| 248 |
+
== [rng.integers(3, 6) for _ in range(100)]
|
| 249 |
+
)
|
| 250 |
+
assert pri.random() == rng.random()
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
@pytest.mark.parametrize(
|
| 254 |
+
("iterable_type", "expected"), ((list, 1), (tuple, 1), (str, "["), (set, 1))
|
| 255 |
+
)
|
| 256 |
+
def test_arbitrary_element(iterable_type, expected):
|
| 257 |
+
iterable = iterable_type([1, 2, 3])
|
| 258 |
+
assert arbitrary_element(iterable) == expected
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@pytest.mark.parametrize(
|
| 262 |
+
"iterator",
|
| 263 |
+
((i for i in range(3)), iter([1, 2, 3])), # generator
|
| 264 |
+
)
|
| 265 |
+
def test_arbitrary_element_raises(iterator):
|
| 266 |
+
"""Value error is raised when input is an iterator."""
|
| 267 |
+
with pytest.raises(ValueError, match="from an iterator"):
|
| 268 |
+
arbitrary_element(iterator)
|
.venv/lib/python3.11/site-packages/networkx/utils/tests/test_random_sequence.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from networkx.utils import (
|
| 4 |
+
powerlaw_sequence,
|
| 5 |
+
random_weighted_sample,
|
| 6 |
+
weighted_choice,
|
| 7 |
+
zipf_rv,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_degree_sequences():
|
| 12 |
+
seq = powerlaw_sequence(10, seed=1)
|
| 13 |
+
seq = powerlaw_sequence(10)
|
| 14 |
+
assert len(seq) == 10
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_zipf_rv():
|
| 18 |
+
r = zipf_rv(2.3, xmin=2, seed=1)
|
| 19 |
+
r = zipf_rv(2.3, 2, 1)
|
| 20 |
+
r = zipf_rv(2.3)
|
| 21 |
+
assert type(r), int
|
| 22 |
+
pytest.raises(ValueError, zipf_rv, 0.5)
|
| 23 |
+
pytest.raises(ValueError, zipf_rv, 2, xmin=0)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def test_random_weighted_sample():
|
| 27 |
+
mapping = {"a": 10, "b": 20}
|
| 28 |
+
s = random_weighted_sample(mapping, 2, seed=1)
|
| 29 |
+
s = random_weighted_sample(mapping, 2)
|
| 30 |
+
assert sorted(s) == sorted(mapping.keys())
|
| 31 |
+
pytest.raises(ValueError, random_weighted_sample, mapping, 3)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_random_weighted_choice():
|
| 35 |
+
mapping = {"a": 10, "b": 0}
|
| 36 |
+
c = weighted_choice(mapping, seed=1)
|
| 37 |
+
c = weighted_choice(mapping)
|
| 38 |
+
assert c == "a"
|
.venv/lib/python3.11/site-packages/oauth2client/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Client library for using OAuth2, especially with Google APIs."""
|
| 16 |
+
|
| 17 |
+
__version__ = '4.1.3'
|
| 18 |
+
|
| 19 |
+
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
|
| 20 |
+
GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code'
|
| 21 |
+
GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'
|
| 22 |
+
GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
|
| 23 |
+
GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo'
|
| 24 |
+
|
.venv/lib/python3.11/site-packages/oauth2client/_helpers.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Helper functions for commonly used utilities."""
|
| 16 |
+
|
| 17 |
+
import base64
|
| 18 |
+
import functools
|
| 19 |
+
import inspect
|
| 20 |
+
import json
|
| 21 |
+
import logging
|
| 22 |
+
import os
|
| 23 |
+
import warnings
|
| 24 |
+
|
| 25 |
+
import six
|
| 26 |
+
from six.moves import urllib
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
POSITIONAL_WARNING = 'WARNING'
|
| 32 |
+
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
| 33 |
+
POSITIONAL_IGNORE = 'IGNORE'
|
| 34 |
+
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
| 35 |
+
POSITIONAL_IGNORE])
|
| 36 |
+
|
| 37 |
+
positional_parameters_enforcement = POSITIONAL_WARNING
|
| 38 |
+
|
| 39 |
+
_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
|
| 40 |
+
_IS_DIR_MESSAGE = '{0}: Is a directory'
|
| 41 |
+
_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def positional(max_positional_args):
|
| 45 |
+
"""A decorator to declare that only the first N arguments my be positional.
|
| 46 |
+
|
| 47 |
+
This decorator makes it easy to support Python 3 style keyword-only
|
| 48 |
+
parameters. For example, in Python 3 it is possible to write::
|
| 49 |
+
|
| 50 |
+
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
| 51 |
+
...
|
| 52 |
+
|
| 53 |
+
All named parameters after ``*`` must be a keyword::
|
| 54 |
+
|
| 55 |
+
fn(10, 'kw1', 'kw2') # Raises exception.
|
| 56 |
+
fn(10, kwonly1='kw1') # Ok.
|
| 57 |
+
|
| 58 |
+
Example
|
| 59 |
+
^^^^^^^
|
| 60 |
+
|
| 61 |
+
To define a function like above, do::
|
| 62 |
+
|
| 63 |
+
@positional(1)
|
| 64 |
+
def fn(pos1, kwonly1=None, kwonly2=None):
|
| 65 |
+
...
|
| 66 |
+
|
| 67 |
+
If no default value is provided to a keyword argument, it becomes a
|
| 68 |
+
required keyword argument::
|
| 69 |
+
|
| 70 |
+
@positional(0)
|
| 71 |
+
def fn(required_kw):
|
| 72 |
+
...
|
| 73 |
+
|
| 74 |
+
This must be called with the keyword parameter::
|
| 75 |
+
|
| 76 |
+
fn() # Raises exception.
|
| 77 |
+
fn(10) # Raises exception.
|
| 78 |
+
fn(required_kw=10) # Ok.
|
| 79 |
+
|
| 80 |
+
When defining instance or class methods always remember to account for
|
| 81 |
+
``self`` and ``cls``::
|
| 82 |
+
|
| 83 |
+
class MyClass(object):
|
| 84 |
+
|
| 85 |
+
@positional(2)
|
| 86 |
+
def my_method(self, pos1, kwonly1=None):
|
| 87 |
+
...
|
| 88 |
+
|
| 89 |
+
@classmethod
|
| 90 |
+
@positional(2)
|
| 91 |
+
def my_method(cls, pos1, kwonly1=None):
|
| 92 |
+
...
|
| 93 |
+
|
| 94 |
+
The positional decorator behavior is controlled by
|
| 95 |
+
``_helpers.positional_parameters_enforcement``, which may be set to
|
| 96 |
+
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
| 97 |
+
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
| 98 |
+
nothing, respectively, if a declaration is violated.
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
max_positional_arguments: Maximum number of positional arguments. All
|
| 102 |
+
parameters after the this index must be
|
| 103 |
+
keyword only.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
A decorator that prevents using arguments after max_positional_args
|
| 107 |
+
from being used as positional parameters.
|
| 108 |
+
|
| 109 |
+
Raises:
|
| 110 |
+
TypeError: if a key-word only argument is provided as a positional
|
| 111 |
+
parameter, but only if
|
| 112 |
+
_helpers.positional_parameters_enforcement is set to
|
| 113 |
+
POSITIONAL_EXCEPTION.
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def positional_decorator(wrapped):
|
| 117 |
+
@functools.wraps(wrapped)
|
| 118 |
+
def positional_wrapper(*args, **kwargs):
|
| 119 |
+
if len(args) > max_positional_args:
|
| 120 |
+
plural_s = ''
|
| 121 |
+
if max_positional_args != 1:
|
| 122 |
+
plural_s = 's'
|
| 123 |
+
message = ('{function}() takes at most {args_max} positional '
|
| 124 |
+
'argument{plural} ({args_given} given)'.format(
|
| 125 |
+
function=wrapped.__name__,
|
| 126 |
+
args_max=max_positional_args,
|
| 127 |
+
args_given=len(args),
|
| 128 |
+
plural=plural_s))
|
| 129 |
+
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
| 130 |
+
raise TypeError(message)
|
| 131 |
+
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
| 132 |
+
logger.warning(message)
|
| 133 |
+
return wrapped(*args, **kwargs)
|
| 134 |
+
return positional_wrapper
|
| 135 |
+
|
| 136 |
+
if isinstance(max_positional_args, six.integer_types):
|
| 137 |
+
return positional_decorator
|
| 138 |
+
else:
|
| 139 |
+
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
| 140 |
+
return positional(len(args) - len(defaults))(max_positional_args)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def scopes_to_string(scopes):
|
| 144 |
+
"""Converts scope value to a string.
|
| 145 |
+
|
| 146 |
+
If scopes is a string then it is simply passed through. If scopes is an
|
| 147 |
+
iterable then a string is returned that is all the individual scopes
|
| 148 |
+
concatenated with spaces.
|
| 149 |
+
|
| 150 |
+
Args:
|
| 151 |
+
scopes: string or iterable of strings, the scopes.
|
| 152 |
+
|
| 153 |
+
Returns:
|
| 154 |
+
The scopes formatted as a single string.
|
| 155 |
+
"""
|
| 156 |
+
if isinstance(scopes, six.string_types):
|
| 157 |
+
return scopes
|
| 158 |
+
else:
|
| 159 |
+
return ' '.join(scopes)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def string_to_scopes(scopes):
|
| 163 |
+
"""Converts stringifed scope value to a list.
|
| 164 |
+
|
| 165 |
+
If scopes is a list then it is simply passed through. If scopes is an
|
| 166 |
+
string then a list of each individual scope is returned.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
scopes: a string or iterable of strings, the scopes.
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
The scopes in a list.
|
| 173 |
+
"""
|
| 174 |
+
if not scopes:
|
| 175 |
+
return []
|
| 176 |
+
elif isinstance(scopes, six.string_types):
|
| 177 |
+
return scopes.split(' ')
|
| 178 |
+
else:
|
| 179 |
+
return scopes
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def parse_unique_urlencoded(content):
|
| 183 |
+
"""Parses unique key-value parameters from urlencoded content.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
content: string, URL-encoded key-value pairs.
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
dict, The key-value pairs from ``content``.
|
| 190 |
+
|
| 191 |
+
Raises:
|
| 192 |
+
ValueError: if one of the keys is repeated.
|
| 193 |
+
"""
|
| 194 |
+
urlencoded_params = urllib.parse.parse_qs(content)
|
| 195 |
+
params = {}
|
| 196 |
+
for key, value in six.iteritems(urlencoded_params):
|
| 197 |
+
if len(value) != 1:
|
| 198 |
+
msg = ('URL-encoded content contains a repeated value:'
|
| 199 |
+
'%s -> %s' % (key, ', '.join(value)))
|
| 200 |
+
raise ValueError(msg)
|
| 201 |
+
params[key] = value[0]
|
| 202 |
+
return params
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def update_query_params(uri, params):
|
| 206 |
+
"""Updates a URI with new query parameters.
|
| 207 |
+
|
| 208 |
+
If a given key from ``params`` is repeated in the ``uri``, then
|
| 209 |
+
the URI will be considered invalid and an error will occur.
|
| 210 |
+
|
| 211 |
+
If the URI is valid, then each value from ``params`` will
|
| 212 |
+
replace the corresponding value in the query parameters (if
|
| 213 |
+
it exists).
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
uri: string, A valid URI, with potential existing query parameters.
|
| 217 |
+
params: dict, A dictionary of query parameters.
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
The same URI but with the new query parameters added.
|
| 221 |
+
"""
|
| 222 |
+
parts = urllib.parse.urlparse(uri)
|
| 223 |
+
query_params = parse_unique_urlencoded(parts.query)
|
| 224 |
+
query_params.update(params)
|
| 225 |
+
new_query = urllib.parse.urlencode(query_params)
|
| 226 |
+
new_parts = parts._replace(query=new_query)
|
| 227 |
+
return urllib.parse.urlunparse(new_parts)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _add_query_parameter(url, name, value):
|
| 231 |
+
"""Adds a query parameter to a url.
|
| 232 |
+
|
| 233 |
+
Replaces the current value if it already exists in the URL.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
url: string, url to add the query parameter to.
|
| 237 |
+
name: string, query parameter name.
|
| 238 |
+
value: string, query parameter value.
|
| 239 |
+
|
| 240 |
+
Returns:
|
| 241 |
+
Updated query parameter. Does not update the url if value is None.
|
| 242 |
+
"""
|
| 243 |
+
if value is None:
|
| 244 |
+
return url
|
| 245 |
+
else:
|
| 246 |
+
return update_query_params(url, {name: value})
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def validate_file(filename):
|
| 250 |
+
if os.path.islink(filename):
|
| 251 |
+
raise IOError(_SYM_LINK_MESSAGE.format(filename))
|
| 252 |
+
elif os.path.isdir(filename):
|
| 253 |
+
raise IOError(_IS_DIR_MESSAGE.format(filename))
|
| 254 |
+
elif not os.path.isfile(filename):
|
| 255 |
+
warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _parse_pem_key(raw_key_input):
|
| 259 |
+
"""Identify and extract PEM keys.
|
| 260 |
+
|
| 261 |
+
Determines whether the given key is in the format of PEM key, and extracts
|
| 262 |
+
the relevant part of the key if it is.
|
| 263 |
+
|
| 264 |
+
Args:
|
| 265 |
+
raw_key_input: The contents of a private key file (either PEM or
|
| 266 |
+
PKCS12).
|
| 267 |
+
|
| 268 |
+
Returns:
|
| 269 |
+
string, The actual key if the contents are from a PEM file, or
|
| 270 |
+
else None.
|
| 271 |
+
"""
|
| 272 |
+
offset = raw_key_input.find(b'-----BEGIN ')
|
| 273 |
+
if offset != -1:
|
| 274 |
+
return raw_key_input[offset:]
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def _json_encode(data):
|
| 278 |
+
return json.dumps(data, separators=(',', ':'))
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def _to_bytes(value, encoding='ascii'):
|
| 282 |
+
"""Converts a string value to bytes, if necessary.
|
| 283 |
+
|
| 284 |
+
Unfortunately, ``six.b`` is insufficient for this task since in
|
| 285 |
+
Python2 it does not modify ``unicode`` objects.
|
| 286 |
+
|
| 287 |
+
Args:
|
| 288 |
+
value: The string/bytes value to be converted.
|
| 289 |
+
encoding: The encoding to use to convert unicode to bytes. Defaults
|
| 290 |
+
to "ascii", which will not allow any characters from ordinals
|
| 291 |
+
larger than 127. Other useful values are "latin-1", which
|
| 292 |
+
which will only allows byte ordinals (up to 255) and "utf-8",
|
| 293 |
+
which will encode any unicode that needs to be.
|
| 294 |
+
|
| 295 |
+
Returns:
|
| 296 |
+
The original value converted to bytes (if unicode) or as passed in
|
| 297 |
+
if it started out as bytes.
|
| 298 |
+
|
| 299 |
+
Raises:
|
| 300 |
+
ValueError if the value could not be converted to bytes.
|
| 301 |
+
"""
|
| 302 |
+
result = (value.encode(encoding)
|
| 303 |
+
if isinstance(value, six.text_type) else value)
|
| 304 |
+
if isinstance(result, six.binary_type):
|
| 305 |
+
return result
|
| 306 |
+
else:
|
| 307 |
+
raise ValueError('{0!r} could not be converted to bytes'.format(value))
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _from_bytes(value):
|
| 311 |
+
"""Converts bytes to a string value, if necessary.
|
| 312 |
+
|
| 313 |
+
Args:
|
| 314 |
+
value: The string/bytes value to be converted.
|
| 315 |
+
|
| 316 |
+
Returns:
|
| 317 |
+
The original value converted to unicode (if bytes) or as passed in
|
| 318 |
+
if it started out as unicode.
|
| 319 |
+
|
| 320 |
+
Raises:
|
| 321 |
+
ValueError if the value could not be converted to unicode.
|
| 322 |
+
"""
|
| 323 |
+
result = (value.decode('utf-8')
|
| 324 |
+
if isinstance(value, six.binary_type) else value)
|
| 325 |
+
if isinstance(result, six.text_type):
|
| 326 |
+
return result
|
| 327 |
+
else:
|
| 328 |
+
raise ValueError(
|
| 329 |
+
'{0!r} could not be converted to unicode'.format(value))
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _urlsafe_b64encode(raw_bytes):
|
| 333 |
+
raw_bytes = _to_bytes(raw_bytes, encoding='utf-8')
|
| 334 |
+
return base64.urlsafe_b64encode(raw_bytes).rstrip(b'=')
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _urlsafe_b64decode(b64string):
|
| 338 |
+
# Guard against unicode strings, which base64 can't handle.
|
| 339 |
+
b64string = _to_bytes(b64string)
|
| 340 |
+
padded = b64string + b'=' * (4 - len(b64string) % 4)
|
| 341 |
+
return base64.urlsafe_b64decode(padded)
|
.venv/lib/python3.11/site-packages/oauth2client/_openssl_crypt.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""OpenSSL Crypto-related routines for oauth2client."""
|
| 15 |
+
|
| 16 |
+
from OpenSSL import crypto
|
| 17 |
+
|
| 18 |
+
from oauth2client import _helpers
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class OpenSSLVerifier(object):
|
| 22 |
+
"""Verifies the signature on a message."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, pubkey):
|
| 25 |
+
"""Constructor.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
pubkey: OpenSSL.crypto.PKey, The public key to verify with.
|
| 29 |
+
"""
|
| 30 |
+
self._pubkey = pubkey
|
| 31 |
+
|
| 32 |
+
def verify(self, message, signature):
|
| 33 |
+
"""Verifies a message against a signature.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
message: string or bytes, The message to verify. If string, will be
|
| 37 |
+
encoded to bytes as utf-8.
|
| 38 |
+
signature: string or bytes, The signature on the message. If string,
|
| 39 |
+
will be encoded to bytes as utf-8.
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
True if message was signed by the private key associated with the
|
| 43 |
+
public key that this object was constructed with.
|
| 44 |
+
"""
|
| 45 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 46 |
+
signature = _helpers._to_bytes(signature, encoding='utf-8')
|
| 47 |
+
try:
|
| 48 |
+
crypto.verify(self._pubkey, signature, message, 'sha256')
|
| 49 |
+
return True
|
| 50 |
+
except crypto.Error:
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
def from_string(key_pem, is_x509_cert):
|
| 55 |
+
"""Construct a Verified instance from a string.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
key_pem: string, public key in PEM format.
|
| 59 |
+
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
| 60 |
+
is expected to be an RSA key in PEM format.
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Verifier instance.
|
| 64 |
+
|
| 65 |
+
Raises:
|
| 66 |
+
OpenSSL.crypto.Error: if the key_pem can't be parsed.
|
| 67 |
+
"""
|
| 68 |
+
key_pem = _helpers._to_bytes(key_pem)
|
| 69 |
+
if is_x509_cert:
|
| 70 |
+
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
| 71 |
+
else:
|
| 72 |
+
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
| 73 |
+
return OpenSSLVerifier(pubkey)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class OpenSSLSigner(object):
|
| 77 |
+
"""Signs messages with a private key."""
|
| 78 |
+
|
| 79 |
+
def __init__(self, pkey):
|
| 80 |
+
"""Constructor.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
pkey: OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
| 84 |
+
"""
|
| 85 |
+
self._key = pkey
|
| 86 |
+
|
| 87 |
+
def sign(self, message):
|
| 88 |
+
"""Signs a message.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
message: bytes, Message to be signed.
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
string, The signature of the message for the given key.
|
| 95 |
+
"""
|
| 96 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 97 |
+
return crypto.sign(self._key, message, 'sha256')
|
| 98 |
+
|
| 99 |
+
@staticmethod
|
| 100 |
+
def from_string(key, password=b'notasecret'):
|
| 101 |
+
"""Construct a Signer instance from a string.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
key: string, private key in PKCS12 or PEM format.
|
| 105 |
+
password: string, password for the private key file.
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
Signer instance.
|
| 109 |
+
|
| 110 |
+
Raises:
|
| 111 |
+
OpenSSL.crypto.Error if the key can't be parsed.
|
| 112 |
+
"""
|
| 113 |
+
key = _helpers._to_bytes(key)
|
| 114 |
+
parsed_pem_key = _helpers._parse_pem_key(key)
|
| 115 |
+
if parsed_pem_key:
|
| 116 |
+
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
| 117 |
+
else:
|
| 118 |
+
password = _helpers._to_bytes(password, encoding='utf-8')
|
| 119 |
+
pkey = crypto.load_pkcs12(key, password).get_privatekey()
|
| 120 |
+
return OpenSSLSigner(pkey)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def pkcs12_key_as_pem(private_key_bytes, private_key_password):
|
| 124 |
+
"""Convert the contents of a PKCS#12 key to PEM using pyOpenSSL.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
private_key_bytes: Bytes. PKCS#12 key in DER format.
|
| 128 |
+
private_key_password: String. Password for PKCS#12 key.
|
| 129 |
+
|
| 130 |
+
Returns:
|
| 131 |
+
String. PEM contents of ``private_key_bytes``.
|
| 132 |
+
"""
|
| 133 |
+
private_key_password = _helpers._to_bytes(private_key_password)
|
| 134 |
+
pkcs12 = crypto.load_pkcs12(private_key_bytes, private_key_password)
|
| 135 |
+
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
| 136 |
+
pkcs12.get_privatekey())
|
.venv/lib/python3.11/site-packages/oauth2client/_pkce.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
|
| 17 |
+
Public Clients
|
| 18 |
+
|
| 19 |
+
See RFC7636.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
import base64
|
| 23 |
+
import hashlib
|
| 24 |
+
import os
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def code_verifier(n_bytes=64):
|
| 28 |
+
"""
|
| 29 |
+
Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
|
| 30 |
+
|
| 31 |
+
This is a 'high-entropy cryptographic random string' that will be
|
| 32 |
+
impractical for an attacker to guess.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
n_bytes: integer between 31 and 96, inclusive. default: 64
|
| 36 |
+
number of bytes of entropy to include in verifier.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Bytestring, representing urlsafe base64-encoded random data.
|
| 40 |
+
"""
|
| 41 |
+
verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
|
| 42 |
+
# https://tools.ietf.org/html/rfc7636#section-4.1
|
| 43 |
+
# minimum length of 43 characters and a maximum length of 128 characters.
|
| 44 |
+
if len(verifier) < 43:
|
| 45 |
+
raise ValueError("Verifier too short. n_bytes must be > 30.")
|
| 46 |
+
elif len(verifier) > 128:
|
| 47 |
+
raise ValueError("Verifier too long. n_bytes must be < 97.")
|
| 48 |
+
else:
|
| 49 |
+
return verifier
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def code_challenge(verifier):
|
| 53 |
+
"""
|
| 54 |
+
Creates a 'code_challenge' as described in section 4.2 of RFC 7636
|
| 55 |
+
by taking the sha256 hash of the verifier and then urlsafe
|
| 56 |
+
base64-encoding it.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
verifier: bytestring, representing a code_verifier as generated by
|
| 60 |
+
code_verifier().
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
|
| 64 |
+
without '=' padding.
|
| 65 |
+
"""
|
| 66 |
+
digest = hashlib.sha256(verifier).digest()
|
| 67 |
+
return base64.urlsafe_b64encode(digest).rstrip(b'=')
|
.venv/lib/python3.11/site-packages/oauth2client/_pure_python_crypt.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Pure Python crypto-related routines for oauth2client.
|
| 16 |
+
|
| 17 |
+
Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages
|
| 18 |
+
to parse PEM files storing PKCS#1 or PKCS#8 keys as well as
|
| 19 |
+
certificates.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from pyasn1.codec.der import decoder
|
| 23 |
+
from pyasn1_modules import pem
|
| 24 |
+
from pyasn1_modules.rfc2459 import Certificate
|
| 25 |
+
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
| 26 |
+
import rsa
|
| 27 |
+
import six
|
| 28 |
+
|
| 29 |
+
from oauth2client import _helpers
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
_PKCS12_ERROR = r"""\
|
| 33 |
+
PKCS12 format is not supported by the RSA library.
|
| 34 |
+
Either install PyOpenSSL, or please convert .p12 format
|
| 35 |
+
to .pem format:
|
| 36 |
+
$ cat key.p12 | \
|
| 37 |
+
> openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \
|
| 38 |
+
> openssl rsa > key.pem
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
_POW2 = (128, 64, 32, 16, 8, 4, 2, 1)
|
| 42 |
+
_PKCS1_MARKER = ('-----BEGIN RSA PRIVATE KEY-----',
|
| 43 |
+
'-----END RSA PRIVATE KEY-----')
|
| 44 |
+
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
|
| 45 |
+
'-----END PRIVATE KEY-----')
|
| 46 |
+
_PKCS8_SPEC = PrivateKeyInfo()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _bit_list_to_bytes(bit_list):
|
| 50 |
+
"""Converts an iterable of 1's and 0's to bytes.
|
| 51 |
+
|
| 52 |
+
Combines the list 8 at a time, treating each group of 8 bits
|
| 53 |
+
as a single byte.
|
| 54 |
+
"""
|
| 55 |
+
num_bits = len(bit_list)
|
| 56 |
+
byte_vals = bytearray()
|
| 57 |
+
for start in six.moves.xrange(0, num_bits, 8):
|
| 58 |
+
curr_bits = bit_list[start:start + 8]
|
| 59 |
+
char_val = sum(val * digit
|
| 60 |
+
for val, digit in zip(_POW2, curr_bits))
|
| 61 |
+
byte_vals.append(char_val)
|
| 62 |
+
return bytes(byte_vals)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class RsaVerifier(object):
|
| 66 |
+
"""Verifies the signature on a message.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
pubkey: rsa.key.PublicKey (or equiv), The public key to verify with.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(self, pubkey):
|
| 73 |
+
self._pubkey = pubkey
|
| 74 |
+
|
| 75 |
+
def verify(self, message, signature):
|
| 76 |
+
"""Verifies a message against a signature.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
message: string or bytes, The message to verify. If string, will be
|
| 80 |
+
encoded to bytes as utf-8.
|
| 81 |
+
signature: string or bytes, The signature on the message. If
|
| 82 |
+
string, will be encoded to bytes as utf-8.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
True if message was signed by the private key associated with the
|
| 86 |
+
public key that this object was constructed with.
|
| 87 |
+
"""
|
| 88 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 89 |
+
try:
|
| 90 |
+
return rsa.pkcs1.verify(message, signature, self._pubkey)
|
| 91 |
+
except (ValueError, rsa.pkcs1.VerificationError):
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
@classmethod
|
| 95 |
+
def from_string(cls, key_pem, is_x509_cert):
|
| 96 |
+
"""Construct an RsaVerifier instance from a string.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
key_pem: string, public key in PEM format.
|
| 100 |
+
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
| 101 |
+
is expected to be an RSA key in PEM format.
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
RsaVerifier instance.
|
| 105 |
+
|
| 106 |
+
Raises:
|
| 107 |
+
ValueError: if the key_pem can't be parsed. In either case, error
|
| 108 |
+
will begin with 'No PEM start marker'. If
|
| 109 |
+
``is_x509_cert`` is True, will fail to find the
|
| 110 |
+
"-----BEGIN CERTIFICATE-----" error, otherwise fails
|
| 111 |
+
to find "-----BEGIN RSA PUBLIC KEY-----".
|
| 112 |
+
"""
|
| 113 |
+
key_pem = _helpers._to_bytes(key_pem)
|
| 114 |
+
if is_x509_cert:
|
| 115 |
+
der = rsa.pem.load_pem(key_pem, 'CERTIFICATE')
|
| 116 |
+
asn1_cert, remaining = decoder.decode(der, asn1Spec=Certificate())
|
| 117 |
+
if remaining != b'':
|
| 118 |
+
raise ValueError('Unused bytes', remaining)
|
| 119 |
+
|
| 120 |
+
cert_info = asn1_cert['tbsCertificate']['subjectPublicKeyInfo']
|
| 121 |
+
key_bytes = _bit_list_to_bytes(cert_info['subjectPublicKey'])
|
| 122 |
+
pubkey = rsa.PublicKey.load_pkcs1(key_bytes, 'DER')
|
| 123 |
+
else:
|
| 124 |
+
pubkey = rsa.PublicKey.load_pkcs1(key_pem, 'PEM')
|
| 125 |
+
return cls(pubkey)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class RsaSigner(object):
|
| 129 |
+
"""Signs messages with a private key.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
pkey: rsa.key.PrivateKey (or equiv), The private key to sign with.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
def __init__(self, pkey):
|
| 136 |
+
self._key = pkey
|
| 137 |
+
|
| 138 |
+
def sign(self, message):
|
| 139 |
+
"""Signs a message.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
message: bytes, Message to be signed.
|
| 143 |
+
|
| 144 |
+
Returns:
|
| 145 |
+
string, The signature of the message for the given key.
|
| 146 |
+
"""
|
| 147 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 148 |
+
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
|
| 149 |
+
|
| 150 |
+
@classmethod
|
| 151 |
+
def from_string(cls, key, password='notasecret'):
|
| 152 |
+
"""Construct an RsaSigner instance from a string.
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
key: string, private key in PEM format.
|
| 156 |
+
password: string, password for private key file. Unused for PEM
|
| 157 |
+
files.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
RsaSigner instance.
|
| 161 |
+
|
| 162 |
+
Raises:
|
| 163 |
+
ValueError if the key cannot be parsed as PKCS#1 or PKCS#8 in
|
| 164 |
+
PEM format.
|
| 165 |
+
"""
|
| 166 |
+
key = _helpers._from_bytes(key) # pem expects str in Py3
|
| 167 |
+
marker_id, key_bytes = pem.readPemBlocksFromFile(
|
| 168 |
+
six.StringIO(key), _PKCS1_MARKER, _PKCS8_MARKER)
|
| 169 |
+
|
| 170 |
+
if marker_id == 0:
|
| 171 |
+
pkey = rsa.key.PrivateKey.load_pkcs1(key_bytes,
|
| 172 |
+
format='DER')
|
| 173 |
+
elif marker_id == 1:
|
| 174 |
+
key_info, remaining = decoder.decode(
|
| 175 |
+
key_bytes, asn1Spec=_PKCS8_SPEC)
|
| 176 |
+
if remaining != b'':
|
| 177 |
+
raise ValueError('Unused bytes', remaining)
|
| 178 |
+
pkey_info = key_info.getComponentByName('privateKey')
|
| 179 |
+
pkey = rsa.key.PrivateKey.load_pkcs1(pkey_info.asOctets(),
|
| 180 |
+
format='DER')
|
| 181 |
+
else:
|
| 182 |
+
raise ValueError('No key could be detected.')
|
| 183 |
+
|
| 184 |
+
return cls(pkey)
|
.venv/lib/python3.11/site-packages/oauth2client/_pycrypto_crypt.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2015 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""pyCrypto Crypto-related routines for oauth2client."""
|
| 15 |
+
|
| 16 |
+
from Crypto.Hash import SHA256
|
| 17 |
+
from Crypto.PublicKey import RSA
|
| 18 |
+
from Crypto.Signature import PKCS1_v1_5
|
| 19 |
+
from Crypto.Util.asn1 import DerSequence
|
| 20 |
+
|
| 21 |
+
from oauth2client import _helpers
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class PyCryptoVerifier(object):
|
| 25 |
+
"""Verifies the signature on a message."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, pubkey):
|
| 28 |
+
"""Constructor.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
pubkey: OpenSSL.crypto.PKey (or equiv), The public key to verify
|
| 32 |
+
with.
|
| 33 |
+
"""
|
| 34 |
+
self._pubkey = pubkey
|
| 35 |
+
|
| 36 |
+
def verify(self, message, signature):
|
| 37 |
+
"""Verifies a message against a signature.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
message: string or bytes, The message to verify. If string, will be
|
| 41 |
+
encoded to bytes as utf-8.
|
| 42 |
+
signature: string or bytes, The signature on the message.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
True if message was signed by the private key associated with the
|
| 46 |
+
public key that this object was constructed with.
|
| 47 |
+
"""
|
| 48 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 49 |
+
return PKCS1_v1_5.new(self._pubkey).verify(
|
| 50 |
+
SHA256.new(message), signature)
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def from_string(key_pem, is_x509_cert):
|
| 54 |
+
"""Construct a Verified instance from a string.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
key_pem: string, public key in PEM format.
|
| 58 |
+
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
|
| 59 |
+
is expected to be an RSA key in PEM format.
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Verifier instance.
|
| 63 |
+
"""
|
| 64 |
+
if is_x509_cert:
|
| 65 |
+
key_pem = _helpers._to_bytes(key_pem)
|
| 66 |
+
pemLines = key_pem.replace(b' ', b'').split()
|
| 67 |
+
certDer = _helpers._urlsafe_b64decode(b''.join(pemLines[1:-1]))
|
| 68 |
+
certSeq = DerSequence()
|
| 69 |
+
certSeq.decode(certDer)
|
| 70 |
+
tbsSeq = DerSequence()
|
| 71 |
+
tbsSeq.decode(certSeq[0])
|
| 72 |
+
pubkey = RSA.importKey(tbsSeq[6])
|
| 73 |
+
else:
|
| 74 |
+
pubkey = RSA.importKey(key_pem)
|
| 75 |
+
return PyCryptoVerifier(pubkey)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class PyCryptoSigner(object):
|
| 79 |
+
"""Signs messages with a private key."""
|
| 80 |
+
|
| 81 |
+
def __init__(self, pkey):
|
| 82 |
+
"""Constructor.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
| 86 |
+
"""
|
| 87 |
+
self._key = pkey
|
| 88 |
+
|
| 89 |
+
def sign(self, message):
|
| 90 |
+
"""Signs a message.
|
| 91 |
+
|
| 92 |
+
Args:
|
| 93 |
+
message: string, Message to be signed.
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
string, The signature of the message for the given key.
|
| 97 |
+
"""
|
| 98 |
+
message = _helpers._to_bytes(message, encoding='utf-8')
|
| 99 |
+
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
|
| 100 |
+
|
| 101 |
+
@staticmethod
|
| 102 |
+
def from_string(key, password='notasecret'):
|
| 103 |
+
"""Construct a Signer instance from a string.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
key: string, private key in PEM format.
|
| 107 |
+
password: string, password for private key file. Unused for PEM
|
| 108 |
+
files.
|
| 109 |
+
|
| 110 |
+
Returns:
|
| 111 |
+
Signer instance.
|
| 112 |
+
|
| 113 |
+
Raises:
|
| 114 |
+
NotImplementedError if the key isn't in PEM format.
|
| 115 |
+
"""
|
| 116 |
+
parsed_pem_key = _helpers._parse_pem_key(_helpers._to_bytes(key))
|
| 117 |
+
if parsed_pem_key:
|
| 118 |
+
pkey = RSA.importKey(parsed_pem_key)
|
| 119 |
+
else:
|
| 120 |
+
raise NotImplementedError(
|
| 121 |
+
'No key in PEM format was detected. This implementation '
|
| 122 |
+
'can only use the PyCrypto library for keys in PEM '
|
| 123 |
+
'format.')
|
| 124 |
+
return PyCryptoSigner(pkey)
|
.venv/lib/python3.11/site-packages/oauth2client/client.py
ADDED
|
@@ -0,0 +1,2170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""An OAuth 2.0 client.
|
| 16 |
+
|
| 17 |
+
Tools for interacting with OAuth 2.0 protected resources.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import collections
|
| 21 |
+
import copy
|
| 22 |
+
import datetime
|
| 23 |
+
import json
|
| 24 |
+
import logging
|
| 25 |
+
import os
|
| 26 |
+
import shutil
|
| 27 |
+
import socket
|
| 28 |
+
import sys
|
| 29 |
+
import tempfile
|
| 30 |
+
|
| 31 |
+
import six
|
| 32 |
+
from six.moves import http_client
|
| 33 |
+
from six.moves import urllib
|
| 34 |
+
|
| 35 |
+
import oauth2client
|
| 36 |
+
from oauth2client import _helpers
|
| 37 |
+
from oauth2client import _pkce
|
| 38 |
+
from oauth2client import clientsecrets
|
| 39 |
+
from oauth2client import transport
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
HAS_OPENSSL = False
|
| 43 |
+
HAS_CRYPTO = False
|
| 44 |
+
try:
|
| 45 |
+
from oauth2client import crypt
|
| 46 |
+
HAS_CRYPTO = True
|
| 47 |
+
HAS_OPENSSL = crypt.OpenSSLVerifier is not None
|
| 48 |
+
except ImportError: # pragma: NO COVER
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
logger = logging.getLogger(__name__)
|
| 53 |
+
|
| 54 |
+
# Expiry is stored in RFC3339 UTC format
|
| 55 |
+
EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
| 56 |
+
|
| 57 |
+
# Which certs to use to validate id_tokens received.
|
| 58 |
+
ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
|
| 59 |
+
# This symbol previously had a typo in the name; we keep the old name
|
| 60 |
+
# around for now, but will remove it in the future.
|
| 61 |
+
ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS
|
| 62 |
+
|
| 63 |
+
# Constant to use for the out of band OAuth 2.0 flow.
|
| 64 |
+
OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
|
| 65 |
+
|
| 66 |
+
# The value representing user credentials.
|
| 67 |
+
AUTHORIZED_USER = 'authorized_user'
|
| 68 |
+
|
| 69 |
+
# The value representing service account credentials.
|
| 70 |
+
SERVICE_ACCOUNT = 'service_account'
|
| 71 |
+
|
| 72 |
+
# The environment variable pointing the file with local
|
| 73 |
+
# Application Default Credentials.
|
| 74 |
+
GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS'
|
| 75 |
+
# The ~/.config subdirectory containing gcloud credentials. Intended
|
| 76 |
+
# to be swapped out in tests.
|
| 77 |
+
_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud'
|
| 78 |
+
# The environment variable name which can replace ~/.config if set.
|
| 79 |
+
_CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG'
|
| 80 |
+
|
| 81 |
+
# The error message we show users when we can't find the Application
|
| 82 |
+
# Default Credentials.
|
| 83 |
+
ADC_HELP_MSG = (
|
| 84 |
+
'The Application Default Credentials are not available. They are '
|
| 85 |
+
'available if running in Google Compute Engine. Otherwise, the '
|
| 86 |
+
'environment variable ' +
|
| 87 |
+
GOOGLE_APPLICATION_CREDENTIALS +
|
| 88 |
+
' must be defined pointing to a file defining the credentials. See '
|
| 89 |
+
'https://developers.google.com/accounts/docs/'
|
| 90 |
+
'application-default-credentials for more information.')
|
| 91 |
+
|
| 92 |
+
_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json'
|
| 93 |
+
|
| 94 |
+
# The access token along with the seconds in which it expires.
|
| 95 |
+
AccessTokenInfo = collections.namedtuple(
|
| 96 |
+
'AccessTokenInfo', ['access_token', 'expires_in'])
|
| 97 |
+
|
| 98 |
+
DEFAULT_ENV_NAME = 'UNKNOWN'
|
| 99 |
+
|
| 100 |
+
# If set to True _get_environment avoid GCE check (_detect_gce_environment)
|
| 101 |
+
NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False')
|
| 102 |
+
|
| 103 |
+
# Timeout in seconds to wait for the GCE metadata server when detecting the
|
| 104 |
+
# GCE environment.
|
| 105 |
+
try:
|
| 106 |
+
GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
|
| 107 |
+
except ValueError: # pragma: NO COVER
|
| 108 |
+
GCE_METADATA_TIMEOUT = 3
|
| 109 |
+
|
| 110 |
+
_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
|
| 111 |
+
_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254')
|
| 112 |
+
_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header
|
| 113 |
+
_DESIRED_METADATA_FLAVOR = 'Google'
|
| 114 |
+
_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
|
| 115 |
+
|
| 116 |
+
# Expose utcnow() at module level to allow for
|
| 117 |
+
# easier testing (by replacing with a stub).
|
| 118 |
+
_UTCNOW = datetime.datetime.utcnow
|
| 119 |
+
|
| 120 |
+
# NOTE: These names were previously defined in this module but have been
|
| 121 |
+
# moved into `oauth2client.transport`,
|
| 122 |
+
clean_headers = transport.clean_headers
|
| 123 |
+
MemoryCache = transport.MemoryCache
|
| 124 |
+
REFRESH_STATUS_CODES = transport.REFRESH_STATUS_CODES
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class SETTINGS(object):
|
| 128 |
+
"""Settings namespace for globally defined values."""
|
| 129 |
+
env_name = None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class Error(Exception):
|
| 133 |
+
"""Base error for this module."""
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class FlowExchangeError(Error):
|
| 137 |
+
"""Error trying to exchange an authorization grant for an access token."""
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class AccessTokenRefreshError(Error):
|
| 141 |
+
"""Error trying to refresh an expired access token."""
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class HttpAccessTokenRefreshError(AccessTokenRefreshError):
|
| 145 |
+
"""Error (with HTTP status) trying to refresh an expired access token."""
|
| 146 |
+
def __init__(self, *args, **kwargs):
|
| 147 |
+
super(HttpAccessTokenRefreshError, self).__init__(*args)
|
| 148 |
+
self.status = kwargs.get('status')
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class TokenRevokeError(Error):
|
| 152 |
+
"""Error trying to revoke a token."""
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class UnknownClientSecretsFlowError(Error):
|
| 156 |
+
"""The client secrets file called for an unknown type of OAuth 2.0 flow."""
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class AccessTokenCredentialsError(Error):
|
| 160 |
+
"""Having only the access_token means no refresh is possible."""
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class VerifyJwtTokenError(Error):
|
| 164 |
+
"""Could not retrieve certificates for validation."""
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class NonAsciiHeaderError(Error):
|
| 168 |
+
"""Header names and values must be ASCII strings."""
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class ApplicationDefaultCredentialsError(Error):
|
| 172 |
+
"""Error retrieving the Application Default Credentials."""
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class OAuth2DeviceCodeError(Error):
|
| 176 |
+
"""Error trying to retrieve a device code."""
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class CryptoUnavailableError(Error, NotImplementedError):
|
| 180 |
+
"""Raised when a crypto library is required, but none is available."""
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _parse_expiry(expiry):
|
| 184 |
+
if expiry and isinstance(expiry, datetime.datetime):
|
| 185 |
+
return expiry.strftime(EXPIRY_FORMAT)
|
| 186 |
+
else:
|
| 187 |
+
return None
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class Credentials(object):
|
| 191 |
+
"""Base class for all Credentials objects.
|
| 192 |
+
|
| 193 |
+
Subclasses must define an authorize() method that applies the credentials
|
| 194 |
+
to an HTTP transport.
|
| 195 |
+
|
| 196 |
+
Subclasses must also specify a classmethod named 'from_json' that takes a
|
| 197 |
+
JSON string as input and returns an instantiated Credentials object.
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
NON_SERIALIZED_MEMBERS = frozenset(['store'])
|
| 201 |
+
|
| 202 |
+
def authorize(self, http):
|
| 203 |
+
"""Take an httplib2.Http instance (or equivalent) and authorizes it.
|
| 204 |
+
|
| 205 |
+
Authorizes it for the set of credentials, usually by replacing
|
| 206 |
+
http.request() with a method that adds in the appropriate headers and
|
| 207 |
+
then delegates to the original Http.request() method.
|
| 208 |
+
|
| 209 |
+
Args:
|
| 210 |
+
http: httplib2.Http, an http object to be used to make the refresh
|
| 211 |
+
request.
|
| 212 |
+
"""
|
| 213 |
+
raise NotImplementedError
|
| 214 |
+
|
| 215 |
+
def refresh(self, http):
|
| 216 |
+
"""Forces a refresh of the access_token.
|
| 217 |
+
|
| 218 |
+
Args:
|
| 219 |
+
http: httplib2.Http, an http object to be used to make the refresh
|
| 220 |
+
request.
|
| 221 |
+
"""
|
| 222 |
+
raise NotImplementedError
|
| 223 |
+
|
| 224 |
+
def revoke(self, http):
|
| 225 |
+
"""Revokes a refresh_token and makes the credentials void.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
http: httplib2.Http, an http object to be used to make the revoke
|
| 229 |
+
request.
|
| 230 |
+
"""
|
| 231 |
+
raise NotImplementedError
|
| 232 |
+
|
| 233 |
+
def apply(self, headers):
|
| 234 |
+
"""Add the authorization to the headers.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
headers: dict, the headers to add the Authorization header to.
|
| 238 |
+
"""
|
| 239 |
+
raise NotImplementedError
|
| 240 |
+
|
| 241 |
+
def _to_json(self, strip, to_serialize=None):
|
| 242 |
+
"""Utility function that creates JSON repr. of a Credentials object.
|
| 243 |
+
|
| 244 |
+
Args:
|
| 245 |
+
strip: array, An array of names of members to exclude from the
|
| 246 |
+
JSON.
|
| 247 |
+
to_serialize: dict, (Optional) The properties for this object
|
| 248 |
+
that will be serialized. This allows callers to
|
| 249 |
+
modify before serializing.
|
| 250 |
+
|
| 251 |
+
Returns:
|
| 252 |
+
string, a JSON representation of this instance, suitable to pass to
|
| 253 |
+
from_json().
|
| 254 |
+
"""
|
| 255 |
+
curr_type = self.__class__
|
| 256 |
+
if to_serialize is None:
|
| 257 |
+
to_serialize = copy.copy(self.__dict__)
|
| 258 |
+
else:
|
| 259 |
+
# Assumes it is a str->str dictionary, so we don't deep copy.
|
| 260 |
+
to_serialize = copy.copy(to_serialize)
|
| 261 |
+
for member in strip:
|
| 262 |
+
if member in to_serialize:
|
| 263 |
+
del to_serialize[member]
|
| 264 |
+
to_serialize['token_expiry'] = _parse_expiry(
|
| 265 |
+
to_serialize.get('token_expiry'))
|
| 266 |
+
# Add in information we will need later to reconstitute this instance.
|
| 267 |
+
to_serialize['_class'] = curr_type.__name__
|
| 268 |
+
to_serialize['_module'] = curr_type.__module__
|
| 269 |
+
for key, val in to_serialize.items():
|
| 270 |
+
if isinstance(val, bytes):
|
| 271 |
+
to_serialize[key] = val.decode('utf-8')
|
| 272 |
+
if isinstance(val, set):
|
| 273 |
+
to_serialize[key] = list(val)
|
| 274 |
+
return json.dumps(to_serialize)
|
| 275 |
+
|
| 276 |
+
def to_json(self):
|
| 277 |
+
"""Creating a JSON representation of an instance of Credentials.
|
| 278 |
+
|
| 279 |
+
Returns:
|
| 280 |
+
string, a JSON representation of this instance, suitable to pass to
|
| 281 |
+
from_json().
|
| 282 |
+
"""
|
| 283 |
+
return self._to_json(self.NON_SERIALIZED_MEMBERS)
|
| 284 |
+
|
| 285 |
+
@classmethod
|
| 286 |
+
def new_from_json(cls, json_data):
|
| 287 |
+
"""Utility class method to instantiate a Credentials subclass from JSON.
|
| 288 |
+
|
| 289 |
+
Expects the JSON string to have been produced by to_json().
|
| 290 |
+
|
| 291 |
+
Args:
|
| 292 |
+
json_data: string or bytes, JSON from to_json().
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
An instance of the subclass of Credentials that was serialized with
|
| 296 |
+
to_json().
|
| 297 |
+
"""
|
| 298 |
+
json_data_as_unicode = _helpers._from_bytes(json_data)
|
| 299 |
+
data = json.loads(json_data_as_unicode)
|
| 300 |
+
# Find and call the right classmethod from_json() to restore
|
| 301 |
+
# the object.
|
| 302 |
+
module_name = data['_module']
|
| 303 |
+
try:
|
| 304 |
+
module_obj = __import__(module_name)
|
| 305 |
+
except ImportError:
|
| 306 |
+
# In case there's an object from the old package structure,
|
| 307 |
+
# update it
|
| 308 |
+
module_name = module_name.replace('.googleapiclient', '')
|
| 309 |
+
module_obj = __import__(module_name)
|
| 310 |
+
|
| 311 |
+
module_obj = __import__(module_name,
|
| 312 |
+
fromlist=module_name.split('.')[:-1])
|
| 313 |
+
kls = getattr(module_obj, data['_class'])
|
| 314 |
+
return kls.from_json(json_data_as_unicode)
|
| 315 |
+
|
| 316 |
+
@classmethod
|
| 317 |
+
def from_json(cls, unused_data):
|
| 318 |
+
"""Instantiate a Credentials object from a JSON description of it.
|
| 319 |
+
|
| 320 |
+
The JSON should have been produced by calling .to_json() on the object.
|
| 321 |
+
|
| 322 |
+
Args:
|
| 323 |
+
unused_data: dict, A deserialized JSON object.
|
| 324 |
+
|
| 325 |
+
Returns:
|
| 326 |
+
An instance of a Credentials subclass.
|
| 327 |
+
"""
|
| 328 |
+
return Credentials()
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class Flow(object):
|
| 332 |
+
"""Base class for all Flow objects."""
|
| 333 |
+
pass
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class Storage(object):
|
| 337 |
+
"""Base class for all Storage objects.
|
| 338 |
+
|
| 339 |
+
Store and retrieve a single credential. This class supports locking
|
| 340 |
+
such that multiple processes and threads can operate on a single
|
| 341 |
+
store.
|
| 342 |
+
"""
|
| 343 |
+
def __init__(self, lock=None):
|
| 344 |
+
"""Create a Storage instance.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
lock: An optional threading.Lock-like object. Must implement at
|
| 348 |
+
least acquire() and release(). Does not need to be
|
| 349 |
+
re-entrant.
|
| 350 |
+
"""
|
| 351 |
+
self._lock = lock
|
| 352 |
+
|
| 353 |
+
def acquire_lock(self):
|
| 354 |
+
"""Acquires any lock necessary to access this Storage.
|
| 355 |
+
|
| 356 |
+
This lock is not reentrant.
|
| 357 |
+
"""
|
| 358 |
+
if self._lock is not None:
|
| 359 |
+
self._lock.acquire()
|
| 360 |
+
|
| 361 |
+
def release_lock(self):
|
| 362 |
+
"""Release the Storage lock.
|
| 363 |
+
|
| 364 |
+
Trying to release a lock that isn't held will result in a
|
| 365 |
+
RuntimeError in the case of a threading.Lock or multiprocessing.Lock.
|
| 366 |
+
"""
|
| 367 |
+
if self._lock is not None:
|
| 368 |
+
self._lock.release()
|
| 369 |
+
|
| 370 |
+
def locked_get(self):
|
| 371 |
+
"""Retrieve credential.
|
| 372 |
+
|
| 373 |
+
The Storage lock must be held when this is called.
|
| 374 |
+
|
| 375 |
+
Returns:
|
| 376 |
+
oauth2client.client.Credentials
|
| 377 |
+
"""
|
| 378 |
+
raise NotImplementedError
|
| 379 |
+
|
| 380 |
+
def locked_put(self, credentials):
|
| 381 |
+
"""Write a credential.
|
| 382 |
+
|
| 383 |
+
The Storage lock must be held when this is called.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
credentials: Credentials, the credentials to store.
|
| 387 |
+
"""
|
| 388 |
+
raise NotImplementedError
|
| 389 |
+
|
| 390 |
+
def locked_delete(self):
|
| 391 |
+
"""Delete a credential.
|
| 392 |
+
|
| 393 |
+
The Storage lock must be held when this is called.
|
| 394 |
+
"""
|
| 395 |
+
raise NotImplementedError
|
| 396 |
+
|
| 397 |
+
def get(self):
|
| 398 |
+
"""Retrieve credential.
|
| 399 |
+
|
| 400 |
+
The Storage lock must *not* be held when this is called.
|
| 401 |
+
|
| 402 |
+
Returns:
|
| 403 |
+
oauth2client.client.Credentials
|
| 404 |
+
"""
|
| 405 |
+
self.acquire_lock()
|
| 406 |
+
try:
|
| 407 |
+
return self.locked_get()
|
| 408 |
+
finally:
|
| 409 |
+
self.release_lock()
|
| 410 |
+
|
| 411 |
+
def put(self, credentials):
|
| 412 |
+
"""Write a credential.
|
| 413 |
+
|
| 414 |
+
The Storage lock must be held when this is called.
|
| 415 |
+
|
| 416 |
+
Args:
|
| 417 |
+
credentials: Credentials, the credentials to store.
|
| 418 |
+
"""
|
| 419 |
+
self.acquire_lock()
|
| 420 |
+
try:
|
| 421 |
+
self.locked_put(credentials)
|
| 422 |
+
finally:
|
| 423 |
+
self.release_lock()
|
| 424 |
+
|
| 425 |
+
def delete(self):
|
| 426 |
+
"""Delete credential.
|
| 427 |
+
|
| 428 |
+
Frees any resources associated with storing the credential.
|
| 429 |
+
The Storage lock must *not* be held when this is called.
|
| 430 |
+
|
| 431 |
+
Returns:
|
| 432 |
+
None
|
| 433 |
+
"""
|
| 434 |
+
self.acquire_lock()
|
| 435 |
+
try:
|
| 436 |
+
return self.locked_delete()
|
| 437 |
+
finally:
|
| 438 |
+
self.release_lock()
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
class OAuth2Credentials(Credentials):
|
| 442 |
+
"""Credentials object for OAuth 2.0.
|
| 443 |
+
|
| 444 |
+
Credentials can be applied to an httplib2.Http object using the authorize()
|
| 445 |
+
method, which then adds the OAuth 2.0 access token to each request.
|
| 446 |
+
|
| 447 |
+
OAuth2Credentials objects may be safely pickled and unpickled.
|
| 448 |
+
"""
|
| 449 |
+
|
| 450 |
+
@_helpers.positional(8)
|
| 451 |
+
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
| 452 |
+
token_expiry, token_uri, user_agent, revoke_uri=None,
|
| 453 |
+
id_token=None, token_response=None, scopes=None,
|
| 454 |
+
token_info_uri=None, id_token_jwt=None):
|
| 455 |
+
"""Create an instance of OAuth2Credentials.
|
| 456 |
+
|
| 457 |
+
This constructor is not usually called by the user, instead
|
| 458 |
+
OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
|
| 459 |
+
|
| 460 |
+
Args:
|
| 461 |
+
access_token: string, access token.
|
| 462 |
+
client_id: string, client identifier.
|
| 463 |
+
client_secret: string, client secret.
|
| 464 |
+
refresh_token: string, refresh token.
|
| 465 |
+
token_expiry: datetime, when the access_token expires.
|
| 466 |
+
token_uri: string, URI of token endpoint.
|
| 467 |
+
user_agent: string, The HTTP User-Agent to provide for this
|
| 468 |
+
application.
|
| 469 |
+
revoke_uri: string, URI for revoke endpoint. Defaults to None; a
|
| 470 |
+
token can't be revoked if this is None.
|
| 471 |
+
id_token: object, The identity of the resource owner.
|
| 472 |
+
token_response: dict, the decoded response to the token request.
|
| 473 |
+
None if a token hasn't been requested yet. Stored
|
| 474 |
+
because some providers (e.g. wordpress.com) include
|
| 475 |
+
extra fields that clients may want.
|
| 476 |
+
scopes: list, authorized scopes for these credentials.
|
| 477 |
+
token_info_uri: string, the URI for the token info endpoint.
|
| 478 |
+
Defaults to None; scopes can not be refreshed if
|
| 479 |
+
this is None.
|
| 480 |
+
id_token_jwt: string, the encoded and signed identity JWT. The
|
| 481 |
+
decoded version of this is stored in id_token.
|
| 482 |
+
|
| 483 |
+
Notes:
|
| 484 |
+
store: callable, A callable that when passed a Credential
|
| 485 |
+
will store the credential back to where it came from.
|
| 486 |
+
This is needed to store the latest access_token if it
|
| 487 |
+
has expired and been refreshed.
|
| 488 |
+
"""
|
| 489 |
+
self.access_token = access_token
|
| 490 |
+
self.client_id = client_id
|
| 491 |
+
self.client_secret = client_secret
|
| 492 |
+
self.refresh_token = refresh_token
|
| 493 |
+
self.store = None
|
| 494 |
+
self.token_expiry = token_expiry
|
| 495 |
+
self.token_uri = token_uri
|
| 496 |
+
self.user_agent = user_agent
|
| 497 |
+
self.revoke_uri = revoke_uri
|
| 498 |
+
self.id_token = id_token
|
| 499 |
+
self.id_token_jwt = id_token_jwt
|
| 500 |
+
self.token_response = token_response
|
| 501 |
+
self.scopes = set(_helpers.string_to_scopes(scopes or []))
|
| 502 |
+
self.token_info_uri = token_info_uri
|
| 503 |
+
|
| 504 |
+
# True if the credentials have been revoked or expired and can't be
|
| 505 |
+
# refreshed.
|
| 506 |
+
self.invalid = False
|
| 507 |
+
|
| 508 |
+
def authorize(self, http):
|
| 509 |
+
"""Authorize an httplib2.Http instance with these credentials.
|
| 510 |
+
|
| 511 |
+
The modified http.request method will add authentication headers to
|
| 512 |
+
each request and will refresh access_tokens when a 401 is received on a
|
| 513 |
+
request. In addition the http.request method has a credentials
|
| 514 |
+
property, http.request.credentials, which is the Credentials object
|
| 515 |
+
that authorized it.
|
| 516 |
+
|
| 517 |
+
Args:
|
| 518 |
+
http: An instance of ``httplib2.Http`` or something that acts
|
| 519 |
+
like it.
|
| 520 |
+
|
| 521 |
+
Returns:
|
| 522 |
+
A modified instance of http that was passed in.
|
| 523 |
+
|
| 524 |
+
Example::
|
| 525 |
+
|
| 526 |
+
h = httplib2.Http()
|
| 527 |
+
h = credentials.authorize(h)
|
| 528 |
+
|
| 529 |
+
You can't create a new OAuth subclass of httplib2.Authentication
|
| 530 |
+
because it never gets passed the absolute URI, which is needed for
|
| 531 |
+
signing. So instead we have to overload 'request' with a closure
|
| 532 |
+
that adds in the Authorization header and then calls the original
|
| 533 |
+
version of 'request()'.
|
| 534 |
+
"""
|
| 535 |
+
transport.wrap_http_for_auth(self, http)
|
| 536 |
+
return http
|
| 537 |
+
|
| 538 |
+
def refresh(self, http):
|
| 539 |
+
"""Forces a refresh of the access_token.
|
| 540 |
+
|
| 541 |
+
Args:
|
| 542 |
+
http: httplib2.Http, an http object to be used to make the refresh
|
| 543 |
+
request.
|
| 544 |
+
"""
|
| 545 |
+
self._refresh(http)
|
| 546 |
+
|
| 547 |
+
def revoke(self, http):
|
| 548 |
+
"""Revokes a refresh_token and makes the credentials void.
|
| 549 |
+
|
| 550 |
+
Args:
|
| 551 |
+
http: httplib2.Http, an http object to be used to make the revoke
|
| 552 |
+
request.
|
| 553 |
+
"""
|
| 554 |
+
self._revoke(http)
|
| 555 |
+
|
| 556 |
+
def apply(self, headers):
|
| 557 |
+
"""Add the authorization to the headers.
|
| 558 |
+
|
| 559 |
+
Args:
|
| 560 |
+
headers: dict, the headers to add the Authorization header to.
|
| 561 |
+
"""
|
| 562 |
+
headers['Authorization'] = 'Bearer ' + self.access_token
|
| 563 |
+
|
| 564 |
+
def has_scopes(self, scopes):
|
| 565 |
+
"""Verify that the credentials are authorized for the given scopes.
|
| 566 |
+
|
| 567 |
+
Returns True if the credentials authorized scopes contain all of the
|
| 568 |
+
scopes given.
|
| 569 |
+
|
| 570 |
+
Args:
|
| 571 |
+
scopes: list or string, the scopes to check.
|
| 572 |
+
|
| 573 |
+
Notes:
|
| 574 |
+
There are cases where the credentials are unaware of which scopes
|
| 575 |
+
are authorized. Notably, credentials obtained and stored before
|
| 576 |
+
this code was added will not have scopes, AccessTokenCredentials do
|
| 577 |
+
not have scopes. In both cases, you can use refresh_scopes() to
|
| 578 |
+
obtain the canonical set of scopes.
|
| 579 |
+
"""
|
| 580 |
+
scopes = _helpers.string_to_scopes(scopes)
|
| 581 |
+
return set(scopes).issubset(self.scopes)
|
| 582 |
+
|
| 583 |
+
def retrieve_scopes(self, http):
|
| 584 |
+
"""Retrieves the canonical list of scopes for this access token.
|
| 585 |
+
|
| 586 |
+
Gets the scopes from the OAuth2 provider.
|
| 587 |
+
|
| 588 |
+
Args:
|
| 589 |
+
http: httplib2.Http, an http object to be used to make the refresh
|
| 590 |
+
request.
|
| 591 |
+
|
| 592 |
+
Returns:
|
| 593 |
+
A set of strings containing the canonical list of scopes.
|
| 594 |
+
"""
|
| 595 |
+
self._retrieve_scopes(http)
|
| 596 |
+
return self.scopes
|
| 597 |
+
|
| 598 |
+
@classmethod
|
| 599 |
+
def from_json(cls, json_data):
|
| 600 |
+
"""Instantiate a Credentials object from a JSON description of it.
|
| 601 |
+
|
| 602 |
+
The JSON should have been produced by calling .to_json() on the object.
|
| 603 |
+
|
| 604 |
+
Args:
|
| 605 |
+
json_data: string or bytes, JSON to deserialize.
|
| 606 |
+
|
| 607 |
+
Returns:
|
| 608 |
+
An instance of a Credentials subclass.
|
| 609 |
+
"""
|
| 610 |
+
data = json.loads(_helpers._from_bytes(json_data))
|
| 611 |
+
if (data.get('token_expiry') and
|
| 612 |
+
not isinstance(data['token_expiry'], datetime.datetime)):
|
| 613 |
+
try:
|
| 614 |
+
data['token_expiry'] = datetime.datetime.strptime(
|
| 615 |
+
data['token_expiry'], EXPIRY_FORMAT)
|
| 616 |
+
except ValueError:
|
| 617 |
+
data['token_expiry'] = None
|
| 618 |
+
retval = cls(
|
| 619 |
+
data['access_token'],
|
| 620 |
+
data['client_id'],
|
| 621 |
+
data['client_secret'],
|
| 622 |
+
data['refresh_token'],
|
| 623 |
+
data['token_expiry'],
|
| 624 |
+
data['token_uri'],
|
| 625 |
+
data['user_agent'],
|
| 626 |
+
revoke_uri=data.get('revoke_uri', None),
|
| 627 |
+
id_token=data.get('id_token', None),
|
| 628 |
+
id_token_jwt=data.get('id_token_jwt', None),
|
| 629 |
+
token_response=data.get('token_response', None),
|
| 630 |
+
scopes=data.get('scopes', None),
|
| 631 |
+
token_info_uri=data.get('token_info_uri', None))
|
| 632 |
+
retval.invalid = data['invalid']
|
| 633 |
+
return retval
|
| 634 |
+
|
| 635 |
+
@property
|
| 636 |
+
def access_token_expired(self):
|
| 637 |
+
"""True if the credential is expired or invalid.
|
| 638 |
+
|
| 639 |
+
If the token_expiry isn't set, we assume the token doesn't expire.
|
| 640 |
+
"""
|
| 641 |
+
if self.invalid:
|
| 642 |
+
return True
|
| 643 |
+
|
| 644 |
+
if not self.token_expiry:
|
| 645 |
+
return False
|
| 646 |
+
|
| 647 |
+
now = _UTCNOW()
|
| 648 |
+
if now >= self.token_expiry:
|
| 649 |
+
logger.info('access_token is expired. Now: %s, token_expiry: %s',
|
| 650 |
+
now, self.token_expiry)
|
| 651 |
+
return True
|
| 652 |
+
return False
|
| 653 |
+
|
| 654 |
+
def get_access_token(self, http=None):
|
| 655 |
+
"""Return the access token and its expiration information.
|
| 656 |
+
|
| 657 |
+
If the token does not exist, get one.
|
| 658 |
+
If the token expired, refresh it.
|
| 659 |
+
"""
|
| 660 |
+
if not self.access_token or self.access_token_expired:
|
| 661 |
+
if not http:
|
| 662 |
+
http = transport.get_http_object()
|
| 663 |
+
self.refresh(http)
|
| 664 |
+
return AccessTokenInfo(access_token=self.access_token,
|
| 665 |
+
expires_in=self._expires_in())
|
| 666 |
+
|
| 667 |
+
def set_store(self, store):
|
| 668 |
+
"""Set the Storage for the credential.
|
| 669 |
+
|
| 670 |
+
Args:
|
| 671 |
+
store: Storage, an implementation of Storage object.
|
| 672 |
+
This is needed to store the latest access_token if it
|
| 673 |
+
has expired and been refreshed. This implementation uses
|
| 674 |
+
locking to check for updates before updating the
|
| 675 |
+
access_token.
|
| 676 |
+
"""
|
| 677 |
+
self.store = store
|
| 678 |
+
|
| 679 |
+
def _expires_in(self):
|
| 680 |
+
"""Return the number of seconds until this token expires.
|
| 681 |
+
|
| 682 |
+
If token_expiry is in the past, this method will return 0, meaning the
|
| 683 |
+
token has already expired.
|
| 684 |
+
|
| 685 |
+
If token_expiry is None, this method will return None. Note that
|
| 686 |
+
returning 0 in such a case would not be fair: the token may still be
|
| 687 |
+
valid; we just don't know anything about it.
|
| 688 |
+
"""
|
| 689 |
+
if self.token_expiry:
|
| 690 |
+
now = _UTCNOW()
|
| 691 |
+
if self.token_expiry > now:
|
| 692 |
+
time_delta = self.token_expiry - now
|
| 693 |
+
# TODO(orestica): return time_delta.total_seconds()
|
| 694 |
+
# once dropping support for Python 2.6
|
| 695 |
+
return time_delta.days * 86400 + time_delta.seconds
|
| 696 |
+
else:
|
| 697 |
+
return 0
|
| 698 |
+
|
| 699 |
+
def _updateFromCredential(self, other):
|
| 700 |
+
"""Update this Credential from another instance."""
|
| 701 |
+
self.__dict__.update(other.__getstate__())
|
| 702 |
+
|
| 703 |
+
def __getstate__(self):
|
| 704 |
+
"""Trim the state down to something that can be pickled."""
|
| 705 |
+
d = copy.copy(self.__dict__)
|
| 706 |
+
del d['store']
|
| 707 |
+
return d
|
| 708 |
+
|
| 709 |
+
def __setstate__(self, state):
|
| 710 |
+
"""Reconstitute the state of the object from being pickled."""
|
| 711 |
+
self.__dict__.update(state)
|
| 712 |
+
self.store = None
|
| 713 |
+
|
| 714 |
+
def _generate_refresh_request_body(self):
|
| 715 |
+
"""Generate the body that will be used in the refresh request."""
|
| 716 |
+
body = urllib.parse.urlencode({
|
| 717 |
+
'grant_type': 'refresh_token',
|
| 718 |
+
'client_id': self.client_id,
|
| 719 |
+
'client_secret': self.client_secret,
|
| 720 |
+
'refresh_token': self.refresh_token,
|
| 721 |
+
})
|
| 722 |
+
return body
|
| 723 |
+
|
| 724 |
+
def _generate_refresh_request_headers(self):
|
| 725 |
+
"""Generate the headers that will be used in the refresh request."""
|
| 726 |
+
headers = {
|
| 727 |
+
'content-type': 'application/x-www-form-urlencoded',
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
if self.user_agent is not None:
|
| 731 |
+
headers['user-agent'] = self.user_agent
|
| 732 |
+
|
| 733 |
+
return headers
|
| 734 |
+
|
| 735 |
+
def _refresh(self, http):
|
| 736 |
+
"""Refreshes the access_token.
|
| 737 |
+
|
| 738 |
+
This method first checks by reading the Storage object if available.
|
| 739 |
+
If a refresh is still needed, it holds the Storage lock until the
|
| 740 |
+
refresh is completed.
|
| 741 |
+
|
| 742 |
+
Args:
|
| 743 |
+
http: an object to be used to make HTTP requests.
|
| 744 |
+
|
| 745 |
+
Raises:
|
| 746 |
+
HttpAccessTokenRefreshError: When the refresh fails.
|
| 747 |
+
"""
|
| 748 |
+
if not self.store:
|
| 749 |
+
self._do_refresh_request(http)
|
| 750 |
+
else:
|
| 751 |
+
self.store.acquire_lock()
|
| 752 |
+
try:
|
| 753 |
+
new_cred = self.store.locked_get()
|
| 754 |
+
|
| 755 |
+
if (new_cred and not new_cred.invalid and
|
| 756 |
+
new_cred.access_token != self.access_token and
|
| 757 |
+
not new_cred.access_token_expired):
|
| 758 |
+
logger.info('Updated access_token read from Storage')
|
| 759 |
+
self._updateFromCredential(new_cred)
|
| 760 |
+
else:
|
| 761 |
+
self._do_refresh_request(http)
|
| 762 |
+
finally:
|
| 763 |
+
self.store.release_lock()
|
| 764 |
+
|
| 765 |
+
def _do_refresh_request(self, http):
|
| 766 |
+
"""Refresh the access_token using the refresh_token.
|
| 767 |
+
|
| 768 |
+
Args:
|
| 769 |
+
http: an object to be used to make HTTP requests.
|
| 770 |
+
|
| 771 |
+
Raises:
|
| 772 |
+
HttpAccessTokenRefreshError: When the refresh fails.
|
| 773 |
+
"""
|
| 774 |
+
body = self._generate_refresh_request_body()
|
| 775 |
+
headers = self._generate_refresh_request_headers()
|
| 776 |
+
|
| 777 |
+
logger.info('Refreshing access_token')
|
| 778 |
+
resp, content = transport.request(
|
| 779 |
+
http, self.token_uri, method='POST',
|
| 780 |
+
body=body, headers=headers)
|
| 781 |
+
content = _helpers._from_bytes(content)
|
| 782 |
+
if resp.status == http_client.OK:
|
| 783 |
+
d = json.loads(content)
|
| 784 |
+
self.token_response = d
|
| 785 |
+
self.access_token = d['access_token']
|
| 786 |
+
self.refresh_token = d.get('refresh_token', self.refresh_token)
|
| 787 |
+
if 'expires_in' in d:
|
| 788 |
+
delta = datetime.timedelta(seconds=int(d['expires_in']))
|
| 789 |
+
self.token_expiry = delta + _UTCNOW()
|
| 790 |
+
else:
|
| 791 |
+
self.token_expiry = None
|
| 792 |
+
if 'id_token' in d:
|
| 793 |
+
self.id_token = _extract_id_token(d['id_token'])
|
| 794 |
+
self.id_token_jwt = d['id_token']
|
| 795 |
+
else:
|
| 796 |
+
self.id_token = None
|
| 797 |
+
self.id_token_jwt = None
|
| 798 |
+
# On temporary refresh errors, the user does not actually have to
|
| 799 |
+
# re-authorize, so we unflag here.
|
| 800 |
+
self.invalid = False
|
| 801 |
+
if self.store:
|
| 802 |
+
self.store.locked_put(self)
|
| 803 |
+
else:
|
| 804 |
+
# An {'error':...} response body means the token is expired or
|
| 805 |
+
# revoked, so we flag the credentials as such.
|
| 806 |
+
logger.info('Failed to retrieve access token: %s', content)
|
| 807 |
+
error_msg = 'Invalid response {0}.'.format(resp.status)
|
| 808 |
+
try:
|
| 809 |
+
d = json.loads(content)
|
| 810 |
+
if 'error' in d:
|
| 811 |
+
error_msg = d['error']
|
| 812 |
+
if 'error_description' in d:
|
| 813 |
+
error_msg += ': ' + d['error_description']
|
| 814 |
+
self.invalid = True
|
| 815 |
+
if self.store is not None:
|
| 816 |
+
self.store.locked_put(self)
|
| 817 |
+
except (TypeError, ValueError):
|
| 818 |
+
pass
|
| 819 |
+
raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
|
| 820 |
+
|
| 821 |
+
def _revoke(self, http):
|
| 822 |
+
"""Revokes this credential and deletes the stored copy (if it exists).
|
| 823 |
+
|
| 824 |
+
Args:
|
| 825 |
+
http: an object to be used to make HTTP requests.
|
| 826 |
+
"""
|
| 827 |
+
self._do_revoke(http, self.refresh_token or self.access_token)
|
| 828 |
+
|
| 829 |
+
def _do_revoke(self, http, token):
|
| 830 |
+
"""Revokes this credential and deletes the stored copy (if it exists).
|
| 831 |
+
|
| 832 |
+
Args:
|
| 833 |
+
http: an object to be used to make HTTP requests.
|
| 834 |
+
token: A string used as the token to be revoked. Can be either an
|
| 835 |
+
access_token or refresh_token.
|
| 836 |
+
|
| 837 |
+
Raises:
|
| 838 |
+
TokenRevokeError: If the revoke request does not return with a
|
| 839 |
+
200 OK.
|
| 840 |
+
"""
|
| 841 |
+
logger.info('Revoking token')
|
| 842 |
+
query_params = {'token': token}
|
| 843 |
+
token_revoke_uri = _helpers.update_query_params(
|
| 844 |
+
self.revoke_uri, query_params)
|
| 845 |
+
resp, content = transport.request(http, token_revoke_uri)
|
| 846 |
+
if resp.status == http_client.METHOD_NOT_ALLOWED:
|
| 847 |
+
body = urllib.parse.urlencode(query_params)
|
| 848 |
+
resp, content = transport.request(http, token_revoke_uri,
|
| 849 |
+
method='POST', body=body)
|
| 850 |
+
if resp.status == http_client.OK:
|
| 851 |
+
self.invalid = True
|
| 852 |
+
else:
|
| 853 |
+
error_msg = 'Invalid response {0}.'.format(resp.status)
|
| 854 |
+
try:
|
| 855 |
+
d = json.loads(_helpers._from_bytes(content))
|
| 856 |
+
if 'error' in d:
|
| 857 |
+
error_msg = d['error']
|
| 858 |
+
except (TypeError, ValueError):
|
| 859 |
+
pass
|
| 860 |
+
raise TokenRevokeError(error_msg)
|
| 861 |
+
|
| 862 |
+
if self.store:
|
| 863 |
+
self.store.delete()
|
| 864 |
+
|
| 865 |
+
def _retrieve_scopes(self, http):
|
| 866 |
+
"""Retrieves the list of authorized scopes from the OAuth2 provider.
|
| 867 |
+
|
| 868 |
+
Args:
|
| 869 |
+
http: an object to be used to make HTTP requests.
|
| 870 |
+
"""
|
| 871 |
+
self._do_retrieve_scopes(http, self.access_token)
|
| 872 |
+
|
| 873 |
+
def _do_retrieve_scopes(self, http, token):
|
| 874 |
+
"""Retrieves the list of authorized scopes from the OAuth2 provider.
|
| 875 |
+
|
| 876 |
+
Args:
|
| 877 |
+
http: an object to be used to make HTTP requests.
|
| 878 |
+
token: A string used as the token to identify the credentials to
|
| 879 |
+
the provider.
|
| 880 |
+
|
| 881 |
+
Raises:
|
| 882 |
+
Error: When refresh fails, indicating the the access token is
|
| 883 |
+
invalid.
|
| 884 |
+
"""
|
| 885 |
+
logger.info('Refreshing scopes')
|
| 886 |
+
query_params = {'access_token': token, 'fields': 'scope'}
|
| 887 |
+
token_info_uri = _helpers.update_query_params(
|
| 888 |
+
self.token_info_uri, query_params)
|
| 889 |
+
resp, content = transport.request(http, token_info_uri)
|
| 890 |
+
content = _helpers._from_bytes(content)
|
| 891 |
+
if resp.status == http_client.OK:
|
| 892 |
+
d = json.loads(content)
|
| 893 |
+
self.scopes = set(_helpers.string_to_scopes(d.get('scope', '')))
|
| 894 |
+
else:
|
| 895 |
+
error_msg = 'Invalid response {0}.'.format(resp.status)
|
| 896 |
+
try:
|
| 897 |
+
d = json.loads(content)
|
| 898 |
+
if 'error_description' in d:
|
| 899 |
+
error_msg = d['error_description']
|
| 900 |
+
except (TypeError, ValueError):
|
| 901 |
+
pass
|
| 902 |
+
raise Error(error_msg)
|
| 903 |
+
|
| 904 |
+
|
| 905 |
+
class AccessTokenCredentials(OAuth2Credentials):
|
| 906 |
+
"""Credentials object for OAuth 2.0.
|
| 907 |
+
|
| 908 |
+
Credentials can be applied to an httplib2.Http object using the
|
| 909 |
+
authorize() method, which then signs each request from that object
|
| 910 |
+
with the OAuth 2.0 access token. This set of credentials is for the
|
| 911 |
+
use case where you have acquired an OAuth 2.0 access_token from
|
| 912 |
+
another place such as a JavaScript client or another web
|
| 913 |
+
application, and wish to use it from Python. Because only the
|
| 914 |
+
access_token is present it can not be refreshed and will in time
|
| 915 |
+
expire.
|
| 916 |
+
|
| 917 |
+
AccessTokenCredentials objects may be safely pickled and unpickled.
|
| 918 |
+
|
| 919 |
+
Usage::
|
| 920 |
+
|
| 921 |
+
credentials = AccessTokenCredentials('<an access token>',
|
| 922 |
+
'my-user-agent/1.0')
|
| 923 |
+
http = httplib2.Http()
|
| 924 |
+
http = credentials.authorize(http)
|
| 925 |
+
|
| 926 |
+
Raises:
|
| 927 |
+
AccessTokenCredentialsExpired: raised when the access_token expires or
|
| 928 |
+
is revoked.
|
| 929 |
+
"""
|
| 930 |
+
|
| 931 |
+
def __init__(self, access_token, user_agent, revoke_uri=None):
|
| 932 |
+
"""Create an instance of OAuth2Credentials
|
| 933 |
+
|
| 934 |
+
This is one of the few types if Credentials that you should contrust,
|
| 935 |
+
Credentials objects are usually instantiated by a Flow.
|
| 936 |
+
|
| 937 |
+
Args:
|
| 938 |
+
access_token: string, access token.
|
| 939 |
+
user_agent: string, The HTTP User-Agent to provide for this
|
| 940 |
+
application.
|
| 941 |
+
revoke_uri: string, URI for revoke endpoint. Defaults to None; a
|
| 942 |
+
token can't be revoked if this is None.
|
| 943 |
+
"""
|
| 944 |
+
super(AccessTokenCredentials, self).__init__(
|
| 945 |
+
access_token,
|
| 946 |
+
None,
|
| 947 |
+
None,
|
| 948 |
+
None,
|
| 949 |
+
None,
|
| 950 |
+
None,
|
| 951 |
+
user_agent,
|
| 952 |
+
revoke_uri=revoke_uri)
|
| 953 |
+
|
| 954 |
+
@classmethod
|
| 955 |
+
def from_json(cls, json_data):
|
| 956 |
+
data = json.loads(_helpers._from_bytes(json_data))
|
| 957 |
+
retval = AccessTokenCredentials(
|
| 958 |
+
data['access_token'],
|
| 959 |
+
data['user_agent'])
|
| 960 |
+
return retval
|
| 961 |
+
|
| 962 |
+
def _refresh(self, http):
|
| 963 |
+
"""Refreshes the access token.
|
| 964 |
+
|
| 965 |
+
Args:
|
| 966 |
+
http: unused HTTP object.
|
| 967 |
+
|
| 968 |
+
Raises:
|
| 969 |
+
AccessTokenCredentialsError: always
|
| 970 |
+
"""
|
| 971 |
+
raise AccessTokenCredentialsError(
|
| 972 |
+
'The access_token is expired or invalid and can\'t be refreshed.')
|
| 973 |
+
|
| 974 |
+
def _revoke(self, http):
|
| 975 |
+
"""Revokes the access_token and deletes the store if available.
|
| 976 |
+
|
| 977 |
+
Args:
|
| 978 |
+
http: an object to be used to make HTTP requests.
|
| 979 |
+
"""
|
| 980 |
+
self._do_revoke(http, self.access_token)
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
def _detect_gce_environment():
|
| 984 |
+
"""Determine if the current environment is Compute Engine.
|
| 985 |
+
|
| 986 |
+
Returns:
|
| 987 |
+
Boolean indicating whether or not the current environment is Google
|
| 988 |
+
Compute Engine.
|
| 989 |
+
"""
|
| 990 |
+
# NOTE: The explicit ``timeout`` is a workaround. The underlying
|
| 991 |
+
# issue is that resolving an unknown host on some networks will take
|
| 992 |
+
# 20-30 seconds; making this timeout short fixes the issue, but
|
| 993 |
+
# could lead to false negatives in the event that we are on GCE, but
|
| 994 |
+
# the metadata resolution was particularly slow. The latter case is
|
| 995 |
+
# "unlikely".
|
| 996 |
+
http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT)
|
| 997 |
+
try:
|
| 998 |
+
response, _ = transport.request(
|
| 999 |
+
http, _GCE_METADATA_URI, headers=_GCE_HEADERS)
|
| 1000 |
+
return (
|
| 1001 |
+
response.status == http_client.OK and
|
| 1002 |
+
response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR)
|
| 1003 |
+
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
|
| 1004 |
+
logger.info('Timeout attempting to reach GCE metadata service.')
|
| 1005 |
+
return False
|
| 1006 |
+
|
| 1007 |
+
|
| 1008 |
+
def _in_gae_environment():
|
| 1009 |
+
"""Detects if the code is running in the App Engine environment.
|
| 1010 |
+
|
| 1011 |
+
Returns:
|
| 1012 |
+
True if running in the GAE environment, False otherwise.
|
| 1013 |
+
"""
|
| 1014 |
+
if SETTINGS.env_name is not None:
|
| 1015 |
+
return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL')
|
| 1016 |
+
|
| 1017 |
+
try:
|
| 1018 |
+
import google.appengine # noqa: unused import
|
| 1019 |
+
except ImportError:
|
| 1020 |
+
pass
|
| 1021 |
+
else:
|
| 1022 |
+
server_software = os.environ.get(_SERVER_SOFTWARE, '')
|
| 1023 |
+
if server_software.startswith('Google App Engine/'):
|
| 1024 |
+
SETTINGS.env_name = 'GAE_PRODUCTION'
|
| 1025 |
+
return True
|
| 1026 |
+
elif server_software.startswith('Development/'):
|
| 1027 |
+
SETTINGS.env_name = 'GAE_LOCAL'
|
| 1028 |
+
return True
|
| 1029 |
+
|
| 1030 |
+
return False
|
| 1031 |
+
|
| 1032 |
+
|
| 1033 |
+
def _in_gce_environment():
|
| 1034 |
+
"""Detect if the code is running in the Compute Engine environment.
|
| 1035 |
+
|
| 1036 |
+
Returns:
|
| 1037 |
+
True if running in the GCE environment, False otherwise.
|
| 1038 |
+
"""
|
| 1039 |
+
if SETTINGS.env_name is not None:
|
| 1040 |
+
return SETTINGS.env_name == 'GCE_PRODUCTION'
|
| 1041 |
+
|
| 1042 |
+
if NO_GCE_CHECK != 'True' and _detect_gce_environment():
|
| 1043 |
+
SETTINGS.env_name = 'GCE_PRODUCTION'
|
| 1044 |
+
return True
|
| 1045 |
+
return False
|
| 1046 |
+
|
| 1047 |
+
|
| 1048 |
+
class GoogleCredentials(OAuth2Credentials):
|
| 1049 |
+
"""Application Default Credentials for use in calling Google APIs.
|
| 1050 |
+
|
| 1051 |
+
The Application Default Credentials are being constructed as a function of
|
| 1052 |
+
the environment where the code is being run.
|
| 1053 |
+
More details can be found on this page:
|
| 1054 |
+
https://developers.google.com/accounts/docs/application-default-credentials
|
| 1055 |
+
|
| 1056 |
+
Here is an example of how to use the Application Default Credentials for a
|
| 1057 |
+
service that requires authentication::
|
| 1058 |
+
|
| 1059 |
+
from googleapiclient.discovery import build
|
| 1060 |
+
from oauth2client.client import GoogleCredentials
|
| 1061 |
+
|
| 1062 |
+
credentials = GoogleCredentials.get_application_default()
|
| 1063 |
+
service = build('compute', 'v1', credentials=credentials)
|
| 1064 |
+
|
| 1065 |
+
PROJECT = 'bamboo-machine-422'
|
| 1066 |
+
ZONE = 'us-central1-a'
|
| 1067 |
+
request = service.instances().list(project=PROJECT, zone=ZONE)
|
| 1068 |
+
response = request.execute()
|
| 1069 |
+
|
| 1070 |
+
print(response)
|
| 1071 |
+
"""
|
| 1072 |
+
|
| 1073 |
+
NON_SERIALIZED_MEMBERS = (
|
| 1074 |
+
frozenset(['_private_key']) |
|
| 1075 |
+
OAuth2Credentials.NON_SERIALIZED_MEMBERS)
|
| 1076 |
+
"""Members that aren't serialized when object is converted to JSON."""
|
| 1077 |
+
|
| 1078 |
+
def __init__(self, access_token, client_id, client_secret, refresh_token,
|
| 1079 |
+
token_expiry, token_uri, user_agent,
|
| 1080 |
+
revoke_uri=oauth2client.GOOGLE_REVOKE_URI):
|
| 1081 |
+
"""Create an instance of GoogleCredentials.
|
| 1082 |
+
|
| 1083 |
+
This constructor is not usually called by the user, instead
|
| 1084 |
+
GoogleCredentials objects are instantiated by
|
| 1085 |
+
GoogleCredentials.from_stream() or
|
| 1086 |
+
GoogleCredentials.get_application_default().
|
| 1087 |
+
|
| 1088 |
+
Args:
|
| 1089 |
+
access_token: string, access token.
|
| 1090 |
+
client_id: string, client identifier.
|
| 1091 |
+
client_secret: string, client secret.
|
| 1092 |
+
refresh_token: string, refresh token.
|
| 1093 |
+
token_expiry: datetime, when the access_token expires.
|
| 1094 |
+
token_uri: string, URI of token endpoint.
|
| 1095 |
+
user_agent: string, The HTTP User-Agent to provide for this
|
| 1096 |
+
application.
|
| 1097 |
+
revoke_uri: string, URI for revoke endpoint. Defaults to
|
| 1098 |
+
oauth2client.GOOGLE_REVOKE_URI; a token can't be
|
| 1099 |
+
revoked if this is None.
|
| 1100 |
+
"""
|
| 1101 |
+
super(GoogleCredentials, self).__init__(
|
| 1102 |
+
access_token, client_id, client_secret, refresh_token,
|
| 1103 |
+
token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
|
| 1104 |
+
|
| 1105 |
+
def create_scoped_required(self):
|
| 1106 |
+
"""Whether this Credentials object is scopeless.
|
| 1107 |
+
|
| 1108 |
+
create_scoped(scopes) method needs to be called in order to create
|
| 1109 |
+
a Credentials object for API calls.
|
| 1110 |
+
"""
|
| 1111 |
+
return False
|
| 1112 |
+
|
| 1113 |
+
def create_scoped(self, scopes):
|
| 1114 |
+
"""Create a Credentials object for the given scopes.
|
| 1115 |
+
|
| 1116 |
+
The Credentials type is preserved.
|
| 1117 |
+
"""
|
| 1118 |
+
return self
|
| 1119 |
+
|
| 1120 |
+
@classmethod
|
| 1121 |
+
def from_json(cls, json_data):
|
| 1122 |
+
# TODO(issue 388): eliminate the circularity that is the reason for
|
| 1123 |
+
# this non-top-level import.
|
| 1124 |
+
from oauth2client import service_account
|
| 1125 |
+
data = json.loads(_helpers._from_bytes(json_data))
|
| 1126 |
+
|
| 1127 |
+
# We handle service_account.ServiceAccountCredentials since it is a
|
| 1128 |
+
# possible return type of GoogleCredentials.get_application_default()
|
| 1129 |
+
if (data['_module'] == 'oauth2client.service_account' and
|
| 1130 |
+
data['_class'] == 'ServiceAccountCredentials'):
|
| 1131 |
+
return service_account.ServiceAccountCredentials.from_json(data)
|
| 1132 |
+
elif (data['_module'] == 'oauth2client.service_account' and
|
| 1133 |
+
data['_class'] == '_JWTAccessCredentials'):
|
| 1134 |
+
return service_account._JWTAccessCredentials.from_json(data)
|
| 1135 |
+
|
| 1136 |
+
token_expiry = _parse_expiry(data.get('token_expiry'))
|
| 1137 |
+
google_credentials = cls(
|
| 1138 |
+
data['access_token'],
|
| 1139 |
+
data['client_id'],
|
| 1140 |
+
data['client_secret'],
|
| 1141 |
+
data['refresh_token'],
|
| 1142 |
+
token_expiry,
|
| 1143 |
+
data['token_uri'],
|
| 1144 |
+
data['user_agent'],
|
| 1145 |
+
revoke_uri=data.get('revoke_uri', None))
|
| 1146 |
+
google_credentials.invalid = data['invalid']
|
| 1147 |
+
return google_credentials
|
| 1148 |
+
|
| 1149 |
+
@property
|
| 1150 |
+
def serialization_data(self):
|
| 1151 |
+
"""Get the fields and values identifying the current credentials."""
|
| 1152 |
+
return {
|
| 1153 |
+
'type': 'authorized_user',
|
| 1154 |
+
'client_id': self.client_id,
|
| 1155 |
+
'client_secret': self.client_secret,
|
| 1156 |
+
'refresh_token': self.refresh_token
|
| 1157 |
+
}
|
| 1158 |
+
|
| 1159 |
+
@staticmethod
|
| 1160 |
+
def _implicit_credentials_from_gae():
|
| 1161 |
+
"""Attempts to get implicit credentials in Google App Engine env.
|
| 1162 |
+
|
| 1163 |
+
If the current environment is not detected as App Engine, returns None,
|
| 1164 |
+
indicating no Google App Engine credentials can be detected from the
|
| 1165 |
+
current environment.
|
| 1166 |
+
|
| 1167 |
+
Returns:
|
| 1168 |
+
None, if not in GAE, else an appengine.AppAssertionCredentials
|
| 1169 |
+
object.
|
| 1170 |
+
"""
|
| 1171 |
+
if not _in_gae_environment():
|
| 1172 |
+
return None
|
| 1173 |
+
|
| 1174 |
+
return _get_application_default_credential_GAE()
|
| 1175 |
+
|
| 1176 |
+
@staticmethod
|
| 1177 |
+
def _implicit_credentials_from_gce():
|
| 1178 |
+
"""Attempts to get implicit credentials in Google Compute Engine env.
|
| 1179 |
+
|
| 1180 |
+
If the current environment is not detected as Compute Engine, returns
|
| 1181 |
+
None, indicating no Google Compute Engine credentials can be detected
|
| 1182 |
+
from the current environment.
|
| 1183 |
+
|
| 1184 |
+
Returns:
|
| 1185 |
+
None, if not in GCE, else a gce.AppAssertionCredentials object.
|
| 1186 |
+
"""
|
| 1187 |
+
if not _in_gce_environment():
|
| 1188 |
+
return None
|
| 1189 |
+
|
| 1190 |
+
return _get_application_default_credential_GCE()
|
| 1191 |
+
|
| 1192 |
+
@staticmethod
|
| 1193 |
+
def _implicit_credentials_from_files():
|
| 1194 |
+
"""Attempts to get implicit credentials from local credential files.
|
| 1195 |
+
|
| 1196 |
+
First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS
|
| 1197 |
+
is set with a filename and then falls back to a configuration file (the
|
| 1198 |
+
"well known" file) associated with the 'gcloud' command line tool.
|
| 1199 |
+
|
| 1200 |
+
Returns:
|
| 1201 |
+
Credentials object associated with the
|
| 1202 |
+
GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if
|
| 1203 |
+
either exist. If neither file is define, returns None, indicating
|
| 1204 |
+
no credentials from a file can detected from the current
|
| 1205 |
+
environment.
|
| 1206 |
+
"""
|
| 1207 |
+
credentials_filename = _get_environment_variable_file()
|
| 1208 |
+
if not credentials_filename:
|
| 1209 |
+
credentials_filename = _get_well_known_file()
|
| 1210 |
+
if os.path.isfile(credentials_filename):
|
| 1211 |
+
extra_help = (' (produced automatically when running'
|
| 1212 |
+
' "gcloud auth login" command)')
|
| 1213 |
+
else:
|
| 1214 |
+
credentials_filename = None
|
| 1215 |
+
else:
|
| 1216 |
+
extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
|
| 1217 |
+
' environment variable)')
|
| 1218 |
+
|
| 1219 |
+
if not credentials_filename:
|
| 1220 |
+
return
|
| 1221 |
+
|
| 1222 |
+
# If we can read the credentials from a file, we don't need to know
|
| 1223 |
+
# what environment we are in.
|
| 1224 |
+
SETTINGS.env_name = DEFAULT_ENV_NAME
|
| 1225 |
+
|
| 1226 |
+
try:
|
| 1227 |
+
return _get_application_default_credential_from_file(
|
| 1228 |
+
credentials_filename)
|
| 1229 |
+
except (ApplicationDefaultCredentialsError, ValueError) as error:
|
| 1230 |
+
_raise_exception_for_reading_json(credentials_filename,
|
| 1231 |
+
extra_help, error)
|
| 1232 |
+
|
| 1233 |
+
@classmethod
|
| 1234 |
+
def _get_implicit_credentials(cls):
|
| 1235 |
+
"""Gets credentials implicitly from the environment.
|
| 1236 |
+
|
| 1237 |
+
Checks environment in order of precedence:
|
| 1238 |
+
- Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to
|
| 1239 |
+
a file with stored credentials information.
|
| 1240 |
+
- Stored "well known" file associated with `gcloud` command line tool.
|
| 1241 |
+
- Google App Engine (production and testing)
|
| 1242 |
+
- Google Compute Engine production environment.
|
| 1243 |
+
|
| 1244 |
+
Raises:
|
| 1245 |
+
ApplicationDefaultCredentialsError: raised when the credentials
|
| 1246 |
+
fail to be retrieved.
|
| 1247 |
+
"""
|
| 1248 |
+
# Environ checks (in order).
|
| 1249 |
+
environ_checkers = [
|
| 1250 |
+
cls._implicit_credentials_from_files,
|
| 1251 |
+
cls._implicit_credentials_from_gae,
|
| 1252 |
+
cls._implicit_credentials_from_gce,
|
| 1253 |
+
]
|
| 1254 |
+
|
| 1255 |
+
for checker in environ_checkers:
|
| 1256 |
+
credentials = checker()
|
| 1257 |
+
if credentials is not None:
|
| 1258 |
+
return credentials
|
| 1259 |
+
|
| 1260 |
+
# If no credentials, fail.
|
| 1261 |
+
raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
|
| 1262 |
+
|
| 1263 |
+
@staticmethod
|
| 1264 |
+
def get_application_default():
|
| 1265 |
+
"""Get the Application Default Credentials for the current environment.
|
| 1266 |
+
|
| 1267 |
+
Raises:
|
| 1268 |
+
ApplicationDefaultCredentialsError: raised when the credentials
|
| 1269 |
+
fail to be retrieved.
|
| 1270 |
+
"""
|
| 1271 |
+
return GoogleCredentials._get_implicit_credentials()
|
| 1272 |
+
|
| 1273 |
+
@staticmethod
|
| 1274 |
+
def from_stream(credential_filename):
|
| 1275 |
+
"""Create a Credentials object by reading information from a file.
|
| 1276 |
+
|
| 1277 |
+
It returns an object of type GoogleCredentials.
|
| 1278 |
+
|
| 1279 |
+
Args:
|
| 1280 |
+
credential_filename: the path to the file from where the
|
| 1281 |
+
credentials are to be read
|
| 1282 |
+
|
| 1283 |
+
Raises:
|
| 1284 |
+
ApplicationDefaultCredentialsError: raised when the credentials
|
| 1285 |
+
fail to be retrieved.
|
| 1286 |
+
"""
|
| 1287 |
+
if credential_filename and os.path.isfile(credential_filename):
|
| 1288 |
+
try:
|
| 1289 |
+
return _get_application_default_credential_from_file(
|
| 1290 |
+
credential_filename)
|
| 1291 |
+
except (ApplicationDefaultCredentialsError, ValueError) as error:
|
| 1292 |
+
extra_help = (' (provided as parameter to the '
|
| 1293 |
+
'from_stream() method)')
|
| 1294 |
+
_raise_exception_for_reading_json(credential_filename,
|
| 1295 |
+
extra_help,
|
| 1296 |
+
error)
|
| 1297 |
+
else:
|
| 1298 |
+
raise ApplicationDefaultCredentialsError(
|
| 1299 |
+
'The parameter passed to the from_stream() '
|
| 1300 |
+
'method should point to a file.')
|
| 1301 |
+
|
| 1302 |
+
|
| 1303 |
+
def _save_private_file(filename, json_contents):
|
| 1304 |
+
"""Saves a file with read-write permissions on for the owner.
|
| 1305 |
+
|
| 1306 |
+
Args:
|
| 1307 |
+
filename: String. Absolute path to file.
|
| 1308 |
+
json_contents: JSON serializable object to be saved.
|
| 1309 |
+
"""
|
| 1310 |
+
temp_filename = tempfile.mktemp()
|
| 1311 |
+
file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
|
| 1312 |
+
with os.fdopen(file_desc, 'w') as file_handle:
|
| 1313 |
+
json.dump(json_contents, file_handle, sort_keys=True,
|
| 1314 |
+
indent=2, separators=(',', ': '))
|
| 1315 |
+
shutil.move(temp_filename, filename)
|
| 1316 |
+
|
| 1317 |
+
|
| 1318 |
+
def save_to_well_known_file(credentials, well_known_file=None):
|
| 1319 |
+
"""Save the provided GoogleCredentials to the well known file.
|
| 1320 |
+
|
| 1321 |
+
Args:
|
| 1322 |
+
credentials: the credentials to be saved to the well known file;
|
| 1323 |
+
it should be an instance of GoogleCredentials
|
| 1324 |
+
well_known_file: the name of the file where the credentials are to be
|
| 1325 |
+
saved; this parameter is supposed to be used for
|
| 1326 |
+
testing only
|
| 1327 |
+
"""
|
| 1328 |
+
# TODO(orestica): move this method to tools.py
|
| 1329 |
+
# once the argparse import gets fixed (it is not present in Python 2.6)
|
| 1330 |
+
|
| 1331 |
+
if well_known_file is None:
|
| 1332 |
+
well_known_file = _get_well_known_file()
|
| 1333 |
+
|
| 1334 |
+
config_dir = os.path.dirname(well_known_file)
|
| 1335 |
+
if not os.path.isdir(config_dir):
|
| 1336 |
+
raise OSError(
|
| 1337 |
+
'Config directory does not exist: {0}'.format(config_dir))
|
| 1338 |
+
|
| 1339 |
+
credentials_data = credentials.serialization_data
|
| 1340 |
+
_save_private_file(well_known_file, credentials_data)
|
| 1341 |
+
|
| 1342 |
+
|
| 1343 |
+
def _get_environment_variable_file():
|
| 1344 |
+
application_default_credential_filename = (
|
| 1345 |
+
os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None))
|
| 1346 |
+
|
| 1347 |
+
if application_default_credential_filename:
|
| 1348 |
+
if os.path.isfile(application_default_credential_filename):
|
| 1349 |
+
return application_default_credential_filename
|
| 1350 |
+
else:
|
| 1351 |
+
raise ApplicationDefaultCredentialsError(
|
| 1352 |
+
'File ' + application_default_credential_filename +
|
| 1353 |
+
' (pointed by ' +
|
| 1354 |
+
GOOGLE_APPLICATION_CREDENTIALS +
|
| 1355 |
+
' environment variable) does not exist!')
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
def _get_well_known_file():
|
| 1359 |
+
"""Get the well known file produced by command 'gcloud auth login'."""
|
| 1360 |
+
# TODO(orestica): Revisit this method once gcloud provides a better way
|
| 1361 |
+
# of pinpointing the exact location of the file.
|
| 1362 |
+
default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR)
|
| 1363 |
+
if default_config_dir is None:
|
| 1364 |
+
if os.name == 'nt':
|
| 1365 |
+
try:
|
| 1366 |
+
default_config_dir = os.path.join(os.environ['APPDATA'],
|
| 1367 |
+
_CLOUDSDK_CONFIG_DIRECTORY)
|
| 1368 |
+
except KeyError:
|
| 1369 |
+
# This should never happen unless someone is really
|
| 1370 |
+
# messing with things.
|
| 1371 |
+
drive = os.environ.get('SystemDrive', 'C:')
|
| 1372 |
+
default_config_dir = os.path.join(drive, '\\',
|
| 1373 |
+
_CLOUDSDK_CONFIG_DIRECTORY)
|
| 1374 |
+
else:
|
| 1375 |
+
default_config_dir = os.path.join(os.path.expanduser('~'),
|
| 1376 |
+
'.config',
|
| 1377 |
+
_CLOUDSDK_CONFIG_DIRECTORY)
|
| 1378 |
+
|
| 1379 |
+
return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE)
|
| 1380 |
+
|
| 1381 |
+
|
| 1382 |
+
def _get_application_default_credential_from_file(filename):
|
| 1383 |
+
"""Build the Application Default Credentials from file."""
|
| 1384 |
+
# read the credentials from the file
|
| 1385 |
+
with open(filename) as file_obj:
|
| 1386 |
+
client_credentials = json.load(file_obj)
|
| 1387 |
+
|
| 1388 |
+
credentials_type = client_credentials.get('type')
|
| 1389 |
+
if credentials_type == AUTHORIZED_USER:
|
| 1390 |
+
required_fields = set(['client_id', 'client_secret', 'refresh_token'])
|
| 1391 |
+
elif credentials_type == SERVICE_ACCOUNT:
|
| 1392 |
+
required_fields = set(['client_id', 'client_email', 'private_key_id',
|
| 1393 |
+
'private_key'])
|
| 1394 |
+
else:
|
| 1395 |
+
raise ApplicationDefaultCredentialsError(
|
| 1396 |
+
"'type' field should be defined (and have one of the '" +
|
| 1397 |
+
AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
|
| 1398 |
+
|
| 1399 |
+
missing_fields = required_fields.difference(client_credentials.keys())
|
| 1400 |
+
|
| 1401 |
+
if missing_fields:
|
| 1402 |
+
_raise_exception_for_missing_fields(missing_fields)
|
| 1403 |
+
|
| 1404 |
+
if client_credentials['type'] == AUTHORIZED_USER:
|
| 1405 |
+
return GoogleCredentials(
|
| 1406 |
+
access_token=None,
|
| 1407 |
+
client_id=client_credentials['client_id'],
|
| 1408 |
+
client_secret=client_credentials['client_secret'],
|
| 1409 |
+
refresh_token=client_credentials['refresh_token'],
|
| 1410 |
+
token_expiry=None,
|
| 1411 |
+
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
| 1412 |
+
user_agent='Python client library')
|
| 1413 |
+
else: # client_credentials['type'] == SERVICE_ACCOUNT
|
| 1414 |
+
from oauth2client import service_account
|
| 1415 |
+
return service_account._JWTAccessCredentials.from_json_keyfile_dict(
|
| 1416 |
+
client_credentials)
|
| 1417 |
+
|
| 1418 |
+
|
| 1419 |
+
def _raise_exception_for_missing_fields(missing_fields):
|
| 1420 |
+
raise ApplicationDefaultCredentialsError(
|
| 1421 |
+
'The following field(s) must be defined: ' + ', '.join(missing_fields))
|
| 1422 |
+
|
| 1423 |
+
|
| 1424 |
+
def _raise_exception_for_reading_json(credential_file,
|
| 1425 |
+
extra_help,
|
| 1426 |
+
error):
|
| 1427 |
+
raise ApplicationDefaultCredentialsError(
|
| 1428 |
+
'An error was encountered while reading json file: ' +
|
| 1429 |
+
credential_file + extra_help + ': ' + str(error))
|
| 1430 |
+
|
| 1431 |
+
|
| 1432 |
+
def _get_application_default_credential_GAE():
|
| 1433 |
+
from oauth2client.contrib.appengine import AppAssertionCredentials
|
| 1434 |
+
|
| 1435 |
+
return AppAssertionCredentials([])
|
| 1436 |
+
|
| 1437 |
+
|
| 1438 |
+
def _get_application_default_credential_GCE():
|
| 1439 |
+
from oauth2client.contrib.gce import AppAssertionCredentials
|
| 1440 |
+
|
| 1441 |
+
return AppAssertionCredentials()
|
| 1442 |
+
|
| 1443 |
+
|
| 1444 |
+
class AssertionCredentials(GoogleCredentials):
|
| 1445 |
+
"""Abstract Credentials object used for OAuth 2.0 assertion grants.
|
| 1446 |
+
|
| 1447 |
+
This credential does not require a flow to instantiate because it
|
| 1448 |
+
represents a two legged flow, and therefore has all of the required
|
| 1449 |
+
information to generate and refresh its own access tokens. It must
|
| 1450 |
+
be subclassed to generate the appropriate assertion string.
|
| 1451 |
+
|
| 1452 |
+
AssertionCredentials objects may be safely pickled and unpickled.
|
| 1453 |
+
"""
|
| 1454 |
+
|
| 1455 |
+
@_helpers.positional(2)
|
| 1456 |
+
def __init__(self, assertion_type, user_agent=None,
|
| 1457 |
+
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
| 1458 |
+
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
| 1459 |
+
**unused_kwargs):
|
| 1460 |
+
"""Constructor for AssertionFlowCredentials.
|
| 1461 |
+
|
| 1462 |
+
Args:
|
| 1463 |
+
assertion_type: string, assertion type that will be declared to the
|
| 1464 |
+
auth server
|
| 1465 |
+
user_agent: string, The HTTP User-Agent to provide for this
|
| 1466 |
+
application.
|
| 1467 |
+
token_uri: string, URI for token endpoint. For convenience defaults
|
| 1468 |
+
to Google's endpoints but any OAuth 2.0 provider can be
|
| 1469 |
+
used.
|
| 1470 |
+
revoke_uri: string, URI for revoke endpoint.
|
| 1471 |
+
"""
|
| 1472 |
+
super(AssertionCredentials, self).__init__(
|
| 1473 |
+
None,
|
| 1474 |
+
None,
|
| 1475 |
+
None,
|
| 1476 |
+
None,
|
| 1477 |
+
None,
|
| 1478 |
+
token_uri,
|
| 1479 |
+
user_agent,
|
| 1480 |
+
revoke_uri=revoke_uri)
|
| 1481 |
+
self.assertion_type = assertion_type
|
| 1482 |
+
|
| 1483 |
+
def _generate_refresh_request_body(self):
|
| 1484 |
+
assertion = self._generate_assertion()
|
| 1485 |
+
|
| 1486 |
+
body = urllib.parse.urlencode({
|
| 1487 |
+
'assertion': assertion,
|
| 1488 |
+
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
| 1489 |
+
})
|
| 1490 |
+
|
| 1491 |
+
return body
|
| 1492 |
+
|
| 1493 |
+
def _generate_assertion(self):
|
| 1494 |
+
"""Generate assertion string to be used in the access token request."""
|
| 1495 |
+
raise NotImplementedError
|
| 1496 |
+
|
| 1497 |
+
def _revoke(self, http):
|
| 1498 |
+
"""Revokes the access_token and deletes the store if available.
|
| 1499 |
+
|
| 1500 |
+
Args:
|
| 1501 |
+
http: an object to be used to make HTTP requests.
|
| 1502 |
+
"""
|
| 1503 |
+
self._do_revoke(http, self.access_token)
|
| 1504 |
+
|
| 1505 |
+
def sign_blob(self, blob):
|
| 1506 |
+
"""Cryptographically sign a blob (of bytes).
|
| 1507 |
+
|
| 1508 |
+
Args:
|
| 1509 |
+
blob: bytes, Message to be signed.
|
| 1510 |
+
|
| 1511 |
+
Returns:
|
| 1512 |
+
tuple, A pair of the private key ID used to sign the blob and
|
| 1513 |
+
the signed contents.
|
| 1514 |
+
"""
|
| 1515 |
+
raise NotImplementedError('This method is abstract.')
|
| 1516 |
+
|
| 1517 |
+
|
| 1518 |
+
def _require_crypto_or_die():
|
| 1519 |
+
"""Ensure we have a crypto library, or throw CryptoUnavailableError.
|
| 1520 |
+
|
| 1521 |
+
The oauth2client.crypt module requires either PyCrypto or PyOpenSSL
|
| 1522 |
+
to be available in order to function, but these are optional
|
| 1523 |
+
dependencies.
|
| 1524 |
+
"""
|
| 1525 |
+
if not HAS_CRYPTO:
|
| 1526 |
+
raise CryptoUnavailableError('No crypto library available')
|
| 1527 |
+
|
| 1528 |
+
|
| 1529 |
+
@_helpers.positional(2)
|
| 1530 |
+
def verify_id_token(id_token, audience, http=None,
|
| 1531 |
+
cert_uri=ID_TOKEN_VERIFICATION_CERTS):
|
| 1532 |
+
"""Verifies a signed JWT id_token.
|
| 1533 |
+
|
| 1534 |
+
This function requires PyOpenSSL and because of that it does not work on
|
| 1535 |
+
App Engine.
|
| 1536 |
+
|
| 1537 |
+
Args:
|
| 1538 |
+
id_token: string, A Signed JWT.
|
| 1539 |
+
audience: string, The audience 'aud' that the token should be for.
|
| 1540 |
+
http: httplib2.Http, instance to use to make the HTTP request. Callers
|
| 1541 |
+
should supply an instance that has caching enabled.
|
| 1542 |
+
cert_uri: string, URI of the certificates in JSON format to
|
| 1543 |
+
verify the JWT against.
|
| 1544 |
+
|
| 1545 |
+
Returns:
|
| 1546 |
+
The deserialized JSON in the JWT.
|
| 1547 |
+
|
| 1548 |
+
Raises:
|
| 1549 |
+
oauth2client.crypt.AppIdentityError: if the JWT fails to verify.
|
| 1550 |
+
CryptoUnavailableError: if no crypto library is available.
|
| 1551 |
+
"""
|
| 1552 |
+
_require_crypto_or_die()
|
| 1553 |
+
if http is None:
|
| 1554 |
+
http = transport.get_cached_http()
|
| 1555 |
+
|
| 1556 |
+
resp, content = transport.request(http, cert_uri)
|
| 1557 |
+
if resp.status == http_client.OK:
|
| 1558 |
+
certs = json.loads(_helpers._from_bytes(content))
|
| 1559 |
+
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
|
| 1560 |
+
else:
|
| 1561 |
+
raise VerifyJwtTokenError('Status code: {0}'.format(resp.status))
|
| 1562 |
+
|
| 1563 |
+
|
| 1564 |
+
def _extract_id_token(id_token):
|
| 1565 |
+
"""Extract the JSON payload from a JWT.
|
| 1566 |
+
|
| 1567 |
+
Does the extraction w/o checking the signature.
|
| 1568 |
+
|
| 1569 |
+
Args:
|
| 1570 |
+
id_token: string or bytestring, OAuth 2.0 id_token.
|
| 1571 |
+
|
| 1572 |
+
Returns:
|
| 1573 |
+
object, The deserialized JSON payload.
|
| 1574 |
+
"""
|
| 1575 |
+
if type(id_token) == bytes:
|
| 1576 |
+
segments = id_token.split(b'.')
|
| 1577 |
+
else:
|
| 1578 |
+
segments = id_token.split(u'.')
|
| 1579 |
+
|
| 1580 |
+
if len(segments) != 3:
|
| 1581 |
+
raise VerifyJwtTokenError(
|
| 1582 |
+
'Wrong number of segments in token: {0}'.format(id_token))
|
| 1583 |
+
|
| 1584 |
+
return json.loads(
|
| 1585 |
+
_helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1])))
|
| 1586 |
+
|
| 1587 |
+
|
| 1588 |
+
def _parse_exchange_token_response(content):
|
| 1589 |
+
"""Parses response of an exchange token request.
|
| 1590 |
+
|
| 1591 |
+
Most providers return JSON but some (e.g. Facebook) return a
|
| 1592 |
+
url-encoded string.
|
| 1593 |
+
|
| 1594 |
+
Args:
|
| 1595 |
+
content: The body of a response
|
| 1596 |
+
|
| 1597 |
+
Returns:
|
| 1598 |
+
Content as a dictionary object. Note that the dict could be empty,
|
| 1599 |
+
i.e. {}. That basically indicates a failure.
|
| 1600 |
+
"""
|
| 1601 |
+
resp = {}
|
| 1602 |
+
content = _helpers._from_bytes(content)
|
| 1603 |
+
try:
|
| 1604 |
+
resp = json.loads(content)
|
| 1605 |
+
except Exception:
|
| 1606 |
+
# different JSON libs raise different exceptions,
|
| 1607 |
+
# so we just do a catch-all here
|
| 1608 |
+
resp = _helpers.parse_unique_urlencoded(content)
|
| 1609 |
+
|
| 1610 |
+
# some providers respond with 'expires', others with 'expires_in'
|
| 1611 |
+
if resp and 'expires' in resp:
|
| 1612 |
+
resp['expires_in'] = resp.pop('expires')
|
| 1613 |
+
|
| 1614 |
+
return resp
|
| 1615 |
+
|
| 1616 |
+
|
| 1617 |
+
@_helpers.positional(4)
|
| 1618 |
+
def credentials_from_code(client_id, client_secret, scope, code,
|
| 1619 |
+
redirect_uri='postmessage', http=None,
|
| 1620 |
+
user_agent=None,
|
| 1621 |
+
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
| 1622 |
+
auth_uri=oauth2client.GOOGLE_AUTH_URI,
|
| 1623 |
+
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
| 1624 |
+
device_uri=oauth2client.GOOGLE_DEVICE_URI,
|
| 1625 |
+
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
|
| 1626 |
+
pkce=False,
|
| 1627 |
+
code_verifier=None):
|
| 1628 |
+
"""Exchanges an authorization code for an OAuth2Credentials object.
|
| 1629 |
+
|
| 1630 |
+
Args:
|
| 1631 |
+
client_id: string, client identifier.
|
| 1632 |
+
client_secret: string, client secret.
|
| 1633 |
+
scope: string or iterable of strings, scope(s) to request.
|
| 1634 |
+
code: string, An authorization code, most likely passed down from
|
| 1635 |
+
the client
|
| 1636 |
+
redirect_uri: string, this is generally set to 'postmessage' to match
|
| 1637 |
+
the redirect_uri that the client specified
|
| 1638 |
+
http: httplib2.Http, optional http instance to use to do the fetch
|
| 1639 |
+
token_uri: string, URI for token endpoint. For convenience defaults
|
| 1640 |
+
to Google's endpoints but any OAuth 2.0 provider can be
|
| 1641 |
+
used.
|
| 1642 |
+
auth_uri: string, URI for authorization endpoint. For convenience
|
| 1643 |
+
defaults to Google's endpoints but any OAuth 2.0 provider
|
| 1644 |
+
can be used.
|
| 1645 |
+
revoke_uri: string, URI for revoke endpoint. For convenience
|
| 1646 |
+
defaults to Google's endpoints but any OAuth 2.0 provider
|
| 1647 |
+
can be used.
|
| 1648 |
+
device_uri: string, URI for device authorization endpoint. For
|
| 1649 |
+
convenience defaults to Google's endpoints but any OAuth
|
| 1650 |
+
2.0 provider can be used.
|
| 1651 |
+
pkce: boolean, default: False, Generate and include a "Proof Key
|
| 1652 |
+
for Code Exchange" (PKCE) with your authorization and token
|
| 1653 |
+
requests. This adds security for installed applications that
|
| 1654 |
+
cannot protect a client_secret. See RFC 7636 for details.
|
| 1655 |
+
code_verifier: bytestring or None, default: None, parameter passed
|
| 1656 |
+
as part of the code exchange when pkce=True. If
|
| 1657 |
+
None, a code_verifier will automatically be
|
| 1658 |
+
generated as part of step1_get_authorize_url(). See
|
| 1659 |
+
RFC 7636 for details.
|
| 1660 |
+
|
| 1661 |
+
Returns:
|
| 1662 |
+
An OAuth2Credentials object.
|
| 1663 |
+
|
| 1664 |
+
Raises:
|
| 1665 |
+
FlowExchangeError if the authorization code cannot be exchanged for an
|
| 1666 |
+
access token
|
| 1667 |
+
"""
|
| 1668 |
+
flow = OAuth2WebServerFlow(client_id, client_secret, scope,
|
| 1669 |
+
redirect_uri=redirect_uri,
|
| 1670 |
+
user_agent=user_agent,
|
| 1671 |
+
auth_uri=auth_uri,
|
| 1672 |
+
token_uri=token_uri,
|
| 1673 |
+
revoke_uri=revoke_uri,
|
| 1674 |
+
device_uri=device_uri,
|
| 1675 |
+
token_info_uri=token_info_uri,
|
| 1676 |
+
pkce=pkce,
|
| 1677 |
+
code_verifier=code_verifier)
|
| 1678 |
+
|
| 1679 |
+
credentials = flow.step2_exchange(code, http=http)
|
| 1680 |
+
return credentials
|
| 1681 |
+
|
| 1682 |
+
|
| 1683 |
+
@_helpers.positional(3)
|
| 1684 |
+
def credentials_from_clientsecrets_and_code(filename, scope, code,
|
| 1685 |
+
message=None,
|
| 1686 |
+
redirect_uri='postmessage',
|
| 1687 |
+
http=None,
|
| 1688 |
+
cache=None,
|
| 1689 |
+
device_uri=None):
|
| 1690 |
+
"""Returns OAuth2Credentials from a clientsecrets file and an auth code.
|
| 1691 |
+
|
| 1692 |
+
Will create the right kind of Flow based on the contents of the
|
| 1693 |
+
clientsecrets file or will raise InvalidClientSecretsError for unknown
|
| 1694 |
+
types of Flows.
|
| 1695 |
+
|
| 1696 |
+
Args:
|
| 1697 |
+
filename: string, File name of clientsecrets.
|
| 1698 |
+
scope: string or iterable of strings, scope(s) to request.
|
| 1699 |
+
code: string, An authorization code, most likely passed down from
|
| 1700 |
+
the client
|
| 1701 |
+
message: string, A friendly string to display to the user if the
|
| 1702 |
+
clientsecrets file is missing or invalid. If message is
|
| 1703 |
+
provided then sys.exit will be called in the case of an error.
|
| 1704 |
+
If message in not provided then
|
| 1705 |
+
clientsecrets.InvalidClientSecretsError will be raised.
|
| 1706 |
+
redirect_uri: string, this is generally set to 'postmessage' to match
|
| 1707 |
+
the redirect_uri that the client specified
|
| 1708 |
+
http: httplib2.Http, optional http instance to use to do the fetch
|
| 1709 |
+
cache: An optional cache service client that implements get() and set()
|
| 1710 |
+
methods. See clientsecrets.loadfile() for details.
|
| 1711 |
+
device_uri: string, OAuth 2.0 device authorization endpoint
|
| 1712 |
+
pkce: boolean, default: False, Generate and include a "Proof Key
|
| 1713 |
+
for Code Exchange" (PKCE) with your authorization and token
|
| 1714 |
+
requests. This adds security for installed applications that
|
| 1715 |
+
cannot protect a client_secret. See RFC 7636 for details.
|
| 1716 |
+
code_verifier: bytestring or None, default: None, parameter passed
|
| 1717 |
+
as part of the code exchange when pkce=True. If
|
| 1718 |
+
None, a code_verifier will automatically be
|
| 1719 |
+
generated as part of step1_get_authorize_url(). See
|
| 1720 |
+
RFC 7636 for details.
|
| 1721 |
+
|
| 1722 |
+
Returns:
|
| 1723 |
+
An OAuth2Credentials object.
|
| 1724 |
+
|
| 1725 |
+
Raises:
|
| 1726 |
+
FlowExchangeError: if the authorization code cannot be exchanged for an
|
| 1727 |
+
access token
|
| 1728 |
+
UnknownClientSecretsFlowError: if the file describes an unknown kind
|
| 1729 |
+
of Flow.
|
| 1730 |
+
clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
|
| 1731 |
+
invalid.
|
| 1732 |
+
"""
|
| 1733 |
+
flow = flow_from_clientsecrets(filename, scope, message=message,
|
| 1734 |
+
cache=cache, redirect_uri=redirect_uri,
|
| 1735 |
+
device_uri=device_uri)
|
| 1736 |
+
credentials = flow.step2_exchange(code, http=http)
|
| 1737 |
+
return credentials
|
| 1738 |
+
|
| 1739 |
+
|
| 1740 |
+
class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
|
| 1741 |
+
'device_code', 'user_code', 'interval', 'verification_url',
|
| 1742 |
+
'user_code_expiry'))):
|
| 1743 |
+
"""Intermediate information the OAuth2 for devices flow."""
|
| 1744 |
+
|
| 1745 |
+
@classmethod
|
| 1746 |
+
def FromResponse(cls, response):
|
| 1747 |
+
"""Create a DeviceFlowInfo from a server response.
|
| 1748 |
+
|
| 1749 |
+
The response should be a dict containing entries as described here:
|
| 1750 |
+
|
| 1751 |
+
http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1
|
| 1752 |
+
"""
|
| 1753 |
+
# device_code, user_code, and verification_url are required.
|
| 1754 |
+
kwargs = {
|
| 1755 |
+
'device_code': response['device_code'],
|
| 1756 |
+
'user_code': response['user_code'],
|
| 1757 |
+
}
|
| 1758 |
+
# The response may list the verification address as either
|
| 1759 |
+
# verification_url or verification_uri, so we check for both.
|
| 1760 |
+
verification_url = response.get(
|
| 1761 |
+
'verification_url', response.get('verification_uri'))
|
| 1762 |
+
if verification_url is None:
|
| 1763 |
+
raise OAuth2DeviceCodeError(
|
| 1764 |
+
'No verification_url provided in server response')
|
| 1765 |
+
kwargs['verification_url'] = verification_url
|
| 1766 |
+
# expires_in and interval are optional.
|
| 1767 |
+
kwargs.update({
|
| 1768 |
+
'interval': response.get('interval'),
|
| 1769 |
+
'user_code_expiry': None,
|
| 1770 |
+
})
|
| 1771 |
+
if 'expires_in' in response:
|
| 1772 |
+
kwargs['user_code_expiry'] = (
|
| 1773 |
+
_UTCNOW() +
|
| 1774 |
+
datetime.timedelta(seconds=int(response['expires_in'])))
|
| 1775 |
+
return cls(**kwargs)
|
| 1776 |
+
|
| 1777 |
+
|
| 1778 |
+
def _oauth2_web_server_flow_params(kwargs):
|
| 1779 |
+
"""Configures redirect URI parameters for OAuth2WebServerFlow."""
|
| 1780 |
+
params = {
|
| 1781 |
+
'access_type': 'offline',
|
| 1782 |
+
'response_type': 'code',
|
| 1783 |
+
}
|
| 1784 |
+
|
| 1785 |
+
params.update(kwargs)
|
| 1786 |
+
|
| 1787 |
+
# Check for the presence of the deprecated approval_prompt param and
|
| 1788 |
+
# warn appropriately.
|
| 1789 |
+
approval_prompt = params.get('approval_prompt')
|
| 1790 |
+
if approval_prompt is not None:
|
| 1791 |
+
logger.warning(
|
| 1792 |
+
'The approval_prompt parameter for OAuth2WebServerFlow is '
|
| 1793 |
+
'deprecated. Please use the prompt parameter instead.')
|
| 1794 |
+
|
| 1795 |
+
if approval_prompt == 'force':
|
| 1796 |
+
logger.warning(
|
| 1797 |
+
'approval_prompt="force" has been adjusted to '
|
| 1798 |
+
'prompt="consent"')
|
| 1799 |
+
params['prompt'] = 'consent'
|
| 1800 |
+
del params['approval_prompt']
|
| 1801 |
+
|
| 1802 |
+
return params
|
| 1803 |
+
|
| 1804 |
+
|
| 1805 |
+
class OAuth2WebServerFlow(Flow):
|
| 1806 |
+
"""Does the Web Server Flow for OAuth 2.0.
|
| 1807 |
+
|
| 1808 |
+
OAuth2WebServerFlow objects may be safely pickled and unpickled.
|
| 1809 |
+
"""
|
| 1810 |
+
|
| 1811 |
+
@_helpers.positional(4)
|
| 1812 |
+
def __init__(self, client_id,
|
| 1813 |
+
client_secret=None,
|
| 1814 |
+
scope=None,
|
| 1815 |
+
redirect_uri=None,
|
| 1816 |
+
user_agent=None,
|
| 1817 |
+
auth_uri=oauth2client.GOOGLE_AUTH_URI,
|
| 1818 |
+
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
| 1819 |
+
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
|
| 1820 |
+
login_hint=None,
|
| 1821 |
+
device_uri=oauth2client.GOOGLE_DEVICE_URI,
|
| 1822 |
+
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
|
| 1823 |
+
authorization_header=None,
|
| 1824 |
+
pkce=False,
|
| 1825 |
+
code_verifier=None,
|
| 1826 |
+
**kwargs):
|
| 1827 |
+
"""Constructor for OAuth2WebServerFlow.
|
| 1828 |
+
|
| 1829 |
+
The kwargs argument is used to set extra query parameters on the
|
| 1830 |
+
auth_uri. For example, the access_type and prompt
|
| 1831 |
+
query parameters can be set via kwargs.
|
| 1832 |
+
|
| 1833 |
+
Args:
|
| 1834 |
+
client_id: string, client identifier.
|
| 1835 |
+
client_secret: string client secret.
|
| 1836 |
+
scope: string or iterable of strings, scope(s) of the credentials
|
| 1837 |
+
being requested.
|
| 1838 |
+
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
|
| 1839 |
+
for a non-web-based application, or a URI that
|
| 1840 |
+
handles the callback from the authorization server.
|
| 1841 |
+
user_agent: string, HTTP User-Agent to provide for this
|
| 1842 |
+
application.
|
| 1843 |
+
auth_uri: string, URI for authorization endpoint. For convenience
|
| 1844 |
+
defaults to Google's endpoints but any OAuth 2.0 provider
|
| 1845 |
+
can be used.
|
| 1846 |
+
token_uri: string, URI for token endpoint. For convenience
|
| 1847 |
+
defaults to Google's endpoints but any OAuth 2.0
|
| 1848 |
+
provider can be used.
|
| 1849 |
+
revoke_uri: string, URI for revoke endpoint. For convenience
|
| 1850 |
+
defaults to Google's endpoints but any OAuth 2.0
|
| 1851 |
+
provider can be used.
|
| 1852 |
+
login_hint: string, Either an email address or domain. Passing this
|
| 1853 |
+
hint will either pre-fill the email box on the sign-in
|
| 1854 |
+
form or select the proper multi-login session, thereby
|
| 1855 |
+
simplifying the login flow.
|
| 1856 |
+
device_uri: string, URI for device authorization endpoint. For
|
| 1857 |
+
convenience defaults to Google's endpoints but any
|
| 1858 |
+
OAuth 2.0 provider can be used.
|
| 1859 |
+
authorization_header: string, For use with OAuth 2.0 providers that
|
| 1860 |
+
require a client to authenticate using a
|
| 1861 |
+
header value instead of passing client_secret
|
| 1862 |
+
in the POST body.
|
| 1863 |
+
pkce: boolean, default: False, Generate and include a "Proof Key
|
| 1864 |
+
for Code Exchange" (PKCE) with your authorization and token
|
| 1865 |
+
requests. This adds security for installed applications that
|
| 1866 |
+
cannot protect a client_secret. See RFC 7636 for details.
|
| 1867 |
+
code_verifier: bytestring or None, default: None, parameter passed
|
| 1868 |
+
as part of the code exchange when pkce=True. If
|
| 1869 |
+
None, a code_verifier will automatically be
|
| 1870 |
+
generated as part of step1_get_authorize_url(). See
|
| 1871 |
+
RFC 7636 for details.
|
| 1872 |
+
**kwargs: dict, The keyword arguments are all optional and required
|
| 1873 |
+
parameters for the OAuth calls.
|
| 1874 |
+
"""
|
| 1875 |
+
# scope is a required argument, but to preserve backwards-compatibility
|
| 1876 |
+
# we don't want to rearrange the positional arguments
|
| 1877 |
+
if scope is None:
|
| 1878 |
+
raise TypeError("The value of scope must not be None")
|
| 1879 |
+
self.client_id = client_id
|
| 1880 |
+
self.client_secret = client_secret
|
| 1881 |
+
self.scope = _helpers.scopes_to_string(scope)
|
| 1882 |
+
self.redirect_uri = redirect_uri
|
| 1883 |
+
self.login_hint = login_hint
|
| 1884 |
+
self.user_agent = user_agent
|
| 1885 |
+
self.auth_uri = auth_uri
|
| 1886 |
+
self.token_uri = token_uri
|
| 1887 |
+
self.revoke_uri = revoke_uri
|
| 1888 |
+
self.device_uri = device_uri
|
| 1889 |
+
self.token_info_uri = token_info_uri
|
| 1890 |
+
self.authorization_header = authorization_header
|
| 1891 |
+
self._pkce = pkce
|
| 1892 |
+
self.code_verifier = code_verifier
|
| 1893 |
+
self.params = _oauth2_web_server_flow_params(kwargs)
|
| 1894 |
+
|
| 1895 |
+
@_helpers.positional(1)
|
| 1896 |
+
def step1_get_authorize_url(self, redirect_uri=None, state=None):
|
| 1897 |
+
"""Returns a URI to redirect to the provider.
|
| 1898 |
+
|
| 1899 |
+
Args:
|
| 1900 |
+
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
|
| 1901 |
+
for a non-web-based application, or a URI that
|
| 1902 |
+
handles the callback from the authorization server.
|
| 1903 |
+
This parameter is deprecated, please move to passing
|
| 1904 |
+
the redirect_uri in via the constructor.
|
| 1905 |
+
state: string, Opaque state string which is passed through the
|
| 1906 |
+
OAuth2 flow and returned to the client as a query parameter
|
| 1907 |
+
in the callback.
|
| 1908 |
+
|
| 1909 |
+
Returns:
|
| 1910 |
+
A URI as a string to redirect the user to begin the authorization
|
| 1911 |
+
flow.
|
| 1912 |
+
"""
|
| 1913 |
+
if redirect_uri is not None:
|
| 1914 |
+
logger.warning((
|
| 1915 |
+
'The redirect_uri parameter for '
|
| 1916 |
+
'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. '
|
| 1917 |
+
'Please move to passing the redirect_uri in via the '
|
| 1918 |
+
'constructor.'))
|
| 1919 |
+
self.redirect_uri = redirect_uri
|
| 1920 |
+
|
| 1921 |
+
if self.redirect_uri is None:
|
| 1922 |
+
raise ValueError('The value of redirect_uri must not be None.')
|
| 1923 |
+
|
| 1924 |
+
query_params = {
|
| 1925 |
+
'client_id': self.client_id,
|
| 1926 |
+
'redirect_uri': self.redirect_uri,
|
| 1927 |
+
'scope': self.scope,
|
| 1928 |
+
}
|
| 1929 |
+
if state is not None:
|
| 1930 |
+
query_params['state'] = state
|
| 1931 |
+
if self.login_hint is not None:
|
| 1932 |
+
query_params['login_hint'] = self.login_hint
|
| 1933 |
+
if self._pkce:
|
| 1934 |
+
if not self.code_verifier:
|
| 1935 |
+
self.code_verifier = _pkce.code_verifier()
|
| 1936 |
+
challenge = _pkce.code_challenge(self.code_verifier)
|
| 1937 |
+
query_params['code_challenge'] = challenge
|
| 1938 |
+
query_params['code_challenge_method'] = 'S256'
|
| 1939 |
+
|
| 1940 |
+
query_params.update(self.params)
|
| 1941 |
+
return _helpers.update_query_params(self.auth_uri, query_params)
|
| 1942 |
+
|
| 1943 |
+
@_helpers.positional(1)
|
| 1944 |
+
def step1_get_device_and_user_codes(self, http=None):
|
| 1945 |
+
"""Returns a user code and the verification URL where to enter it
|
| 1946 |
+
|
| 1947 |
+
Returns:
|
| 1948 |
+
A user code as a string for the user to authorize the application
|
| 1949 |
+
An URL as a string where the user has to enter the code
|
| 1950 |
+
"""
|
| 1951 |
+
if self.device_uri is None:
|
| 1952 |
+
raise ValueError('The value of device_uri must not be None.')
|
| 1953 |
+
|
| 1954 |
+
body = urllib.parse.urlencode({
|
| 1955 |
+
'client_id': self.client_id,
|
| 1956 |
+
'scope': self.scope,
|
| 1957 |
+
})
|
| 1958 |
+
headers = {
|
| 1959 |
+
'content-type': 'application/x-www-form-urlencoded',
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
if self.user_agent is not None:
|
| 1963 |
+
headers['user-agent'] = self.user_agent
|
| 1964 |
+
|
| 1965 |
+
if http is None:
|
| 1966 |
+
http = transport.get_http_object()
|
| 1967 |
+
|
| 1968 |
+
resp, content = transport.request(
|
| 1969 |
+
http, self.device_uri, method='POST', body=body, headers=headers)
|
| 1970 |
+
content = _helpers._from_bytes(content)
|
| 1971 |
+
if resp.status == http_client.OK:
|
| 1972 |
+
try:
|
| 1973 |
+
flow_info = json.loads(content)
|
| 1974 |
+
except ValueError as exc:
|
| 1975 |
+
raise OAuth2DeviceCodeError(
|
| 1976 |
+
'Could not parse server response as JSON: "{0}", '
|
| 1977 |
+
'error: "{1}"'.format(content, exc))
|
| 1978 |
+
return DeviceFlowInfo.FromResponse(flow_info)
|
| 1979 |
+
else:
|
| 1980 |
+
error_msg = 'Invalid response {0}.'.format(resp.status)
|
| 1981 |
+
try:
|
| 1982 |
+
error_dict = json.loads(content)
|
| 1983 |
+
if 'error' in error_dict:
|
| 1984 |
+
error_msg += ' Error: {0}'.format(error_dict['error'])
|
| 1985 |
+
except ValueError:
|
| 1986 |
+
# Couldn't decode a JSON response, stick with the
|
| 1987 |
+
# default message.
|
| 1988 |
+
pass
|
| 1989 |
+
raise OAuth2DeviceCodeError(error_msg)
|
| 1990 |
+
|
| 1991 |
+
@_helpers.positional(2)
|
| 1992 |
+
def step2_exchange(self, code=None, http=None, device_flow_info=None):
|
| 1993 |
+
"""Exchanges a code for OAuth2Credentials.
|
| 1994 |
+
|
| 1995 |
+
Args:
|
| 1996 |
+
code: string, a dict-like object, or None. For a non-device
|
| 1997 |
+
flow, this is either the response code as a string, or a
|
| 1998 |
+
dictionary of query parameters to the redirect_uri. For a
|
| 1999 |
+
device flow, this should be None.
|
| 2000 |
+
http: httplib2.Http, optional http instance to use when fetching
|
| 2001 |
+
credentials.
|
| 2002 |
+
device_flow_info: DeviceFlowInfo, return value from step1 in the
|
| 2003 |
+
case of a device flow.
|
| 2004 |
+
|
| 2005 |
+
Returns:
|
| 2006 |
+
An OAuth2Credentials object that can be used to authorize requests.
|
| 2007 |
+
|
| 2008 |
+
Raises:
|
| 2009 |
+
FlowExchangeError: if a problem occurred exchanging the code for a
|
| 2010 |
+
refresh_token.
|
| 2011 |
+
ValueError: if code and device_flow_info are both provided or both
|
| 2012 |
+
missing.
|
| 2013 |
+
"""
|
| 2014 |
+
if code is None and device_flow_info is None:
|
| 2015 |
+
raise ValueError('No code or device_flow_info provided.')
|
| 2016 |
+
if code is not None and device_flow_info is not None:
|
| 2017 |
+
raise ValueError('Cannot provide both code and device_flow_info.')
|
| 2018 |
+
|
| 2019 |
+
if code is None:
|
| 2020 |
+
code = device_flow_info.device_code
|
| 2021 |
+
elif not isinstance(code, (six.string_types, six.binary_type)):
|
| 2022 |
+
if 'code' not in code:
|
| 2023 |
+
raise FlowExchangeError(code.get(
|
| 2024 |
+
'error', 'No code was supplied in the query parameters.'))
|
| 2025 |
+
code = code['code']
|
| 2026 |
+
|
| 2027 |
+
post_data = {
|
| 2028 |
+
'client_id': self.client_id,
|
| 2029 |
+
'code': code,
|
| 2030 |
+
'scope': self.scope,
|
| 2031 |
+
}
|
| 2032 |
+
if self.client_secret is not None:
|
| 2033 |
+
post_data['client_secret'] = self.client_secret
|
| 2034 |
+
if self._pkce:
|
| 2035 |
+
post_data['code_verifier'] = self.code_verifier
|
| 2036 |
+
if device_flow_info is not None:
|
| 2037 |
+
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
|
| 2038 |
+
else:
|
| 2039 |
+
post_data['grant_type'] = 'authorization_code'
|
| 2040 |
+
post_data['redirect_uri'] = self.redirect_uri
|
| 2041 |
+
body = urllib.parse.urlencode(post_data)
|
| 2042 |
+
headers = {
|
| 2043 |
+
'content-type': 'application/x-www-form-urlencoded',
|
| 2044 |
+
}
|
| 2045 |
+
if self.authorization_header is not None:
|
| 2046 |
+
headers['Authorization'] = self.authorization_header
|
| 2047 |
+
if self.user_agent is not None:
|
| 2048 |
+
headers['user-agent'] = self.user_agent
|
| 2049 |
+
|
| 2050 |
+
if http is None:
|
| 2051 |
+
http = transport.get_http_object()
|
| 2052 |
+
|
| 2053 |
+
resp, content = transport.request(
|
| 2054 |
+
http, self.token_uri, method='POST', body=body, headers=headers)
|
| 2055 |
+
d = _parse_exchange_token_response(content)
|
| 2056 |
+
if resp.status == http_client.OK and 'access_token' in d:
|
| 2057 |
+
access_token = d['access_token']
|
| 2058 |
+
refresh_token = d.get('refresh_token', None)
|
| 2059 |
+
if not refresh_token:
|
| 2060 |
+
logger.info(
|
| 2061 |
+
'Received token response with no refresh_token. Consider '
|
| 2062 |
+
"reauthenticating with prompt='consent'.")
|
| 2063 |
+
token_expiry = None
|
| 2064 |
+
if 'expires_in' in d:
|
| 2065 |
+
delta = datetime.timedelta(seconds=int(d['expires_in']))
|
| 2066 |
+
token_expiry = delta + _UTCNOW()
|
| 2067 |
+
|
| 2068 |
+
extracted_id_token = None
|
| 2069 |
+
id_token_jwt = None
|
| 2070 |
+
if 'id_token' in d:
|
| 2071 |
+
extracted_id_token = _extract_id_token(d['id_token'])
|
| 2072 |
+
id_token_jwt = d['id_token']
|
| 2073 |
+
|
| 2074 |
+
logger.info('Successfully retrieved access token')
|
| 2075 |
+
return OAuth2Credentials(
|
| 2076 |
+
access_token, self.client_id, self.client_secret,
|
| 2077 |
+
refresh_token, token_expiry, self.token_uri, self.user_agent,
|
| 2078 |
+
revoke_uri=self.revoke_uri, id_token=extracted_id_token,
|
| 2079 |
+
id_token_jwt=id_token_jwt, token_response=d, scopes=self.scope,
|
| 2080 |
+
token_info_uri=self.token_info_uri)
|
| 2081 |
+
else:
|
| 2082 |
+
logger.info('Failed to retrieve access token: %s', content)
|
| 2083 |
+
if 'error' in d:
|
| 2084 |
+
# you never know what those providers got to say
|
| 2085 |
+
error_msg = (str(d['error']) +
|
| 2086 |
+
str(d.get('error_description', '')))
|
| 2087 |
+
else:
|
| 2088 |
+
error_msg = 'Invalid response: {0}.'.format(str(resp.status))
|
| 2089 |
+
raise FlowExchangeError(error_msg)
|
| 2090 |
+
|
| 2091 |
+
|
| 2092 |
+
@_helpers.positional(2)
|
| 2093 |
+
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
|
| 2094 |
+
message=None, cache=None, login_hint=None,
|
| 2095 |
+
device_uri=None, pkce=None, code_verifier=None,
|
| 2096 |
+
prompt=None):
|
| 2097 |
+
"""Create a Flow from a clientsecrets file.
|
| 2098 |
+
|
| 2099 |
+
Will create the right kind of Flow based on the contents of the
|
| 2100 |
+
clientsecrets file or will raise InvalidClientSecretsError for unknown
|
| 2101 |
+
types of Flows.
|
| 2102 |
+
|
| 2103 |
+
Args:
|
| 2104 |
+
filename: string, File name of client secrets.
|
| 2105 |
+
scope: string or iterable of strings, scope(s) to request.
|
| 2106 |
+
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
|
| 2107 |
+
a non-web-based application, or a URI that handles the
|
| 2108 |
+
callback from the authorization server.
|
| 2109 |
+
message: string, A friendly string to display to the user if the
|
| 2110 |
+
clientsecrets file is missing or invalid. If message is
|
| 2111 |
+
provided then sys.exit will be called in the case of an error.
|
| 2112 |
+
If message in not provided then
|
| 2113 |
+
clientsecrets.InvalidClientSecretsError will be raised.
|
| 2114 |
+
cache: An optional cache service client that implements get() and set()
|
| 2115 |
+
methods. See clientsecrets.loadfile() for details.
|
| 2116 |
+
login_hint: string, Either an email address or domain. Passing this
|
| 2117 |
+
hint will either pre-fill the email box on the sign-in form
|
| 2118 |
+
or select the proper multi-login session, thereby
|
| 2119 |
+
simplifying the login flow.
|
| 2120 |
+
device_uri: string, URI for device authorization endpoint. For
|
| 2121 |
+
convenience defaults to Google's endpoints but any
|
| 2122 |
+
OAuth 2.0 provider can be used.
|
| 2123 |
+
|
| 2124 |
+
Returns:
|
| 2125 |
+
A Flow object.
|
| 2126 |
+
|
| 2127 |
+
Raises:
|
| 2128 |
+
UnknownClientSecretsFlowError: if the file describes an unknown kind of
|
| 2129 |
+
Flow.
|
| 2130 |
+
clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
|
| 2131 |
+
invalid.
|
| 2132 |
+
"""
|
| 2133 |
+
try:
|
| 2134 |
+
client_type, client_info = clientsecrets.loadfile(filename,
|
| 2135 |
+
cache=cache)
|
| 2136 |
+
if client_type in (clientsecrets.TYPE_WEB,
|
| 2137 |
+
clientsecrets.TYPE_INSTALLED):
|
| 2138 |
+
constructor_kwargs = {
|
| 2139 |
+
'redirect_uri': redirect_uri,
|
| 2140 |
+
'auth_uri': client_info['auth_uri'],
|
| 2141 |
+
'token_uri': client_info['token_uri'],
|
| 2142 |
+
'login_hint': login_hint,
|
| 2143 |
+
}
|
| 2144 |
+
revoke_uri = client_info.get('revoke_uri')
|
| 2145 |
+
optional = (
|
| 2146 |
+
'revoke_uri',
|
| 2147 |
+
'device_uri',
|
| 2148 |
+
'pkce',
|
| 2149 |
+
'code_verifier',
|
| 2150 |
+
'prompt'
|
| 2151 |
+
)
|
| 2152 |
+
for param in optional:
|
| 2153 |
+
if locals()[param] is not None:
|
| 2154 |
+
constructor_kwargs[param] = locals()[param]
|
| 2155 |
+
|
| 2156 |
+
return OAuth2WebServerFlow(
|
| 2157 |
+
client_info['client_id'], client_info['client_secret'],
|
| 2158 |
+
scope, **constructor_kwargs)
|
| 2159 |
+
|
| 2160 |
+
except clientsecrets.InvalidClientSecretsError as e:
|
| 2161 |
+
if message is not None:
|
| 2162 |
+
if e.args:
|
| 2163 |
+
message = ('The client secrets were invalid: '
|
| 2164 |
+
'\n{0}\n{1}'.format(e, message))
|
| 2165 |
+
sys.exit(message)
|
| 2166 |
+
else:
|
| 2167 |
+
raise
|
| 2168 |
+
else:
|
| 2169 |
+
raise UnknownClientSecretsFlowError(
|
| 2170 |
+
'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type))
|
.venv/lib/python3.11/site-packages/oauth2client/clientsecrets.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2014 Google Inc. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""Utilities for reading OAuth 2.0 client secret files.
|
| 16 |
+
|
| 17 |
+
A client_secrets.json file contains all the information needed to interact with
|
| 18 |
+
an OAuth 2.0 protected service.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import json
|
| 22 |
+
|
| 23 |
+
import six
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Properties that make a client_secrets.json file valid.
|
| 27 |
+
TYPE_WEB = 'web'
|
| 28 |
+
TYPE_INSTALLED = 'installed'
|
| 29 |
+
|
| 30 |
+
VALID_CLIENT = {
|
| 31 |
+
TYPE_WEB: {
|
| 32 |
+
'required': [
|
| 33 |
+
'client_id',
|
| 34 |
+
'client_secret',
|
| 35 |
+
'redirect_uris',
|
| 36 |
+
'auth_uri',
|
| 37 |
+
'token_uri',
|
| 38 |
+
],
|
| 39 |
+
'string': [
|
| 40 |
+
'client_id',
|
| 41 |
+
'client_secret',
|
| 42 |
+
],
|
| 43 |
+
},
|
| 44 |
+
TYPE_INSTALLED: {
|
| 45 |
+
'required': [
|
| 46 |
+
'client_id',
|
| 47 |
+
'client_secret',
|
| 48 |
+
'redirect_uris',
|
| 49 |
+
'auth_uri',
|
| 50 |
+
'token_uri',
|
| 51 |
+
],
|
| 52 |
+
'string': [
|
| 53 |
+
'client_id',
|
| 54 |
+
'client_secret',
|
| 55 |
+
],
|
| 56 |
+
},
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class Error(Exception):
|
| 61 |
+
"""Base error for this module."""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class InvalidClientSecretsError(Error):
|
| 65 |
+
"""Format of ClientSecrets file is invalid."""
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _validate_clientsecrets(clientsecrets_dict):
|
| 69 |
+
"""Validate parsed client secrets from a file.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
clientsecrets_dict: dict, a dictionary holding the client secrets.
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
tuple, a string of the client type and the information parsed
|
| 76 |
+
from the file.
|
| 77 |
+
"""
|
| 78 |
+
_INVALID_FILE_FORMAT_MSG = (
|
| 79 |
+
'Invalid file format. See '
|
| 80 |
+
'https://developers.google.com/api-client-library/'
|
| 81 |
+
'python/guide/aaa_client_secrets')
|
| 82 |
+
|
| 83 |
+
if clientsecrets_dict is None:
|
| 84 |
+
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
|
| 85 |
+
try:
|
| 86 |
+
(client_type, client_info), = clientsecrets_dict.items()
|
| 87 |
+
except (ValueError, AttributeError):
|
| 88 |
+
raise InvalidClientSecretsError(
|
| 89 |
+
_INVALID_FILE_FORMAT_MSG + ' '
|
| 90 |
+
'Expected a JSON object with a single property for a "web" or '
|
| 91 |
+
'"installed" application')
|
| 92 |
+
|
| 93 |
+
if client_type not in VALID_CLIENT:
|
| 94 |
+
raise InvalidClientSecretsError(
|
| 95 |
+
'Unknown client type: {0}.'.format(client_type))
|
| 96 |
+
|
| 97 |
+
for prop_name in VALID_CLIENT[client_type]['required']:
|
| 98 |
+
if prop_name not in client_info:
|
| 99 |
+
raise InvalidClientSecretsError(
|
| 100 |
+
'Missing property "{0}" in a client type of "{1}".'.format(
|
| 101 |
+
prop_name, client_type))
|
| 102 |
+
for prop_name in VALID_CLIENT[client_type]['string']:
|
| 103 |
+
if client_info[prop_name].startswith('[['):
|
| 104 |
+
raise InvalidClientSecretsError(
|
| 105 |
+
'Property "{0}" is not configured.'.format(prop_name))
|
| 106 |
+
return client_type, client_info
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def load(fp):
|
| 110 |
+
obj = json.load(fp)
|
| 111 |
+
return _validate_clientsecrets(obj)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def loads(s):
|
| 115 |
+
obj = json.loads(s)
|
| 116 |
+
return _validate_clientsecrets(obj)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _loadfile(filename):
|
| 120 |
+
try:
|
| 121 |
+
with open(filename, 'r') as fp:
|
| 122 |
+
obj = json.load(fp)
|
| 123 |
+
except IOError as exc:
|
| 124 |
+
raise InvalidClientSecretsError('Error opening file', exc.filename,
|
| 125 |
+
exc.strerror, exc.errno)
|
| 126 |
+
return _validate_clientsecrets(obj)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def loadfile(filename, cache=None):
|
| 130 |
+
"""Loading of client_secrets JSON file, optionally backed by a cache.
|
| 131 |
+
|
| 132 |
+
Typical cache storage would be App Engine memcache service,
|
| 133 |
+
but you can pass in any other cache client that implements
|
| 134 |
+
these methods:
|
| 135 |
+
|
| 136 |
+
* ``get(key, namespace=ns)``
|
| 137 |
+
* ``set(key, value, namespace=ns)``
|
| 138 |
+
|
| 139 |
+
Usage::
|
| 140 |
+
|
| 141 |
+
# without caching
|
| 142 |
+
client_type, client_info = loadfile('secrets.json')
|
| 143 |
+
# using App Engine memcache service
|
| 144 |
+
from google.appengine.api import memcache
|
| 145 |
+
client_type, client_info = loadfile('secrets.json', cache=memcache)
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
filename: string, Path to a client_secrets.json file on a filesystem.
|
| 149 |
+
cache: An optional cache service client that implements get() and set()
|
| 150 |
+
methods. If not specified, the file is always being loaded from
|
| 151 |
+
a filesystem.
|
| 152 |
+
|
| 153 |
+
Raises:
|
| 154 |
+
InvalidClientSecretsError: In case of a validation error or some
|
| 155 |
+
I/O failure. Can happen only on cache miss.
|
| 156 |
+
|
| 157 |
+
Returns:
|
| 158 |
+
(client_type, client_info) tuple, as _loadfile() normally would.
|
| 159 |
+
JSON contents is validated only during first load. Cache hits are not
|
| 160 |
+
validated.
|
| 161 |
+
"""
|
| 162 |
+
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
|
| 163 |
+
|
| 164 |
+
if not cache:
|
| 165 |
+
return _loadfile(filename)
|
| 166 |
+
|
| 167 |
+
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
|
| 168 |
+
if obj is None:
|
| 169 |
+
client_type, client_info = _loadfile(filename)
|
| 170 |
+
obj = {client_type: client_info}
|
| 171 |
+
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
|
| 172 |
+
|
| 173 |
+
return next(six.iteritems(obj))
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Contributed modules.
|
| 2 |
+
|
| 3 |
+
Contrib contains modules that are not considered part of the core oauth2client
|
| 4 |
+
library but provide additional functionality. These modules are intended to
|
| 5 |
+
make it easier to use oauth2client.
|
| 6 |
+
"""
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (428 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/_appengine_ndb.cpython-311.pyc
ADDED
|
Binary file (6.84 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/_metadata.cpython-311.pyc
ADDED
|
Binary file (4.76 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/appengine.cpython-311.pyc
ADDED
|
Binary file (44 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/devshell.cpython-311.pyc
ADDED
|
Binary file (7.41 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/dictionary_storage.cpython-311.pyc
ADDED
|
Binary file (2.92 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/flask_util.cpython-311.pyc
ADDED
|
Binary file (22.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/gce.cpython-311.pyc
ADDED
|
Binary file (6.75 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/keyring_storage.cpython-311.pyc
ADDED
|
Binary file (3.8 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/multiprocess_file_storage.cpython-311.pyc
ADDED
|
Binary file (15.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/sqlalchemy.cpython-311.pyc
ADDED
|
Binary file (6.35 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/oauth2client/contrib/__pycache__/xsrfutil.cpython-311.pyc
ADDED
|
Binary file (4.11 kB). View file
|
|
|