Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add signal functionality to manage events (after_token_refresh) #52

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ locustfile.py

*.pyc

*.egg-info/
*.egg/
.idea/
htmlcov/
docs/_*/

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
docs/_*/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
39 changes: 36 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pycrest.cache.DummyCache doesn't cache anything.
>>> import pycrest
>>> from pycrest.cache import FileCache
>>> file_cache = FileCache(path='/tmp/pycrest_cache')
>>> eve = pycrest.EVE(cache=file_cache)
>>> eve = pycrest.EVE(cache=file_cache)

.. highlight:: none

Expand Down Expand Up @@ -190,8 +190,8 @@ This will disable the cache for everything you will do using PyCrest. No call or
.. highlight:: none

**Disable caching on demand**
You can disable the caching for a specific ``get()`` call you don't want to cache, by simply adding ``caching=False|None`` to the call parameters.
For example:
You can disable the caching for a specific ``get()`` call you don't want to cache, by simply adding ``caching=False|None`` to the call parameters.
For example:

.. highlight:: python

Expand All @@ -200,3 +200,36 @@ For example:
>>> regions = crest_root.regions(caching=False)

.. highlight:: none

Signals
-------
Signals are "events" that will be triggered in some circumstances and they can have one or more functions attached as receivers of the event.
These functions will be called automatically when the signal is fired.

To subscribe to any of these signals, you just need to import it and add your receiver to it. In the same way, you also can remove an receiver from an signal whenever you want, in case you don't want the event to trigger your receiver again.

.. highlight:: python

>>> from pycrest.event import after_token_refresh
>>> after_token_refresh.add_receiver(your_receiver_function) # from now, your_receiver_function will be triggered with this signal

>>> after_token_refresh.remove_receiver(your_receiver_function) # once you do this, your_receiver_function will never be triggered, unless you add it again

.. highlight:: none

List of signals
~~~~~~~~~~~~~~~
after_token_refresh
^^^^^^^^^^^^^^^^^^^
This signal is triggered as soon as the authorizations are refreshed using the refresh_token.

List of argument given to the receivers :
+---------------+--------+--------------------------------------+
| Arguments | Type | Description |
+===============+========+======================================+
| access_token | String | The new access token used to log in |
+---------------+--------+--------------------------------------+
| refresh_token | String | The refresh token used to refresh |
+---------------+--------+--------------------------------------+
| expires | int | The timestamps when the token expires|
+---------------+--------+--------------------------------------+
54 changes: 41 additions & 13 deletions pycrest/eve.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import base64
import requests
import time

from pycrest import version
from pycrest.compat import bytes_, text_
from pycrest.errors import APIException, UnsupportedHTTPMethodException
from pycrest.compat import bytes_
from pycrest.compat import text_
from pycrest.errors import APIException
from pycrest.errors import UnsupportedHTTPMethodException
from pycrest.events import after_token_refresh
from requests.adapters import HTTPAdapter
try:
from urllib.parse import urlparse, urlunparse, parse_qsl
Expand All @@ -16,7 +20,10 @@
from urllib import quote
import logging
import re
from pycrest.cache import DictCache, APICache, DummyCache

from pycrest.cache import APICache
from pycrest.cache import DictCache
from pycrest.cache import DummyCache

