pypi package 'wipac-rest-tools'

Popularity: Medium (more popular than 90% of all packages)
Description: REST tools in python - common code for client and server
Installation: pip install wipac-rest-tools
Last version: 1.3.6 (Download)
Homepage: https://github.com/WIPACrepo/rest-tools
Size: 23.88 kB
License: MIT
Keywords: python, rest, tools, utilities, opentelemetry, tracing, telemetry, wipac, icecube

Activity

Last modified: August 9, 2022 5:36 PM (3 days ago)
Versions released in one year: 7
Weekly downloads: 102
03/13/202205/29/2022015030045060001234released versions / week
  • Versions released
  • Weekly downloads

What's new in version 1.3.6

Delta between version 1.3.5 and version 1.3.6

Source: Github
Commits:
  • ae23dd4abe35961bfde22ed1c2eb3fe6f12ec808, August 9, 2022 5:33 PM:
    make secret optional. add update_func to pass on token updates. (#70)
    
    * make secret optional. add update_func to pass on token updates.
    
    * fix typing
    
    * fix typing (try 2)
    
    * fix a few bugs when using / not using client secret
  • 3166d02a0c25249a7a953faf108555ed88ac17ef, August 9, 2022 5:36 PM:
    1.3.6
    
    Automatically generated by python-semantic-release
Files changed:
CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
 
3
  <!--next-version-placeholder-->
4
 
 
 
 
5
  ## v1.3.5 (2022-08-05)
6
 
7
 
2
 
3
  <!--next-version-placeholder-->
4
 
5
+ ## v1.3.6 (2022-08-09)
6
+
7
+
8
  ## v1.3.5 (2022-08-05)
9
 
10
 
rest_tools/__init__.py CHANGED
@@ -5,7 +5,7 @@
5
  # is zero for an official release, positive for a development branch,
6
  # or negative for a release candidate or beta (after the base version
7
  # number has been incremented)
8
- __version__ = "1.3.5"
9
  version_info = (
10
  int(__version__.split(".")[0]),
11
  int(__version__.split(".")[1]),
5
  # is zero for an official release, positive for a development branch,
6
  # or negative for a release candidate or beta (after the base version
7
  # number has been incremented)
8
+ __version__ = "1.3.6"
9
  version_info = (
10
  int(__version__.split(".")[0]),
11
  int(__version__.split(".")[1]),
rest_tools/client/client.py CHANGED
@@ -298,17 +298,19 @@ class OpenIDRestClient(RestClient):
298
  token_url (str): base address of token service
299
  refresh_token (str): initial refresh token
300
  client_id (str): client id
301
- client_secret (str): client secret
302
- timeout (int): request timeout
303
- retries (int): number of retries to attempt
 
304
  """
305
  def __init__(
306
  self,
307
  address: str,
308
  token_url: str,
309
  refresh_token: str,
310
  client_id: str,
311
- client_secret: str,
 
312
  **kwargs: Any
313
  ) -> None:
314
  super().__init__(address, **kwargs)
@@ -319,14 +321,15 @@ def __init__(
319
  self.access_token = None
320
  self.refresh_token: Optional[Union[str, bytes]] = refresh_token
321
  self.token_func = True # type: ignore
 
322
  self._get_token()
323
 
324
  def _get_token(self) -> None:
325
  if self.access_token:
326
  # check if expired
327
  try:
328
  data = self.auth.validate(self.access_token)
329
- if data['exp'] < time.time()-5:
330
  raise Exception()
331
  return
332
  except Exception:
@@ -339,8 +342,9 @@ def _get_token(self) -> None:
339
  'grant_type': 'refresh_token',
340
  'refresh_token': self.refresh_token,
341
  'client_id': self.client_id,
342
- 'client_secret': self.client_secret,
343
  }
 
 
344
 
345
  try:
346
  r = requests.post(self.auth.token_url, data=args)
@@ -352,6 +356,8 @@ def _get_token(self) -> None:
352
  self.logger.debug('OpenID token refreshed')
353
  self.access_token = req['access_token']
354
  self.refresh_token = req['refresh_token'] if 'refresh_token' in req else None
 
 
355
  return
356
 
357
  raise Exception('No token available')
298
  token_url (str): base address of token service
299
  refresh_token (str): initial refresh token
300
  client_id (str): client id
301
+ client_secret (str): client secret (optional - required for refresh tokens)
302
+ update_func (callable): a function that gets called when the access and refresh tokens are updated (optional)
303
+ timeout (int): request timeout (optional)
304
+ retries (int): number of retries to attempt (optional)
305
  """
306
  def __init__(
307
  self,
308
  address: str,
309
  token_url: str,
310
  refresh_token: str,
311
  client_id: str,
312
+ client_secret: Optional[str] = None,
313
+ update_func: Optional[Callable[[Union[str, bytes], Optional[Union[str, bytes]]], None]] = None,
314
  **kwargs: Any
315
  ) -> None:
316
  super().__init__(address, **kwargs)
321
  self.access_token = None
322
  self.refresh_token: Optional[Union[str, bytes]] = refresh_token
323
  self.token_func = True # type: ignore
324
+ self.update_func = update_func
325
  self._get_token()
326
 
327
  def _get_token(self) -> None:
328
  if self.access_token:
329
  # check if expired
330
  try:
331
  data = self.auth.validate(self.access_token)
332
+ if data['exp'] < time.time()-self._token_expire_delay_offset:
333
  raise Exception()
334
  return
335
  except Exception:
342
  'grant_type': 'refresh_token',
343
  'refresh_token': self.refresh_token,
344
  'client_id': self.client_id,
 
345
  }
346
+ if self.client_secret:
347
+ args['client_secret'] = self.client_secret
348
 
349
  try:
350
  r = requests.post(self.auth.token_url, data=args)
356
  self.logger.debug('OpenID token refreshed')
357
  self.access_token = req['access_token']
358
  self.refresh_token = req['refresh_token'] if 'refresh_token' in req else None
359
+ if self.access_token and self.update_func:
360
+ self.update_func(self.access_token, self.refresh_token)
361
  return
362
 
363
  raise Exception('No token available')
rest_tools/server/handler.py CHANGED
@@ -383,24 +383,27 @@ def initialize(self, oauth_client_id, oauth_client_secret, oauth_client_scope=No
383
  if oauth_client_scope:
384
  self.oauth_client_scope = oauth_client_scope.split()
385
  else:
386
- self.oauth_client_scope = ['offline_access', 'profile', 'groups']
 
 
387
 
388
  async def get_authenticated_user(
389
  self, redirect_uri: str, code: str
390
  ) -> Dict[str, Any]:
391
  http = self.get_auth_http_client()
392
- body = urllib.parse.urlencode({
393
  'redirect_uri': redirect_uri,
394
  'code': code,
395
  'client_id': self.oauth_client_id,
396
- 'client_secret': self.oauth_client_secret,
397
  'grant_type': 'authorization_code',
398
- })
 
 
399
  response = await http.fetch(
400
  self._OAUTH_ACCESS_TOKEN_URL,
401
  method='POST',
402
  headers={'Content-Type': 'application/x-www-form-urlencoded'},
403
- body=body,
404
  )
405
  ret = tornado.escape.json_decode(response.body)
406
  if not ret.get('id_token', ''):
@@ -447,14 +450,23 @@ async def get(self):
447
  redirect_uri=self.get_login_url(),
448
  code=self.get_argument('code'),
449
  )
 
 
 
 
 
 
 
 
 
450
  # Save the user with e.g. set_secure_cookie
451
  self.set_secure_cookie('access_token', user['access_token'],
452
- expires_days=float(user['expires_in'])/3600/24)
453
  if 'refresh_token' in user:
454
  self.set_secure_cookie('refresh_token', user['refresh_token'],
455
- expires_days=float(user.get('refresh_expires_in', 86400))/3600/24)
456
  self.set_secure_cookie('identity', tornado.escape.json_encode(user['id_token']),
457
- expires_days=float(user.get('refresh_expires_in', 86400))/3600/24)
458
  if data.get('redirect', None):
459
  url = data['redirect']
460
  if 'state' in data:
@@ -466,8 +478,11 @@ async def get(self):
466
  raise tornado.web.HTTPError(400, reason='missing redirect')
467
  else:
468
  state = {}
469
- if self.get_argument('redirect', False):
470
- state['redirect'] = self.get_argument('redirect')
 
 
 
471
  elif not self.settings.get('debug', False):
472
  raise tornado.web.HTTPError(400, 'missing redirect')
473
  if self.get_argument('state', False):
383
  if oauth_client_scope:
384
  self.oauth_client_scope = oauth_client_scope.split()
385
  else:
386
+ self.oauth_client_scope = ['profile', 'groups']
387
+ if oauth_client_secret:
388
+ self.oauth_client_scope.append('offline_access')
389
 
390
  async def get_authenticated_user(
391
  self, redirect_uri: str, code: str
392
  ) -> Dict[str, Any]:
393
  http = self.get_auth_http_client()
394
+ body = {
395
  'redirect_uri': redirect_uri,
396
  'code': code,
397
  'client_id': self.oauth_client_id,
 
398
  'grant_type': 'authorization_code',
399
+ }
400
+ if self.oauth_client_secret:
401
+ body['client_secret'] = self.oauth_client_secret
402
  response = await http.fetch(
403
  self._OAUTH_ACCESS_TOKEN_URL,
404
  method='POST',
405
  headers={'Content-Type': 'application/x-www-form-urlencoded'},
406
+ body=urllib.parse.urlencode(body),
407
  )
408
  ret = tornado.escape.json_decode(response.body)
409
  if not ret.get('id_token', ''):
450
  redirect_uri=self.get_login_url(),
451
  code=self.get_argument('code'),
452
  )
453
+
454
+ # set expire times (can be 0 in user data, which is an invalid cookie)
455
+ access_expire = user.get('expires_in', 0)
456
+ if not access_expire:
457
+ access_expire = 1800
458
+ refresh_expire = user.get('refresh_expires_in', 0)
459
+ if not refresh_expire:
460
+ refresh_expire = 86400
461
+
462
  # Save the user with e.g. set_secure_cookie
463
  self.set_secure_cookie('access_token', user['access_token'],
464
+ expires_days=float(access_expire)/3600/24)
465
  if 'refresh_token' in user:
466
  self.set_secure_cookie('refresh_token', user['refresh_token'],
467
+ expires_days=float(refresh_expire)/3600/24)
468
  self.set_secure_cookie('identity', tornado.escape.json_encode(user['id_token']),
469
+ expires_days=float(refresh_expire)/3600/24)
470
  if data.get('redirect', None):
471
  url = data['redirect']
472
  if 'state' in data:
478
  raise tornado.web.HTTPError(400, reason='missing redirect')
479
  else:
480
  state = {}
481
+ redirect = self.get_argument('next', None)
482
+ if not redirect:
483
+ redirect = self.get_argument('redirect', None)
484
+ if redirect:
485
+ state['redirect'] = redirect
486
  elif not self.settings.get('debug', False):
487
  raise tornado.web.HTTPError(400, 'missing redirect')
488
  if self.get_argument('state', False):

Readme

<!--- Top of README Badges (automated) ---> [![PyPI](https://img.shields.io/pypi/v/wipac-rest-tools)](https://pypi.org/project/wipac-rest-tools/) [![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/WIPACrepo/rest-tools?include_prereleases)](https://github.com/WIPACrepo/rest-tools/) [![PyPI - License](https://img.shields.io/pypi/l/wipac-rest-tools)](https://github.com/WIPACrepo/rest-tools/blob/master/LICENSE) [![Lines of code](https://img.shields.io/tokei/lines/github/WIPACrepo/rest-tools)](https://github.com/WIPACrepo/rest-tools/) [![GitHub issues](https://img.shields.io/github/issues/WIPACrepo/rest-tools)](https://github.com/WIPACrepo/rest-tools/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aopen) [![GitHub pull requests](https://img.shields.io/github/issues-pr/WIPACrepo/rest-tools)](https://github.com/WIPACrepo/rest-tools/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Aopen) <!--- End of README Badges (automated) ---> # rest-tools This project contains REST tools in python, as common code for multiple other projects under https://github.com/WIPACrepo. All code uses python [asyncio](https://docs.python.org/3/library/asyncio.html), so is fully asyncronous. Note that both the client and server assume starting the asyncio loop happens elsewhere - they do not start the loop themselves. ## Client A REST API client exists under `rest_tools.client`. Use as: ```python from rest_tools.client import RestClient api = RestClient('http://my.site.here/api', token='XXXX') ret = await api.request('GET', '/fruits/apple') ret = await api.request('POST', '/fruits', {'name': 'banana'}) ``` ## Server A REST API server exists under `rest_tools.server`. Use as: ```python import asyncio from rest_tools.server import RestServer, RestHandler class Fruits(RestHandler): def post(self): # handle a new fruit self.write({}) server = RestServer() server.add_route('/fruits', Fruits) server.startup(address='my.site.here', port=8080) asyncio.get_event_loop().run_forever() ``` The server uses [Tornado](https://tornado.readthedocs.io) to handle HTTP connections. It is recommended to use Apache or Nginx as a front-facing proxy, to handle TLS sessions and non-standard HTTP requests in production.