From 2999b90b22f0c3bf76aedd0272ac62ed5a601bf3 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 08:56:31 +0800 Subject: [PATCH 01/10] Don't import basiciw unless using the functionality it offers. --- i3pystatus/network.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/i3pystatus/network.py b/i3pystatus/network.py index cca478a..23ececf 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import netifaces -import basiciw import psutil from i3pystatus import IntervalModule from i3pystatus.core.color import ColorRangeModule @@ -74,7 +73,7 @@ class NetworkInfo(): Retrieve network information. """ - def __init__(self, interface, ignore_interfaces, detached_down, unknown_up): + def __init__(self, interface, ignore_interfaces, detached_down, unknown_up, get_wifi_info=False): if interface not in netifaces.interfaces() and not detached_down: raise RuntimeError( "Unknown interface {iface}!".format(iface=interface)) @@ -82,6 +81,7 @@ class NetworkInfo(): self.ignore_interfaces = ignore_interfaces self.detached_down = detached_down self.unknown_up = unknown_up + self.get_wifi_info = get_wifi_info def get_info(self, interface): format_dict = dict(v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="") @@ -133,9 +133,13 @@ class NetworkInfo(): break return info - @staticmethod - def extract_wireless_info(interface): + def extract_wireless_info(self, interface): info = dict(essid="", freq="", quality=0.0, quality_bar="") + if not self.get_wifi_info: + return info + + import basiciw + try: iwi = basiciw.iwinfo(interface) except Exception: @@ -284,8 +288,15 @@ class Network(IntervalModule, ColorRangeModule): on_rightclick = "cycle_interface" def init(self): + # Don't require importing basiciw unless using the functionality it offers. + if any(s in self.format_up or s in self.format_up for s in ['essid', 'freq', 'quality', 'quality_bar']): + get_wifi_info = True + else: + get_wifi_info = False + + self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up, + get_wifi_info) self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) - self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up) self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit)) self.kbs_arr = [0.0] * self.graph_width From d3e8fe9b6a8aa2ac354a294bb3c22af3da7df224 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 09:36:43 +0800 Subject: [PATCH 02/10] Don't require importing psutil unless using the functionality it offers --- i3pystatus/network.py | 50 ++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/i3pystatus/network.py b/i3pystatus/network.py index 23ececf..f11da78 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import netifaces -import psutil from i3pystatus import IntervalModule from i3pystatus.core.color import ColorRangeModule from i3pystatus.core.util import make_graph, round_dict, make_bar @@ -135,6 +134,8 @@ class NetworkInfo(): def extract_wireless_info(self, interface): info = dict(essid="", freq="", quality=0.0, quality_bar="") + + # Just return empty values if we're not using any Wifi functionality if not self.get_wifi_info: return info @@ -173,6 +174,8 @@ class NetworkTraffic(): self.round_size = round_size def update_counters(self, interface): + import psutil + self.pnic_before = self.pnic counters = psutil.net_io_counters(pernic=True) self.pnic = counters[interface] if interface in counters else None @@ -289,14 +292,21 @@ class Network(IntervalModule, ColorRangeModule): def init(self): # Don't require importing basiciw unless using the functionality it offers. - if any(s in self.format_up or s in self.format_up for s in ['essid', 'freq', 'quality', 'quality_bar']): + if any(s in self.format_up or s in self.format_up for s in + ['essid', 'freq', 'quality', 'quality_bar']): get_wifi_info = True else: get_wifi_info = False self.network_info = NetworkInfo(self.interface, self.ignore_interfaces, self.detached_down, self.unknown_up, get_wifi_info) - self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) + + # Don't require importing psutil unless using the functionality it offers. + if any(s in self.format_up or s in self.format_down for s in + ['bytes_sent', 'bytes_recv', 'packets_sent', 'packets_recv', 'network_graph']): + self.network_traffic = NetworkTraffic(self.unknown_up, self.divisor, self.round_size) + else: + self.network_traffic = None self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit)) self.kbs_arr = [0.0] * self.graph_width @@ -309,8 +319,9 @@ class Network(IntervalModule, ColorRangeModule): elif len(interfaces) > 0: self.interface = interfaces[0] - self.network_traffic.clear_counters() - self.kbs_arr = [0.0] * self.graph_width + if self.network_traffic: + self.network_traffic.clear_counters() + self.kbs_arr = [0.0] * self.graph_width def get_network_graph(self, kbs): # Cycle array by inserting at the start and chopping off the last element @@ -322,28 +333,27 @@ class Network(IntervalModule, ColorRangeModule): format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="", essid="", freq="", quality="", quality_bar="") + color = None + if self.network_traffic: + network_usage = self.network_traffic.get_usage(self.interface) + format_values.update(network_usage) + if self.graph_type == 'input': + kbs = network_usage['bytes_recv'] + elif self.graph_type == 'output': + kbs = network_usage['bytes_sent'] + else: + raise Exception("graph_type must be either 'input' or 'output'!") - network_usage = self.network_traffic.get_usage(self.interface) - format_values.update(network_usage) + format_values['network_graph'] = self.get_network_graph(kbs) + format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) + color = self.get_gradient(kbs, self.colors, self.upper_limit) network_info = self.network_info.get_info(self.interface) format_values.update(network_info) - if self.graph_type == 'input': - kbs = network_usage['bytes_recv'] - elif self.graph_type == 'output': - kbs = network_usage['bytes_sent'] - else: - raise Exception("graph_type must be either 'input' or 'output'!") - - format_values['network_graph'] = self.get_network_graph(kbs) - format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) format_values['interface'] = self.interface - if sysfs_interface_up(self.interface, self.unknown_up): - if self.dynamic_color: - color = self.get_gradient(kbs, self.colors, self.upper_limit) - else: + if not self.dynamic_color: color = self.color_up self.output = { From a40ba18272112e180205910f5c8007e8b1ecbd0d Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 10:10:31 +0800 Subject: [PATCH 03/10] Allow users to scroll through interfaces. --- i3pystatus/network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/i3pystatus/network.py b/i3pystatus/network.py index f11da78..9d021e9 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -289,6 +289,8 @@ class Network(IntervalModule, ColorRangeModule): on_leftclick = "nm-connection-editor" on_rightclick = "cycle_interface" + on_upscroll = ['cycle_interface', 1] + on_downscroll = ['cycle_interface', -1] def init(self): # Don't require importing basiciw unless using the functionality it offers. @@ -311,10 +313,10 @@ class Network(IntervalModule, ColorRangeModule): self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit)) self.kbs_arr = [0.0] * self.graph_width - def cycle_interface(self): + def cycle_interface(self, increment=1): interfaces = [i for i in netifaces.interfaces() if i not in self.ignore_interfaces] if self.interface in interfaces: - next_index = (interfaces.index(self.interface) + 1) % len(interfaces) + next_index = (interfaces.index(self.interface) + increment) % len(interfaces) self.interface = interfaces[next_index] elif len(interfaces) > 0: self.interface = interfaces[0] From 05349256dea035fadfa112a1a8cc4b8ba4f6a9a0 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 18:56:25 +0800 Subject: [PATCH 04/10] Added Github module. --- i3pystatus/github.py | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 i3pystatus/github.py diff --git a/i3pystatus/github.py b/i3pystatus/github.py new file mode 100644 index 0000000..60d2285 --- /dev/null +++ b/i3pystatus/github.py @@ -0,0 +1,56 @@ +from i3pystatus import IntervalModule +import requests +import json +from i3pystatus.core import ConfigError +from i3pystatus.core.util import user_open, internet, require + + +class Github(IntervalModule): + """ + Formatters: + + * `{unread}` - contains the value of unread_marker when there are pending notifications + * `{unread_count}` - number of unread notifications, empty if 0 + """ + + unread_marker = u"●" + unread = '' + color = '#78EAF2' + username = '' + password = '' + format = '{unread}' + interval = 600 + + on_leftclick = 'open_github' + + settings = ( + ('format', 'format string'), + ('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'), + ("username", ""), + ("password", ""), + ("color", "") + ) + + def open_github(self): + user_open('https://github.com/' + self.username) + + @require(internet) + def run(self): + format_values = dict(unread_count='', unread='') + + response = requests.get('https://api.github.com/notifications', auth=(self.username, self.password)) + data = json.loads(response.text) + + # Bad credentials + if isinstance(data, dict): + raise ConfigError(data['message']) + + unread = len(data) + if unread > 0: + format_values['unread_count'] = unread + format_values['unread'] = self.unread_marker + + self.output = { + 'full_text': self.format.format(**format_values), + 'color': self.color + } From 37c70634a826cf3836bc684f97064acc3c5ab3a7 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 18:56:50 +0800 Subject: [PATCH 05/10] Prevent webrowser.open() writing to stdout when opening link. --- i3pystatus/core/util.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index c3b093e..098644c 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -439,7 +439,16 @@ def user_open(url_or_command): scheme = urlparse(url_or_command).scheme if scheme == 'http' or scheme == 'https': import webbrowser - webbrowser.open(url_or_command) + import os + # webbrowser.open() sometimes prints a message for some reason and confuses i3 + # Redirect stdout briefly to prevent this from happening. + savout = os.dup(1) + os.close(1) + os.open(os.devnull, os.O_RDWR) + try: + webbrowser.open(url_or_command) + finally: + os.dup2(savout, 1) else: import subprocess subprocess.Popen(url_or_command, shell=True) From 97e8e8346cb3463670cea51e88ef26bc186d811f Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 18:59:54 +0800 Subject: [PATCH 06/10] Added method to open users mailbox. --- i3pystatus/reddit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/i3pystatus/reddit.py b/i3pystatus/reddit.py index b5a0f73..f448ecd 100644 --- a/i3pystatus/reddit.py +++ b/i3pystatus/reddit.py @@ -62,7 +62,6 @@ class Reddit(IntervalModule): } on_leftclick = "open_permalink" - on_click = "open_link" _permalink = "" _url = "" @@ -136,6 +135,9 @@ class Reddit(IntervalModule): "color": color, } + def open_mail(self): + user_open('https://www.reddit.com/message/unread/') + def open_permalink(self): user_open(self._permalink) From e00a25aebe5bd419edc6fc630804d3e140bd5838 Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 11 Jan 2015 19:06:48 +0800 Subject: [PATCH 07/10] Document dependency on requests module. --- i3pystatus/github.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/i3pystatus/github.py b/i3pystatus/github.py index 60d2285..6e3a375 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -7,6 +7,9 @@ from i3pystatus.core.util import user_open, internet, require class Github(IntervalModule): """ + Check Github for pending notifications. + Requires `requests` + Formatters: * `{unread}` - contains the value of unread_marker when there are pending notifications From fc76b543dac566cad1b5570f09ac3d214a5dd7ec Mon Sep 17 00:00:00 2001 From: facetoe Date: Sun, 18 Jan 2015 11:27:43 +0800 Subject: [PATCH 08/10] Added whosonlocation module --- i3pystatus/github.py | 2 +- i3pystatus/whosonlocation.py | 102 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 i3pystatus/whosonlocation.py diff --git a/i3pystatus/github.py b/i3pystatus/github.py index 6e3a375..a40c306 100644 --- a/i3pystatus/github.py +++ b/i3pystatus/github.py @@ -7,7 +7,7 @@ from i3pystatus.core.util import user_open, internet, require class Github(IntervalModule): """ - Check Github for pending notifications. + Check Github for pending notifications. Requires `requests` Formatters: diff --git a/i3pystatus/whosonlocation.py b/i3pystatus/whosonlocation.py new file mode 100644 index 0000000..b975777 --- /dev/null +++ b/i3pystatus/whosonlocation.py @@ -0,0 +1,102 @@ +from i3pystatus import IntervalModule +import requests +from collections import OrderedDict +from bs4 import BeautifulSoup + + +class WhosOnLocation(): + + email = None + password = None + session = None + + def __init__(self, email, password): + self.email = email + self.password = password + self.session = requests.Session() + + def login(self): + login_details = {'email_input': self.email, + 'password_input': self.password, + '_redirect_url': '', + 'continue_submit': 'Login'} + r = self.session.post('https://login.whosonlocation.com/login', data=login_details) + return r.url == 'https://au.whosonlocation.com/home?justloggedin=true' + + def get_status(self): + r = self.session.get('https://au.whosonlocation.com/home?justloggedin=true') + html = BeautifulSoup(r.content) + status = html.body.find("span", {"class": "my-status-name"}) + if status: + return status.text + + def on_site(self): + return self.__change_status('onsite') + + def off_site(self): + return self.__change_status('offsite') + + def __change_status(self, status): + r = self.session.post('https://au.whosonlocation.com/ajax/changestatus', data={'status': status}) + return r.json() + + # _type can be org or location + def search(self, keyword, _type='location'): + payload = {'keyword': keyword, 'type': _type} + r = self.session.get('https://au.whosonlocation.com/home/search', params=payload) + return self.__parse_results(BeautifulSoup(r.content)) + + @staticmethod + def __parse_results(page): + titles = ['Name', 'Title', 'Department', 'Current Location', 'Home Location'] + table = page.body.find_all("tr", {"class": "dataRow"}) + results = [] + for row in table: + values = [v.string for v in row.findAll('td', {'class': 'truncate'})] + results.append(OrderedDict(zip(titles, values))) + return results + + +class WOL(IntervalModule): + location = None + email = None + password = None + + settings = ( + 'email', + 'password' + ) + + color_on_site = '#00FF00' + color_off_site = '#ff0000' + format = 'Status: {status}' + status = None + + on_leftclick = 'change_status' + + def init(self): + self.location = WhosOnLocation(self.email, self.password) + if not self.location.login(): + raise Exception("Failed to login") + + def change_status(self): + if self.status == 'On-Site': + self.location.off_site() + elif self.status == 'Off-Site': + self.location.on_site() + + def run(self): + self.status = self.location.get_status() + color = None + + if self.status == 'Off-Site': + color = self.color_off_site + elif self.status == 'On-Site': + color = self.color_on_site + + self.output = { + "full_text": self.format.format( + status=self.status + ), + "color": color + } From d18988827f96847d831d6bad28918adc17720ab5 Mon Sep 17 00:00:00 2001 From: facetoe Date: Tue, 20 Jan 2015 08:29:24 +0800 Subject: [PATCH 09/10] Documented dependency on beautifulsoup4. --- docs/conf.py | 1 + i3pystatus/whosonlocation.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index cda1e9e..33b32c8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,6 +30,7 @@ MOCK_MODULES = [ "i3pystatus.pulseaudio.pulse", "notmuch", "requests", + "beautifulsoup4" ] for mod_name in MOCK_MODULES: diff --git a/i3pystatus/whosonlocation.py b/i3pystatus/whosonlocation.py index b975777..dad46dc 100644 --- a/i3pystatus/whosonlocation.py +++ b/i3pystatus/whosonlocation.py @@ -58,6 +58,11 @@ class WhosOnLocation(): class WOL(IntervalModule): + """ + Change your whosonlocation.com status. + + Requires the PyPi module `beautifulsoup4` + """ location = None email = None password = None From 23b60fd2b7fd15005dafe772072bd5aad66752ac Mon Sep 17 00:00:00 2001 From: facetoe Date: Tue, 20 Jan 2015 18:01:27 +0800 Subject: [PATCH 10/10] Added bs4 --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 33b32c8..c29cb33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ MOCK_MODULES = [ "i3pystatus.pulseaudio.pulse", "notmuch", "requests", - "beautifulsoup4" + "bs4" ] for mod_name in MOCK_MODULES: