edgegrid-python 2.0.6__py3-none-any.whl → 2.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- akamai/edgegrid/__init__.py +1 -1
- {edgegrid_python-2.0.6.dist-info → edgegrid_python-2.0.7.dist-info}/METADATA +1 -1
- edgegrid_python-2.0.7.dist-info/RECORD +22 -0
- {edgegrid_python-2.0.6.dist-info → edgegrid_python-2.0.7.dist-info}/top_level.txt +0 -1
- build/lib/akamai/edgegrid/__init__.py +0 -41
- build/lib/akamai/edgegrid/edgegrid.py +0 -323
- build/lib/akamai/edgegrid/edgerc.py +0 -37
- build/lib/akamai/edgegrid/test/__init__.py +0 -0
- build/lib/akamai/edgegrid/test/conftest.py +0 -44
- build/lib/akamai/edgegrid/test/test_edgegrid.py +0 -390
- build/lib/ci/check_namespace_pkg.py +0 -33
- build/lib/ci/coverage_report.py +0 -71
- build/lib/examples/create-credentials.py +0 -34
- build/lib/examples/delete-credentials.py +0 -35
- build/lib/examples/get-credentials.py +0 -37
- build/lib/examples/update-credentials.py +0 -47
- edgegrid_python-2.0.6.dist-info/RECORD +0 -34
- {edgegrid_python-2.0.6.dist-info → edgegrid_python-2.0.7.dist-info}/WHEEL +0 -0
- {edgegrid_python-2.0.6.dist-info → edgegrid_python-2.0.7.dist-info}/licenses/LICENSE +0 -0
akamai/edgegrid/__init__.py
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
akamai/edgegrid/__init__.py,sha256=axi_ih3o2dMhmelFG-rGihoCIl7hh4ulCMFjHQkpeNs,1217
|
|
2
|
+
akamai/edgegrid/edgegrid.py,sha256=pzJQ7qjC5KZIIM_yrhdVHUuJe6CMFzt-z1ucQeN2-vk,12002
|
|
3
|
+
akamai/edgegrid/edgerc.py,sha256=T9RoKyWnVIIOwAVaJZ3QV5xBAJ9vz7XiyUZ8tMET8dI,1170
|
|
4
|
+
akamai/edgegrid/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
akamai/edgegrid/test/conftest.py,sha256=ewP2gqZI0POKP0d3QQuD0yiPYkPM9sRqLBekk00GnFQ,908
|
|
6
|
+
akamai/edgegrid/test/edgerc_that_doesnt_parse,sha256=j1uEFSxZo_o9pYuj6QdxWIOSmRTLLlLPLWQ8sziHm1A,138
|
|
7
|
+
akamai/edgegrid/test/sample_edgerc,sha256=0DsP3KEoLXjd7fhkKcVoCT5cM1S69iNZmO8VvdwKWLA,1084
|
|
8
|
+
akamai/edgegrid/test/sample_file.txt,sha256=EBMsdifxSkOKTPB-eqA7be67wJ5YWG_G8NjoNdtZqBM,22
|
|
9
|
+
akamai/edgegrid/test/test_edgegrid.py,sha256=GhX1CjO3CS_cVV6hFOyOKL8ovxvEZpHbqrFBGmTKkw8,13506
|
|
10
|
+
akamai/edgegrid/test/testcases.json,sha256=81LV7AgNTmHSX4X56dZszrVk-Zk1Jw5vkvI6DFJazSE,15040
|
|
11
|
+
akamai/edgegrid/test/testdata.json,sha256=kJ0DYk94SJeatnvdqYTCw-itxMUdbIhzVta3rIYAVT0,1149
|
|
12
|
+
ci/check_namespace_pkg.py,sha256=mUy-bcH8KV2ujgkgFFJ_yJ2XzVn1_YdQhXDQeCENNnc,1594
|
|
13
|
+
ci/coverage_report.py,sha256=jykNDfg5MTHCCwEg6_GtgyUQO4B2Tt3Ws8SrKPtXmYI,2273
|
|
14
|
+
edgegrid_python-2.0.7.dist-info/licenses/LICENSE,sha256=aai-NM26EYkRybAI1eWSlozmBr8-EQ1GWLJe_9qc8M4,10294
|
|
15
|
+
examples/create-credentials.py,sha256=z-9ItuXNI3lFhMv3A5-dZC0QfSgTGvNJciQOPNWBurQ,1191
|
|
16
|
+
examples/delete-credentials.py,sha256=HM6a8fy-44beXKLhs_JVk8ChdOJlvF1s4XwWn9VATqE,1422
|
|
17
|
+
examples/get-credentials.py,sha256=duI0pRRwX9--Eibnp4EsIRQebwnsygzmCEScemKD27w,1198
|
|
18
|
+
examples/update-credentials.py,sha256=BEahXLHjIXuBW77wuQK98fbKSxlibugG1nQD1nvVcN0,1783
|
|
19
|
+
edgegrid_python-2.0.7.dist-info/METADATA,sha256=XcJplukAOtiEKus_QyncNaS6mmTNla2_rfZQgOxd9mI,9208
|
|
20
|
+
edgegrid_python-2.0.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
edgegrid_python-2.0.7.dist-info/top_level.txt,sha256=DuVRQFKn3UwJ6bDLD_ZYN02ri7mCMhx9mCXfeSmiqK4,19
|
|
22
|
+
edgegrid_python-2.0.7.dist-info/RECORD,,
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
akamai.edgegrid
|
|
3
|
-
~~~~~~~~~~~~~~~
|
|
4
|
-
|
|
5
|
-
This library provides an authentication handler for Requests that implements the
|
|
6
|
-
Akamai {OPEN} EdgeGrid client authentication protocol as
|
|
7
|
-
specified by https://developer.akamai.com/introduction/Client_Auth.html.
|
|
8
|
-
For more information visit https://developer.akamai.com.
|
|
9
|
-
|
|
10
|
-
usage:
|
|
11
|
-
|
|
12
|
-
>>> import requests
|
|
13
|
-
>>> from akamai.edgegrid import EdgeGridAuth
|
|
14
|
-
>>> from urlparse import urljoin
|
|
15
|
-
|
|
16
|
-
>>> baseurl = 'https://akaa-WWWWWWWWWWWW.luna.akamaiapis.net/'
|
|
17
|
-
>>> s = requests.Session()
|
|
18
|
-
>>> s.auth = EdgeGridAuth(
|
|
19
|
-
client_token='akab-XXXXXXXXXXXXXXXXXXXXXXX',
|
|
20
|
-
client_secret='YYYYYYYYYYYYYYYYYYYYYYYYYY',
|
|
21
|
-
access_token='akab-ZZZZZZZZZZZZZZZZZZZZZZZZZZZ'
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
... now you have a requests session object that can be used to make {OPEN} requests
|
|
25
|
-
|
|
26
|
-
>>> result = s.get(urljoin(baseurl, '/diagnostic-tools/v1/locations'))
|
|
27
|
-
>>> result.status_code
|
|
28
|
-
200
|
|
29
|
-
>>> result.json()['locations'][0]
|
|
30
|
-
Hongkong, Hong Kong
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
from .edgegrid import EdgeGridAuth
|
|
34
|
-
from .edgerc import EdgeRc
|
|
35
|
-
|
|
36
|
-
__all__ = ['EdgeGridAuth', 'EdgeRc']
|
|
37
|
-
|
|
38
|
-
__title__ = 'edgegrid-python'
|
|
39
|
-
__version__ = '2.0.6'
|
|
40
|
-
__license__ = 'Apache 2.0'
|
|
41
|
-
__copyright__ = 'Copyright 2026 Akamai Technologies'
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
# pylint: disable=too-many-arguments,missing-function-docstring
|
|
2
|
-
"""EdgeGrid requests Auth handler"""
|
|
3
|
-
|
|
4
|
-
import logging
|
|
5
|
-
import uuid
|
|
6
|
-
import hashlib
|
|
7
|
-
import hmac
|
|
8
|
-
import base64
|
|
9
|
-
import re
|
|
10
|
-
import os
|
|
11
|
-
from time import gmtime, strftime
|
|
12
|
-
from urllib.parse import urlparse
|
|
13
|
-
|
|
14
|
-
from requests.auth import AuthBase
|
|
15
|
-
|
|
16
|
-
from .edgerc import EdgeRc
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
__all__ = ['EdgeGridAuth']
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def eg_timestamp():
|
|
24
|
-
"""Generates EdgeGrid compatible timestamp"""
|
|
25
|
-
return strftime('%Y%m%dT%H:%M:%S+0000', gmtime())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def new_nonce():
|
|
29
|
-
return uuid.uuid4()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def base64_hmac_sha256(data, key):
|
|
33
|
-
return base64.b64encode(
|
|
34
|
-
hmac.new(
|
|
35
|
-
key.encode('utf8'),
|
|
36
|
-
data.encode('utf8'),
|
|
37
|
-
hashlib.sha256).digest()
|
|
38
|
-
).decode('utf8')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def base64_sha256(data):
|
|
42
|
-
digest = hashlib.sha256(data).digest()
|
|
43
|
-
return base64.b64encode(digest).decode('utf8')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def read_stream_and_rewind(f, max_read):
|
|
47
|
-
"""Reads up to read_max bytes from a python file object (like _io.BufferedReader)
|
|
48
|
-
or a MultipartEncoder object, then rewinds the stream.
|
|
49
|
-
|
|
50
|
-
The read() method of these objects is decorated by httpie with a side-effect code which
|
|
51
|
-
prints the body content to stdout when the 'B' option is specified for --print. However,
|
|
52
|
-
it does not trigger in the pre-request phase when this plugin is executed. Still, after reading
|
|
53
|
-
we must set the stream position to the beginning to not impact subsequent reads outside
|
|
54
|
-
the plugin. (We don't assume we can be passed a partially read stream to the plugin.)
|
|
55
|
-
|
|
56
|
-
Raises TypeError if read() or seek() is not supported by f or f._buffer. May potentially raise
|
|
57
|
-
OSError for any failed I/O operation, in particular io.UnsupportedOperation if the stream is
|
|
58
|
-
not seekable (e.g. is a pipe which we don't expect here as httpie reads pipe contents and
|
|
59
|
-
sets body as bytes).
|
|
60
|
-
"""
|
|
61
|
-
try:
|
|
62
|
-
res = f.read(max_read)
|
|
63
|
-
except AttributeError as exc:
|
|
64
|
-
raise TypeError(f'akamai.edgegrid: unexpected body type: {type(f).__name__}') from exc
|
|
65
|
-
|
|
66
|
-
try:
|
|
67
|
-
f.seek(0)
|
|
68
|
-
except AttributeError:
|
|
69
|
-
# a MultipartEncoder
|
|
70
|
-
try:
|
|
71
|
-
# During read(), MultipartEncoder lazily loads its upload parts into self._buffer
|
|
72
|
-
# depending on the requested number of bytes. Then a regular read() on self._buffer
|
|
73
|
-
# is performed. Therefore, rewinding self._buffer effectively rewinds the whole
|
|
74
|
-
# MultipartEncoder content.
|
|
75
|
-
# pylint: disable=protected-access
|
|
76
|
-
f._buffer.seek(0)
|
|
77
|
-
except AttributeError as exc:
|
|
78
|
-
raise TypeError(f'akamai.edgegrid: unexpected body type: {type(f).__name__}') from exc
|
|
79
|
-
return res
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def read_body_content(body, max_body):
|
|
83
|
-
"""The body argument may be one of the following:
|
|
84
|
-
1. bytes object
|
|
85
|
-
2. str object
|
|
86
|
-
3. _io.BufferedReader object for body input from file
|
|
87
|
-
4. requests_toolbelt.MultipartEncoder object for multipart form requests
|
|
88
|
-
5. httpie.uploads.ChunkedUploadStream object for chunked transfer encoding
|
|
89
|
-
(when --chunked, currently not supported)
|
|
90
|
-
May raise TypeError for unexpected input type or OSError for I/O operations.
|
|
91
|
-
"""
|
|
92
|
-
if isinstance(body, bytes):
|
|
93
|
-
return body[:max_body]
|
|
94
|
-
if isinstance(body, str):
|
|
95
|
-
return body.encode('utf8')[:max_body]
|
|
96
|
-
return read_stream_and_rewind(body, max_body)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def determine_body_len(body):
|
|
100
|
-
"""May raise exception if body appears to be a file (is not a str, bytes or MultipartEncoder)
|
|
101
|
-
but either:
|
|
102
|
-
- has no fileno method (TypeError)
|
|
103
|
-
- raises OSError while trying to calculate the length using the file descriptor"""
|
|
104
|
-
if isinstance(body, bytes):
|
|
105
|
-
return len(body)
|
|
106
|
-
if isinstance(body, str):
|
|
107
|
-
return len(body.encode('utf8'))
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
# a MultipartEncoder?
|
|
111
|
-
return body.len
|
|
112
|
-
except AttributeError:
|
|
113
|
-
# a file object?
|
|
114
|
-
try:
|
|
115
|
-
return os.stat(body.fileno()).st_size
|
|
116
|
-
except AttributeError as exc:
|
|
117
|
-
raise TypeError(
|
|
118
|
-
f'akamai.edgegrid: unexpected body type: {type(body).__name__}') from exc
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class EdgeGridAuth(AuthBase):
|
|
122
|
-
"""A Requests authentication handler that provides Akamai {OPEN} EdgeGrid support.
|
|
123
|
-
|
|
124
|
-
Basic Usage::
|
|
125
|
-
>>> import requests
|
|
126
|
-
>>> from akamai.edgegrid import EdgeGridAuth
|
|
127
|
-
>>> s = requests.Session()
|
|
128
|
-
>>> s.auth = EdgeGridAuth(
|
|
129
|
-
client_token='cccccccccccccccccc',
|
|
130
|
-
client_secret='sssssssssssssssss',
|
|
131
|
-
access_token='aaaaaaaaaaaaaaaaa'
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
"""
|
|
135
|
-
|
|
136
|
-
def __init__(self, client_token, client_secret, access_token,
|
|
137
|
-
*, headers_to_sign=(), max_body=131072):
|
|
138
|
-
"""Initialize authentication using the given parameters from the Akamai OPEN APIs
|
|
139
|
-
Interface:
|
|
140
|
-
|
|
141
|
-
:param client_token: Client token provided by "Credentials" ui
|
|
142
|
-
:param client_secret: Client secret provided by "Credentials" ui
|
|
143
|
-
:param access_token: Access token provided by "Authorizations" ui
|
|
144
|
-
:param headers_to_sign: An ordered list header names that will be included in
|
|
145
|
-
the signature. This will be provided by specific APIs. (default [])
|
|
146
|
-
:param max_body: Maximum content body size for POST requests. This will be provided by
|
|
147
|
-
specific APIs. (default 131072)
|
|
148
|
-
|
|
149
|
-
"""
|
|
150
|
-
# pylint: disable=invalid-name
|
|
151
|
-
self.ah = EdgeGridAuthHeaders(
|
|
152
|
-
client_token,
|
|
153
|
-
client_secret,
|
|
154
|
-
access_token,
|
|
155
|
-
headers_to_sign=headers_to_sign,
|
|
156
|
-
max_body=max_body
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
@staticmethod
|
|
160
|
-
def from_edgerc(rcinput, section='default'):
|
|
161
|
-
"""
|
|
162
|
-
Returns an EdgeGridAuth object from the configuration from the given section
|
|
163
|
-
of the given edgerc file.
|
|
164
|
-
|
|
165
|
-
:param rcinput: EdgeRc instance or path to the edgerc file
|
|
166
|
-
:param section: the section to use (this is the [bracketed] part of the edgerc,
|
|
167
|
-
default is 'default')
|
|
168
|
-
|
|
169
|
-
"""
|
|
170
|
-
if isinstance(rcinput, EdgeRc):
|
|
171
|
-
edgerc = rcinput
|
|
172
|
-
else:
|
|
173
|
-
edgerc = EdgeRc(rcinput)
|
|
174
|
-
|
|
175
|
-
return EdgeGridAuth(
|
|
176
|
-
client_token=edgerc.get(section, 'client_token'),
|
|
177
|
-
client_secret=edgerc.get(section, 'client_secret'),
|
|
178
|
-
access_token=edgerc.get(section, 'access_token'),
|
|
179
|
-
headers_to_sign=edgerc.getlist(section, 'headers_to_sign'),
|
|
180
|
-
max_body=edgerc.getint(section, 'max_body')
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
def handle_redirect(self, res, **_):
|
|
184
|
-
if res.is_redirect:
|
|
185
|
-
redirect_location = res.headers['location']
|
|
186
|
-
|
|
187
|
-
logger.debug("signing the redirected url: %s", redirect_location)
|
|
188
|
-
request_to_sign = res.request.copy()
|
|
189
|
-
request_to_sign.url = redirect_location
|
|
190
|
-
|
|
191
|
-
res.request.headers['Authorization'] = self.ah.make_auth_header(
|
|
192
|
-
request_to_sign, eg_timestamp(), new_nonce())
|
|
193
|
-
|
|
194
|
-
def __call__(self, r):
|
|
195
|
-
timestamp = eg_timestamp()
|
|
196
|
-
nonce = new_nonce()
|
|
197
|
-
|
|
198
|
-
r.headers['Authorization'] = self.ah.make_auth_header(r, timestamp, nonce)
|
|
199
|
-
r.register_hook('response', self.handle_redirect)
|
|
200
|
-
return r
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
class EdgeGridAuthHeaders:
|
|
204
|
-
"""
|
|
205
|
-
A class for preparing requests authentication headers needed for
|
|
206
|
-
Akamai {OPEN} EdgeGrid support.
|
|
207
|
-
"""
|
|
208
|
-
def __init__(self, client_token, client_secret, access_token,
|
|
209
|
-
*, headers_to_sign=(), max_body=131072):
|
|
210
|
-
self.client_token = client_token
|
|
211
|
-
self.client_secret = client_secret
|
|
212
|
-
self.access_token = access_token
|
|
213
|
-
self.headers_to_sign = [h.lower() for h in headers_to_sign]
|
|
214
|
-
self.max_body = max_body
|
|
215
|
-
|
|
216
|
-
def make_signing_key(self, timestamp):
|
|
217
|
-
signing_key = base64_hmac_sha256(timestamp, self.client_secret)
|
|
218
|
-
logger.debug('signing key: %s', signing_key)
|
|
219
|
-
return signing_key
|
|
220
|
-
|
|
221
|
-
def canonicalize_headers(self, headers):
|
|
222
|
-
spaces_re = re.compile('\\s+')
|
|
223
|
-
|
|
224
|
-
# note: r.headers is a case-insensitive dict and self.headers_to_sign
|
|
225
|
-
# should already be in lowercase at this point
|
|
226
|
-
# pylint: disable=consider-using-f-string
|
|
227
|
-
return '\t'.join([
|
|
228
|
-
"%s:%s" % (h, spaces_re.sub(' ', headers[h].strip()))
|
|
229
|
-
for h in self.headers_to_sign if h in headers
|
|
230
|
-
])
|
|
231
|
-
|
|
232
|
-
def make_content_hash(self, body, method):
|
|
233
|
-
logger.debug("body is '%s'", body)
|
|
234
|
-
content_hash = ""
|
|
235
|
-
if method == 'POST':
|
|
236
|
-
buf = read_body_content(body, self.max_body)
|
|
237
|
-
if buf:
|
|
238
|
-
logger.debug("signing content: %s", buf)
|
|
239
|
-
content_hash = base64_sha256(buf)
|
|
240
|
-
try:
|
|
241
|
-
body_len = determine_body_len(body)
|
|
242
|
-
if body_len > self.max_body:
|
|
243
|
-
logger.debug(
|
|
244
|
-
"data length %d is larger than maximum %d "
|
|
245
|
-
"and will be truncated for computing the hash",
|
|
246
|
-
body_len, self.max_body)
|
|
247
|
-
except (TypeError, OSError) as e:
|
|
248
|
-
# body length is needed only for debugging: just log a possible exception
|
|
249
|
-
logger.warning("cannot determine length of request body=%s: %s", body, e)
|
|
250
|
-
logger.debug("content hash is '%s'", content_hash)
|
|
251
|
-
return content_hash
|
|
252
|
-
|
|
253
|
-
@staticmethod
|
|
254
|
-
def get_header_versions(header=None):
|
|
255
|
-
if header is None:
|
|
256
|
-
header = {}
|
|
257
|
-
|
|
258
|
-
version_header = ''
|
|
259
|
-
akamai_cli = os.getenv('AKAMAI_CLI')
|
|
260
|
-
akamai_cli_version = os.getenv('AKAMAI_CLI_VERSION')
|
|
261
|
-
if akamai_cli and akamai_cli_version:
|
|
262
|
-
version_header += " AkamaiCLI/" + akamai_cli_version
|
|
263
|
-
|
|
264
|
-
akamai_cli_command = os.getenv('AKAMAI_CLI_COMMAND')
|
|
265
|
-
akamai_cli_command_version = os.getenv('AKAMAI_CLI_COMMAND_VERSION')
|
|
266
|
-
if akamai_cli_command and akamai_cli_command_version:
|
|
267
|
-
version_header += " AkamaiCLI-" + akamai_cli_command + \
|
|
268
|
-
"/" + akamai_cli_command_version
|
|
269
|
-
|
|
270
|
-
if version_header != '':
|
|
271
|
-
if 'User-Agent' not in header:
|
|
272
|
-
header['User-Agent'] = version_header.strip()
|
|
273
|
-
else:
|
|
274
|
-
header['User-Agent'] += version_header
|
|
275
|
-
|
|
276
|
-
return header
|
|
277
|
-
|
|
278
|
-
def make_data_to_sign(self, request, auth_header):
|
|
279
|
-
parsed_url = urlparse(request.url)
|
|
280
|
-
|
|
281
|
-
if request.headers.get('Host', False):
|
|
282
|
-
netloc = request.headers['Host']
|
|
283
|
-
else:
|
|
284
|
-
netloc = parsed_url.netloc
|
|
285
|
-
|
|
286
|
-
self.get_header_versions(request.headers)
|
|
287
|
-
|
|
288
|
-
data_to_sign = '\t'.join([
|
|
289
|
-
request.method,
|
|
290
|
-
parsed_url.scheme,
|
|
291
|
-
netloc,
|
|
292
|
-
# Note: relative URL constraints are handled by requests when it sets up 'r'
|
|
293
|
-
parsed_url.path + (';' + parsed_url.params if parsed_url.params else "") +
|
|
294
|
-
('?' + parsed_url.query if parsed_url.query else ""),
|
|
295
|
-
self.canonicalize_headers(request.headers),
|
|
296
|
-
self.make_content_hash(request.body or '', request.method),
|
|
297
|
-
auth_header
|
|
298
|
-
])
|
|
299
|
-
logger.debug('data to sign: %s', '\\t'.join(data_to_sign.split('\t')))
|
|
300
|
-
return data_to_sign
|
|
301
|
-
|
|
302
|
-
def sign_request(self, request, timestamp, auth_header):
|
|
303
|
-
return base64_hmac_sha256(
|
|
304
|
-
self.make_data_to_sign(request, auth_header),
|
|
305
|
-
self.make_signing_key(timestamp)
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
def make_auth_header(self, request, timestamp, nonce):
|
|
309
|
-
kvps = [
|
|
310
|
-
('client_token', self.client_token),
|
|
311
|
-
('access_token', self.access_token),
|
|
312
|
-
('timestamp', timestamp),
|
|
313
|
-
('nonce', nonce),
|
|
314
|
-
]
|
|
315
|
-
auth_header = "EG1-HMAC-SHA256 " + \
|
|
316
|
-
';'.join([f"{k}={v}" for k, v in kvps]) + ';'
|
|
317
|
-
logger.debug('unsigned authorization header: %s', auth_header)
|
|
318
|
-
|
|
319
|
-
signed_auth_header = auth_header + \
|
|
320
|
-
'signature=' + self.sign_request(request, timestamp, auth_header)
|
|
321
|
-
|
|
322
|
-
logger.debug('signed authorization header: %s', signed_auth_header)
|
|
323
|
-
return signed_auth_header
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Support for .edgerc file format"""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from configparser import ConfigParser
|
|
5
|
-
from os.path import expanduser
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class EdgeRc(ConfigParser):
|
|
11
|
-
"""Class for managing .edgerc files"""
|
|
12
|
-
def __init__(self, filename):
|
|
13
|
-
ConfigParser.__init__(self,
|
|
14
|
-
{'client_token': '',
|
|
15
|
-
'client_secret': '',
|
|
16
|
-
'host': '',
|
|
17
|
-
'access_token': '',
|
|
18
|
-
'max_body': '131072',
|
|
19
|
-
'headers_to_sign': 'None'})
|
|
20
|
-
logger.debug("loading edgerc from %s", filename)
|
|
21
|
-
|
|
22
|
-
self.read(expanduser(filename))
|
|
23
|
-
|
|
24
|
-
logger.debug("successfully loaded edgerc")
|
|
25
|
-
|
|
26
|
-
def optionxform(self, optionstr):
|
|
27
|
-
"""support both max_body and max-body style keys"""
|
|
28
|
-
return optionstr.replace('-', '_')
|
|
29
|
-
|
|
30
|
-
def getlist(self, section, option):
|
|
31
|
-
"""
|
|
32
|
-
returns the named option as a list, splitting the original value by ','
|
|
33
|
-
"""
|
|
34
|
-
value = self.get(section, option)
|
|
35
|
-
if value:
|
|
36
|
-
return value.split(',')
|
|
37
|
-
return None
|
|
File without changes
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# pylint: disable=missing-function-docstring
|
|
2
|
-
"""Unit tests helpers"""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
test_dir = os.path.abspath(os.path.dirname(__file__))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def cases():
|
|
12
|
-
with open(f'{test_dir}/testcases.json', encoding="utf-8") as data:
|
|
13
|
-
data = json.load(data)
|
|
14
|
-
return data
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture(scope="package")
|
|
18
|
-
def testdata():
|
|
19
|
-
with open(f'{test_dir}/testdata.json', encoding="utf-8") as data:
|
|
20
|
-
data = json.load(data)
|
|
21
|
-
return data
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def names(tests):
|
|
25
|
-
result = []
|
|
26
|
-
for test in tests:
|
|
27
|
-
result.append(test["testName"])
|
|
28
|
-
return result
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@pytest.fixture
|
|
32
|
-
def multipart_fields():
|
|
33
|
-
with open(f'{test_dir}/sample_file.txt', "rb") as f:
|
|
34
|
-
result = {
|
|
35
|
-
"foo": "bar",
|
|
36
|
-
"baz": ("sample_file.txt", f),
|
|
37
|
-
}
|
|
38
|
-
yield result
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@pytest.fixture
|
|
42
|
-
def sample_file():
|
|
43
|
-
with open(f'{test_dir}/sample_file.txt', "rb") as f:
|
|
44
|
-
yield f
|
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
# pylint: disable=missing-function-docstring
|
|
2
|
-
"""unit tests for edgegrid. It runs tests from testcases.json"""
|
|
3
|
-
|
|
4
|
-
import io
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
|
-
import unittest.mock
|
|
9
|
-
from urllib.parse import urljoin
|
|
10
|
-
|
|
11
|
-
import requests
|
|
12
|
-
import requests_toolbelt
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
|
-
import akamai.edgegrid.edgegrid as eg
|
|
16
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
17
|
-
from akamai.edgegrid.test.conftest import cases, names, test_dir
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
EXPECTED_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx='
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_edge_grid_auth_headers(testdata):
|
|
25
|
-
auth_headers = eg.EdgeGridAuthHeaders(
|
|
26
|
-
client_token=testdata['client_token'],
|
|
27
|
-
client_secret=testdata['client_secret'],
|
|
28
|
-
access_token=testdata['access_token'],
|
|
29
|
-
headers_to_sign=testdata['headers_to_sign'],
|
|
30
|
-
max_body=testdata['max_body']
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
sign_key = auth_headers.make_signing_key(testdata['timestamp'])
|
|
34
|
-
assert sign_key == testdata["sign_key_test"]
|
|
35
|
-
|
|
36
|
-
content_hash = auth_headers.make_content_hash(
|
|
37
|
-
body="test_body",
|
|
38
|
-
method="POST"
|
|
39
|
-
)
|
|
40
|
-
assert content_hash == testdata["content_hash_test"]
|
|
41
|
-
|
|
42
|
-
header = auth_headers.get_header_versions()
|
|
43
|
-
assert header == {}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_make_content_hash_logs_warning_for_unknown_body_len(testdata, caplog):
|
|
47
|
-
auth_headers = eg.EdgeGridAuthHeaders(
|
|
48
|
-
client_token=testdata['client_token'],
|
|
49
|
-
client_secret=testdata['client_secret'],
|
|
50
|
-
access_token=testdata['access_token'],
|
|
51
|
-
headers_to_sign=testdata['headers_to_sign'],
|
|
52
|
-
max_body=testdata['max_body']
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
def throwing_determine_body_len(_):
|
|
56
|
-
raise OSError('boom')
|
|
57
|
-
|
|
58
|
-
with unittest.mock.patch('akamai.edgegrid.edgegrid.determine_body_len',
|
|
59
|
-
throwing_determine_body_len):
|
|
60
|
-
content_hash = auth_headers.make_content_hash(body="test_body", method="POST")
|
|
61
|
-
assert content_hash == testdata["content_hash_test"]
|
|
62
|
-
assert re.match(r'WARNING.+cannot determine length of request body=.+:\s+boom',
|
|
63
|
-
caplog.text)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@pytest.mark.parametrize("testcase", cases(), ids=names(cases()))
|
|
67
|
-
def test_edge_grid(testdata, testcase):
|
|
68
|
-
auth = EdgeGridAuth(
|
|
69
|
-
client_token=testdata['client_token'],
|
|
70
|
-
client_secret=testdata['client_secret'],
|
|
71
|
-
access_token=testdata['access_token'],
|
|
72
|
-
headers_to_sign=testdata['headers_to_sign'],
|
|
73
|
-
max_body=testdata['max_body']
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
headers = {}
|
|
77
|
-
if 'headers' in testcase['request']:
|
|
78
|
-
for request_headers in testcase['request']['headers']:
|
|
79
|
-
for key, val in request_headers.items():
|
|
80
|
-
headers[key] = val
|
|
81
|
-
|
|
82
|
-
req = requests.Request(
|
|
83
|
-
method=testcase['request']['method'],
|
|
84
|
-
url=urljoin(
|
|
85
|
-
testdata['base_url'],
|
|
86
|
-
testcase['request']['path']),
|
|
87
|
-
headers=headers,
|
|
88
|
-
data=testcase['request'].get('data')
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# The auth plugin is called on a PreparedRequest object, not a Request (see
|
|
92
|
-
# PreparedRequest.prepare()). That's why we want req to be a PreparedRequest
|
|
93
|
-
# in order to test make_auth_header with proper data.
|
|
94
|
-
if testcase.get('failsWithMessage') is None:
|
|
95
|
-
req = req.prepare()
|
|
96
|
-
data_to_sign = auth.ah.make_data_to_sign(req, "")
|
|
97
|
-
auth_header = auth.ah.make_auth_header(req, testdata['timestamp'], testdata['nonce'])
|
|
98
|
-
assert auth_header == testcase['expectedAuthorization']
|
|
99
|
-
assert data_to_sign == testcase['expectedDataToSign']
|
|
100
|
-
else:
|
|
101
|
-
with pytest.raises(Exception) as exc_info:
|
|
102
|
-
req.prepare()
|
|
103
|
-
assert str(exc_info.value) == testcase['failsWithMessage']
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_nonce():
|
|
107
|
-
count = 100
|
|
108
|
-
nonces = set()
|
|
109
|
-
while count > 0:
|
|
110
|
-
nonce = eg.new_nonce()
|
|
111
|
-
assert nonce not in nonces
|
|
112
|
-
count -= 1
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def test_timestamp():
|
|
116
|
-
valid_timestamp = re.compile(r"""
|
|
117
|
-
^
|
|
118
|
-
\d{4} # year
|
|
119
|
-
[0-1][0-9] # month
|
|
120
|
-
[0-3][0-9] # day
|
|
121
|
-
T
|
|
122
|
-
[0-2][0-9] # hour
|
|
123
|
-
:
|
|
124
|
-
[0-5][0-9] # minute
|
|
125
|
-
:
|
|
126
|
-
[0-5][0-9] # second
|
|
127
|
-
\+0000 # timezone
|
|
128
|
-
$
|
|
129
|
-
""", re.VERBOSE)
|
|
130
|
-
assert re.match(valid_timestamp, eg.eg_timestamp())
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_defaults():
|
|
134
|
-
auth = EdgeGridAuth(
|
|
135
|
-
client_token='xxx', client_secret='xxx', access_token='xxx'
|
|
136
|
-
)
|
|
137
|
-
assert auth.ah.max_body == 131072
|
|
138
|
-
assert auth.ah.headers_to_sign == []
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def test_edgerc_default():
|
|
142
|
-
auth = EdgeGridAuth.from_edgerc(os.path.join(test_dir, 'sample_edgerc'))
|
|
143
|
-
assert auth.ah.client_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx'
|
|
144
|
-
assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET
|
|
145
|
-
assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx'
|
|
146
|
-
assert auth.ah.max_body == 131072
|
|
147
|
-
assert auth.ah.headers_to_sign == ['none']
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def test_edgerc_broken():
|
|
151
|
-
auth = EdgeGridAuth.from_edgerc(
|
|
152
|
-
os.path.join(test_dir, 'sample_edgerc'), 'broken')
|
|
153
|
-
assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET
|
|
154
|
-
assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx'
|
|
155
|
-
assert auth.ah.max_body == 128 * 1024
|
|
156
|
-
assert auth.ah.headers_to_sign == ['none']
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def test_edgerc_unparseable():
|
|
160
|
-
with pytest.raises(BaseException):
|
|
161
|
-
EdgeGridAuth.from_edgerc(
|
|
162
|
-
os.path.join(test_dir, 'edgerc_that_doesnt_parse'))
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def test_edgerc_headers():
|
|
166
|
-
auth = EdgeGridAuth.from_edgerc(
|
|
167
|
-
os.path.join(test_dir, 'sample_edgerc'), 'headers')
|
|
168
|
-
assert auth.ah.headers_to_sign == ['x-mything1', 'x-mything2']
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def test_get_header_versions():
|
|
172
|
-
auth = EdgeGridAuth.from_edgerc(
|
|
173
|
-
os.path.join(test_dir, 'sample_edgerc'), 'headers')
|
|
174
|
-
header = auth.ah.get_header_versions()
|
|
175
|
-
assert 'user-agent' not in header
|
|
176
|
-
|
|
177
|
-
header = auth.ah.get_header_versions({'User-Agent': 'testvalue'})
|
|
178
|
-
assert 'User-Agent' in header
|
|
179
|
-
|
|
180
|
-
# setting environment variables with hardcoded `1.0.0` value, just for this test.
|
|
181
|
-
# These variables are cleared at the end of this test.
|
|
182
|
-
os.environ["AKAMAI_CLI"] = '1.0.0'
|
|
183
|
-
os.environ["AKAMAI_CLI_VERSION"] = '1.0.0'
|
|
184
|
-
|
|
185
|
-
header = auth.ah.get_header_versions()
|
|
186
|
-
assert 'User-Agent' in header
|
|
187
|
-
assert header['User-Agent'] == 'AkamaiCLI/1.0.0'
|
|
188
|
-
|
|
189
|
-
header = auth.ah.get_header_versions({'User-Agent': 'test-agent'})
|
|
190
|
-
assert 'User-Agent' in header
|
|
191
|
-
assert header['User-Agent'] == 'test-agent AkamaiCLI/1.0.0'
|
|
192
|
-
|
|
193
|
-
os.environ["AKAMAI_CLI_COMMAND"] = '1.0.0'
|
|
194
|
-
os.environ["AKAMAI_CLI_COMMAND_VERSION"] = '1.0.0'
|
|
195
|
-
|
|
196
|
-
header = auth.ah.get_header_versions()
|
|
197
|
-
assert 'User-Agent' in header
|
|
198
|
-
assert header['User-Agent'] == 'AkamaiCLI/1.0.0 AkamaiCLI-1.0.0/1.0.0'
|
|
199
|
-
|
|
200
|
-
header = auth.ah.get_header_versions({'User-Agent': 'testvalue'})
|
|
201
|
-
assert 'User-Agent' in header
|
|
202
|
-
assert header['User-Agent'] == 'testvalue AkamaiCLI/1.0.0 AkamaiCLI-1.0.0/1.0.0'
|
|
203
|
-
|
|
204
|
-
del os.environ['AKAMAI_CLI']
|
|
205
|
-
del os.environ['AKAMAI_CLI_VERSION']
|
|
206
|
-
del os.environ['AKAMAI_CLI_COMMAND']
|
|
207
|
-
del os.environ['AKAMAI_CLI_COMMAND_VERSION']
|
|
208
|
-
|
|
209
|
-
assert 'AKAMAI_CLI' not in os.environ
|
|
210
|
-
assert 'AKAMAI_CLI_VERSION' not in os.environ
|
|
211
|
-
assert 'AKAMAI_CLI_COMMAND' not in os.environ
|
|
212
|
-
assert 'AKAMAI_CLI_COMMAND_VERSION' not in os.environ
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def test_edgerc_from_object():
|
|
216
|
-
auth = EdgeGridAuth.from_edgerc(
|
|
217
|
-
EdgeRc(os.path.join(test_dir, 'sample_edgerc')))
|
|
218
|
-
assert auth.ah.client_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx'
|
|
219
|
-
assert auth.ah.client_secret == EXPECTED_CLIENT_SECRET
|
|
220
|
-
assert auth.ah.access_token == 'xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx'
|
|
221
|
-
assert auth.ah.max_body == 131072
|
|
222
|
-
assert auth.ah.headers_to_sign == ['none']
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def test_edgerc_dashes():
|
|
226
|
-
auth = EdgeGridAuth.from_edgerc(
|
|
227
|
-
os.path.join(test_dir, 'sample_edgerc'), 'dashes')
|
|
228
|
-
assert auth.ah.max_body == 128 * 1024
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
class TestReadBodyContent:
|
|
232
|
-
"""Test read_body_content"""
|
|
233
|
-
def test_reading_from_str(self):
|
|
234
|
-
assert eg.read_body_content('foobar', 10) == b'foobar'
|
|
235
|
-
|
|
236
|
-
def test_reading_from_str_truncated(self):
|
|
237
|
-
assert eg.read_body_content('foobar', 3) == b'foo'
|
|
238
|
-
|
|
239
|
-
def test_reading_from_bytes(self):
|
|
240
|
-
assert eg.read_body_content(b'foobar', 10) == b'foobar'
|
|
241
|
-
|
|
242
|
-
def test_reading_from_bytes_truncated(self):
|
|
243
|
-
assert eg.read_body_content(b'foobar', 3) == b'foo'
|
|
244
|
-
|
|
245
|
-
def test_basic_reading_from_file_object(self, sample_file):
|
|
246
|
-
assert eg.read_body_content(sample_file, 32) == b'this is a sample file.'
|
|
247
|
-
|
|
248
|
-
def test_basic_reading_from_multipart_encoder(self):
|
|
249
|
-
def encoder():
|
|
250
|
-
return requests_toolbelt.MultipartEncoder({'foo': 'bar'}, boundary='baz')
|
|
251
|
-
|
|
252
|
-
assert eg.read_body_content(encoder(), 1024) == encoder().to_string()
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
class TestReadStreamAndRewind:
|
|
256
|
-
"""Test read_stream_and_rewind"""
|
|
257
|
-
def test_with_file_object(self, sample_file):
|
|
258
|
-
assert eg.read_stream_and_rewind(sample_file, 32) == b'this is a sample file.'
|
|
259
|
-
assert sample_file.read() == b'this is a sample file.'
|
|
260
|
-
|
|
261
|
-
def test_with_file_object_truncated(self, sample_file):
|
|
262
|
-
assert eg.read_stream_and_rewind(sample_file, 4) == b'this'
|
|
263
|
-
assert sample_file.read() == b'this is a sample file.'
|
|
264
|
-
|
|
265
|
-
def test_with_multipart_encoder(self, multipart_fields):
|
|
266
|
-
encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "multipart_boundary")
|
|
267
|
-
|
|
268
|
-
buf = eg.read_stream_and_rewind(encoder, 1024)
|
|
269
|
-
assert buf == encoder.to_string()
|
|
270
|
-
assert buf.startswith(b'--multipart_boundary')
|
|
271
|
-
assert len(buf) == encoder.len
|
|
272
|
-
|
|
273
|
-
def test_with_multipart_encoder_truncated(self, multipart_fields):
|
|
274
|
-
encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "multipart_boundary")
|
|
275
|
-
|
|
276
|
-
assert eg.read_stream_and_rewind(encoder, 20) == b'--multipart_boundary'
|
|
277
|
-
buf = eg.read_stream_and_rewind(encoder, 1024)
|
|
278
|
-
assert buf == encoder.to_string()
|
|
279
|
-
assert buf.startswith(b'--multipart_boundary')
|
|
280
|
-
assert len(buf) == encoder.len
|
|
281
|
-
|
|
282
|
-
def test_raises_when_input_has_no_read_method(self):
|
|
283
|
-
with pytest.raises(TypeError) as excinfo:
|
|
284
|
-
eg.read_stream_and_rewind('foo', 10)
|
|
285
|
-
assert excinfo.match('akamai.edgegrid: unexpected body type: str')
|
|
286
|
-
assert str(excinfo.value.__cause__) == "'str' object has no attribute 'read'"
|
|
287
|
-
|
|
288
|
-
def test_raises_when_input_has_no_seek_method(self):
|
|
289
|
-
# pylint: disable=missing-class-docstring,too-few-public-methods
|
|
290
|
-
class DummyReader:
|
|
291
|
-
def read(self, _):
|
|
292
|
-
return b'Hello'
|
|
293
|
-
|
|
294
|
-
with pytest.raises(TypeError) as excinfo:
|
|
295
|
-
eg.read_stream_and_rewind(DummyReader(), 10)
|
|
296
|
-
assert excinfo.match('akamai.edgegrid: unexpected body type: DummyReader')
|
|
297
|
-
assert str(excinfo.value.__cause__) == "'DummyReader' object has no attribute '_buffer'"
|
|
298
|
-
|
|
299
|
-
def test_raises_when_stream_not_seekable(self):
|
|
300
|
-
r, w = os.pipe()
|
|
301
|
-
os.write(w, b'Hello, pipe!')
|
|
302
|
-
os.close(w)
|
|
303
|
-
with pytest.raises(io.UnsupportedOperation) as excinfo:
|
|
304
|
-
with open(r, 'rb') as pipe:
|
|
305
|
-
eg.read_stream_and_rewind(pipe, 10)
|
|
306
|
-
assert excinfo.match('not seekable')
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
class TestDetermineBodyLen:
|
|
310
|
-
"""Test determine_body_len"""
|
|
311
|
-
def test_with_str(self):
|
|
312
|
-
assert eg.determine_body_len('foobarbaz') == 9
|
|
313
|
-
|
|
314
|
-
def test_with_bytes(self):
|
|
315
|
-
assert eg.determine_body_len(b'foobarbaz') == 9
|
|
316
|
-
|
|
317
|
-
def test_with_file(self, sample_file):
|
|
318
|
-
assert eg.determine_body_len(sample_file) == len('this is a sample file.')
|
|
319
|
-
|
|
320
|
-
def test_with_multipart_encoder(self, multipart_fields):
|
|
321
|
-
encoder = requests_toolbelt.MultipartEncoder(multipart_fields, "boundary")
|
|
322
|
-
assert eg.determine_body_len(encoder) == encoder.len
|
|
323
|
-
|
|
324
|
-
def test_raises_on_unknown_body_type(self):
|
|
325
|
-
with pytest.raises(TypeError) as excinfo:
|
|
326
|
-
eg.determine_body_len({'foo': 'bar'})
|
|
327
|
-
assert excinfo.match('akamai.edgegrid: unexpected body type: dict')
|
|
328
|
-
assert str(excinfo.value.__cause__) == "'dict' object has no attribute 'fileno'"
|
|
329
|
-
|
|
330
|
-
def test_raises_when_filelike_body_does_not_support_file_descriptors(self):
|
|
331
|
-
with pytest.raises(io.UnsupportedOperation) as excinfo:
|
|
332
|
-
eg.determine_body_len(io.StringIO("Hello, string"))
|
|
333
|
-
assert excinfo.match('fileno')
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def test_json(testdata):
|
|
337
|
-
auth = EdgeGridAuth(
|
|
338
|
-
client_token=testdata['client_token'],
|
|
339
|
-
client_secret=testdata['client_secret'],
|
|
340
|
-
access_token=testdata['access_token'],
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
params = {
|
|
344
|
-
'extended': 'true',
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
data = {
|
|
348
|
-
'key': 'value',
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
request = requests.Request(
|
|
352
|
-
method='POST',
|
|
353
|
-
url=urljoin(testdata['base_url'], '/testapi/v1/t3'),
|
|
354
|
-
params=params,
|
|
355
|
-
json=data,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
req = request.prepare()
|
|
359
|
-
auth_header = auth.ah.make_auth_header(req, testdata['timestamp'], testdata['nonce'])
|
|
360
|
-
|
|
361
|
-
assert auth_header == testdata['jsontest_hash']
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def test_multipart_encoder(testdata, multipart_fields):
|
|
365
|
-
auth = EdgeGridAuth(
|
|
366
|
-
client_token=testdata["client_token"],
|
|
367
|
-
client_secret=testdata["client_secret"],
|
|
368
|
-
access_token=testdata["access_token"],
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
params = {
|
|
372
|
-
"extended": "true",
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
data = requests_toolbelt.MultipartEncoder(
|
|
376
|
-
fields=multipart_fields,
|
|
377
|
-
boundary="multipart_boundary",
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
request = requests.Request(
|
|
381
|
-
method="POST",
|
|
382
|
-
url=urljoin(testdata["base_url"], "/testapi/v1/t3"),
|
|
383
|
-
params=params,
|
|
384
|
-
data=data,
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
req = request.prepare()
|
|
388
|
-
auth_header = auth.ah.make_auth_header(req, testdata["timestamp"], testdata["nonce"])
|
|
389
|
-
|
|
390
|
-
assert auth_header == testdata["multipart_hash_test"]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Namespace package import test — run by ci/test_namespace_pkg.sh after both
|
|
3
|
-
edgegrid-python and the temporary akamai-dummy fragment are installed.
|
|
4
|
-
Requires REPO_DIR env var pointing to the edgegrid-python repo root.
|
|
5
|
-
"""
|
|
6
|
-
import akamai
|
|
7
|
-
import akamai.dummy
|
|
8
|
-
import akamai.edgegrid
|
|
9
|
-
import os
|
|
10
|
-
|
|
11
|
-
from akamai.dummy import SENTINEL
|
|
12
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
13
|
-
|
|
14
|
-
assert SENTINEL == "akamai.dummy is here", f"Unexpected value: {SENTINEL!r}"
|
|
15
|
-
|
|
16
|
-
# Exercise real EdgeRc code — parse the sample_edgerc shipped with the package.
|
|
17
|
-
sample_edgerc = os.path.join(os.environ["REPO_DIR"], "akamai", "edgegrid", "test", "sample_edgerc")
|
|
18
|
-
rc = EdgeRc(sample_edgerc)
|
|
19
|
-
assert rc.get("default", "host") == "xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/", "EdgeRc.get failed"
|
|
20
|
-
|
|
21
|
-
# Both fragments must live under the *same* namespace package object.
|
|
22
|
-
# Each entry in akamai.__path__ is the `akamai/` dir from one fragment.
|
|
23
|
-
# Derive expected dirs by going up one level from each sub-package's __file__.
|
|
24
|
-
real_paths = [os.path.realpath(p) for p in akamai.__path__ if os.path.isdir(p)]
|
|
25
|
-
|
|
26
|
-
edgegrid_frag = os.path.realpath(os.path.join(os.path.dirname(akamai.edgegrid.__file__), ".."))
|
|
27
|
-
dummy_frag = os.path.realpath(os.path.join(os.path.dirname(akamai.dummy.__file__), ".."))
|
|
28
|
-
|
|
29
|
-
assert edgegrid_frag in real_paths, f"edgegrid fragment {edgegrid_frag!r} not in {real_paths}"
|
|
30
|
-
assert dummy_frag in real_paths, f"dummy fragment {dummy_frag!r} not in {real_paths}"
|
|
31
|
-
|
|
32
|
-
print(f"OK — akamai.edgegrid is reachable from {edgegrid_frag}")
|
|
33
|
-
print(f"OK — akamai.dummy is reachable from {dummy_frag}")
|
build/lib/ci/coverage_report.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Generate a coverage summary markdown table from Cobertura XML files.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
python ci/coverage_report.py <version>:<path/to/cobertura.xml> ...
|
|
6
|
-
|
|
7
|
-
Each positional argument is a colon-separated pair of a label (e.g. the
|
|
8
|
-
Python version) and the path to a Cobertura-format XML coverage report.
|
|
9
|
-
The script reads the top-level ``line-rate`` attribute from each file,
|
|
10
|
-
formats it as a percentage, and prints a markdown table to stdout.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import argparse
|
|
14
|
-
import sys
|
|
15
|
-
import xml.etree.ElementTree as ET
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def parse_line_coverage(xml_path: str) -> float:
|
|
19
|
-
"""Return the top-level line coverage rate (0–1) from a Cobertura XML file."""
|
|
20
|
-
try:
|
|
21
|
-
tree = ET.parse(xml_path) # noqa: S314 — file is a trusted CI artifact
|
|
22
|
-
except ET.ParseError as exc:
|
|
23
|
-
raise SystemExit(f"Failed to parse {xml_path!r}: {exc}") from exc
|
|
24
|
-
root = tree.getroot()
|
|
25
|
-
try:
|
|
26
|
-
return float(root.attrib["line-rate"])
|
|
27
|
-
except (KeyError, ValueError) as exc:
|
|
28
|
-
raise SystemExit(f"Missing or invalid 'line-rate' in {xml_path!r}: {exc}") from exc
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def build_table(rows: list[tuple[str, float]]) -> str:
|
|
32
|
-
lines = [
|
|
33
|
-
"## Coverage Report",
|
|
34
|
-
"",
|
|
35
|
-
"| Python Version | Line Coverage |",
|
|
36
|
-
"| :---: | :---: |",
|
|
37
|
-
]
|
|
38
|
-
for version, rate in sorted(rows):
|
|
39
|
-
pct = rate * 100
|
|
40
|
-
badge = "✅" if pct >= 80 else "❌"
|
|
41
|
-
lines.append(f"| {version} | {badge} {pct:.1f}% |")
|
|
42
|
-
return "\n".join(lines) + "\n"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def main() -> None:
|
|
46
|
-
parser = argparse.ArgumentParser(
|
|
47
|
-
description="Summarise Cobertura coverage reports into a GitHub step summary."
|
|
48
|
-
)
|
|
49
|
-
parser.add_argument(
|
|
50
|
-
"entries",
|
|
51
|
-
nargs="+",
|
|
52
|
-
metavar="VERSION:XML_PATH",
|
|
53
|
-
help="Colon-separated pair of a label and the path to a Cobertura XML file.",
|
|
54
|
-
)
|
|
55
|
-
args = parser.parse_args()
|
|
56
|
-
|
|
57
|
-
rows: list[tuple[str, float]] = []
|
|
58
|
-
for entry in args.entries:
|
|
59
|
-
if ":" not in entry:
|
|
60
|
-
print(f"Error: expected VERSION:PATH, got {entry!r}", file=sys.stderr)
|
|
61
|
-
sys.exit(1)
|
|
62
|
-
version, xml_path = entry.split(":", 1)
|
|
63
|
-
rate = parse_line_coverage(xml_path)
|
|
64
|
-
rows.append((version, rate))
|
|
65
|
-
|
|
66
|
-
table = build_table(rows)
|
|
67
|
-
print(table)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if __name__ == "__main__":
|
|
71
|
-
main()
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# This example creates your new API client credentials.
|
|
2
|
-
#
|
|
3
|
-
# To run this example:
|
|
4
|
-
#
|
|
5
|
-
# 1. Specify the location of your .edgerc file and the section header of the set of credentials to use.
|
|
6
|
-
#
|
|
7
|
-
## The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default.
|
|
8
|
-
#
|
|
9
|
-
# 2. Open a Terminal or shell instance and run "python examples/create-credentials.py".
|
|
10
|
-
#
|
|
11
|
-
# A successful call returns a new API client with its credentialId. Use this ID in both the update and delete examples.
|
|
12
|
-
#
|
|
13
|
-
# For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/post-self-credentials.
|
|
14
|
-
|
|
15
|
-
import requests
|
|
16
|
-
import json
|
|
17
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
18
|
-
from urllib.parse import urljoin
|
|
19
|
-
|
|
20
|
-
edgerc = EdgeRc('~/.edgerc')
|
|
21
|
-
section = 'default'
|
|
22
|
-
baseurl = 'https://%s' % edgerc.get(section, 'host')
|
|
23
|
-
|
|
24
|
-
session = requests.Session()
|
|
25
|
-
session.auth = EdgeGridAuth.from_edgerc(edgerc, section)
|
|
26
|
-
|
|
27
|
-
path = '/identity-management/v3/api-clients/self/credentials'
|
|
28
|
-
headers = {
|
|
29
|
-
"Accept": "application/json"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
result = session.post(urljoin(baseurl, path), headers=headers)
|
|
33
|
-
print(result.status_code)
|
|
34
|
-
print(json.dumps(result.json(), indent=2))
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# This example deletes your API client credentials.
|
|
2
|
-
#
|
|
3
|
-
# To run this example:
|
|
4
|
-
#
|
|
5
|
-
# 1. Specify the location of your .edgerc file and the section header of the set of credentials to use.
|
|
6
|
-
#
|
|
7
|
-
# The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default.
|
|
8
|
-
#
|
|
9
|
-
# 2. Add the credentialId from the update example to the path. You can only delete inactive credentials. Sending the request on an active set will return a 400. Use the update credentials example for deactivation.
|
|
10
|
-
#
|
|
11
|
-
# **Important:** Don't use the credentials you're actively using when deleting a set of credentials. Otherwise, you'll block your access to the Akamai APIs.
|
|
12
|
-
#
|
|
13
|
-
# 3. Open a Terminal or shell instance and run "python examples/delete-credentials.py".
|
|
14
|
-
#
|
|
15
|
-
# A successful call returns "" null.
|
|
16
|
-
#
|
|
17
|
-
# For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/delete-self-credential.
|
|
18
|
-
|
|
19
|
-
import requests
|
|
20
|
-
import json
|
|
21
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
22
|
-
from urllib.parse import urljoin
|
|
23
|
-
|
|
24
|
-
edgerc = EdgeRc('~/.edgerc')
|
|
25
|
-
section = 'default'
|
|
26
|
-
baseurl = 'https://%s' % edgerc.get(section, 'host')
|
|
27
|
-
|
|
28
|
-
session = requests.Session()
|
|
29
|
-
session.auth = EdgeGridAuth.from_edgerc(edgerc, section)
|
|
30
|
-
credentialId = 123456
|
|
31
|
-
|
|
32
|
-
path = '/identity-management/v3/api-clients/self/credentials/{}'.format(credentialId)
|
|
33
|
-
|
|
34
|
-
result = session.delete(urljoin(baseurl, path))
|
|
35
|
-
print(result.status_code)
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# This example returns a list of your API client credentials.
|
|
2
|
-
#
|
|
3
|
-
# To run this example:
|
|
4
|
-
#
|
|
5
|
-
# 1. Specify the location of your .edgerc file and the section header of the set of credentials to use.
|
|
6
|
-
#
|
|
7
|
-
# The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default.
|
|
8
|
-
#
|
|
9
|
-
# 2. Open a Terminal or shell instance and run "python examples/get-credentials.py".
|
|
10
|
-
#
|
|
11
|
-
# A successful call returns your credentials grouped by credentialId.
|
|
12
|
-
#
|
|
13
|
-
# For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/get-self-credentials.
|
|
14
|
-
|
|
15
|
-
import requests
|
|
16
|
-
import json
|
|
17
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
18
|
-
from urllib.parse import urljoin
|
|
19
|
-
|
|
20
|
-
edgerc = EdgeRc('~/.edgerc')
|
|
21
|
-
section = 'default'
|
|
22
|
-
baseurl = 'https://%s' % edgerc.get(section, 'host')
|
|
23
|
-
|
|
24
|
-
session = requests.Session()
|
|
25
|
-
session.auth = EdgeGridAuth.from_edgerc(edgerc, section)
|
|
26
|
-
|
|
27
|
-
path = '/identity-management/v3/api-clients/self/credentials'
|
|
28
|
-
headers = {
|
|
29
|
-
"Accept": "application/json"}
|
|
30
|
-
querystring = {
|
|
31
|
-
"actions": True
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
result = session.get(urljoin(baseurl, path), headers=headers, params=querystring)
|
|
35
|
-
print(result.status_code)
|
|
36
|
-
print(json.dumps(result.json(), indent=2))
|
|
37
|
-
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# This example updates the credentials from the create credentials example.
|
|
2
|
-
#
|
|
3
|
-
# To run this example:
|
|
4
|
-
#
|
|
5
|
-
# 1. Specify the location of your .edgerc file and the section header of the set of credentials to use.
|
|
6
|
-
#
|
|
7
|
-
# The defaults here expect the .edgerc at your home directory and use the credentials under the heading of default.
|
|
8
|
-
#
|
|
9
|
-
# 2. Add the credentialId for the set of credentials created using the create example as a path parameter.
|
|
10
|
-
#
|
|
11
|
-
# 3. Edit the expiresOn date to today's date. Optionally, you can change the description value.
|
|
12
|
-
#
|
|
13
|
-
# **Important:** Don't use the credentials you're actively using when inactivating a set of credentials. Otherwise, you'll block your access to the Akamai APIs.
|
|
14
|
-
#
|
|
15
|
-
# 4. Open a Terminal or shell instance and run "python examples/update-credentials.py".
|
|
16
|
-
#
|
|
17
|
-
# A successful call returns.
|
|
18
|
-
#
|
|
19
|
-
# For more information on the call used in this example, see https://techdocs.akamai.com/iam-api/reference/put-self-credential.
|
|
20
|
-
|
|
21
|
-
import requests
|
|
22
|
-
import json
|
|
23
|
-
from akamai.edgegrid import EdgeGridAuth, EdgeRc
|
|
24
|
-
from urllib.parse import urljoin
|
|
25
|
-
|
|
26
|
-
edgerc = EdgeRc('~/.edgerc')
|
|
27
|
-
section = 'default'
|
|
28
|
-
baseurl = 'https://%s' % edgerc.get(section, 'host')
|
|
29
|
-
|
|
30
|
-
session = requests.Session()
|
|
31
|
-
session.auth = EdgeGridAuth.from_edgerc(edgerc, section)
|
|
32
|
-
credentialId = 123456
|
|
33
|
-
|
|
34
|
-
path = '/identity-management/v3/api-clients/self/credentials/{}'.format(credentialId)
|
|
35
|
-
headers = {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
"Accept": "application/json"}
|
|
38
|
-
payload = {
|
|
39
|
-
"status": "INACTIVE",
|
|
40
|
-
"expiresOn": "2024-12-30T22:09:24.000Z", # The date cannot be more than two years out or it will return a 400
|
|
41
|
-
"description": "Update this credential"
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
result = session.put(urljoin(baseurl, path), headers=headers, json=payload)
|
|
45
|
-
print(result.status_code)
|
|
46
|
-
print(json.dumps(result.json(), indent=2))
|
|
47
|
-
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
akamai/edgegrid/__init__.py,sha256=fzAqUKSNm_UqybPY6sXyTsNJYbuSBebdBqPJd43FKjg,1217
|
|
2
|
-
akamai/edgegrid/edgegrid.py,sha256=pzJQ7qjC5KZIIM_yrhdVHUuJe6CMFzt-z1ucQeN2-vk,12002
|
|
3
|
-
akamai/edgegrid/edgerc.py,sha256=T9RoKyWnVIIOwAVaJZ3QV5xBAJ9vz7XiyUZ8tMET8dI,1170
|
|
4
|
-
akamai/edgegrid/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
akamai/edgegrid/test/conftest.py,sha256=ewP2gqZI0POKP0d3QQuD0yiPYkPM9sRqLBekk00GnFQ,908
|
|
6
|
-
akamai/edgegrid/test/edgerc_that_doesnt_parse,sha256=j1uEFSxZo_o9pYuj6QdxWIOSmRTLLlLPLWQ8sziHm1A,138
|
|
7
|
-
akamai/edgegrid/test/sample_edgerc,sha256=0DsP3KEoLXjd7fhkKcVoCT5cM1S69iNZmO8VvdwKWLA,1084
|
|
8
|
-
akamai/edgegrid/test/sample_file.txt,sha256=EBMsdifxSkOKTPB-eqA7be67wJ5YWG_G8NjoNdtZqBM,22
|
|
9
|
-
akamai/edgegrid/test/test_edgegrid.py,sha256=GhX1CjO3CS_cVV6hFOyOKL8ovxvEZpHbqrFBGmTKkw8,13506
|
|
10
|
-
akamai/edgegrid/test/testcases.json,sha256=81LV7AgNTmHSX4X56dZszrVk-Zk1Jw5vkvI6DFJazSE,15040
|
|
11
|
-
akamai/edgegrid/test/testdata.json,sha256=kJ0DYk94SJeatnvdqYTCw-itxMUdbIhzVta3rIYAVT0,1149
|
|
12
|
-
build/lib/akamai/edgegrid/__init__.py,sha256=fzAqUKSNm_UqybPY6sXyTsNJYbuSBebdBqPJd43FKjg,1217
|
|
13
|
-
build/lib/akamai/edgegrid/edgegrid.py,sha256=pzJQ7qjC5KZIIM_yrhdVHUuJe6CMFzt-z1ucQeN2-vk,12002
|
|
14
|
-
build/lib/akamai/edgegrid/edgerc.py,sha256=T9RoKyWnVIIOwAVaJZ3QV5xBAJ9vz7XiyUZ8tMET8dI,1170
|
|
15
|
-
build/lib/akamai/edgegrid/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
build/lib/akamai/edgegrid/test/conftest.py,sha256=ewP2gqZI0POKP0d3QQuD0yiPYkPM9sRqLBekk00GnFQ,908
|
|
17
|
-
build/lib/akamai/edgegrid/test/test_edgegrid.py,sha256=GhX1CjO3CS_cVV6hFOyOKL8ovxvEZpHbqrFBGmTKkw8,13506
|
|
18
|
-
build/lib/ci/check_namespace_pkg.py,sha256=mUy-bcH8KV2ujgkgFFJ_yJ2XzVn1_YdQhXDQeCENNnc,1594
|
|
19
|
-
build/lib/ci/coverage_report.py,sha256=jykNDfg5MTHCCwEg6_GtgyUQO4B2Tt3Ws8SrKPtXmYI,2273
|
|
20
|
-
build/lib/examples/create-credentials.py,sha256=z-9ItuXNI3lFhMv3A5-dZC0QfSgTGvNJciQOPNWBurQ,1191
|
|
21
|
-
build/lib/examples/delete-credentials.py,sha256=HM6a8fy-44beXKLhs_JVk8ChdOJlvF1s4XwWn9VATqE,1422
|
|
22
|
-
build/lib/examples/get-credentials.py,sha256=duI0pRRwX9--Eibnp4EsIRQebwnsygzmCEScemKD27w,1198
|
|
23
|
-
build/lib/examples/update-credentials.py,sha256=BEahXLHjIXuBW77wuQK98fbKSxlibugG1nQD1nvVcN0,1783
|
|
24
|
-
ci/check_namespace_pkg.py,sha256=mUy-bcH8KV2ujgkgFFJ_yJ2XzVn1_YdQhXDQeCENNnc,1594
|
|
25
|
-
ci/coverage_report.py,sha256=jykNDfg5MTHCCwEg6_GtgyUQO4B2Tt3Ws8SrKPtXmYI,2273
|
|
26
|
-
edgegrid_python-2.0.6.dist-info/licenses/LICENSE,sha256=aai-NM26EYkRybAI1eWSlozmBr8-EQ1GWLJe_9qc8M4,10294
|
|
27
|
-
examples/create-credentials.py,sha256=z-9ItuXNI3lFhMv3A5-dZC0QfSgTGvNJciQOPNWBurQ,1191
|
|
28
|
-
examples/delete-credentials.py,sha256=HM6a8fy-44beXKLhs_JVk8ChdOJlvF1s4XwWn9VATqE,1422
|
|
29
|
-
examples/get-credentials.py,sha256=duI0pRRwX9--Eibnp4EsIRQebwnsygzmCEScemKD27w,1198
|
|
30
|
-
examples/update-credentials.py,sha256=BEahXLHjIXuBW77wuQK98fbKSxlibugG1nQD1nvVcN0,1783
|
|
31
|
-
edgegrid_python-2.0.6.dist-info/METADATA,sha256=D27jiABY-ZRgoTXiqtRN1w6SVmfKe-JC34itGjUuBZY,9208
|
|
32
|
-
edgegrid_python-2.0.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
33
|
-
edgegrid_python-2.0.6.dist-info/top_level.txt,sha256=ttvQo9dlYgqcI6oEw8zYerOg7riO5DwNVGFS_I_o8E8,25
|
|
34
|
-
edgegrid_python-2.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|