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
Example #2
0
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)
Example #3
0
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)