PK!toasts/__init__.pyPK!qx toasts/app.py """ toasts.app.py ~~~~~~~~~~~~~~ Main App class. """ import sys import time import traceback # TODO: get list of common errors produced by requests import requests from . import wrappers from .clients import CLIENTS from .exceptions import AuthError, UnexpectedResponse class ToastsApp(): """ The main app class that runs the app. Attributed: config (wrappers.Preferences): Object for getting saved user preferences. """ def __init__(self): """ Args: config_path (str): Path to the user config file for preferences """ self.config = wrappers.Preferences() self.clients = self.config.get('general.clients') self.notifier = wrappers.Notifier( timeout=self.config.get('general.notif_timeout'), max_show=self.config.get('general.notif_max_show') ) # FIXME: refactor run method (McCabe rating is 13) def run(self): """Launch the app and start showing notifications.""" if not self.clients: self.notifier.show_error( 'No clients enabled - please enable atleast one client in the' ' config file and restart the app.' ) # TODO: include location of config file in error message self.exit_with_error('No clients enabled') client_list = [] for client in self.clients: try: client_obj = CLIENTS[client] client_list.append(client_obj(config=self.config)) except KeyError: self.notifier.show_error( "Invalid client name specified in config file - {}.\ Please give a valid client name and restart the app.\ ".format(client) ) self.exit_with_error('Invalid client name "{}"'.format(client)) except AuthError as err: msg = 'Invalid credentials for {}.'.format(client_obj.NAME) self.notifier.show_error(title=str(err), msg=msg) self.exit_with_error(msg) while True: try: for client in client_list: try: notifs = client.get_notifications() self.notifier.show_notif( title='Notification from {}'.format(client.NAME.title()), msgs=notifs, icon=client.NAME ) except AuthError as err: msg = 'Invalid credentials for {}.'.format(client.NAME) self.notifier.show_error(title=str(err), msg=msg) self.exit_with_error(msg) except UnexpectedResponse as err: sys.stderr.write(str(err) + '\n') except (requests.Timeout, requests.ConnectionError): pass except Exception as err: self.notifier.show_error( 'A critical error caused Toasts to crash.\ Please restart the app.' ) self.exit_with_error(traceback.format_exc()) sleep_sec = self.config.get('general.check_every') time.sleep(sleep_sec * 60) # convert to seconds @staticmethod def exit_with_error(msg): """ Exit from the app with an exit status of 1. Args: msg (str): Message to print to stderr. """ sys.exit('ERROR(toasts) - ' + msg) PK!toasts/clients/__init__.py """ toasts.clients ~~~~~~~~~~~~~~ Contains client classes used for fetching notifications from various websites. """ from .github import GitHubClient CLIENTS = { 'github': GitHubClient, } PK!I toasts/clients/base.py """ toasts.clients.base ~~~~~~~~~~~~~~~~~~~ This module contains base classes for designing concrete client classes. """ import os from abc import ABCMeta, abstractmethod from .. import wrappers from ..exceptions import AuthError class Client(metaclass=ABCMeta): """ Base class for all clients. Attributes: NAME (str): Name of the client, like 'github', 'stack overflow'. API_ENDPOINT (str): URL of the api endpoint used to get notifications. session (requests.Session): Session object to do api requests with. """ NAME = None API_ENDPOINT = None def __init__(self, config): """ Args: config (toasts.wrappers.Preferences): Contains preferences set by user. """ self.config = config rt = self.config.get('general.notif_timeout') self.session = wrappers.Session(request_timeout=rt) @abstractmethod def authenticate(self): pass @abstractmethod def get_notifications(self): """ Get notifications from the specified site through `API_ENDPOINT`. Returns: list of str: Text to displayed as notification. Each item in the list is a seperate notification. Empty list is returned if there are no new notifications. Raises: toasts.exceptions.AuthError: Invalid credentials. """ pass @abstractmethod def _parse_json_data(self, data): """ Parse the json data containing the notifications. Called by `get_notifications`. Args: data(json): Python object from `json.loads`. Returns: list of str: Each item of the list is a notification to be displayed as such. """ pass class PersonalAccessTokenClient(Client): """ Clients that use a personal access token to get resources(notifications) from a site. Personal access tokens have to be usually aquired by the user manually """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.authenticate() def authenticate(self): def get_env_var(key): name = self.config.get('.'.join(['sites', self.NAME, key])) return os.getenv(name) username = get_env_var('username') token = get_env_var('token') if not (username and token): raise AuthError(self.NAME) self.session.auth = (username, token) PK!z<<toasts/clients/github.py """ toasts.clients.github ~~~~~~~~~~~~~~~~~~~~~ Client for getting notifications from GitHub - www.github.com See https://developer.github.com/v3/activity/notifications/ for api docs. """ from .base import PersonalAccessTokenClient from ..exceptions import AuthError, UnexpectedResponse class GitHubClient(PersonalAccessTokenClient): NAME = 'github' API_ENDPOINT = 'https://api.github.com/notifications' def get_notifications(self): response = self.session.get(self.API_ENDPOINT) if response.status_code == 200: return self._parse_json_data(response.json()) elif response.status_code == 304: # not modified; no new notifications return [] elif response.status_code == 401: # unauthorized raise AuthError('GitHub') else: raise UnexpectedResponse('GitHub', response.status_code) def _parse_json_data(self, data): # See the response section in # https://developer.github.com/v3/activity/notifications#list-your-notifications notifs = [] for event in data: metainfo = event['subject'] metainfo['repo_name'] = event['repository']['full_name'] text = '{type}: {title} ({repo_name})'.format(**metainfo) notifs.append(text) return notifs PK!dztoasts/data/config.yaml# Config file for toasts general: # List of sites to enable; comma seperated list # Default: [] clients: [] # Connection timeout, in seconds # Default: 7 ; Minimum value: 1 conn_timeout: 7 # Check for notifications every ___ minutes # Default: 3 ; Minimum value: 2 check_every: 3 # Show notification for ___ seconds # Default: 7 ; Minimum value: 2 notif_timeout: 7 # Maximum number of notifications to show at a time, of individual clients. # Default: 2 # Note: Value of -1 will show all notifications; it may clutter your workspace. notif_max_show: 2 sites: github: # *Environment variable* which holds your github username # Default: GH_UNAME username: GH_UNAME # *Environment variable* which holds a personal access token for authentication # Default: GH_TOKEN token: GH_TOKEN PK!dztoasts/data/config.yaml# Config file for toasts general: # List of sites to enable; comma seperated list # Default: [] clients: [] # Connection timeout, in seconds # Default: 7 ; Minimum value: 1 conn_timeout: 7 # Check for notifications every ___ minutes # Default: 3 ; Minimum value: 2 check_every: 3 # Show notification for ___ seconds # Default: 7 ; Minimum value: 2 notif_timeout: 7 # Maximum number of notifications to show at a time, of individual clients. # Default: 2 # Note: Value of -1 will show all notifications; it may clutter your workspace. notif_max_show: 2 sites: github: # *Environment variable* which holds your github username # Default: GH_UNAME username: GH_UNAME # *Environment variable* which holds a personal access token for authentication # Default: GH_TOKEN token: GH_TOKEN PK!-Rtoasts/data/icons/github.pngPNG  IHDRxx9d6tEXtSoftwareAdobe ImageReadyqe<$iTXtXML:com.adobe.xmp D >IDATx] U~YXP`+ץDqQH jSIWJ1 &m2k4Q̤FsFCAeI(@Q0\\>|<3.s>;?y{ztttEWc0s:f-sFؗ'10 bncnenfg;7qQJu|j6!)I󳼍ao27c̫c-̅]V`o9y92!=a;̟1gMe*$O0S.09V([wK*G?͜<ė1g3*Ob>|0.„vM\6T|qdA"4憤=_avbKUVϼs&9~\\$]Eald^|= ]x*+/*6wq ]S\8foU-6>n$ Mb5Sĵ0/mPKo3!g9tˤ8D0i-Ć)f\PEx3;vo++n$8UlCS`l\AhY^'mE,J@q-Y un8N&/? zgdoGn&(& 8qО߫ɉBTe҂"1Xօw}]R`yR[ jyl (`oOf~ې1^Ytq89r [eL# h3bC _`ܟ#_\s7d\zc 'ccp\>oV* V@zxOeo|+UmC7ZCi?o0"**Unủ"lvGF#zmp$]b++!kyҽ-ʍji'&X?)VVNth[U˕m$I\rѴ!4ar|+p!әڝN超P9R#xKKCԒ _qܤZzq'φ$괸e} Y$NIb!mS^?df|(9K!K4\Ey{ yW7"x6 r/)~!=eɇcS) C qgvq_ng~?Bq [W(-1:QKA |{E.IT 窵6ךo̔\ ۜ[d=M2 &?IOWظsQecO8Nl`\ W\ﳢV%21w3'1k 7li~-,Ze}BW7GBVl{̓)5lEL*"K0Bиz>a*i]+c~O]WƈG 5> 4OS[^nD wFݩȤLJ>9@7»M8t&dEžwxF̹U79y~klw.Uu>3Gyu=y "6~V>3?"?p:Ŝ8xO?~Y_6 q䄑q3H/w]ʼ>1aGl;up.`7yw."7Q2w"TM,v&=$ 3öӂ,  [O1F@E *qa@!T&N-qяe-1c–@k188X/ nzstظ%<#:je֛k$d [<-9Jc(y SoA,6UZvvt ]hR\ Is"r'&fͦ J1TxjW^KDI: wLFrG nvOvEaeA6=DtBFbq D >IDATx] U~YXP`+ץDqQH jSIWJ1 &m2k4Q̤FsFCAeI(@Q0\\>|<3.s>;?y{ztttEWc0s:f-sFؗ'10 bncnenfg;7qQJu|j6!)I󳼍ao27c̫c-̅]V`o9y92!=a;̟1gMe*$O0S.09V([wK*G?͜<ė1g3*Ob>|0.„vM\6T|qdA"4憤=_avbKUVϼs&9~\\$]Eald^|= ]x*+/*6wq ]S\8foU-6>n$ Mb5Sĵ0/mPKo3!g9tˤ8D0i-Ć)f\PEx3;vo++n$8UlCS`l\AhY^'mE,J@q-Y un8N&/? zgdoGn&(& 8qО߫ɉBTe҂"1Xօw}]R`yR[ jyl (`oOf~ې1^Ytq89r [eL# h3bC _`ܟ#_\s7d\zc 'ccp\>oV* V@zxOeo|+UmC7ZCi?o0"**Unủ"lvGF#zmp$]b++!kyҽ-ʍji'&X?)VVNth[U˕m$I\rѴ!4ar|+p!әڝN超P9R#xKKCԒ _qܤZzq'φ$괸e} Y$NIb!mS^?df|(9K!K4\Ey{ yW7"x6 r/)~!=eɇcS) C qgvq_ng~?Bq [W(-1:QKA |{E.IT 窵6ךo̔\ ۜ[d=M2 &?IOWظsQecO8Nl`\ W\ﳢV%21w3'1k 7li~-,Ze}BW7GBVl{̓)5lEL*"K0Bиz>a*i]+c~O]WƈG 5> 4OS[^nD wFݩȤLJ>9@7»M8t&dEžwxF̹U79y~klw.Uu>3Gyu=y "6~V>3?"?p:Ŝ8xO?~Y_6 q䄑q3H/w]ʼ>1aGl;up.`7yw."7Q2w"TM,v&=$ 3öӂ,  [O1F@E *qa@!T&N-qяe-1c–@k188X/ nzstظ%<#:je֛k$d [<-9Jc(y SoA,6UZvvt ]hR\ Is"r'&fͦ J1TxjW^KDI: wLFrG nvOvEaeA6=DtBFbqtoasts/exceptions.py """ toasts.exceptions ~~~~~~~~~~~~~~~~~ Contains exceptions raised by toasts """ class ToastError(Exception): """Base class for all toasts exceptions.""" pass class AuthError(ToastError): """Raised when invalid credentials are supplied""" def __init__(self, client): msg = 'Authentication error for {}'.format(client) super().__init__(msg) class UnexpectedResponse(ToastError): """Raised when the status code of an operation is not the expected one""" def __init__(self, client, status_code): msg = 'Unexpected response from {0} (status code: {1})'.format( client, status_code) super().__init__(msg) PK!|?7??toasts/main.py from .app import ToastsApp def main(): ToastsApp().run() PK!N]rtoasts/util.py """ toasts.util.py ~~~~~~~~~~~~~~ Utilities for toasts """ import os HERE = os.path.dirname(os.path.abspath(__file__)) def get_icon(icon): """Get absolute path to `icon`, where icon is a string, like 'github'.""" icon += '.png' # TODO: see if png is supported across all platforms return os.path.join(HERE, 'data', 'icons', icon) def get_default_config_path(): """Get the path to the default config file (data/config.yaml).""" return os.path.join(HERE, 'data', 'config.yaml') PK!<+ftoasts/wrappers.py """ toasts.wrappers.py ~~~~~~~~~~~~~~~~~~ Wrapper objects for the app. """ import os import time import shutil import functools import plyer import confuse import requests from . import util class Notifier(): """ Show desktop notifications. Attributes: disp_timeout (int): Show a notification for this much seconds. max_show (int): Maximum number of notifications to show at a time. Show only this much number of notifications, if there are too many to show (more than `max_show` in `msgs` argument of `show_notif` method). An additional notification will be shown, saying that there are `len(msgs) - max_show` more notifications to show. If value is -1, all messages will be shown. """ def __init__(self, timeout, max_show): self.disp_timeout = timeout self.max_show = max_show if max_show >= 0 else None def show_notif(self, title, msgs, icon): """ Show a notification. Args: title (str): Title of notification. msgs (iterable of str): List of notifications to display. Every item in `msgs` is a message that has to displayed in seperate notifications. Every item will be displayed unless the `notif_max_show` attribute has a non-zero value. All the notifications will have the same icon and title. icon (str): Name of icon to be used. `icon` should be the name of a file in `toasts/data/icons/`, stripped off it's extension. Eg: github (not github.png) """ icon_path = util.get_icon(icon) def notify(msg): plyer.notification.notify( title=title, message=msg, app_icon=icon_path, app_name='toasts', timeout=self.disp_timeout ) msgs_to_show = msgs[0 : self.max_show] unshown = len(msgs) - len(msgs_to_show) # count of suppressed msgs for msg in msgs_to_show: notify(msg) time.sleep(3) # give some time to read the notification if unshown: msg = ( 'You have {} more notification(s) from this website. ' 'Please go to the website to see them.'.format(unshown) ) notify(msg) def show_error(self, msg, title='An error occured in Toasts'): """Show an error message as notification. `msg` is a string.""" self.show_notif(title=title, msgs=[msg], icon='error') class Session(requests.Session): def __init__(self, request_timeout, *args, **kwargs): super().__init__(*args, **kwargs) self.get = functools.partial(self.get, timeout=request_timeout) # TODO: wrap requests.exceptions.ConnectionError and requests.exceptions.Timeout class Preferences: """ Class used for fetching preferences. """ CONFIG_DIR = os.path.join(confuse.config_dirs()[0], 'toasts') USER_CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yaml') DEFAULT_CONFIG_FILE = util.get_default_config_path() def __init__(self): if not os.path.exists(self.USER_CONFIG_FILE): self.create_config_file() # confuse looks in system specific directories for config files (config.yaml) self._config = confuse.Configuration(appname='toasts') # TODO: supply 2nd argument of Configuration def create_config_file(self): """ Create a config file with default settings in `CONFIG_DIR`. Overwrites existing config file. """ if not os.path.exists(self.CONFIG_DIR): os.makedirs(self.CONFIG_DIR) shutil.copy(self.DEFAULT_CONFIG_FILE, self.CONFIG_DIR) def get(self, opt): """ Return the value of the `opt` option. Args: opt (str): name of option - "general.clients", "sites.github.token", etc. """ val = self._config for key in opt.split('.'): val = val[key] return val.get() PK!H{&+'toasts-0.1.1.dist-info/entry_points.txtN+I/N.,()*O,.)PzyV PK!ti .."toasts-0.1.1.dist-info/LICENSE.txt MIT License Copyright (c) 2018 Gokul Soumya Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HlŃTTtoasts-0.1.1.dist-info/WHEEL A н#J@Z|Jmqvh&#hڭw!Ѭ"J˫( } %PK!HXjMtoasts-0.1.1.dist-info/METADATAWn8}WMKŤM͆IA1-kIԐ9c;uhkKw=L: '/Xߋwٹ(d;-NwWuQ3Tڑ/SCF2zhDQr'oj$G>r"4\O(S2m,lOQ%H%sv{\VDչu1&S{͎ji4ɮBcHS]+#mt9)}fuυ(NWw:ˏ*9)}G#Km8F=dЧ/|Oz׋+:YKUtt"ʩkTOJ6u@ se9. LO=Fڮ3`>&#[rd$]k1:U_qvht KXIO2IRB:d\1'УNO1E U 3ek:K$֢#d 3ƶ/s)0TS2ep|#֩<6#|8弁wc_U׮X"<>ukr⻂y_r;?|B,_Y[0\HOJ6''0@-"Jz4gi6 kbT6ч7аh .x e&,R[NUYO1?@'Q@ U{:P c=IkReJFH f M»*@8T-ya؅2uz6y~㕪P?£íQh1AnB_nV\PR7]dht*h~MWʀoK-OrESk8nIֹ(< :A,RG{𨹇[<E{T 6.|?:9bz @ls "5"al~=9l͆Fƛ?( c!b`9*r.;Qۻ,\7O~ ц4-YU~ Z1RcHN)0*mY5li婜I":Xy]Dd9VFtPoΏNz;i$| dv}ܛ>OՄTSWi6(!-cV(BzQ֫Aj+ϢfI }xM2}sHN~Hβ>5c@*Pk.5*5KABH,̸QWIDC:~=:;]@ W)ܚR6QE4,nɔ=U!uuG5]5?gTEM U^cO]Le?-O53~ϛ}̽EL|*8~%lui-Byg;S>ubx҅-|[d[]UZ*|uI-=<^_(5 c9>=ۣ3x͘#a 1'Gy-t} ƪg:O O;៘}WV"V_;l}ϐwieoJz(~}T 5_^ZARO7O v ^N%6*`yڬ5cX!#J qPK!H{ptoasts-0.1.1.dist-info/RECORDɒJ}? T1$eP@ L &Oaqkّ/Ο<|!l!ÏA ep?@N_tӛvlt2nʖQ9/Q ~)-kFl uaݭ$REnQ,O/%¬.Y$ǂJk)gJ%ږ@đH+O1MsZrWDgzԑqYcX&m&'W@\xU\WAH8s#[{%r{ Jvui,㱿4gҵ9,>Qs}yeRȓVy桮DIN_/tQ1n+"S#vWt@fs.YD(6ed '[vo 9$ E[D}S)RRd;ŊA ,2vPXu'Ty.JRĜ\ќ-iY VR_'Fwa[G}ǚ2 g??mS"՞ЋRSnZO@3H^^ L6>&GwpigV $MMYmv;/T&"oQ~֛Lڃq0ʎ8斧hGtoasts/exceptions.pyPK!|?7??>Jtoasts/main.pyPK!N]rJtoasts/util.pyPK!<+fLtoasts/wrappers.pyPK!H{&+']toasts-0.1.1.dist-info/entry_points.txtPK!ti .."]toasts-0.1.1.dist-info/LICENSE.txtPK!HlŃTTatoasts-0.1.1.dist-info/WHEELPK!HXjMbtoasts-0.1.1.dist-info/METADATAPK!H{pitoasts-0.1.1.dist-info/RECORDPKl