deceit 0.4__tar.gz → 0.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deceit
3
- Version: 0.4
3
+ Version: 0.7
4
4
  Summary: deceit
5
5
  Home-page: https://bitbucket.org/dbuy/deceit
6
6
  Author: dev
@@ -1,25 +1,30 @@
1
1
  import json
2
2
  import logging
3
3
  import posixpath
4
+ from datetime import datetime, timedelta
5
+ from time import time
4
6
  from typing import Optional, Dict, Any
7
+ from pytz import utc
5
8
  import requests
6
9
  from requests import PreparedRequest
7
10
  from requests.models import Request, Response
8
-
11
+ from oauthlib.oauth2 import BackendApplicationClient
12
+ from oauthlib.oauth2.rfc6749.tokens import prepare_bearer_headers
13
+ from requests_oauthlib.oauth2_session import OAuth2Session
9
14
  from .adapters import RetryAdapter
10
15
  from .encoders import JsonEncoder
11
16
  from .exceptions import ApiException
12
17
 
13
18
 
14
-
15
19
  class ApiClient:
16
20
  def __init__(self, *args, base_url=None, default_timeout=None,
17
21
  exception_class=None, **kwargs):
18
22
  self.base_url = base_url
19
- adapter = RetryAdapter(timeout=default_timeout)
23
+ self.default_timeout = default_timeout
24
+ self.adapter = RetryAdapter(timeout=self.default_timeout)
20
25
  self.session = requests.Session()
21
- self.session.mount('https://', adapter)
22
- self.session.mount('http://', adapter)
26
+ self.session.mount('https://', self.adapter)
27
+ self.session.mount('http://', self.adapter)
23
28
  self.log = logging.getLogger(__name__)
24
29
  self.exception_class = exception_class or ApiException
25
30
 
