PK!toasts/__init__.pyPK!qq q toasts/app.py """ toasts.app ~~~~~~~~~~ Main App class. """ import sys import time import traceback # TODO: get list of common errors produced by requests import requests from .clients import CLIENTS from .exceptions import AuthError, UnexpectedResponse from .wrappers import Notifier, Preferences, ErrorNotification class ToastsApp: """ The main app class that runs the app. Attributes: config (wrappers.Preferences): Object for getting saved user preferences. clients (list): Names of enabled clients. notifier (wrappers.Notifier): Used to show notifications. """ def __init__(self): self.config = Preferences() self.clients = self.config.get("general.clients") self.notifier = 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: msg = ( "No clients enabled - please enable atleast one client " "in the config file and restart the app." ) self.notifier.show_error(ErrorNotification(msg=msg)) # 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: msg = ( "Invalid client name specified in config file - {}. " "Please give a valid client name and restart the app." ).format(client) self.notifier.show_error(ErrorNotification(msg=msg)) self.exit_with_error('Invalid client name "{}"'.format(client)) except AuthError as err: self.notifier.show_error(ErrorNotification(msg=str(err))) self.exit_with_error(msg=str(err)) while True: try: for client in client_list: try: notifs = client.get_notifications() self.notifier.show_notif(notifs) except AuthError as err: self.notifier.show_error(ErrorNotification(msg=str(err))) self.exit_with_error(msg=str(err)) except UnexpectedResponse as err: sys.stderr.write(str(err) + "\n") except (requests.Timeout, requests.ConnectionError): pass except Exception as err: msg = ( "A critical error caused Toasts to crash. " "Please restart the app." ) self.notifier.show_error(ErrorNotification(msg)) self.exit_with_error(traceback.format_exc()) sleep_min = self.config.get("general.check_every") time.sleep(sleep_min * 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!y-toasts/clients/__init__.py """ toasts.clients ~~~~~~~~~~~~~~ Contains client classes used for fetching notifications from various websites. """ from .github import GitHubClient CLIENTS = {"github": GitHubClient} PK!q0 0 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 toasts.wrappers.Notification: Each item in the list is a seperate notification to be shown. Empty list is returned if there are no new notifications. Raises: toasts.exceptions.AuthError: Invalid credentials. toasts.exceptions.UnexpectedResponse: Recieved an unknown status code. """ pass @abstractmethod def _parse_json_data(self, data): """ Parse the json data containing the notifications. Called by `get_notifications`. Args: data (object): Python object from `json.loads`, usually a `dict`. Returns: list of dict: dicts of the form {"msg": "...", "uid": 12} in a list. """ 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 acquired 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!&toasts/clients/github.py """ toasts.clients.github ~~~~~~~~~~~~~~~~~~~~~ Client for getting notifications from GitHub - https://www.github.com See https://developer.github.com/v3/activity/notifications/ for api docs. """ from .base import PersonalAccessTokenClient from ..wrappers import ClientNotification 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: notifs = self._parse_json_data(response.json()) return [ClientNotification(**data) for data in notifs] 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"] msg = "{type}: {title} ({repo_name})".format(**metainfo) parsed = {"msg": msg, "uid": event["id"], "client": "github"} notifs.append(parsed) 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!-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= 0 else None self._history = [] def show_notif(self, notifs): """ Show a notification. Args: notifs (list of ClientNotification): List of notifications to show. All the notifications will be from the same client at a time. """ new_notifs = [n for n in notifs if n not in self._history] self._history.extend(new_notifs) msgs_to_show = new_notifs[0 : self.max_show] unshown = len(new_notifs) - len(msgs_to_show) # count of suppressed msgs for notification in msgs_to_show: self._notify(notification) 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) ) # get name of client that made the notifs; all are from the same client client = notifs[0].client n = ClientNotification(msg=msg, client=client, uid=None) self._notify(n) def show_error(self, error): """ Show an error message as notification. Args: error (ErrorNotification): Notification object. """ self._notify(error) def _notify(self, notif_obj): """ Actual method that shows a notification. Args: notif_obj (BaseNotification): Notification object. """ plyer.notification.notify( title=notif_obj.title, message=notif_obj.msg, app_icon=notif_obj.icon_path, app_name="toasts", timeout=self.disp_timeout, ) 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 _ConfuseConfig(confuse.Configuration): """ A custom Configuration object to be used by `Preferences`. This is *not* the object used for fetching preferences by the app. """ def __init__(self, appname, config, default): self.config = config # absolute path to user config self.default_config = default # absolute path to default config file super().__init__(appname) def user_config_path(self): return self.config def _add_default_source(self): filename = self.default_config self.add(confuse.ConfigSource(confuse.load_yaml(filename), filename, True)) 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) # by using the app's name self._config = _ConfuseConfig( appname="toasts", config=self.USER_CONFIG_FILE, default=self.DEFAULT_CONFIG_FILE, ) 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.2.dist-info/entry_points.txtN+I/N.,()*O,.)PzyV PK!ti .."toasts-0.1.2.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!H,TTtoasts-0.1.2.dist-info/WHEEL 1 0 =Rn>ZD:_dhRUQTk-k]m`Q'iPK!Hzԝ׻.toasts-0.1.2.dist-info/METADATAWkoF>^ v R~$-*;:&ڱ;]A`ȑ85aaY_ʒok D=LyYJ/_uڴ#٨y#wn4GNy*x=х0t4ΚM)iV;߇.,nNImf$ے|юxourR*;7WaNm \JOuZ+q|e,d[E=peOϟ==~xǏ1Ώ0$`Z/_nC]9.R$h}뮖~blC/^n_깲yC PaC[TOOGQuh5v>{\au^֫gW)t-ͬbHv=ɮ'W+RIMp %"JWU~έN<'XSk,:"&\+fVqUTWNjVeNL;Z.xNԮL+VF^ԚsP|fSXeIO0 ^LeŐ'za4LlꛇlO g#qH紺yo&P3 ܾ_ղ#MIk<+9`?o1U  %RC0\{&28 >yxگϒ/?D*G1Dr\LX ?ԐKc/xH*",f0\ZWNv#JS흶H{]2[c lvuOHGM*mGӌ syD~h+D+UWU.{ H /_$}:W".SfolbQPK!HEntoasts-0.1.2.dist-info/RECORDuɲH}= xpQ Qf7 C 2mD슾/! \!oNf*D:-+`U6ߏʁ~ :a~$lX~];C]t.@FG8݋E 2|=(=ӶcIZ\-Fa|h0PE`;Āɚ^4p1L=z`xJ߷$hzV{pė+8lY,/~Ml뷗XUp^cK zKq5J M cQy7ӣ` #n+"(F-gZYTFwdv/ #Ismǖ )wHX+7؇\$ŸV!? e5a[E:b@UnkR̃\$hqũ(]rL8ȶ_xvڎk7L#ԽMϫj^lW_O5g'W t4xY\p?NW%#he1j>@jL;*)NnUkiEkkh'ϽhPFJXrKbcݵZPlmר=I<魞$F*n9;m$U6dehqvȬ 星\-W&_PK!toasts/__init__.pyPK!qq q 0toasts/app.pyPK!y- toasts/clients/__init__.pyPK!q0 0 toasts/clients/base.pyPK!&%toasts/clients/github.pyPK!dzYtoasts/data/config.yamlPK!-R#toasts/data/icons/github.pngPK!VA3toasts/exceptions.pyPK!6toasts/main.pyPK!ǀ[7toasts/util.pyPK!R{9toasts/wrappers.pyPK!H{&+'EStoasts-0.1.2.dist-info/entry_points.txtPK!ti .."Stoasts-0.1.2.dist-info/LICENSE.txtPK!H,TTXtoasts-0.1.2.dist-info/WHEELPK!Hzԝ׻.Xtoasts-0.1.2.dist-info/METADATAPK!HEn_toasts-0.1.2.dist-info/RECORDPK]b