edgegrid-python 1.3.1__py3-none-any.whl → 2.0.0__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.
@@ -32,25 +32,10 @@ usage:
32
32
 
33
33
  from .edgegrid import EdgeGridAuth
34
34
  from .edgerc import EdgeRc
35
+
35
36
  __all__ = ['EdgeGridAuth', 'EdgeRc']
36
37
 
37
38
  __title__ = 'edgegrid-python'
38
- __version__ = '1.3.1'
39
- __author__ = 'Jonathan Landis <jlandis@akamai.com>'
40
- __maintainer__ = 'Akamai Developer Experience team <dl-devexp-eng@akamai.com>'
39
+ __version__ = '2.0.0'
41
40
  __license__ = 'Apache 2.0'
42
- __copyright__ = 'Copyright 2021 Akamai Technologies'
43
-
44
- # Copyright 2021 Akamai Technologies, Inc. All Rights Reserved
45
- #
46
- # Licensed under the Apache License, Version 2.0 (the "License");
47
- # you may not use this file except in compliance with the License.
48
- # You may obtain a copy of the License at
49
- #
50
- # http://www.apache.org/licenses/LICENSE-2.0
51
- #
52
- # Unless required by applicable law or agreed to in writing, software
53
- # distributed under the License is distributed on an "AS IS" BASIS,
54
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
55
- # See the License for the specific language governing permissions and
56
- # limitations under the License.
41
+ __copyright__ = 'Copyright 2024 Akamai Technologies'
@@ -1,23 +1,5 @@
1
- # EdgeGrid requests Auth handler
2
- #
3
- # Original author: Jonathan Landis <jlandis@akamai.com>
4
- # Package maintainer: Akamai Developer Experience team <dl-devexp-eng@akamai.com>
5
- #
6
- # For more information visit https://developer.akamai.com
7
-
8
- # Copyright 2021 Akamai Technologies, Inc. All Rights Reserved
9
- #
10
- # Licensed under the Apache License, Version 2.0 (the "License");
11
- # you may not use this file except in compliance with the License.
12
- # You may obtain a copy of the License at
13
- #
14
- # http://www.apache.org/licenses/LICENSE-2.0
15
- #
16
- # Unless required by applicable law or agreed to in writing, software
17
- # distributed under the License is distributed on an "AS IS" BASIS,
18
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
- # See the License for the specific language governing permissions and
20
- # limitations under the License.
1
+ # pylint: disable=too-many-arguments,missing-function-docstring
2
+ """EdgeGrid requests Auth handler"""
21
3
 
22
4
  import logging
23
5
  import uuid
@@ -25,19 +7,13 @@ import hashlib
25
7
  import hmac
26
8
  import base64
27
9
  import re
28
- import sys
29
10
  import os
30
- from requests.auth import AuthBase
31
11
  from time import gmtime, strftime
12
+ from urllib.parse import urlparse
13
+
14
+ from requests.auth import AuthBase
32
15
 
33
- if sys.version_info[0] >= 3:
34
- # python3
35
- from urllib.parse import urlparse
36
- else:
37
- # python2.7
38
- from urlparse import urlparse
39
- import urllib3.contrib.pyopenssl
40
- urllib3.contrib.pyopenssl.inject_into_urllib3()
16
+ from .edgerc import EdgeRc
41
17
 
42
18
  logger = logging.getLogger(__name__)
43
19
 
@@ -45,6 +21,7 @@ __all__ = ['EdgeGridAuth']
45
21
 
46
22
 
47
23
  def eg_timestamp():
24
+ """Generates EdgeGrid compatible timestamp"""
48
25
  return strftime('%Y%m%dT%H:%M:%S+0000', gmtime())
49
26
 
50
27
 
@@ -61,26 +38,84 @@ def base64_hmac_sha256(data, key):
61
38
  ).decode('utf8')
62
39
 
63
40
 