logger = logging.getLogger("pycrest.eve")
cache_re = re.compile(r'max-age=([0-9]+)')
Expand All @@ -33,9 +40,11 @@ def __init__(
'''Initialises a PyCrest object

Keyword arguments:
additional_headers - a list of http headers that will be sent to the server
additional_headers - a list of http headers
that will be sent to the server
user_agent - a custom user agent
cache - an instance of an APICache object that will cache HTTP Requests.
cache - an instance of an APICache object
that will cache HTTP Requests.
Default is DictCache, pass cache=None to disable caching.
'''
# Set up a Requests Session
Expand Down Expand Up @@ -219,7 +228,8 @@ def __getattr__(self, item):

def auth_uri(self, scopes=None, state=None):
s = [] if not scopes else scopes
return "%s/authorize?response_type=code&redirect_uri=%s&client_id=%s%s%s" % (
return ("%s/authorize?response_type=code&redirect_uri=%s"
"&client_id=%s%s%s") % (
self._oauth_endpoint,
quote(self.redirect_uri, safe=''),
self.client_id,
Expand Down Expand Up @@ -308,7 +318,10 @@ def __init__(

def __call__(self, caching=True):
if not self._data:
self._data = APIObject(self.get(self._endpoint, caching=caching), self)
self._data = APIObject(
self.get(self._endpoint, caching=caching),
self
)
return self._data

def whoami(self):
Expand All @@ -327,6 +340,10 @@ def refresh(self):
self.expires = int(time.time()) + res['expires_in']
self._session.headers.update(
{"Authorization": "Bearer %s" % self.token})

# trigger the signal
after_token_refresh.send(**res)

return self # for backwards compatibility

def get(self, resource, params={}, caching=True):
Expand Down Expand Up @@ -383,22 +400,33 @@ def __call__(self, **kwargs):

# Caching is now handled by APIConnection
if 'href' in self._dict:
method = kwargs.pop('method', 'get') # default to get: historic behaviour
# default to get: historic behaviour
method = kwargs.pop('method', 'get')
data = kwargs.pop('data', {})
caching = kwargs.pop('caching', True) # default caching to true, for get requests
# default caching to true, for get requests
caching = kwargs.pop('caching', True)

# retain compatibility with historic method of passing parameters.
# Slightly unsatisfactory - what if data is dict-like but not a dict?
# Slightly unsatisfactory; what if data is dict-like but not a dict
if isinstance(data, dict):
for arg in kwargs:
data[arg] = kwargs[arg]

if method == 'post':
return APIObject(self.connection.post(self._dict['href'], data=data), self.connection)
return APIObject(
self.connection.post(self._dict['href'], data=data),
self.connection
)
elif method == 'put':
return APIObject(self.connection.put(self._dict['href'], data=data), self.connection)
return APIObject(
self.connection.put(self._dict['href'], data=data),
self.connection
)
elif method == 'delete':
return APIObject(self.connection.delete(self._dict['href']), self.connection)
return APIObject(
self.connection.delete(self._dict['href']),
self.connection
)
elif method == 'get':
return APIObject(self.connection.get(self._dict['href'],
params=data,
Expand Down
58 changes: 58 additions & 0 deletions pycrest/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- encoding: utf-8 -*-
import logging
import sys

logger = logging.getLogger("pycrest.events")


class Signal(object):
def __init__(self):
""" Alarm constructor. """
self.event_receivers = []

def add_receiver(self, receiver):
""" Add a receiver to the list of receivers.

:param receiver: a callable variable
"""
if not callable(receiver):
raise TypeError("receiver must be callable")
self.event_receivers.append(receiver)

def remove_receiver(self, receiver):
""" Remove a receiver to the list of receivers.

:param receiver: a callable variable
"""
if receiver in self.event_receivers:
self.event_receivers.remove(receiver)

def send(self, **kwargs):
""" Trigger all receiver and pass them the parameters
If an exception is raised, it will stop the process and all receivers
may not be triggered at this moment.

:param kwargs: all arguments from the event.
"""
for receiver in self.event_receivers:
receiver(**kwargs)

def send_robust(self, **kwargs):
""" Trigger all receiver and pass them the parameters
If an exception is raised it will be catched and displayed as error
in the logger (if defined).

:param kwargs: all arguments from the event.
"""
for receiver in self.event_receivers:
try:
receiver(**kwargs)
except Exception as err:
if not hasattr(err, '__traceback__'):
logger.error(sys.exc_info()[2])
else:
logger.error(err.__traceback__)


# define required alarms
after_token_refresh = Signal()
Loading