From 5203cd88a360d52954c916cef0ebeae17ef9e657 Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Mon, 24 Dec 2018 12:46:08 +0100 Subject: [PATCH 1/6] [modules/spaceapi] Fix ArgumentException This ArgumentException was caused by me failing to rename one occurence of a parameter when refactoring code. This went under my radar as the API I'm usually testing against, was offline at that time, too. So I expceted to see an Error. Just not this one. Well, the fix comes late, but better late than never :) Signed-off-by: Tobias Manske --- bumblebee/modules/spaceapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index 785f796..29dd14e 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -67,7 +67,7 @@ class Module(bumblebee.engine.Module): def update(self, widgets): try: - with requests.get(self._url, timeout=self.timeout) as u: + with requests.get(self._url, timeout=self._timeout) as u: json = u.json() self._open = json["state"]["open"] self._name = self.parameter("name", default=json["space"]) From 0be81ec1f71f0716322f28783134b3c29a8ea8e3 Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Fri, 4 Jan 2019 15:39:55 +0100 Subject: [PATCH 2/6] [modules/spaceapi] rewrote module to use threading Also I'm not catching every exception anymore. Signed-off-by: Tobias Manske --- bumblebee/modules/spaceapi.py | 96 ++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index 29dd14e..543e64e 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -7,11 +7,7 @@ Requires the following libraries: Parameters: * spaceapi.url: String representation of the api endpoint - * spaceapi.name: String overwriting the space name - * spaceapi.prefix: Prefix for the space string - * spaceapi.interval: time between updates in minutes - * spaceapi.timeout: Maximum time in seconds to wait for a response from API - endpoint + * spaceapi.format: Format string for the output """ import bumblebee.input @@ -19,6 +15,8 @@ import bumblebee.output import bumblebee.engine import requests +import threading +import sys class Module(bumblebee.engine.Module): @@ -27,54 +25,60 @@ class Module(bumblebee.engine.Module): engine, config, bumblebee.output.Widget(full_text=self.getState) ) - # Represents the state of the hackerspace - self._open = False - # Set to true if there was an error calling the spaceapi - self._error = False + self._data = {} + self._error = None + + self._threadingCount = 0 + # The URL representing the api endpoint - self._url = self.parameter("url", - default="http://club.entropia.de/spaceapi") - # Space Name, can be set manually in case of multiple widgets, - # so you're able to distinguish - self._name = self.parameter("name", default="") - - # The timeout prevents the statusbar from blocking when the destination - # can't be reached. - self._timeout = self.parameter("timeout", default=2) - - # Only execute every 5 minutes by default - self.interval(self.parameter("interval", default=5)) - - def getState(self, widget): - text = self.parameter("prefix", default="") - text += self._name + ": " - - if self._error: - text += "ERROR" - elif self._open: - text += "Open" - else: - text += "Closed" - return text + self._url = self.parameter("url", default="http://club.entropia.de/spaceapi") + self._format = self.parameter("format", default=" %%space%%: %%state%%") def state(self, widget): - if self._error: + try: + if self._error is not None: + return ["critical"] + elif self._data['state']['open']: + return ["warning"] + else: + return [] + except KeyError: return ["critical"] - elif self._open: - return ["warning"] - else: - return [] def update(self, widgets): + if self._threadingCount == 0: + thread = threading.Thread(target=self.get_api_async, args=()) + thread.start() + self._threadingCount = ( + 0 if self._threadingCount > 300 else self._threadingCount + 1 + ) + + def getState(self, widget): + text = self._format + if self._error is not None: + text = self._error + else: + try: + text = text.replace("%%space%%", self._data['space']) + if self._data['state']['open']: + text = text.replace("%%state%%", "Open") + else: + text = text.replace("%%state%%", "Closed") + except KeyError: + text = "KeyError" + return text + + def get_api_async(self): try: - with requests.get(self._url, timeout=self._timeout) as u: - json = u.json() - self._open = json["state"]["open"] - self._name = self.parameter("name", default=json["space"]) - self._error = False - except Exception: - # Displays ERROR status - self._error = True + with requests.get(self._url, timeout=10) as u: + self._data = u.json() + self._error = None + except requests.exceptions.Timeout: + self._error = "Timeout" + except requests.exceptions.HTTPError: + self._error = "HTTP Error" + # except Exception: + # self._error = "CRITICAL ERROR" # Author: Tobias Manske From e95652fc058a0931b1ec43d7cbb8be297f45eed5 Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Sat, 5 Jan 2019 19:34:21 +0100 Subject: [PATCH 3/6] [modules/spaceapi] format string to json parser Signed-off-by: Tobias Manske --- bumblebee/modules/spaceapi.py | 60 +++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index 543e64e..89fd341 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -4,10 +4,11 @@ Requires the following libraries: * requests + * regex Parameters: * spaceapi.url: String representation of the api endpoint - * spaceapi.format: Format string for the output + * spaceapi.format: Format string for the output (refer to code) """ import bumblebee.input @@ -16,7 +17,40 @@ import bumblebee.engine import requests import threading -import sys +import re +from json.decoder import JSONDecodeError + + +def formatStringBuilder(s: str, json: dict) -> str: + """ + This function seems to be in dire need of some explanation so here it is: + It basically searches the format string for strings of the pattern + %%ITEM.IN.TREE[%IFTRUE%IFFALSE]%%. For example to query the state of + the space you'd write %%state.open%% as it's located in json[state][open] + according to the API specificaion. As the output of true or false doesn't + look to great you can specify the text you want to have shown so you'd + write %%state.open%Open%Closed%% to overwrite true/false with Open/Closed. + """ + identifiers = re.findall("%%.*?%%", s) + for i in identifiers: + ic = i[2:-2] # Discard %% + j = ic.split("%") + + if len(j) != 3 and len(j) != 1: + return "INVALID SYNTAX" + + arr = j[0].split(".") + repl = json + for a in arr: # Walk the JSON tree to find replacement + repl = repl[a] + + if len(j) == 1: + s = s.replace(i, repl) + elif repl: + s = s.replace(i, j[1]) + else: + s = s.replace(i, j[2]) + return s class Module(bumblebee.engine.Module): @@ -32,13 +66,15 @@ class Module(bumblebee.engine.Module): # The URL representing the api endpoint self._url = self.parameter("url", default="http://club.entropia.de/spaceapi") - self._format = self.parameter("format", default=" %%space%%: %%state%%") + self._format = self.parameter( + "format", default=" %%space%%: %%state.open%Open%Closed%%" + ) def state(self, widget): try: if self._error is not None: return ["critical"] - elif self._data['state']['open']: + elif self._data["state"]["open"]: return ["warning"] else: return [] @@ -59,27 +95,23 @@ class Module(bumblebee.engine.Module): text = self._error else: try: - text = text.replace("%%space%%", self._data['space']) - if self._data['state']['open']: - text = text.replace("%%state%%", "Open") - else: - text = text.replace("%%state%%", "Closed") + text = formatStringBuilder(self._format, self._data) except KeyError: text = "KeyError" return text def get_api_async(self): try: - with requests.get(self._url, timeout=10) as u: - self._data = u.json() + with requests.get(self._url, timeout=10) as request: + self._data = request.json() self._error = None except requests.exceptions.Timeout: self._error = "Timeout" except requests.exceptions.HTTPError: self._error = "HTTP Error" - # except Exception: - # self._error = "CRITICAL ERROR" + except JSONDecodeError: + self._error = "Not a JSON response" -# Author: Tobias Manske +# Author: Tobias Manske # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 54024f1dddb6c98bdaffc444b793066726e9061a Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Sun, 6 Jan 2019 22:57:25 +0100 Subject: [PATCH 4/6] [modules/spaceapi] Reload on left mouse button Signed-off-by: Tobias Manske --- bumblebee/modules/spaceapi.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index 89fd341..d4caf69 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -26,7 +26,7 @@ def formatStringBuilder(s: str, json: dict) -> str: This function seems to be in dire need of some explanation so here it is: It basically searches the format string for strings of the pattern %%ITEM.IN.TREE[%IFTRUE%IFFALSE]%%. For example to query the state of - the space you'd write %%state.open%% as it's located in json[state][open] + the hackspace you'd write %%state.open%% as it's located in json[state][open] according to the API specificaion. As the output of true or false doesn't look to great you can specify the text you want to have shown so you'd write %%state.open%Open%Closed%% to overwrite true/false with Open/Closed. @@ -59,6 +59,9 @@ class Module(bumblebee.engine.Module): engine, config, bumblebee.output.Widget(full_text=self.getState) ) + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd=self.__forceReload) + self._data = {} self._error = None @@ -112,6 +115,10 @@ class Module(bumblebee.engine.Module): except JSONDecodeError: self._error = "Not a JSON response" + # left_mouse_button handler + def __forceReload(self, event): + self._threadingCount += 300 + self._error = "RELOADING" # Author: Tobias Manske # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From ab309f873b7b16e846186b7cd08f26328312b185 Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Mon, 7 Jan 2019 01:10:06 +0100 Subject: [PATCH 5/6] [modules/spaceapi] Python2.7 compatability --- bumblebee/modules/spaceapi.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index d4caf69..3ab7724 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # pylint: disable=C0111,R0903 """Displays the state of a spaceapi endpoint @@ -18,10 +21,10 @@ import bumblebee.engine import requests import threading import re -from json.decoder import JSONDecodeError +import json -def formatStringBuilder(s: str, json: dict) -> str: +def formatStringBuilder(s, json): """ This function seems to be in dire need of some explanation so here it is: It basically searches the format string for strings of the pattern @@ -70,7 +73,7 @@ class Module(bumblebee.engine.Module): # The URL representing the api endpoint self._url = self.parameter("url", default="http://club.entropia.de/spaceapi") self._format = self.parameter( - "format", default=" %%space%%: %%state.open%Open%Closed%%" + "format", default=u" %%space%%: %%state.open%Open%Closed%%" ) def state(self, widget): @@ -106,13 +109,15 @@ class Module(bumblebee.engine.Module): def get_api_async(self): try: with requests.get(self._url, timeout=10) as request: - self._data = request.json() + # Can't implement error handling for python2.7 if I use + # request.json() as it uses simplejson in newer versions + self._data = json.loads(request.text) self._error = None except requests.exceptions.Timeout: self._error = "Timeout" except requests.exceptions.HTTPError: self._error = "HTTP Error" - except JSONDecodeError: + except ValueError: self._error = "Not a JSON response" # left_mouse_button handler From 521b382131900afede5e9a35c5eb7b4dece6eb2a Mon Sep 17 00:00:00 2001 From: Tobias Manske Date: Mon, 7 Jan 2019 01:37:10 +0100 Subject: [PATCH 6/6] [modules/spaceapi] Improve documentation / help text --- bumblebee/modules/spaceapi.py | 44 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/bumblebee/modules/spaceapi.py b/bumblebee/modules/spaceapi.py index 3ab7724..10b0bdc 100644 --- a/bumblebee/modules/spaceapi.py +++ b/bumblebee/modules/spaceapi.py @@ -3,7 +3,9 @@ # pylint: disable=C0111,R0903 -"""Displays the state of a spaceapi endpoint +"""Displays the state of a Space API endpoint +Space API is an API for hackspaces based on JSON. See spaceapi.io for +an example. Requires the following libraries: * requests @@ -11,7 +13,17 @@ Requires the following libraries: Parameters: * spaceapi.url: String representation of the api endpoint - * spaceapi.format: Format string for the output (refer to code) + * spaceapi.format: Format string for the output + +Format Strings: + * Format strings are indicated by double %% + * They represent a leaf in the JSON tree, layers seperated by "." + * Boolean values can be overwritten by appending "%true%false" + in the format string + * Example: to reference "open" in "{"state":{"open": true}}" + you would write "%%state.open%%", if you also want + to say "Open/Closed" depending on the boolean you + would write "%%state.open%Open%Closed%%" """ import bumblebee.input @@ -26,32 +38,30 @@ import json def formatStringBuilder(s, json): """ - This function seems to be in dire need of some explanation so here it is: - It basically searches the format string for strings of the pattern - %%ITEM.IN.TREE[%IFTRUE%IFFALSE]%%. For example to query the state of - the hackspace you'd write %%state.open%% as it's located in json[state][open] - according to the API specificaion. As the output of true or false doesn't - look to great you can specify the text you want to have shown so you'd - write %%state.open%Open%Closed%% to overwrite true/false with Open/Closed. + Parses Format Strings + Parameter: + s -> format string + json -> the spaceapi response object """ identifiers = re.findall("%%.*?%%", s) for i in identifiers: ic = i[2:-2] # Discard %% j = ic.split("%") + # Only neither of, or both true AND false may be overwritten if len(j) != 3 and len(j) != 1: - return "INVALID SYNTAX" + return "INVALID FORMAT STRING" arr = j[0].split(".") repl = json - for a in arr: # Walk the JSON tree to find replacement + for a in arr: # Walk the JSON tree to find replacement string repl = repl[a] - if len(j) == 1: + if len(j) == 1: # no overwrite s = s.replace(i, repl) - elif repl: + elif repl: # overwrite for Trfor True s = s.replace(i, j[1]) - else: + else: # overwrite for False s = s.replace(i, j[2]) return s @@ -62,8 +72,9 @@ class Module(bumblebee.engine.Module): engine, config, bumblebee.output.Widget(full_text=self.getState) ) - engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, - cmd=self.__forceReload) + engine.input.register_callback( + self, button=bumblebee.input.LEFT_MOUSE, cmd=self.__forceReload + ) self._data = {} self._error = None @@ -125,5 +136,6 @@ class Module(bumblebee.engine.Module): self._threadingCount += 300 self._error = "RELOADING" + # Author: Tobias Manske # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4