64
- def get_multipart_body(encoder, size=-1):
65
- multipart_body = encoder.read(size)
66
- encoder._buffer.seek(0)
67
- return multipart_body
41
+ def base64_sha256(data):
42
+ digest = hashlib.sha256(data).digest()
43
+ return base64.b64encode(digest).decode('utf8')
68
44
 
69
45
 
70
- def base64_sha256(data):
71
- if isinstance(data, str):
72
- data = data.encode('utf8')
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
+ """
73
61
  try:
74
- return base64.b64encode(hashlib.sha256(data).digest()).decode('utf8')
75
- except TypeError:
76
- return base64.b64encode(hashlib.sha256(get_multipart_body(data)).digest()).decode('utf8')
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
77
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'))
78
108
 
79
- def get_prepared_body_len(prepared_body):
80
109
  try:
81
- return len(prepared_body)
82
- except TypeError:
83
- return prepared_body.len
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
84
119
 
85
120
 
86
121
  class EdgeGridAuth(AuthBase):
@@ -99,7 +134,7 @@ class EdgeGridAuth(AuthBase):
99
134
  """
100
135
 
101
136
  def __init__(self, client_token, client_secret, access_token,
102
- headers_to_sign=(), max_body=131072):
137
+ *, headers_to_sign=(), max_body=131072):
103
138
  """Initialize authentication using the given parameters from the Akamai OPEN APIs
104
139
  Interface:
105
140
 
@@ -112,12 +147,13 @@ class EdgeGridAuth(AuthBase):
112
147
  specific APIs. (default 131072)
113
148
 
114
149
  """
150
+ # pylint: disable=invalid-name
115
151
  self.ah = EdgeGridAuthHeaders(
116
152
  client_token,
117
153
  client_secret,
118
154
  access_token,
119
- headers_to_sign,
120
- max_body
155
+ headers_to_sign=headers_to_sign,
156
+ max_body=max_body
121
157
  )
122
158
 
123
159
  @staticmethod
@@ -131,21 +167,20 @@ class EdgeGridAuth(AuthBase):
131
167
  default is 'default')
132
168
 