@@ -61,16 +66,17 @@ class ApiClient:
61
66
  """
62
67
  headers = self.headers()
63
68
  headers.update(kwargs.pop('headers', None) or {})
64
- timeout = kwargs.pop('timeout', None)
69
+ timeout = kwargs.pop('timeout', None) or self.default_timeout
65
70
  send_kwargs = {}
66
71
  if timeout is not None:
67
72
  send_kwargs['timeout'] = timeout
68
73
  base_url = self.base_url
69
74
  url = posixpath.join(base_url, route) if base_url else route
75
+ body = None
70
76
  if json_data:
71
77
  body = json.dumps(json_data, cls=JsonEncoder)
72
78
  headers.setdefault('content-type', 'application/json')
73
- else:
79
+ elif form_data:
74
80
  body = form_data
75
81
  headers.setdefault('content-type', 'application/x-www-form-urlencoded')
76
82
  request = Request(
@@ -97,7 +103,7 @@ class ApiClient:
97
103
  raise self.exception_class.from_response(response)
98
104
  try:
99
105
  return response.json()
100
- except json.JSONDecodeError:
106
+ except json.JSONDecodeError: # pragma: nocover
101
107
  pass
102
108
  return None
103
109
 
@@ -123,3 +129,62 @@ class ApiClient:
123
129
  return self.send(
124
130
  'patch', route, params=params, form_data=form_data,
125
131
  json_data=json_data, **kwargs)
132
+
133
+
134
+ class BackendOauth2Client(ApiClient):
135
+ def __init__(self, conf, *args, base_url=None, default_timeout=None,
136
+ exception_class=None, token_url=None,
137
+ client_id=None, password=None, scopes=None,
138
+ include_client_id=None, **kwargs):
139
+ super().__init__(
140
+ base_url=base_url or conf.base_url or None,
141
+ default_timeout=default_timeout or conf.default_timeout or None,
142
+ exception_class=exception_class, **kwargs)
143
+ self.token_url = token_url or conf.token_url
144
+ self.client_id = client_id or conf.client_id
145
+ self.token_expires_at = time()
146
+ self.client_secret = password or conf.client_secret or conf.password
147
+ self.scopes = scopes or conf.scopes or []
148
+ self.session = OAuth2Session(client=BackendApplicationClient(
149
+ client_id=self.client_id,
150
+ scope=self.scope))
151
+ self.include_client_id = include_client_id or conf.get('include_client_id')
152
+ self.session.mount('https://', self.adapter)
153
+
154
+ @property
155
+ def scope(self):
156
+ if self.scopes:
157
+ scopes = ' '.join(self.scopes)
158
+ return scopes
159
+ return None
160
+
161
+ def fetch_token(self, **kwargs):
162
+ tm_now = time()
163
+ self.session.fetch_token(
164
+ self.token_url,
165
+ client_id=self.client_id,
166
+ client_secret=self.client_secret,
167
+ include_client_id=self.include_client_id,
168
+ scope=self.scope,
169
+ **kwargs)
170
+ expires_at = self.session.token.get('expires_at')
171
+ if not expires_at:
172
+ # microsoft apis can use `expires_on`
173
+ expires_at = self.session.token.get('expires_on')
174
+ if not expires_at:
175
+ # some other apis use `expires_in`
176
+ expires_at = self.session.token.get('expires_in') + tm_now
177
+ self.token_expires_at = expires_at - 300
178
+
179
+ @classmethod
180
+ def now(cls):
181
+ return datetime.now(utc)
182
+
183
+ @property
184
+ def expired(self):
185
+ return time() >= self.token_expires_at
186
+
187
+ def presend(self, request: PreparedRequest):
188
+ if self.expired:
189
+ self.fetch_token()
190
+ prepare_bearer_headers(self.session.access_token, request.headers)
@@ -3,12 +3,14 @@ from requests.models import Response
3
3
 
4
4
 
5
5
  class ApiException(Exception):
6
- def __init__(self, status_code=None, content=None, text=None, data=None):
7
- super().__init__(status_code, content, text, data)
6
+ def __init__(self, status_code=None, content=None, text=None, data=None,
7
+ headers=None):
8
+ super().__init__(status_code, content, text, data, headers)
8
9
  self.status_code = status_code
9
10
  self.content = content
10
11
  self.text = text
11
12
  self.data = data
13
+ self.headers = headers
12
14
 
13
15
  def __str__(self):
14
16
  klass = self.__class__.__name__.lower()
@@ -23,7 +25,12 @@ class ApiException(Exception):
23
25
  body = json.dumps(self.data, indent=2)
24
26
  else:
25
27
  body = self.text
26
- p.text(f'[{klass}] [{self.status_code}] => {body}')
28
+ st_headers = ''
29
+ if self.headers:
30
+ st = json.dumps(self.headers, indent=2)
31
+ st_headers = f'[{klass}] [{self.status_code}] / headers => {st}'
32
+ p.text(f'[{klass}] [{self.status_code}] => {body}' + st_headers)
33
+ p.text()
27
34
 
28
35
  @classmethod
29
36
  def from_response(cls, response: Response):
@@ -33,10 +40,12 @@ class ApiException(Exception):
33
40
  response.content,
34
41
  response.text,
35
42
  response.json(),
43
+ response.headers,
36
44
  )
37
45
  except json.JSONDecodeError:
38
46
  return cls(
39
47
  response.status_code,
40
48
  response.content,
41
49
  response.text,
50
+ headers=response.headers,
42
51
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: deceit
3
- Version: 0.4
3
+ Version: 0.7
4
4
  Summary: deceit
5
5
  Home-page: https://bitbucket.org/dbuy/deceit
6
6
  Author: dev
@@ -1,4 +1,6 @@
1
1
  requests
2
+ requests_oauthlib
3
+ pytz
2
4
 
3
5
  [dev]
4
6
  pytest
@@ -10,3 +12,5 @@ pytest-pspec
10
12
  ipython
11
13
  bumpversion
12
14
  pytest-docker
15
+ waddle
16
+ convocations
@@ -7,4 +7,6 @@ pytest-pspec
7
7
  ipython
8
8
  bumpversion
9
9
  pytest-docker
10
+ waddle
11
+ convocations
10
12
 
@@ -7,7 +7,7 @@ from importlib.resources import open_text
7
7
  # Version info -- read without importing
8
8
  _locals = {}
9
9
 
10
- version = '0.4'
10
+ version = '0.7'
11
11
 
12
12
  # PyYAML ships a split Python 2/3 codebase. Unfortunately, some pip versions
13
13
  # attempt to interpret both halves of PyYAML, yielding SyntaxErrors. Thus, we
@@ -70,6 +70,8 @@ setup(
70
70
  ],
71
71
  install_requires=[
72
72
  'requests',
73
+ 'requests_oauthlib',
74
+ 'pytz',
73
75
  ],
74
76
  extras_require=extras,
75
77
  )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes