class GuiToolApp(TkAsyncioBaseApp): def __init__(self, root): super().__init__(root) self.window = root root.title("Pushbullet Account Management") self.log = logging.getLogger(__name__) # Data self._pushbullet = None # type: AsyncPushbullet self.pushbullet_listener = None # type: LiveStreamListener self.key_var = tk.StringVar() # type: tk.StringVar # API key self.pushes_var = tk.StringVar( ) # type: tk.StringVar # Used in text box to display pushes received self.status_var = tk.StringVar( ) # type: tk.StringVar # Bound to bottom of window status bar self.proxy = os.environ.get("https_proxy") or os.environ.get( "http_proxy") # type: str # Related to Devices self.device_detail_var = tk.StringVar( ) # type: tk.StringVar # Used in text box to display device details self.devices_in_listbox = None # type: Tuple[Device] # Cached devices that were retrieved self.device_tab_index = None # type: int # The index for the Devices tab # View / Control self.btn_connect = None # type: tk.Button self.btn_disconnect = None # type: tk.Button self.lb_device = None # type: tk.Listbox self.btn_load_devices = None # type: tk.Button self.lbl_photo = None # type: tk.Label self.lbl_status = None # type: tk.Label self.create_widgets() # Connections / Bindings tkinter_tools.bind_tk_var_to_method(partial(PREFS.set, "api_key"), self.key_var) self.key_var.set(API_KEY) @property def status(self): return str(self.status_var.get()) @status.setter def status(self, val): self.tk(self.status_var.set, val) @property def pushbullet(self) -> AsyncPushbullet: current_key = self.key_var.get() if self._pushbullet is not None: if current_key != self._pushbullet.api_key: self._pushbullet.close_all_threadsafe() self._pushbullet = None if self._pushbullet is None: self._pushbullet = AsyncPushbullet( api_key=current_key, # loop=self.ioloop, verify_ssl=False, proxy=self.proxy) return self._pushbullet @pushbullet.setter def pushbullet(self, val: AsyncPushbullet): if val is None and self._pushbullet is not None: self._pushbullet.close_all_threadsafe() self._pushbullet = val def ioloop_exception_happened(self, extype, ex, tb, func): self.status = ex def create_widgets(self): parent = self.window # API Key frm_key = tk.Frame(parent) frm_key.grid(row=0, column=0, sticky="NSEW") lbl_key = tk.Label(frm_key, text="API Key:") lbl_key.grid(row=0, column=0, sticky=tk.W) txt_key = tk.Entry(frm_key, textvariable=self.key_var) txt_key.grid(row=0, column=1, sticky=tk.W + tk.E, columnspan=2) btn_oauth2 = tk.Button(frm_key, text="Authenticate online...") btn_oauth2.configure(command=partial(self.oauth2_clicked, btn_oauth2)) btn_oauth2.grid(row=0, column=2, sticky=tk.W) tk.Grid.grid_columnconfigure(frm_key, 1, weight=1) tk.Grid.grid_columnconfigure(parent, 0, weight=1) # Top level notebook notebook = ttk.Notebook(parent) notebook.grid(row=1, column=0, sticky="NSEW", columnspan=2) # tk.Grid.grid_columnconfigure(parent, 0, weight=1) tk.Grid.grid_rowconfigure(parent, 1, weight=1) notebook.bind("<<NotebookTabChanged>>", self.notebook_tab_changed) # Status line status_line = tk.Frame(parent, borderwidth=2, relief=tk.GROOVE) status_line.grid(row=999, column=0, sticky="EW", columnspan=2) self.lbl_photo = tk.Label( status_line) # , text="", width=16, height=16) self.lbl_photo.grid(row=0, column=0, sticky=tk.W) self.lbl_status = tk.Label(status_line, textvar=self.status_var) self.lbl_status.grid(row=0, column=1, sticky=tk.W) # Tab: Pushes pushes_frame = tk.Frame(notebook) notebook.add(pushes_frame, text="Pushes") self.create_widgets_pushes(pushes_frame) # Tab: Devices devices_frame = tk.Frame(notebook) notebook.add(devices_frame, text="Devices") self.device_tab_index = notebook.index( tk.END) - 1 # save tab pos for later self.create_widgets_devices(devices_frame) def create_widgets_pushes(self, parent: tk.Frame): self.btn_connect = tk.Button(parent, text="Connect", command=self.connect_button_clicked) self.btn_connect.grid(row=0, column=0, sticky=tk.W) self.btn_disconnect = tk.Button(parent, text="Disconnect", command=self.disconnect_button_clicked) self.btn_disconnect.grid(row=0, column=1, sticky=tk.W) self.btn_disconnect.configure(state=tk.DISABLED) btn_clear = tk.Button(parent, text="Clear", command=partial(self.pushes_var.set, "")) btn_clear.grid(row=0, column=2) txt_data = tkinter_tools.BindableTextArea(parent, textvariable=self.pushes_var, width=60, height=20) txt_data.grid(row=1, column=0, sticky="NSEW", columnspan=3) tk.Grid.grid_columnconfigure(parent, 0, weight=1) tk.Grid.grid_columnconfigure(parent, 1, weight=1) tk.Grid.grid_rowconfigure(parent, 1, weight=1) def create_widgets_devices(self, parent: tk.Frame): scrollbar = tk.Scrollbar(parent, orient=tk.VERTICAL) self.lb_device = tk.Listbox(parent, yscrollcommand=scrollbar.set) scrollbar.config(command=self.lb_device.yview) self.lb_device.grid(row=0, column=0, sticky="NSEW") self.lb_device.bind("<Double-Button-1>", self.device_list_double_clicked) self.btn_load_devices = tk.Button(parent, text="Load Devices", command=self.load_devices_clicked) self.btn_load_devices.grid(row=1, column=0, sticky="EW") txt_device_details = tkinter_tools.BindableTextArea( parent, textvariable=self.device_detail_var, width=80, height=10) txt_device_details.grid(row=0, column=1, sticky="NSEW") tk.Grid.grid_columnconfigure(parent, 1, weight=1) tk.Grid.grid_rowconfigure(parent, 0, weight=1) # ######## G U I E V E N T S ######## def notebook_tab_changed(self, event): nb = event.widget # type: ttk.Notebook index = nb.index("current") if index == self.device_tab_index: # If there are no devices loaded, go ahead and try if self.devices_in_listbox is None: self.load_devices_clicked() def oauth2_clicked(self, btn: tk.Button): btn.configure(state=tk.DISABLED) self.status = "Authenticating online using OAuth2..." async def _auth(): token = await oauth2.async_gain_oauth2_access() if token: self.tk(self.key_var.set, token) self.status = "Authentication using OAuth2 succeeded." else: self.status = "Authentication using OAuth2 failed." btn.configure(state=tk.NORMAL) self.io(_auth()) def connect_button_clicked(self): self.status = "Connecting to Pushbullet..." self.btn_connect.configure(state=tk.DISABLED) self.btn_disconnect.configure(state=tk.DISABLED) if self.pushbullet is not None: self.pushbullet = None if self.pushbullet_listener is not None: pl = self.pushbullet_listener # type: LiveStreamListener if pl is not None: self.io(pl.close()) self.pushbullet_listener = None async def _listen(): pl2 = None # type: LiveStreamListener try: await self.verify_key() async with LiveStreamListener(self.pushbullet, types=()) as pl2: self.pushbullet_listener = pl2 await self.pushlistener_connected(pl2) async for push in pl2: await self.push_received(push, pl2) except Exception as ex: pass print("guitool _listen caught exception", ex) finally: # if pl2 is not None: await self.pushlistener_closed(pl2) self.io(_listen()) def disconnect_button_clicked(self): self.status = "Disconnecting from Pushbullet..." self.io(self.pushbullet_listener.close()) def load_devices_clicked(self): self.btn_load_devices.configure(state=tk.DISABLED) self.status = "Loading devices..." self.lb_device.delete(0, tk.END) self.lb_device.insert(tk.END, "Loading...") self.devices_in_listbox = None async def _load(): try: await self.verify_key() self.devices_in_listbox = tuple( await self.pushbullet.async_get_devices()) self.tk(self.lb_device.delete, 0, tk.END) for dev in self.devices_in_listbox: self.tk(self.lb_device.insert, tk.END, str(dev.nickname)) self.status = "Loaded {} devices".format( len(self.devices_in_listbox)) except Exception as ex: self.tk(self.lb_device.delete, 0, tk.END) self.status = "Error retrieving devices: {}".format(ex) raise ex finally: self.tk(self.btn_load_devices.configure, state=tk.NORMAL) # asyncio.run_coroutine_threadsafe(_load(), self.ioloop) self.io(_load()) def device_list_double_clicked(self, event): items = self.lb_device.curselection() if len(items) == 0: print("No item selected") return if self.devices_in_listbox is None: print("No devices have been loaded") device = self.devices_in_listbox[int(items[0])] self.device_detail_var.set(repr(device)) # ######## C A L L B A C K S ######## async def pushlistener_connected(self, listener: LiveStreamListener): self.status = "Connected to Pushbullet" try: me = await self.pushbullet.async_get_user() self.status = "Connected to Pushbullet: {}".format(me.get("name")) except Exception as ex: # print("To include image support: pip install pillow") pass finally: self.tk(self.btn_connect.configure, state=tk.DISABLED) self.tk(self.btn_disconnect.configure, state=tk.NORMAL) async def pushlistener_closed(self, listener: LiveStreamListener): # print_function_name() self.status = "Disconnected from Pushbullet" self.tk(self.btn_connect.configure, state=tk.NORMAL) self.tk(self.btn_disconnect.configure, state=tk.DISABLED) async def push_received(self, p: dict, listener: LiveStreamListener): # print("Push received:", p) push_type = p.get("type") if push_type == "push": push_type = "ephemeral" prev = self.pushes_var.get() prev += "Type: {}\n{}\n\n".format(push_type, pprint.pformat(p)) self.tk(self.pushes_var.set, prev) # ######## O T H E R ######## async def verify_key(self): self.status = "Verifying API key..." api_key = self.key_var.get() try: await self.pushbullet.async_verify_key() self.status = "Valid API key: {}".format(api_key) if getattr(self.lbl_photo, "image_ref", None) is None: async def _load_pic(): try: me = await self.pushbullet.async_get_user() if "image_url" in me: image_url = me.get("image_url") try: msg = await self.pushbullet._async_get_data( image_url) except Exception as ex_get: self.log.info( "Could not retrieve user photo from url {}" .format(image_url)) else: photo_bytes = io.BytesIO(msg.get("raw")) img = Image.open(photo_bytes) label_size = self.lbl_photo.winfo_height() img = img.resize((label_size, label_size), Image.ANTIALIAS) photo = PhotoImage(img) self.tk(self.lbl_photo.configure, image=photo) self.lbl_photo.image_ref = photo # Save for garbage collection protection self.log.info( "Loaded user image from url {}".format( image_url)) except Exception as ex: # print(ex) print("To include image support: pip install pillow") # ex.with_traceback() # raise ex asyncio.get_event_loop().create_task(_load_pic()) except Exception as e: self.status = "Invalid API key: {}".format(api_key) self.tk(self.lbl_photo.configure, image="") self.lbl_photo.image_ref = None raise e
class PushApp(): def __init__(self, root): self.window = root root.title("Async Pushbullet Upload Demo") self.log = logging.getLogger(__name__) # Data self.ioloop = None # type: asyncio.AbstractEventLoop self.pushbullet = None # type: AsyncPushbullet self.pushbullet_listener = None # type: LiveStreamListener self.key_var = tk.StringVar() # API key self.pushes_var = tk.StringVar() self.filename_var = tk.StringVar() self.btn_upload = None # type: tk.Button self.proxy_var = tk.StringVar() # View / Control self.create_widgets() # Connections self.create_io_loop() self.key_var.set(API_KEY) self.filename_var.set(__file__) self.proxy_var.set(PROXY) def create_widgets(self): """ API Key: [ ] <Connect> Filename: [ ] <Browse> <Upload> Pushes: +----------------------------+ | | +----------------------------+ """ row = 0 # API Key lbl_key = tk.Label(self.window, text="API Key:") lbl_key.grid(row=row, column=0, sticky=tk.W) txt_key = tk.Entry(self.window, textvariable=self.key_var) txt_key.grid(row=row, column=1, sticky=tk.W + tk.E) tk.Grid.grid_columnconfigure(self.window, 1, weight=1) txt_key.bind('<Return>', lambda x: self.connect_button_clicked()) row += 1 btn_connect = tk.Button(self.window, text="Connect", command=self.connect_button_clicked) btn_connect.grid(row=row, column=1, sticky=tk.W) row += 1 # Proxy, if we want to show it # lbl_proxy = tk.Label(self.window, text="Proxy") # lbl_proxy.grid(row=row, column=0, sticky=tk.W) # txt_proxy = tk.Entry(self.window, textvariable=self.proxy_var) # txt_proxy.grid(row=row, column=1, sticky=tk.W + tk.E) # row += 1 # File: [ ] lbl_file = tk.Label(self.window, text="File:") lbl_file.grid(row=row, column=0, sticky=tk.W) txt_file = tk.Entry(self.window, textvariable=self.filename_var) txt_file.grid(row=row, column=1, sticky=tk.W + tk.E) row += 1 # <Browse> <Upload> button_frame = tk.Frame(self.window) button_frame.grid(row=row, column=0, columnspan=2, sticky=tk.W + tk.E) row += 1 btn_browse = tk.Button(button_frame, text="Browse...", command=self.browse_button_clicked) btn_browse.grid(row=0, column=0, sticky=tk.E) self.btn_upload = tk.Button(button_frame, text="Upload and Push", command=self.upload_button_clicked, state=tk.DISABLED) self.btn_upload.grid(row=0, column=1, sticky=tk.W) # Incoming pushes # +------------+ # | | # +------------+ lbl_data = tk.Label(self.window, text="Incoming Pushes...") lbl_data.grid(row=row, column=0, sticky=tk.W) row += 1 txt_data = BindableTextArea(self.window, textvariable=self.pushes_var, width=80, height=10) txt_data.grid(row=row, column=0, columnspan=2) def create_io_loop(self): """Creates a new thread to manage an asyncio event loop specifically for IO to/from Pushbullet.""" assert self.ioloop is None # This should only ever be run once def _run(loop): asyncio.set_event_loop(loop) loop.run_forever() self.ioloop = asyncio.new_event_loop() self.ioloop.set_exception_handler(self._ioloop_exc_handler) threading.Thread(target=partial(_run, self.ioloop), name="Thread-asyncio", daemon=True).start() def _ioloop_exc_handler(self, loop: asyncio.BaseEventLoop, context: dict): if "exception" in context: self.status = context["exception"] self.status = str(context) # Handle this more robustly in real-world code def connect_button_clicked(self): self.pushes_var.set("Connecting...") self.close() async def _listen(): try: self.pushbullet = AsyncPushbullet(self.key_var.get(), verify_ssl=False, proxy=self.proxy_var.get()) async with LiveStreamListener(self.pushbullet) as pl2: self.pushbullet_listener = pl2 await self.connected(pl2) async for push in pl2: await self.push_received(push, pl2) except Exception as ex: print("Exception:", ex) finally: await self.disconnected(self.pushbullet_listener) asyncio.run_coroutine_threadsafe(_listen(), self.ioloop) def close(self): if self.pushbullet is not None: self.pushbullet.close_all_threadsafe() self.pushbullet = None if self.pushbullet_listener is not None: assert self.ioloop is not None pl = self.pushbullet_listener asyncio.run_coroutine_threadsafe(pl.close(), self.ioloop) self.pushbullet_listener = None def browse_button_clicked(self): print("browse_button_clicked") resp = filedialog.askopenfilename(parent=self.window, title="Open a File to Push") if resp != "": self.filename_var.set(resp) def upload_button_clicked(self): self.pushes_var.set(self.pushes_var.get() + "Uploading...") self.btn_upload["state"] = tk.DISABLED filename = self.filename_var.get() asyncio.run_coroutine_threadsafe(self.upload_file(filename), loop=self.ioloop) async def upload_file(self, filename: str): # This is the actual upload command info = await self.pushbullet.async_upload_file(filename) # Push a notification of the upload "as a file": await self.pushbullet.async_push_file(info["file_name"], info["file_url"], info["file_type"], title="File Arrived!", body="Please enjoy your file") # Push a notification of the upload "as a link": await self.pushbullet.async_push_link("Link to File Arrived!", info["file_url"], body="Please enjoy your file") self.btn_upload["state"] = tk.NORMAL self.pushes_var.set(self.pushes_var.get() + "Uploaded\n") async def connected(self, listener: LiveStreamListener): self.btn_upload["state"] = tk.NORMAL self.pushes_var.set(self.pushes_var.get() + "Connected\n") async def disconnected(self, listener: LiveStreamListener): self.btn_upload["state"] = tk.DISABLED self.pushes_var.set(self.pushes_var.get() + "Disconnected\n") async def push_received(self, p: dict, listener: LiveStreamListener): print("Push received:", p) prev = self.pushes_var.get() prev += "{}\n\n".format(p) self.pushes_var.set(prev)
class PushApp(): def __init__(self, root): self.window = root root.title("Async Pushbullet Demo") self.log = logging.getLogger(__name__) # Data self.pushbullet = None # type: AsyncPushbullet self.pushbullet_listener = None # type: LiveStreamListener self.key_var = tk.StringVar() # API key self.pushes_var = tk.StringVar() self.ioloop = None # type: asyncio.BaseEventLoop self.proxy_var = tk.StringVar() # View / Control self.create_widgets() # Connections self.create_io_loop() self.key_var.set(API_KEY) self.proxy_var.set(PROXY) def create_widgets(self): """ API Key: [ ] <Connect> Pushes: +----------------------------+ | | +----------------------------+ """ # API Key lbl_key = tk.Label(self.window, text="API Key:") lbl_key.grid(row=0, column=0, sticky=tk.W) txt_key = tk.Entry(self.window, textvariable=self.key_var) txt_key.grid(row=0, column=1, sticky=tk.W + tk.E) tk.Grid.grid_columnconfigure(self.window, 1, weight=1) txt_key.bind('<Return>', lambda x: self.connect_button_clicked()) btn_connect = tk.Button(self.window, text="Connect", command=self.connect_button_clicked) btn_connect.grid(row=1, column=1, sticky=tk.W) btn_disconnect = tk.Button(self.window, text="Disconnect", command=self.disconnect_button_clicked) btn_disconnect.grid(row=2, column=1, sticky=tk.W) lbl_data = tk.Label(self.window, text="Incoming Pushes...") lbl_data.grid(row=4, column=0, sticky=tk.W) txt_data = BindableTextArea(self.window, textvariable=self.pushes_var, width=80, height=10) txt_data.grid(row=5, column=0, columnspan=2, sticky="NSEW") tk.Grid.grid_rowconfigure(self.window, 5, weight=1) def connect_button_clicked(self): self.close() async def _listen(): try: self.pushbullet = AsyncPushbullet(self.key_var.get(), verify_ssl=False, proxy=self.proxy_var.get()) async with LiveStreamListener(self.pushbullet) as pl2: self.pushbullet_listener = pl2 await self.connected(pl2) async for push in pl2: await self.push_received(push, pl2) except Exception as ex: print("Exception:", ex) finally: await self.disconnected(self.pushbullet_listener) asyncio.run_coroutine_threadsafe(_listen(), self.ioloop) def create_io_loop(self): """Creates a new thread to manage an asyncio event loop specifically for IO to/from Pushbullet.""" assert self.ioloop is None # This should only ever be run once def _run(loop): asyncio.set_event_loop(loop) loop.run_forever() self.ioloop = asyncio.new_event_loop() self.ioloop.set_exception_handler(self._ioloop_exc_handler) threading.Thread(target=partial(_run, self.ioloop), name="Thread-asyncio", daemon=True).start() def _ioloop_exc_handler(self, loop: asyncio.BaseEventLoop, context: dict): if "exception" in context: self.status = context["exception"] self.status = str(context) # Handle this more robustly in real-world code def close(self): if self.pushbullet is not None: self.pushbullet.close_all_threadsafe() self.pushbullet = None if self.pushbullet_listener is not None: assert self.ioloop is not None pl = self.pushbullet_listener asyncio.run_coroutine_threadsafe(pl.close(), self.ioloop) self.pushbullet_listener = None def disconnect_button_clicked(self): self.close() async def connected(self, listener: LiveStreamListener): print("Connected to websocket") async def disconnected(self, listener: LiveStreamListener): print("Disconnected from websocket") async def push_received(self, p: dict, listener: LiveStreamListener): print("Push received:", p) prev = self.pushes_var.get() prev += "{}\n\n".format(p) self.pushes_var.set(prev)