133
169
  """
134
- from .edgerc import EdgeRc
135
170
  if isinstance(rcinput, EdgeRc):
136
- rc = rcinput
171
+ edgerc = rcinput
137
172
  else:
138
- rc = EdgeRc(rcinput)
173
+ edgerc = EdgeRc(rcinput)
139
174
 
140
175
  return EdgeGridAuth(
141
- client_token=rc.get(section, 'client_token'),
142
- client_secret=rc.get(section, 'client_secret'),
143
- access_token=rc.get(section, 'access_token'),
144
- headers_to_sign=rc.getlist(section, 'headers_to_sign'),
145
- max_body=rc.getint(section, 'max_body')
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')
146
181
  )
147
182
 
148
- def handle_redirect(self, res, **kwargs):
183
+ def handle_redirect(self, res, **_):
149
184
  if res.is_redirect:
150
185
  redirect_location = res.headers['location']
151
186
 
@@ -154,34 +189,24 @@ class EdgeGridAuth(AuthBase):
154
189
  request_to_sign.url = redirect_location
155
190
 
156
191
  res.request.headers['Authorization'] = self.ah.make_auth_header(
157
- request_to_sign.url,
158
- request_to_sign.headers,
159
- request_to_sign.method,
160
- request_to_sign.body,
161
- eg_timestamp(),
162
- new_nonce()
163
- )
192
+ request_to_sign, eg_timestamp(), new_nonce())
164
193
 
165
194
  def __call__(self, r):
166
195
  timestamp = eg_timestamp()
167
196
  nonce = new_nonce()
168
197
 
169
- r.headers['Authorization'] = self.ah.make_auth_header(
170
- r.url,
171
- r.headers,
172
- r.method,
173
- r.body,
174
- timestamp,
175
- nonce
176
- )
198
+ r.headers['Authorization'] = self.ah.make_auth_header(r, timestamp, nonce)
177
199
  r.register_hook('response', self.handle_redirect)
178
200
  return r
179
201
 
180
202
 
181
- class EdgeGridAuthHeaders():
182
-
203
+ class EdgeGridAuthHeaders:
204
+ """
205
+ A class for preparing requests authentication headers needed for
206
+ Akamai {OPEN} EdgeGrid support.
207
+ """
183
208
  def __init__(self, client_token, client_secret, access_token,
184
- headers_to_sign=(), max_body=131072):
209
+ *, headers_to_sign=(), max_body=131072):
185
210
  self.client_token = client_token
186
211
  self.client_secret = client_secret
187
212
  self.access_token = access_token
@@ -197,38 +222,36 @@ class EdgeGridAuthHeaders():
197
222
  spaces_re = re.compile('\\s+')
198
223
 
199
224
  # note: r.headers is a case-insensitive dict and self.headers_to_sign
200
- # should already be lowercased at this point
225
+ # should already be in lowercase at this point
226
+ # pylint: disable=consider-using-f-string
201
227
  return '\t'.join([
202
228
  "%s:%s" % (h, spaces_re.sub(' ', headers[h].strip()))
203
229
  for h in self.headers_to_sign if h in headers
204
230
  ])
205
231
 
206
232
  def make_content_hash(self, body, method):
233
+ logger.debug("body is '%s'", body)
207
234
  content_hash = ""
208
- prepared_body = body
209
- logger.debug("body is '%s'", prepared_body)
210
-
211
- if method == 'POST' and get_prepared_body_len(prepared_body) > 0:
212
- logger.debug("signing content: %s", prepared_body)
213
- if get_prepared_body_len(prepared_body) > self.max_body:
214
- logger.debug(
215
- "data length %d is larger than maximum %d",
216
- get_prepared_body_len(prepared_body), self.max_body
217
- )
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)
218
240
  try:
219
- prepared_body = prepared_body[0:self.max_body]
220
- except TypeError:
221
- prepared_body = get_multipart_body(prepared_body, self.max_body)
222
- logger.debug(
223
- "data truncated to %d for computing the hash",
224
- get_prepared_body_len(prepared_body))
225
-
226
- content_hash = base64_sha256(prepared_body)
227
-
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)
228
250
  logger.debug("content hash is '%s'", content_hash)
229
251
  return content_hash
230
252
 
231
- def get_header_versions(self, header=None):
253
+ @staticmethod
254
+ def get_header_versions(header=None):
232
255
  if header is None:
233
256
  header = {}
234
257
 
@@ -242,7 +265,7 @@ class EdgeGridAuthHeaders():
242
265
  akamai_cli_command_version = os.getenv('AKAMAI_CLI_COMMAND_VERSION')
243
266
  if akamai_cli_command and akamai_cli_command_version:
244
267
  version_header += " AkamaiCLI-" + akamai_cli_command + \
245
- "/" + akamai_cli_command_version
268
+ "/" + akamai_cli_command_version
246
269
 
247
270
  if version_header != '':
248
271
  if 'User-Agent' not in header:
@@ -252,37 +275,37 @@ class EdgeGridAuthHeaders():
252
275
 
253
276
  return header
254
277
 
255
- def make_data_to_sign(self, url, headers, auth_header, method, body):
256
- parsed_url = urlparse(url)
278
+ def make_data_to_sign(self, request, auth_header):
279
+ parsed_url = urlparse(request.url)
257
280
 
258
- if headers.get('Host', False):
259
- netloc = headers['Host']
281
+ if request.headers.get('Host', False):
282
+ netloc = request.headers['Host']
260
283
  else:
261
284
  netloc = parsed_url.netloc
262
285
 
263
- self.get_header_versions(headers)
286
+ self.get_header_versions(request.headers)
264
287
 
265
288
  data_to_sign = '\t'.join([
266
- method,
289
+ request.method,
267
290
  parsed_url.scheme,
268
291
  netloc,
269
292
  # Note: relative URL constraints are handled by requests when it sets up 'r'
270
293
  parsed_url.path + (';' + parsed_url.params if parsed_url.params else "") +
271
294
  ('?' + parsed_url.query if parsed_url.query else ""),
272
- self.canonicalize_headers(headers),
273
- self.make_content_hash(body or '', method),
295
+ self.canonicalize_headers(request.headers),
296
+ self.make_content_hash(request.body or '', request.method),
274
297
  auth_header
275
298
  ])
276
299
  logger.debug('data to sign: %s', '\\t'.join(data_to_sign.split('\t')))
277
300
  return data_to_sign
278
301
 
279
- def sign_request(self, url, headers, method, body, timestamp, auth_header):
302
+ def sign_request(self, request, timestamp, auth_header):
280
303
  return base64_hmac_sha256(
281
- self.make_data_to_sign(url, headers, auth_header, method, body),
304
+ self.make_data_to_sign(request, auth_header),
282
305
  self.make_signing_key(timestamp)
283
306
  )
284
307
 
285
- def make_auth_header(self, url, headers, method, body, timestamp, nonce):
308
+ def make_auth_header(self, request, timestamp, nonce):
286
309
  kvps = [
287
310
  ('client_token', self.client_token),
288
311
  ('access_token', self.access_token),
@@ -290,11 +313,11 @@ class EdgeGridAuthHeaders():
290
313
  ('nonce', nonce),
291
314
  ]
292
315
  auth_header = "EG1-HMAC-SHA256 " + \
293
- ';'.join(["%s=%s" % kvp for kvp in kvps]) + ';'
316
+ ';'.join([f"{k}={v}" for k, v in kvps]) + ';'
294
317
  logger.debug('unsigned authorization header: %s', auth_header)
295
318
 
296
319
  signed_auth_header = auth_header + \
297
- 'signature=' + self.sign_request(url, headers, method, body, timestamp, auth_header)
320
+ 'signature=' + self.sign_request(request, timestamp, auth_header)
298
321
 
299
322
  logger.debug('signed authorization header: %s', signed_auth_header)
300
323
  return signed_auth_header
akamai/edgegrid/edgerc.py CHANGED
@@ -1,35 +1,14 @@
1
- # support for .edgerc file format
2
- #
3
- # Copyright 2021 Akamai Technologies, Inc. All Rights Reserved
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
1
+ """Support for .edgerc file format"""
16
2
 
17
3
  import logging
18
- import sys
4
+ from configparser import ConfigParser
19
5
  from os.path import expanduser
20
6
 
21
- if sys.version_info[0] >= 3:
22
- # python3
23
- from configparser import ConfigParser
24
- else:
25
- # python2.7
26
- from ConfigParser import ConfigParser
27
-
28
-
29
7
  logger = logging.getLogger(__name__)
30
8
 
31
9
 
32
10
  class EdgeRc(ConfigParser):
11
+ """Class for managing .edgerc files"""
33
12
  def __init__(self, filename):
34
13
  ConfigParser.__init__(self,
35
14
  {'client_token': '',
@@ -50,11 +29,9 @@ class EdgeRc(ConfigParser):
50
29
 
51
30
  def getlist(self, section, option):
52
31
  """
53
- returns the named option as a list, splitting the original value
54
- by ','
32
+ returns the named option as a list, splitting the original value by ','
55
33
  """
56
34
  value = self.get(section, option)
57
35
  if value:
58
36
  return value.split(',')
59
- else:
60
- return None
37
+ return None
@@ -0,0 +1,44 @@
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,4 +1,4 @@
1
1
  [default]
2
2
  client_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
3
3
  access_token = xxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
4
- max_body = 131072
4
+ max_body = xx131072