├── asyncpushbullet ├── __version__.py ├── _compat.py ├── __init__.py ├── filetype.py ├── errors.py ├── subscription.py ├── helpers.py ├── tqio.py ├── chat.py ├── channel.py ├── listener.py ├── device.py ├── prefs.py ├── log_handler.py ├── ephemeral_comm.py ├── oauth2.py ├── websocket_server.py ├── websocket_client.py ├── async_listeners.py └── command_line_push.py ├── setup.cfg ├── tests ├── test.png ├── test_e2e.py ├── test_auth.py ├── test_filetypes.py ├── test_chats.py ├── test_channels.py └── test_devices.py ├── .gitignore ├── requirements.txt ├── changelog.md ├── .travis.yml ├── examples ├── push_example.py ├── exec_python_example.py ├── respond_to_listen_exec_simple.py ├── push_ephemeral.py ├── list_devices.py ├── upload_file_example.py ├── push_listener_example.py ├── custom_ephemeral_stream_example.py ├── basic_example.py ├── respond_to_listen_imagesnap.py ├── mirror_example.py ├── exec_python_imagesnap.py ├── tk_listener_example.py ├── tk_upload_example.py ├── retrieve_all_pushes_with_iterator_example.py ├── tk_asyncio_base.py ├── push_console.py ├── tkinter_tools.py └── guitoolapp.py ├── LICENSE └── setup.py /asyncpushbullet/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.19.4" 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /tests/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rharder/asyncpushbullet/HEAD/tests/test.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | pushbullet.py.egg-info/ 5 | asyncpushbullet.egg-info/ 6 | .env 7 | .idea 8 | api_key.txt 9 | venv/ 10 | credentials*.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ### 2 | # This file is only here to make sure that something like 3 | # 4 | # pip install -e . 5 | # 6 | # works as expected. Requirements can be found in setup.py. 7 | ### 8 | 9 | . 10 | -------------------------------------------------------------------------------- /asyncpushbullet/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | PY2 = sys.version_info[0] == 2 4 | 5 | if PY2: 6 | 7 | standard_b64encode = lambda x: x.encode("base64") 8 | 9 | else: 10 | 11 | from base64 import standard_b64encode 12 | 13 | __all__ = ['standard_b64encode'] 14 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 0.4.1 4 | 5 | - Fix installation, update the API url. 6 | 7 | - 0.4.0 8 | 9 | - API changes, direct access to the device object. 10 | 11 | - 0.3.0 12 | 13 | - Add list style access to devices. 14 | 15 | - 0.2.1 16 | 17 | - Updated documentation, no code changes. 18 | 19 | - 0.2.0 20 | 21 | - Add support for file uploads. 22 | 23 | - 0.1.1 24 | 25 | - Fix for error during installation. 26 | 27 | - 0.1.0 28 | 29 | - Initial version. 30 | -------------------------------------------------------------------------------- /tests/test_e2e.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import asyncpushbullet 5 | from binascii import a2b_base64 6 | 7 | API_KEY = os.environ["PUSHBULLET_API_KEY"] 8 | 9 | def test_decryption(): 10 | pb = asyncpushbullet.Pushbullet(API_KEY, encryption_password="hunter2") 11 | pb._encryption_key = a2b_base64("1sW28zp7CWv5TtGjlQpDHHG4Cbr9v36fG5o4f74LsKg=") 12 | 13 | test_data = "MSfJxxY5YdjttlfUkCaKA57qU9SuCN8+ZhYg/xieI+lDnQ==" 14 | decrypted = pb._decrypt_data(test_data) 15 | 16 | assert decrypted == "meow!" 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | #- '3.5' 4 | - '3.6' 5 | - '3.7' 6 | dist: xenial 7 | sudo: true 8 | 9 | before_install: 10 | - "pip install -U pip" 11 | - "python setup.py install" 12 | 13 | install: 14 | - pip install . 15 | - pip install coveralls 16 | - pip install cryptography 17 | - pip install requests 18 | - pip install aiohttp 19 | - pip install tqdm 20 | - pip install python-magic 21 | 22 | script: coverage run --source pushbullet -m py.test 23 | 24 | env: 25 | - PUSHBULLET_API_KEY=RrFnc1xaeQXnRrr2auoGA1e8pQ8MWmMF 26 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import asyncpushbullet 5 | 6 | API_KEY = os.environ["PUSHBULLET_API_KEY"] 7 | 8 | def test_auth_fail(): 9 | with pytest.raises(asyncpushbullet.InvalidKeyError) as exinfo: 10 | pb = asyncpushbullet.Pushbullet("faultykey") 11 | # pb.session # Triggers a connection 12 | pb.verify_key() 13 | 14 | 15 | def test_auth_success(): 16 | pb = asyncpushbullet.Pushbullet(API_KEY) 17 | _ = pb.get_user() 18 | assert pb._user_info["name"] == os.environ.get("PUSHBULLET_NAME", "Pushbullet Tester") 19 | -------------------------------------------------------------------------------- /tests/test_filetypes.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from asyncpushbullet import filetype 4 | 5 | 6 | class TestFiletypes: 7 | 8 | def test_mimetype(self): 9 | filename = 'tests/test.png' 10 | # with open(filename, "rb") as pic: 11 | output = filetype._magic_get_file_type(filename) 12 | assert output == ('image/png') 13 | 14 | def test_guess_file_type(self): 15 | import mimetypes 16 | filetype.mimetypes = mimetypes 17 | filename = 'tests/test.png' 18 | output = filetype._guess_file_type(filename) 19 | assert output == 'image/png' 20 | -------------------------------------------------------------------------------- /asyncpushbullet/__init__.py: -------------------------------------------------------------------------------- 1 | from .__version__ import __version__ 2 | 3 | from .errors import PushbulletError, InvalidKeyError, HttpError 4 | 5 | from .pushbullet import Pushbullet 6 | from .async_pushbullet import AsyncPushbullet 7 | from .async_listeners import LiveStreamListener 8 | from .log_handler import PushbulletLogHandler, AsyncPushbulletLogHandler 9 | 10 | from .device import Device 11 | from .channel import Channel 12 | from .chat import Chat 13 | from .subscription import Subscription 14 | from .ephemeral_comm import EphemeralComm 15 | 16 | from .oauth2 import gain_oauth2_access, get_oauth2_key, async_gain_oauth2_access 17 | -------------------------------------------------------------------------------- /asyncpushbullet/filetype.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def _magic_get_file_type(filename): 4 | with open(filename, "rb") as f: 5 | file_type = magic.from_buffer(f.read(1024), mime=True) 6 | return maybe_decode(file_type) 7 | 8 | 9 | def _guess_file_type(filename): 10 | return mimetypes.guess_type(filename)[0] 11 | 12 | 13 | # return str on python3. Don't want to unconditionally 14 | # decode because that results in unicode on python2 15 | def maybe_decode(s): 16 | if str == bytes: 17 | return s.decode('utf-8') 18 | else: 19 | return s 20 | 21 | 22 | try: 23 | import magic 24 | except Exception: 25 | import mimetypes 26 | 27 | get_file_type = _guess_file_type 28 | else: 29 | get_file_type = _magic_get_file_type 30 | -------------------------------------------------------------------------------- /examples/push_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import pprint 5 | import sys 6 | 7 | import lorem 8 | 9 | sys.path.append("..") # Since examples are buried one level into source tree 10 | from asyncpushbullet import Pushbullet 11 | 12 | __author__ = "Robert Harder" 13 | __email__ = "rob@iharder.net" 14 | 15 | API_KEY = "" # YOUR API KEY 16 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 17 | 18 | 19 | def main(): 20 | pb = Pushbullet(API_KEY, proxy=PROXY) 21 | 22 | title = "Greetings" 23 | body = "Welcome to accessing Pushbullet with Python" 24 | body = lorem.sentence() 25 | resp = pb.push_note(title, body) 26 | print("Response", pprint.pformat(resp)) 27 | 28 | 29 | if __name__ == "__main__": 30 | if API_KEY == "": 31 | with open("../api_key.txt") as f: 32 | API_KEY = f.read().strip() 33 | main() 34 | -------------------------------------------------------------------------------- /examples/exec_python_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Example of how to have an executable action that is straight python code 4 | """ 5 | import asyncio 6 | from pprint import pprint 7 | 8 | from asyncpushbullet import AsyncPushbullet 9 | from asyncpushbullet.command_line_listen import ListenApp, Action 10 | 11 | 12 | 13 | # class MyAction(Action): 14 | 15 | # async def on_push(push: dict, app: ListenApp): 16 | async def on_push(push: dict, pb: AsyncPushbullet): 17 | print("title={}, body={}".format(push.get("title"), push.get("body")), flush=True) 18 | 19 | # raise Exception("FOO! {}".format(__name__)) 20 | 21 | await asyncio.sleep(1) 22 | 23 | if push.get("body", "").strip().lower() == "a": 24 | # pb.log.info("{} sending a note".format(__file__)) 25 | # pb = app.account 26 | p = await pb.async_push_note(title="Got an A!", body="foo") 27 | 28 | # await app.respond(title="my response", body="foo") 29 | # return p 30 | -------------------------------------------------------------------------------- /examples/respond_to_listen_exec_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Experimental script that responds to a ListenApp exec command. 5 | """ 6 | import io 7 | import json 8 | import os 9 | import shutil 10 | import subprocess 11 | import sys 12 | import tempfile 13 | 14 | sys.path.append("..") # Since examples are buried one level into source tree 15 | from asyncpushbullet import Pushbullet, AsyncPushbullet 16 | 17 | ENCODING = "utf-8" 18 | API_KEY = "" # YOUR API KEY 19 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 20 | 21 | 22 | def main(): 23 | with io.open(sys.stdin.fileno(), mode="r", encoding="utf-8") as f: 24 | stdin = f.readlines() 25 | del f 26 | 27 | title = stdin[0].strip() 28 | body = "\n".join(stdin[1:]) 29 | 30 | print("Title was: {}".format(title)) 31 | print("Body was: {}".format(body)) 32 | 33 | 34 | if __name__ == "__main__": 35 | 36 | if API_KEY == "": 37 | with open("../api_key.txt") as f: 38 | API_KEY = f.read().strip() 39 | 40 | main() 41 | -------------------------------------------------------------------------------- /tests/test_chats.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import time 4 | 5 | import mock 6 | 7 | from asyncpushbullet import chat 8 | 9 | 10 | class TestChats: 11 | 12 | def setup_class(self): 13 | self.contact_email = "test.chat@example.com" 14 | chat_info = { 15 | "active": True, "created": time.time(), "modified": time.time(), 16 | "with": { 17 | "name": "test chat", 18 | "status": "user", "email": self.contact_email, 19 | "email_normalized": "testcontact@example.com"}} 20 | 21 | self.account = mock.Mock(return_value=True) 22 | self.chat = chat.Chat(self.account, chat_info) 23 | 24 | def test_encoding_support(self): 25 | # We're not actually intersted in the output, just that it doesn't 26 | # cause any errors. 27 | print(self.chat) 28 | 29 | def test_push(self): 30 | data = {"title": "test title", "muted": True} 31 | self.chat._push(data) 32 | pushed_data = {"title": "test title", "email": self.contact_email, "muted": True} 33 | self.account._push.assert_called_with(pushed_data) 34 | -------------------------------------------------------------------------------- /asyncpushbullet/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Exit codes used on command line 4 | __EXIT_NO_ERROR__ = 0 5 | __ERR_API_KEY_NOT_GIVEN__ = 1 6 | __ERR_INVALID_API_KEY__ = 2 7 | __ERR_CONNECTING_TO_PB__ = 3 8 | __ERR_FILE_NOT_FOUND__ = 4 9 | __ERR_DEVICE_NOT_FOUND__ = 5 10 | __ERR_NOTHING_TO_DO__ = 6 11 | __ERR_KEYBOARD_INTERRUPT__ = 7 12 | __ERR_UNKNOWN__ = 99 13 | 14 | 15 | class PushbulletError(Exception): 16 | 17 | def __str__(self): 18 | newargs = [] 19 | for arg in self.args: 20 | if isinstance(arg, BaseException): 21 | newargs += [arg.__class__.__name__, str(arg)] 22 | else: 23 | newargs.append(str(arg)) 24 | 25 | s = "{}: {}".format(self.__class__.__name__, " ".join(newargs)) 26 | return s 27 | 28 | 29 | class HttpError(PushbulletError): 30 | 31 | def __init__(self, code, err_msg, pushbullet_msg, *kargs, **kwargs): 32 | super().__init__(code, err_msg, pushbullet_msg, *kargs, **kwargs) 33 | self.code = code 34 | self.err_msg = err_msg 35 | self.pushbullet_msg = pushbullet_msg 36 | 37 | 38 | class InvalidKeyError(HttpError): 39 | pass 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright for portions of project async_pushbullet are held by 2 | Richard Borcsik 2013 as part of project pushbullet.py. 3 | All other copyright for project async_pushbullet are held by 4 | Robert Harder 2017. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 7 | and associated documentation files (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies 13 | or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 16 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/push_ephemeral.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import os 5 | import sys 6 | import traceback 7 | 8 | sys.path.append("..") # Since examples are buried one level into source tree 9 | from asyncpushbullet import AsyncPushbullet 10 | 11 | __author__ = "Robert Harder" 12 | __email__ = "rob@iharder.net" 13 | 14 | API_KEY = "" # YOUR API KEY 15 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 16 | 17 | 18 | def main(): 19 | # pb = AsyncPushbullet(API_KEY, proxy=PROXY) 20 | 21 | msg = {"foo": "bar", 42: "23", "type": "synchronous_example", "a_list_of_none":[None]} 22 | # pb.push_ephemeral(msg) # Synchronous IO 23 | 24 | async def _run(): 25 | try: 26 | async with AsyncPushbullet(API_KEY, proxy=PROXY) as pb: 27 | msg["type"] = "asynchronous_example" 28 | await pb.async_push_ephemeral(msg) # Asynchronous IO 29 | 30 | # await pb.async_close() 31 | except Exception as ex: 32 | print("ERROR:", ex, file=sys.stderr, flush=True) 33 | traceback.print_tb(sys.exc_info()[2]) 34 | 35 | loop = asyncio.get_event_loop() 36 | loop.run_until_complete(_run()) 37 | 38 | 39 | if __name__ == "__main__": 40 | if API_KEY == "": 41 | with open("../api_key.txt") as f: 42 | API_KEY = f.read().strip() 43 | main() 44 | -------------------------------------------------------------------------------- /examples/list_devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import logging 5 | import os 6 | import pprint 7 | import sys 8 | import traceback 9 | 10 | sys.path.append("..") # Since examples are buried one level into source tree 11 | from asyncpushbullet import AsyncPushbullet 12 | 13 | __author__ = "Robert Harder" 14 | __email__ = "rob@iharder.net" 15 | 16 | API_KEY = "" # YOUR API KEY 17 | 18 | 19 | # logging.basicConfig(level=logging.DEBUG) 20 | 21 | def main(): 22 | proxy = os.environ.get("https_proxy") or os.environ.get("http_proxy") 23 | pb = AsyncPushbullet(API_KEY, proxy=proxy) 24 | 25 | async def _run(): 26 | devices = await pb.async_get_devices() 27 | print("Devices:") 28 | for d in devices: 29 | print("\t", d.nickname) 30 | 31 | # Name of a device? 32 | if devices: 33 | name = devices[0].nickname 34 | this_device = await pb.async_get_device(nickname=name) 35 | print("Retrieved device by it's name '{}': {}".format(name, this_device)) 36 | 37 | # Do we have a device named foobar? Returns None if not found. 38 | name = "foobar" 39 | this_device = await pb.async_get_device(nickname=name) 40 | print("Retrieved device by it's name '{}': {}".format(name, this_device)) 41 | 42 | await pb.async_close() 43 | 44 | loop = asyncio.get_event_loop() 45 | loop.run_until_complete(_run()) 46 | 47 | 48 | if __name__ == "__main__": 49 | if API_KEY == "": 50 | with open("../api_key.txt") as f: 51 | API_KEY = f.read().strip() 52 | main() 53 | -------------------------------------------------------------------------------- /asyncpushbullet/subscription.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pprint 6 | 7 | from .channel import Channel 8 | from .helpers import use_appropriate_encoding, reify 9 | 10 | 11 | class Subscription: 12 | SUBSCRIPTION_ATTRIBUTES = ("iden", "active", "created", "modified", "muted") 13 | 14 | def __init__(self, account, subscription_info): 15 | self._account = account 16 | self.subscription_info = subscription_info 17 | self.channel = Channel(account, subscription_info.get("channel")) # type: Channel 18 | 19 | # Transfer attributes 20 | for attr in self.SUBSCRIPTION_ATTRIBUTES: 21 | setattr(self, attr, subscription_info.get(attr)) 22 | 23 | 24 | @use_appropriate_encoding 25 | def __str__(self): 26 | return "Subscription({})".format(str(self.channel)) 27 | 28 | @use_appropriate_encoding 29 | def __repr__(self): 30 | attr_map = {k: self.__getattribute__(k) for k in self.SUBSCRIPTION_ATTRIBUTES} 31 | attr_str = pprint.pformat(attr_map) 32 | _str = "Subscription({}".format(repr(self.channel)) 33 | _str += ",\n{}".format(attr_str) 34 | return _str 35 | 36 | @reify 37 | def iden(self): 38 | return getattr(self, "iden") 39 | 40 | @reify 41 | def active(self): 42 | return getattr(self, "active") 43 | 44 | @reify 45 | def created(self): 46 | return getattr(self, "created") 47 | 48 | @reify 49 | def modified(self): 50 | return getattr(self, "modified") 51 | 52 | @reify 53 | def muted(self): 54 | return getattr(self, "muted") 55 | -------------------------------------------------------------------------------- /asyncpushbullet/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import sys 6 | from functools import update_wrapper 7 | 8 | import aiohttp 9 | 10 | 11 | def use_appropriate_encoding(fn): 12 | if sys.version_info[0] < 3: 13 | def _fn(*args, **kwargs): 14 | return fn(*args, **kwargs).encode(sys.stdout.encoding or 'utf-8') 15 | 16 | return _fn 17 | else: 18 | return fn 19 | 20 | 21 | def print_function_name(enclosing_class=None): 22 | try: 23 | if enclosing_class is None: 24 | classname = "" 25 | elif isinstance(enclosing_class, str): 26 | classname = enclosing_class 27 | else: 28 | classname = enclosing_class.__class__.__name__ 29 | import inspect 30 | name = inspect.getframeinfo(inspect.currentframe().f_back).function 31 | print('\033[94m{}.{}\033[99m'.format(classname, name), flush=True) 32 | except AttributeError as ae: 33 | raise ae 34 | pass # Likely caused by lack of stack frame support where currentframe() returns None. 35 | except KeyError as ke: 36 | raise ke 37 | pass # In case the function name is not found in the globals dictionary. 38 | 39 | class reify(): 40 | """ 41 | From https://github.com/Pylons/pyramid and their BSD-style license 42 | """ 43 | def __init__(self, wrapped): 44 | self.wrapped = wrapped 45 | update_wrapper(self, wrapped) 46 | 47 | def __get__(self, inst, objtype=None): 48 | if inst is None: 49 | return self 50 | val = self.wrapped(inst) 51 | setattr(inst, self.wrapped.__name__, val) 52 | return val 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/upload_file_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Demonstrates how to upload and push a file. 5 | """ 6 | import asyncio 7 | import os 8 | import sys 9 | 10 | import logging 11 | 12 | sys.path.append("..") # Since examples are buried one level into source tree 13 | from asyncpushbullet import AsyncPushbullet 14 | 15 | __author__ = 'Robert Harder' 16 | __email__ = "rob@iharder.net" 17 | 18 | API_KEY = "" # YOUR API KEY 19 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 20 | 21 | 22 | def main(): 23 | """ Uses a callback scheduled on an event loop""" 24 | 25 | pb = AsyncPushbullet(API_KEY, verify_ssl=False, proxy=PROXY) 26 | loop = asyncio.get_event_loop() 27 | loop.run_until_complete(upload_file(pb, __file__)) # Upload this source code file as an example 28 | 29 | 30 | async def upload_file(pb: AsyncPushbullet, filename: str): 31 | 32 | # This is the actual upload command 33 | # info = pb.upload_file_to_transfer_sh(filename) # Synchro to pushbullet 34 | info = await pb.async_upload_file(filename) # Async to pushbullet 35 | # info = await pb.async_upload_file_to_transfer_sh(filename) # Async via transfer.sh 36 | 37 | # Push a notification of the upload "as a file": 38 | await pb.async_push_file(info["file_name"], info["file_url"], info["file_type"], 39 | title="File Arrived!", body="Please enjoy your file") 40 | 41 | # Also push a notification of the upload "as a link": 42 | await pb.async_push_link("Link to File Arrived!", info["file_url"], body="Please enjoy your file") 43 | 44 | await pb.async_close() 45 | 46 | 47 | if __name__ == '__main__': 48 | if API_KEY == "": 49 | with open("../api_key.txt") as f: 50 | API_KEY = f.read().strip() 51 | try: 52 | main() 53 | except KeyboardInterrupt: 54 | print("Quitting") 55 | pass 56 | -------------------------------------------------------------------------------- /asyncpushbullet/tqio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Show upload progress using tqdm and aiohttp. 4 | Example in tqio_upload_progress.py. 5 | 6 | PasteBin: http://pastebin.com/ksEfNJZN 7 | Source: https://github.com/rharder/handy 8 | """ 9 | import io 10 | import os 11 | import time 12 | 13 | from tqdm import tqdm # pip install tqdm 14 | 15 | __author__ = "Robert Harder" 16 | __email__ = "rob@iharder.net" 17 | __license__ = "Public Domain" 18 | 19 | 20 | class tqio(io.BufferedReader): 21 | SLOW_DELAY = 0.1 22 | 23 | def __init__(self, file_path, descr=None, slow_it_down=False): 24 | super().__init__(open(file_path, "rb")) 25 | print(end="", flush=True) # Flush output buffer to help tqdm 26 | self.t = tqdm(desc=descr, 27 | unit="bytes", 28 | unit_scale=True, 29 | total=os.path.getsize(file_path)) 30 | 31 | # Artificially slow down transfer for illustration 32 | self.slow_it_down = slow_it_down 33 | 34 | def __enter__(self): 35 | print(end="", flush=True) # Flush output buffer to help tqdm 36 | return self 37 | 38 | def __exit__(self, exc_type, exc_value, traceback): 39 | self.close() 40 | 41 | def read(self, *args, **kwargs): 42 | if self.slow_it_down: 43 | chunk = super().read(64) 44 | self.t.update(len(chunk)) 45 | time.sleep(self.SLOW_DELAY) 46 | return chunk 47 | else: 48 | # Keep these three lines after getting 49 | # rid of slow-it-down code for illustration. 50 | chunk = super().read(*args, **kwargs) 51 | self.t.update(len(chunk)) 52 | return chunk 53 | 54 | 55 | def readline(self, *args, **kwargs): 56 | line = super().readline(*args, **kwargs) 57 | self.t.update(len(line)) 58 | if self.slow_it_down: 59 | time.sleep(self.SLOW_DELAY) 60 | return line 61 | 62 | def close(self, *args, **kwargs): 63 | self.t.close() 64 | super().close() 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/push_listener_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Demonstrates how to consume new pushes in an asyncio for loop. 5 | """ 6 | import asyncio 7 | import logging 8 | import os 9 | import pprint 10 | import sys 11 | 12 | sys.path.append("..") # Since examples are buried one level into source tree 13 | from asyncpushbullet import AsyncPushbullet, oauth2 14 | from asyncpushbullet.async_listeners import LiveStreamListener 15 | 16 | __author__ = 'Robert Harder' 17 | __email__ = "rob@iharder.net" 18 | 19 | API_KEY = "" # YOUR API KEY 20 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 21 | 22 | 23 | # logging.basicConfig(level=logging.DEBUG) 24 | 25 | def main(): 26 | async def _run(): 27 | print("Connectiong to Pushbullet...", end="", flush=True) 28 | try: 29 | types_try_this = None 30 | # types_try_this=("push",) # Default 31 | # types_try_this=("tickle") 32 | # types_try_this=("tickle", "push", "ephemeral") 33 | # types_try_this=("nop",) 34 | types_try_this = () # Everything 35 | async with AsyncPushbullet(api_key=API_KEY, proxy=PROXY) as pb: 36 | async with LiveStreamListener(pb, types=types_try_this) as lsl: 37 | print("Connected.", flush=True) 38 | 39 | # Wait indefinitely for pushes and other notifications 40 | async for item in lsl: 41 | print("Live stream item:", pprint.pformat(item)) 42 | 43 | except Exception as ex: 44 | print("_run() exception:", ex) 45 | 46 | print("Disconnected.", flush=True) 47 | # await asyncio.sleep(10) 48 | 49 | loop = asyncio.get_event_loop() 50 | loop.run_until_complete(_run()) 51 | 52 | 53 | if __name__ == '__main__': 54 | API_KEY = oauth2.get_oauth2_key() 55 | if not API_KEY: 56 | print("Reading API key from file") 57 | with open("../api_key.txt") as f: 58 | API_KEY = f.read().strip() 59 | 60 | try: 61 | main() 62 | except KeyboardInterrupt: 63 | print("Quitting") 64 | pass 65 | -------------------------------------------------------------------------------- /examples/custom_ephemeral_stream_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import os 5 | import sys 6 | import traceback 7 | from asyncio import futures 8 | from collections import namedtuple 9 | from typing import TypeVar, Generic, AsyncIterator 10 | 11 | from asyncpushbullet.ephemeral_comm import EphemeralComm 12 | 13 | sys.path.append("..") # Since examples are buried one level into source tree 14 | from asyncpushbullet import AsyncPushbullet, LiveStreamListener 15 | 16 | __author__ = "Robert Harder" 17 | __email__ = "rob@iharder.net" 18 | 19 | API_KEY = "" # YOUR API KEY 20 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 21 | 22 | Msg = namedtuple("Msg", ["title", "body"], defaults=(None, None)) 23 | 24 | 25 | def main(): 26 | q = asyncio.Queue() 27 | 28 | async def _listen(): 29 | try: 30 | async with AsyncPushbullet(API_KEY, proxy=PROXY) as pb: 31 | async with EphemeralComm(pb, Msg) as ec: # type: EphemeralComm[Msg] 32 | await q.put(ec) 33 | print(await ec.next(.5)) 34 | print(await ec.next(.5)) 35 | print(await ec.next(.5)) 36 | await ec.close() 37 | 38 | except Exception as ex: 39 | print("ERROR:", type(ex), ex, file=sys.stderr, flush=True) 40 | traceback.print_tb(sys.exc_info()[2]) 41 | finally: 42 | print("AsyncPushbullet disconnected.", flush=True) 43 | 44 | async def _send_stuff(): 45 | ec: EphemeralComm = await q.get() 46 | await asyncio.sleep(1) 47 | # msg = {"type": "mystuff"} 48 | # msg["what else do I want to say"] = "Just add extra keys" 49 | # msg = MyStuff(data={"what else do I want to say": "Just add extra keys"}) 50 | # msg = {"type":"mystuff", "what else do I want to say": "Just add extra keys"} 51 | msg = Msg(title="mytitle", body="my body") 52 | try: 53 | await ec.send(msg) 54 | except Exception as ex: 55 | print("ERROR:", type(ex), ex, file=sys.stderr, flush=True) 56 | traceback.print_tb(sys.exc_info()[2]) 57 | 58 | loop = asyncio.get_event_loop() 59 | loop.create_task(_send_stuff()) 60 | loop.run_until_complete(_listen()) 61 | 62 | 63 | if __name__ == "__main__": 64 | if API_KEY == "": 65 | with open("../api_key.txt") as f: 66 | API_KEY = f.read().strip() 67 | main() 68 | -------------------------------------------------------------------------------- /examples/basic_example.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | A basic, complete example of using AsyncPushbullet to interact with the Pushbullet.com service. 5 | """ 6 | import asyncio 7 | import os 8 | import sys 9 | import traceback 10 | 11 | sys.path.append("..") # Since examples are buried one level into source tree 12 | from asyncpushbullet import AsyncPushbullet, InvalidKeyError, PushbulletError, LiveStreamListener 13 | 14 | API_KEY = "" # YOUR API KEY 15 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 16 | EXIT_INVALID_KEY = 1 17 | EXIT_PUSHBULLET_ERROR = 2 18 | EXIT_OTHER = 3 19 | 20 | 21 | def main(): 22 | async def _run(): 23 | try: 24 | async with AsyncPushbullet(API_KEY, proxy=PROXY) as pb: 25 | 26 | # List devices 27 | devices = await pb.async_get_devices() 28 | print("Devices:") 29 | for dev in devices: 30 | print("\t", dev) 31 | 32 | # Send a push 33 | push = await pb.async_push_note(title="Success", body="I did it!") 34 | print("Push sent:", push) 35 | 36 | # Ways to listen for pushes 37 | async with LiveStreamListener(pb) as lsl: 38 | # This will retrieve the previous push because it occurred 39 | # after the enclosing AsyncPushbullet connection was made 40 | push = await lsl.next_push() 41 | print("Previous push, now received:", push) 42 | 43 | # Alternately get pushes with a 3 second inter-push timeout 44 | print("Awaiting pushes with 3 second inter-push timeout...") 45 | async for push in lsl.timeout(3): 46 | print("Push received:", push) 47 | 48 | # Alternately get pushes forever 49 | print("Awaiting pushes forever...") 50 | async for push in lsl: 51 | print("Push received:", push) 52 | 53 | except InvalidKeyError as ke: 54 | print(ke, file=sys.stderr) 55 | return EXIT_INVALID_KEY 56 | 57 | except PushbulletError as pe: 58 | print(pe, file=sys.stderr) 59 | return EXIT_PUSHBULLET_ERROR 60 | 61 | except Exception as ex: 62 | print(ex, file=sys.stderr) 63 | traceback.print_tb(sys.exc_info()[2]) 64 | return EXIT_OTHER 65 | 66 | loop = asyncio.get_event_loop() 67 | return loop.run_until_complete(_run()) 68 | 69 | 70 | if __name__ == "__main__": 71 | if API_KEY == "": 72 | try: 73 | with open("../api_key.txt") as f: 74 | API_KEY = f.read().strip() 75 | except Exception: 76 | pass 77 | sys.exit(main()) 78 | -------------------------------------------------------------------------------- /asyncpushbullet/chat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pprint 6 | from typing import Dict 7 | 8 | # from asyncpushbullet import Pushbullet 9 | from .helpers import use_appropriate_encoding, reify 10 | 11 | 12 | class Chat: 13 | CHAT_ATTRIBUTES = ("iden", "active", "created", "modified", "muted", "with") 14 | CHAT_WITH_ATTRIBUTES = ("email", "email_normalized", "iden", "image_url", "type", "name") 15 | 16 | def __init__(self, account, chat_info): 17 | self._account = account 18 | self.chat_info = chat_info # type: Dict 19 | self.with_info = chat_info.get("with", dict()) # type: Dict 20 | 21 | # Transfer attributes 22 | for attr in self.CHAT_ATTRIBUTES: 23 | setattr(self, attr, chat_info.get(attr)) 24 | 25 | # Transfer attributes of "with" ie, the contact on the other end 26 | for attr in self.CHAT_WITH_ATTRIBUTES: 27 | attr_name = "with_{}".format(attr) 28 | setattr(self, attr_name, self.with_info.get(attr)) 29 | 30 | def _push(self, data): 31 | data["email"] = self.with_email 32 | return self._account._push(data) 33 | 34 | @use_appropriate_encoding 35 | def __str__(self): 36 | return "Chat('{0}' <{1}>)".format(self.with_name, self.with_email_normalized) 37 | 38 | 39 | @use_appropriate_encoding 40 | def __repr__(self): 41 | attr_map = {k: self.__getattribute__(k) for k in self.CHAT_ATTRIBUTES} 42 | attr_str = pprint.pformat(attr_map) 43 | _str = "Chat('{}' <{}> :\n{})".format(self.with_name, self.with_email_normalized, attr_str) 44 | # _str = "Chat('{}',\n{})".format(self.nickname or "nameless (iden: {})" 45 | # .format(self.iden), attr_str) 46 | return _str 47 | 48 | @reify 49 | def iden(self): 50 | return getattr(self, "iden") 51 | 52 | @reify 53 | def active(self): 54 | return getattr(self, "active") 55 | 56 | @reify 57 | def created(self): 58 | return getattr(self, "created") 59 | 60 | @reify 61 | def modified(self): 62 | return getattr(self, "modified") 63 | 64 | @reify 65 | def muted(self): 66 | return getattr(self, "muted") 67 | 68 | @reify 69 | def with_email(self): 70 | return getattr(self, "with_email") 71 | 72 | @reify 73 | def with_email_normalized(self): 74 | return getattr(self, "with_email_normalized") 75 | 76 | @reify 77 | def with_iden(self): 78 | return getattr(self, "with_iden") 79 | 80 | @reify 81 | def with_image_url(self): 82 | return getattr(self, "with_image_url") 83 | 84 | @reify 85 | def with_type(self): 86 | return getattr(self, "with_type") 87 | 88 | @reify 89 | def with_name(self): 90 | return getattr(self, "with_name") 91 | -------------------------------------------------------------------------------- /tests/test_channels.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import mock 4 | import time 5 | 6 | from asyncpushbullet import channel 7 | 8 | 9 | class TestChannels: 10 | 11 | @classmethod 12 | def setup_class(cls): 13 | cls.tag = "test_tag" 14 | channel_info = {'iden': "test_iden", 'name': 'test channel', 'created': time.time(), 'modified': time.time(), 'tag': cls.tag, 'active': True} 15 | cls.account = mock.Mock(return_value=True) 16 | cls.channel = channel.Channel(cls.account, channel_info) 17 | 18 | def test_encoding_support(self): 19 | # We're not actually intersted in the output, just that it doesn't 20 | # cause any errors. 21 | print(self.channel) 22 | 23 | # def test_repr(self): 24 | # assert repr(self.channel) == "Channel(name: 'test channel' tag: '%s')" % self.tag 25 | 26 | def test_push_note(self): 27 | title = "test title" 28 | body = "test body" 29 | self.channel.push_note(title, body) 30 | pushed_data = {"type": "note", "title": title, "body": body, "channel_tag": self.tag} 31 | self.account._push.assert_called_with(pushed_data) 32 | 33 | # def test_push_address(self): 34 | # name = "test name" 35 | # address = "test address" 36 | # self.channel.push_address(name, address) 37 | # pushed_data = {"type": "note", "title": name, "body": address, "channel_tag": self.tag} 38 | # self.account._push.assert_called_with(pushed_data) 39 | 40 | # def test_push_list(self): 41 | # title = "test title" 42 | # items = ["test item 1", "test item 2"] 43 | # self.channel.push_list(title, items) 44 | # pushed_data = {"type": "note", "title": title, "body": ",".join(items), "channel_tag": self.tag} 45 | # self.account._push.assert_called_with(pushed_data) 46 | 47 | def test_push_link(self): 48 | title = "test title" 49 | url = "http://test.url" 50 | body = "test body" 51 | self.channel.push_link(title, url, body) 52 | pushed_data = {"type": "link", "title": title, "url": url, "body": body, "channel_tag": self.tag} 53 | self.account._push.assert_called_with(pushed_data) 54 | 55 | def test_push_file(self): 56 | file_name = "test_file.name" 57 | file_url = "http://file.url" 58 | file_type = "test/type" 59 | body = "test body" 60 | title = "test title" 61 | self.channel.push_file(file_name, file_url, file_type, body=body, title=title) 62 | self.account.push_file.assert_called_with(file_name, file_url, file_type, body=body, title=title, channel=self.channel) 63 | 64 | def test_push(self): 65 | data = {"title": "test title"} 66 | self.channel._push(data) 67 | pushed_data = {"title": "test title", "channel_tag": self.tag} 68 | self.account._push.assert_called_with(pushed_data) 69 | -------------------------------------------------------------------------------- /tests/test_devices.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import time 4 | 5 | import mock 6 | 7 | from asyncpushbullet import device 8 | 9 | 10 | class TestDevices: 11 | 12 | def setup_class(cls): 13 | cls.iden = "test_iden" 14 | device_info = {"active": True, "iden": cls.iden, "created": time.time(), "modified": time.time(), 15 | "icon": "system", "generated_nickname": False, "nickname": "test dev", "manufacturer": "test c", 16 | "model": "test m", "has_sms": False} 17 | cls.account = mock.Mock(return_value=True) 18 | cls.device = device.Device(cls.account, device_info) 19 | 20 | def test_encoding_support(self): 21 | # We're not actually intersted in the output, just that it doesn't 22 | # cause any errors. 23 | print(self.device) 24 | 25 | # def test_repr(self): 26 | # assert repr(self.device) == "Device('test dev')" 27 | 28 | def test_push_note(self): 29 | title = "test title" 30 | body = "test body" 31 | self.device.push_note(title, body) 32 | pushed_data = {"type": "note", "title": title, "body": body, "device_iden": self.iden} 33 | self.account._push.assert_called_with(pushed_data) 34 | 35 | # def test_push_address(self): 36 | # name = "test name" 37 | # address = "test address" 38 | # self.device.push_address(name, address) 39 | # pushed_data = {"type": "note", "title": name, "body": address, "device_iden": self.iden} 40 | # self.account._push.assert_called_with(pushed_data) 41 | 42 | # def test_push_list(self): 43 | # title = "test title" 44 | # items = ["test item 1", "test item 2"] 45 | # self.device.push_list(title, items) 46 | # pushed_data = {"type": "note", "title": title, "body": ",".join(items), "device_iden": self.iden} 47 | # self.account._push.assert_called_with(pushed_data) 48 | 49 | def test_push_link(self): 50 | title = "test title" 51 | url = "http://test.url" 52 | body = "test body" 53 | self.device.push_link(title, url, body) 54 | pushed_data = {"type": "link", "title": title, "url": url, "body": body, "device_iden": self.iden} 55 | self.account._push.assert_called_with(pushed_data) 56 | 57 | def test_push_file(self): 58 | file_name = "test_file.name" 59 | file_url = "http://file.url" 60 | file_type = "test/type" 61 | body = "test body" 62 | title = "test title" 63 | self.device.push_file(file_name, file_url, file_type, body=body, title=title) 64 | self.account.push_file.assert_called_with(file_name, file_url, file_type, title=title, body=body, 65 | device=self.device) 66 | 67 | def test_push(self): 68 | data = {"title": "test title"} 69 | self.device._push(data) 70 | pushed_data = {"title": "test title", "device_iden": self.iden} 71 | self.account._push.assert_called_with(pushed_data) 72 | -------------------------------------------------------------------------------- /asyncpushbullet/channel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pprint 6 | import warnings 7 | from typing import Dict 8 | 9 | # from asyncpushbullet import Pushbullet 10 | from .helpers import use_appropriate_encoding, reify 11 | 12 | 13 | class Channel: 14 | CHANNEL_ATTRIBUTES = ("name", "description", "created", "modified", 15 | "iden", "tag", "image_url", "website_url") 16 | 17 | def __init__(self, account, channel_info): 18 | self._account = account 19 | self.channel_info = channel_info # type: Dict 20 | 21 | for attr in self.CHANNEL_ATTRIBUTES: 22 | setattr(self, attr, channel_info.get(attr)) 23 | 24 | def push_note(self, title, body): 25 | data = {"type": "note", "title": title, "body": body} 26 | return self._push(data) 27 | 28 | # def push_address(self, name, address): 29 | # warnings.warn("Address push type is removed. This push will be sent as note.") 30 | # return self.push_note(name, address) 31 | # 32 | # def push_list(self, title, items): 33 | # warnings.warn("List push type is removed. This push will be sent as note.") 34 | # return self.push_note(title, ",".join(items)) 35 | 36 | def push_link(self, title, url, body=None): 37 | data = {"type": "link", "title": title, "url": url, "body": body} 38 | return self._push(data) 39 | 40 | def push_file(self, file_name, file_url, file_type, body=None, title=None): 41 | return self._account.push_file(file_name, file_url, file_type, body=body, title=title, channel=self) 42 | 43 | def _push(self, data): 44 | data["channel_tag"] = self.tag 45 | return self._account._push(data) 46 | 47 | @use_appropriate_encoding 48 | def __str__(self): 49 | _str = "Channel('{}', tag: '{}')".format(self.name or "nameless (iden: {})" 50 | .format(self.iden), self.tag) 51 | return _str 52 | 53 | def __repr__(self): 54 | attr_map = {k: self.__getattribute__(k) for k in self.CHANNEL_ATTRIBUTES} 55 | attr_str = pprint.pformat(attr_map) 56 | 57 | _str = "Channel('{}', tag: '{}'".format(self.name or "nameless (iden: {})" 58 | .format(self.iden), self.tag) 59 | 60 | _str += ",\n{})".format(attr_str) 61 | return _str 62 | 63 | @reify 64 | def iden(self): 65 | return getattr(self, "iden") 66 | 67 | @reify 68 | def tag(self): 69 | return getattr(self, "tag") 70 | 71 | @reify 72 | def name(self): 73 | return getattr(self, "name") 74 | 75 | @reify 76 | def description(self): 77 | return getattr(self, "description") 78 | 79 | @reify 80 | def created(self): 81 | return getattr(self, "created") 82 | 83 | @reify 84 | def modified(self): 85 | return getattr(self, "modified") 86 | 87 | @reify 88 | def image_url(self): 89 | return getattr(self, "image_url") 90 | 91 | @reify 92 | def website_url(self): 93 | return getattr(self, "website_url") 94 | -------------------------------------------------------------------------------- /asyncpushbullet/listener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Used with non-asyncio Pushbullet class. 3 | If you are not using the asyncio capabilities of this package, 4 | it is recommended that you stick with Igor's own package.""" 5 | 6 | __author__ = 'Igor Maculan ' 7 | 8 | import json 9 | import logging 10 | import time 11 | from threading import Thread 12 | 13 | import websocket # not sure which package this refers to. -RH 14 | 15 | log = logging.getLogger('pushbullet.Listener') 16 | 17 | WEBSOCKET_URL = 'wss://stream.pushbullet.com/websocket/' 18 | 19 | 20 | class Listener(Thread, websocket.WebSocketApp): 21 | def __init__(self, account, 22 | on_push=None, 23 | on_error=None, 24 | http_proxy_host=None, 25 | http_proxy_port=None): 26 | """ 27 | :param api_key: pushbullet Key 28 | :param on_push: function that get's called on all pushes 29 | :param http_proxy_host: host proxy (ie localhost) 30 | :param http_proxy_port: host port (ie 3128) 31 | """ 32 | self._account = account 33 | self._api_key = self._account.api_key 34 | self.on_error = on_error 35 | 36 | Thread.__init__(self) 37 | websocket.WebSocketApp.__init__(self, WEBSOCKET_URL + self._api_key, 38 | on_open=self.on_open, 39 | on_error=self.on_error, 40 | on_message=self.on_message, 41 | on_close=self.on_close) 42 | 43 | self.connected = False 44 | self.last_update = time.time() 45 | 46 | self.on_push = on_push 47 | 48 | # History 49 | self.history = None 50 | self.clean_history() 51 | 52 | # proxy configuration 53 | self.http_proxy_host = http_proxy_host 54 | self.http_proxy_port = http_proxy_port 55 | self.proxies = None 56 | if http_proxy_port is not None and http_proxy_port is not None: 57 | self.proxies = { 58 | "http": "http://" + http_proxy_host + ":" + str(http_proxy_port), 59 | "https": "http://" + http_proxy_host + ":" + str(http_proxy_port), 60 | } 61 | 62 | def clean_history(self): 63 | self.history = [] 64 | 65 | def on_open(self, ws): 66 | self.connected = True 67 | self.last_update = time.time() 68 | 69 | def on_close(self, ws): 70 | log.debug('Listener closed') 71 | self.connected = False 72 | 73 | def on_message(self, ws, message): 74 | log.debug('Message received:' + message) 75 | try: 76 | json_message = json.loads(message) 77 | if json_message["type"] != "nop": 78 | self.on_push(json_message) 79 | except Exception as e: 80 | logging.exception(e) 81 | 82 | def run_forever(self, sockopt=None, sslopt=None, ping_interval=0, ping_timeout=None): 83 | websocket.WebSocketApp.run_forever(self, sockopt=sockopt, sslopt=sslopt, ping_interval=ping_interval, 84 | ping_timeout=ping_timeout, 85 | http_proxy_host=self.http_proxy_host, 86 | http_proxy_port=self.http_proxy_port) 87 | 88 | def run(self): 89 | self.run_forever() 90 | -------------------------------------------------------------------------------- /examples/respond_to_listen_imagesnap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Experimental script that responds to a ListenApp exec command. 5 | """ 6 | import asyncio 7 | import json 8 | import os 9 | import subprocess 10 | import sys 11 | import tempfile 12 | 13 | sys.path.append("..") # Since examples are buried one level into source tree 14 | from asyncpushbullet import AsyncPushbullet 15 | 16 | ENCODING = "utf-8" 17 | API_KEY = "" # YOUR API KEY 18 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 19 | 20 | 21 | def main(): 22 | recvd_push = json.loads(sys.stdin.read()) # Throw whatever exceptions to stderr 23 | 24 | if recvd_push.get("body", "").lower().strip() == "imagesnap": 25 | 26 | # Temp file to house the image file 27 | temp_img = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") 28 | temp_img.close() 29 | 30 | try: 31 | 32 | # Take a picture and upload 33 | # PRETEND TO TAKE A PICTURE 34 | import shutil 35 | fakepic = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshot.jpg") 36 | shutil.copy(fakepic, temp_img.name) 37 | 38 | # Take a picture 39 | # proc = subprocess.run(["imagesnap", temp_img.name], 40 | proc = subprocess.run(["notepad.exe"], # Debugging 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | timeout=10, 44 | encoding=ENCODING) 45 | 46 | # Upload picture 47 | with AsyncPushbullet(API_KEY, proxy=PROXY) as pb: 48 | # resp = pb.upload_file(temp_img.name) # Upload here 49 | resp = pb.upload_file_to_transfer_sh(temp_img.name) # Upload here 50 | del pb 51 | 52 | file_type = resp.get("file_type") 53 | file_url = resp.get("file_url") 54 | file_name = resp.get("file_name") 55 | 56 | # Provide a response via stdout 57 | stdout_txt = proc.stdout 58 | stderr_txt = proc.stderr 59 | myresp = { 60 | "type": "file", 61 | "title": "Imagesnap", 62 | "body": "{}\n{}".format(stdout_txt, stderr_txt).strip(), 63 | "file_name": file_name, 64 | "file_type": file_type, 65 | "file_url": file_url, 66 | "received_push": recvd_push 67 | } 68 | dev_iden = recvd_push.get("source_device_iden") 69 | if dev_iden is not None: 70 | myresp["device_iden"] = dev_iden 71 | 72 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "response.json"), "w") as fout: 73 | fout.write(json.dumps(myresp, indent=4)) 74 | 75 | print(json.dumps(myresp), flush=True) 76 | 77 | except Exception as e: 78 | # raise e 79 | print("Error:", e, file=sys.stderr) 80 | 81 | finally: 82 | 83 | os.remove(temp_img.name) 84 | 85 | 86 | if __name__ == "__main__": 87 | 88 | if "PUSHBULLET_API_KEY" in os.environ: 89 | API_KEY = os.environ["PUSHBULLET_API_KEY"].strip() 90 | 91 | if API_KEY == "": 92 | with open("../api_key.txt") as f: 93 | API_KEY = f.read().strip() 94 | 95 | main() 96 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import shutil 4 | import sys 5 | 6 | from setuptools import setup 7 | # You may need to pip install twine 8 | 9 | with open("./asyncpushbullet/__version__.py") as version_file: 10 | version = version_file.read().split("\"")[1] 11 | 12 | # version += "a2" 13 | # sys.argv.append("install") 14 | 15 | print(f"Version {version}") 16 | 17 | if len(sys.argv) < 2: 18 | cmd = input("Command (build | test | publish | all ): ") 19 | sys.argv.append(cmd) 20 | 21 | if sys.argv[-1] in ('build', 'all'): 22 | print("BUILD") 23 | for path in ("./build", "./dist", "./asyncpushbullet.egg-info"): 24 | try: 25 | print("Removing {} ...".format(path), end="", flush=True) 26 | shutil.rmtree(path) 27 | except: 28 | print("Could not remove {}".format(path), flush=True) 29 | else: 30 | print("Removed.") 31 | 32 | try: 33 | # os.system('python3 setup.py sdist bdist_wheel') 34 | os.system('python3 setup.py sdist') 35 | except: 36 | print("Do you need to pip install wheel?", file=sys.stderr) 37 | # os.system('python3 setup.py sdist bdist_wheel') 38 | # sys.exit() 39 | 40 | if sys.argv[-1] in ('test', 'all'): 41 | print("TEST") 42 | try: 43 | os.system('twine upload -r pypitest dist/*') 44 | except: 45 | print("Do you need to pip install twine?", file=sys.stderr) 46 | # sys.exit() 47 | 48 | if sys.argv[-1] in ('publish', 'all'): 49 | print("PUBLISH") 50 | try: 51 | os.system('twine upload -r pypi dist/*') 52 | except: 53 | print("Do you need to pip install twine?", file=sys.stderr) 54 | # sys.exit() 55 | 56 | if sys.argv[-1] in ('build', 'test', 'publish', 'all'): 57 | sys.exit() 58 | 59 | install_reqs = [ 60 | "requests", 61 | "python-magic", 62 | "aiohttp", 63 | "tqdm", 64 | "appdirs" 65 | # , "pillow" 66 | ] 67 | 68 | 69 | def read(fname): 70 | try: 71 | with open(os.path.join(os.path.dirname(__file__), fname)) as f: 72 | return f.read() 73 | except IOError: 74 | return "" 75 | 76 | 77 | setup( 78 | name="asyncpushbullet", 79 | version=version, 80 | author="Robert Harder, Richard Borcsik", 81 | author_email="rob@iharder.net, borcsikrichard@gmail.com", 82 | description=("A synchronous and asyncio-based client for pushbullet.com"), 83 | license="MIT", 84 | keywords="push android pushbullet notification", 85 | url="https://github.com/rharder/asyncpushbullet", 86 | download_url="https://github.com/rharder/asyncpushbullet/tarball/" + version, 87 | packages=['asyncpushbullet'], 88 | long_description=read('readme.rst'), 89 | classifiers=[ 90 | "Development Status :: 4 - Beta", 91 | "Intended Audience :: Developers", 92 | "Programming Language :: Python", 93 | "Natural Language :: English", 94 | "License :: OSI Approved :: MIT License", 95 | "Programming Language :: Python :: 3", 96 | # "Programming Language :: Python :: 3.5", 97 | "Programming Language :: Python :: 3.6", 98 | "Programming Language :: Python :: 3.7", 99 | "Topic :: Software Development :: Libraries :: Python Modules", 100 | "Topic :: Utilities" 101 | ], 102 | install_requires=install_reqs, 103 | extras_require={ 104 | 'GUI': ["pillow"] 105 | }, 106 | entry_points={ 107 | "console_scripts": ["pbpush=asyncpushbullet.command_line_push:main_pbpush", 108 | "pbtransfer=asyncpushbullet.command_line_push:main_pbtransfer", 109 | "pblisten=asyncpushbullet.command_line_listen:main" 110 | ] 111 | } 112 | ) 113 | -------------------------------------------------------------------------------- /examples/mirror_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | A simple example showing how to mirror notifications. 5 | 6 | This example has not been verified with the asyncio-based code. 7 | """ 8 | 9 | import base64 10 | import hashlib 11 | import json 12 | import os 13 | import subprocess 14 | import sys 15 | import time 16 | 17 | from asyncpushbullet import Pushbullet 18 | from asyncpushbullet.listener import Listener 19 | 20 | 21 | class Mirrorer: 22 | 23 | def __init__(self, auth_key, temp_folder, device_name, last_push=time.time(), device_iden=None): 24 | self.temp_folder = temp_folder 25 | if not os.path.exists(self.temp_folder): 26 | os.makedirs(temp_folder) 27 | 28 | self._auth_key = auth_key 29 | self.pb = Pushbullet(self._auth_key) 30 | self.listener = Listener(self.pb, self.watcher) 31 | 32 | self.last_push = last_push 33 | 34 | self.device = None 35 | if device_iden: 36 | devices = self.pb.get_devices() 37 | results = [d for d in devices if d.iden == device_iden and d.active] 38 | self.device = results[0] if results else None 39 | 40 | if not self.device: 41 | try: 42 | device = self.pb.new_device(device_name) 43 | print("Created new device:", device_name, "iden:", device.iden) 44 | self.device = device 45 | except: 46 | print("Error: Unable to create device") 47 | raise 48 | 49 | self.check_pushes() 50 | 51 | def save_icon(self, b64_asset): 52 | hash = hashlib.md5(b64_asset.encode()).hexdigest() 53 | path = os.path.join(self.temp_folder, hash) 54 | if os.path.exists(path): 55 | return path 56 | else: 57 | decoded = base64.b64decode(b64_asset) 58 | with open(path, "wb") as image: 59 | image.write(decoded) 60 | return path 61 | 62 | def check_pushes(self): 63 | pushes = self.pb.get_pushes(self.last_push) 64 | for push in pushes: 65 | if not isinstance(push, dict): 66 | # not a push object 67 | continue 68 | if ((push.get("target_device_iden", self.device.iden) == self.device.iden) and not ( 69 | push.get("dismissed", True))): 70 | self.notify(push.get("title", ""), push.get("body", "")) 71 | self.pb.dismiss_push(push.get("iden")) 72 | self.last_push = max(self.last_push, push.get("created")) 73 | 74 | def watcher(self, push): 75 | if push["type"] == "push" and push["push"]["type"] == "mirror": 76 | print("MIRROR") 77 | image_path = self.save_icon(push["push"]["icon"]) 78 | self.notify(push["push"]["title"], 79 | push["push"]["body"], image_path) 80 | elif push["type"] == "tickle": 81 | print("TICKLE") 82 | self.check_pushes() 83 | 84 | def notify(self, title, body, image=None): 85 | subprocess.Popen(["notify-send", title, body, "-i", image or ""]) 86 | print(title) 87 | print(body) 88 | 89 | def dump_config(self, path): 90 | config = {"temp_folder": self.temp_folder, 91 | "auth_key": self._auth_key, 92 | "device_name": self.device.nickname, 93 | "device_iden": self.device.iden} 94 | with open(path, "w") as conf: 95 | json.dump(config, conf) 96 | 97 | def run(self): 98 | try: 99 | self.listener.run_forever() 100 | except KeyboardInterrupt: 101 | self.listener.close() 102 | 103 | 104 | def main(): 105 | config_file = sys.argv[1] 106 | with open(config_file) as conf: 107 | config = json.load(conf) 108 | 109 | m = Mirrorer(**config) 110 | m.run() 111 | m.dump_config(config_file) 112 | 113 | 114 | if __name__ == '__main__': 115 | main() 116 | -------------------------------------------------------------------------------- /asyncpushbullet/device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | import pprint 6 | from typing import Dict 7 | 8 | # from asyncpushbullet import Pushbullet 9 | from .helpers import use_appropriate_encoding, reify 10 | 11 | 12 | class Device: 13 | DEVICE_ATTRIBUTES = ("push_token", "app_version", "fingerprint", "created", "modified", 14 | "active", "nickname", "generated_nickname", "manufacturer", "icon", 15 | "model", "has_sms", "key_fingerprint", "iden") 16 | 17 | def __init__(self, account, device_info): 18 | self._account = account 19 | self.device_info = device_info # type: Dict 20 | 21 | if not device_info.get("icon", None): 22 | device_info["icon"] = "system" 23 | 24 | for attr in self.DEVICE_ATTRIBUTES: 25 | setattr(self, attr, device_info.get(attr)) 26 | 27 | def push_note(self, title, body): 28 | data = {"type": "note", "title": title, "body": body} 29 | return self._push(data) 30 | 31 | # def push_address(self, name, address): 32 | # warnings.warn("Address push type is removed. This push will be sent as note.") 33 | # return self.push_note(name, address) 34 | 35 | # def push_list(self, title, items): 36 | # warnings.warn("List push type is removed. This push will be sent as note.") 37 | # return self.push_note(title, ",".join(items)) 38 | 39 | def push_link(self, title, url, body=None): 40 | data = {"type": "link", "title": title, "url": url, "body": body} 41 | return self._push(data) 42 | 43 | def push_file(self, file_name, file_url, file_type, body=None, title=None): 44 | return self._account.push_file(file_name, file_url, file_type, body=body, title=title, device=self) 45 | 46 | def _push(self, data): 47 | data["device_iden"] = self.iden 48 | if getattr(self, "push_token") is not None: 49 | data["push_token"] = self.push_token 50 | print("Including push token {} in push coming from device {}".format(self.push_token, self)) 51 | else: 52 | print("Skipping push token") 53 | return self._account._push(data) 54 | 55 | @use_appropriate_encoding 56 | def __str__(self): 57 | _str = "Device('{}')".format(self.nickname or "nameless (iden: {})" 58 | .format(self.iden)) 59 | return _str 60 | 61 | @use_appropriate_encoding 62 | def __repr__(self): 63 | attr_map = {k: self.__getattribute__(k) for k in self.DEVICE_ATTRIBUTES} 64 | attr_str = pprint.pformat(attr_map) 65 | _str = str(self) + ",\n{})".format(attr_str) 66 | return _str 67 | 68 | @reify 69 | def iden(self): 70 | return getattr(self, "iden") 71 | 72 | @reify 73 | def push_token(self): 74 | return getattr(self, "push_token") 75 | 76 | @reify 77 | def app_version(self): 78 | return getattr(self, "app_version") 79 | 80 | @reify 81 | def fingerprint(self): 82 | return getattr(self, "fingerprint") 83 | 84 | @reify 85 | def created(self): 86 | return getattr(self, "created") 87 | 88 | @reify 89 | def modified(self): 90 | return getattr(self, "modified") 91 | 92 | @reify 93 | def active(self): 94 | return getattr(self, "active") 95 | 96 | @reify 97 | def nickname(self): 98 | return getattr(self, "nickname") 99 | 100 | @reify 101 | def generated_nickname(self): 102 | return getattr(self, "generated_nickname") 103 | 104 | @reify 105 | def manufacturer(self): 106 | return getattr(self, "manufacturer") 107 | 108 | @reify 109 | def icon(self): 110 | return getattr(self, "icon") 111 | 112 | @reify 113 | def model(self): 114 | return getattr(self, "model") 115 | 116 | @reify 117 | def has_sms(self): 118 | return getattr(self, "has_sms") 119 | 120 | @reify 121 | def key_fingerprint(self): 122 | return getattr(self, "key_fingerprint") 123 | -------------------------------------------------------------------------------- /asyncpushbullet/prefs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handles saving preferences between occurrences of running a script. 3 | Source: https://github.com/rharder/handy 4 | """ 5 | 6 | import os 7 | import shelve # For Prefs class 8 | import sys 9 | 10 | import appdirs # pip install appdirs 11 | 12 | 13 | class Prefs: 14 | """ 15 | Handles saving preferences between occurrences of running the script. 16 | """ 17 | 18 | def __init__(self, app_name, app_author): 19 | self.app_name = app_name 20 | self.app_author = app_author 21 | self.memory_backup = {} 22 | self.config_file = Prefs.__create_config_file(app_name, app_author) 23 | # print(self.config_file) 24 | 25 | def get(self, key, default=None): 26 | """ 27 | Returns a value from the saved preferences. 28 | 29 | Based on the string key, the value will be returned. You can pass a default value as a second parameter, 30 | which will be returned if a saved value cannot be found for that key. If no default is provided, None 31 | will be returned in such a case. 32 | :param str key: The key to look up 33 | :param default: Default value if key is not found 34 | :return: The saved value or the default if not found 35 | """ 36 | mem = self.__memory() 37 | val = default 38 | if key in mem: 39 | val = mem[key] 40 | return val 41 | 42 | def set(self, key, val): 43 | """ 44 | Saves a value in the preferences. 45 | 46 | Based on the string key, the value will be saved in the preferences. The function will return the 47 | value as well, which can help with embedding the set function in a larger expression. 48 | :param str key: The key to save under 49 | :param val: The value to save 50 | :return: The value 51 | """ 52 | mem = self.__memory() 53 | mem[key] = val 54 | try: 55 | mem.close() 56 | except: 57 | # print("error closing") 58 | pass 59 | return val 60 | 61 | def __memory(self): 62 | """ 63 | Returns a dictionary-like object for reading/writing preferences. 64 | 65 | If possible this will be a saved prefs representation from the 'shelve' module, but it will return a 66 | regular dictionary if that fails. If the regular dictionary is returned, then this Prefs class will work 67 | during the current runtime, but nothing will be saved. 68 | :return: A dictionary-like object for reading/writing preferences. 69 | """ 70 | try: 71 | return shelve.open(self.config_file) 72 | except Exception as e: 73 | print(self.__class__.__name__, e) 74 | return self.memory_backup 75 | 76 | @staticmethod 77 | def __create_config_file(app_name, app_author): 78 | """ 79 | Tries to find a folder and file to save preferences. 80 | 81 | :param app_name: Author of the app 82 | :param app_author: Name of the app 83 | :return: A config file to use for saving prefs or None if unable 84 | """ 85 | config_file = None 86 | # Try the platform-appropriate preferences folder 87 | config_dir = appdirs.user_config_dir(app_name, app_author) 88 | if not os.path.isdir(config_dir): 89 | os.makedirs(config_dir) 90 | config_file = os.path.join(config_dir, "config") 91 | 92 | # Try users home directory 93 | if config_file is None: 94 | home_dir = os.path.expanduser("~") 95 | config_dir = os.path.join(home_dir, ".{}".format(app_name)) 96 | if not os.path.isdir(config_dir): 97 | try: 98 | os.makedirs(config_dir) 99 | config_file = os.path.join(config_dir, "config") 100 | except: 101 | print("Could not make preferences folder at {}".format( 102 | config_dir), file=sys.stderr) 103 | 104 | if config_file is None: 105 | print("Could not make a preferences folder. Settings will not be saved.", file=sys.stderr) 106 | 107 | return config_file 108 | # end class Prefs 109 | 110 | -------------------------------------------------------------------------------- /examples/exec_python_imagesnap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Experimental script that responds to a ListenApp exec command. 5 | """ 6 | import asyncio 7 | import json 8 | import os 9 | import subprocess 10 | import sys 11 | import tempfile 12 | 13 | # sys.path.append("..") # Since examples are buried one level into source tree 14 | import traceback 15 | 16 | from asyncpushbullet import AsyncPushbullet 17 | 18 | ENCODING = "utf-8" 19 | 20 | 21 | async def on_push(recvd_push: dict, pb: AsyncPushbullet): 22 | 23 | # await asyncio.sleep(99) 24 | if recvd_push.get("body", "").lower().strip() == "imagesnap": 25 | 26 | # Temp file to house the image file 27 | temp_img = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") 28 | temp_img.close() 29 | 30 | try: 31 | # Take a picture and upload 32 | # PRETEND TO TAKE A PICTURE DURING DEBUGGING 33 | # import shutil 34 | # fakepic = os.path.join(os.path.dirname(os.path.abspath(__file__)), "snapshot.jpg") 35 | # shutil.copy(fakepic, temp_img.name) 36 | 37 | stdout_txt = None # type: str 38 | stderr_txt = None # type: str 39 | 40 | cmd_path = "imagesnap" 41 | # cmd_path = "notepad.exe" 42 | # cmd_path = "clip.exe" 43 | cmd_args = [temp_img.name] 44 | 45 | if sys.platform == "win32": 46 | 47 | # Using subprocess.run hangs up the event thread, but if we're 48 | # running on Windows, we cannot launch subprocesses on a Selector loop, 49 | # only a Proactor loop. The end result is that we cannot do asyncio 50 | # subprocesses on the loop where this function is called, and we end 51 | # up blocking the loop waiting for the process to end. 52 | # Also subprocess.run will ignore a timeout cancellation caused by whatever 53 | # is calling on_push, so be careful with subprocess.run, and always 54 | # include your own timeout=xxx parameter. 55 | 56 | proc = subprocess.run([cmd_path] + cmd_args, 57 | stdout=subprocess.PIPE, 58 | stderr=subprocess.PIPE, 59 | timeout=10, 60 | encoding=ENCODING) 61 | stdout_txt = proc.stdout 62 | stderr_txt = proc.stderr 63 | else: 64 | # Non-Windows platforms 65 | proc = await asyncio.create_subprocess_exec(cmd_path, *cmd_args, 66 | stdin=asyncio.subprocess.PIPE, 67 | stdout=asyncio.subprocess.PIPE, 68 | stderr=asyncio.subprocess.PIPE) 69 | stdout_data, stderr_data = await asyncio.wait_for(proc.communicate(input=b''), 70 | timeout=10) 71 | stdout_txt = stdout_data.decode(encoding=ENCODING) 72 | stderr_txt = stderr_data.decode(encoding=ENCODING) 73 | 74 | # Upload picture 75 | resp = await pb.async_upload_file_to_transfer_sh(temp_img.name) 76 | file_type = resp.get("file_type") 77 | file_url = resp.get("file_url") 78 | file_name = resp.get("file_name") 79 | 80 | # Provide a response 81 | dev_iden = recvd_push.get("source_device_iden") 82 | dev = await pb.async_get_device(iden=dev_iden) 83 | await pb.async_push_file(file_name=file_name, file_url=file_url, file_type=file_type, 84 | title="Imagesnap", body="{}\n{}".format(stdout_txt, stderr_txt).strip(), 85 | device=dev) 86 | print("File uploaded and pushed: {}".format(file_url)) 87 | # except asyncio.CancelledError as ce: 88 | # # print("CANCELLED!", ce) 89 | # # traceback.print_tb(sys.exc_info()[2]) 90 | # raise ce 91 | # 92 | except Exception as e: 93 | # print("Error in {}:".format(__name__), e, file=sys.stderr) 94 | # traceback.print_tb(sys.exc_info()[2]) 95 | raise e 96 | 97 | finally: 98 | os.remove(temp_img.name) 99 | -------------------------------------------------------------------------------- /asyncpushbullet/log_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Python log handler that publishes to Pushbullet""" 3 | import asyncio 4 | import logging 5 | import os 6 | 7 | from asyncpushbullet import AsyncPushbullet, Pushbullet 8 | 9 | __author__ = "Robert Harder" 10 | __email__ = "rob@iharder.net" 11 | 12 | 13 | class PushbulletLogHandler(logging.Handler): 14 | """ 15 | A handler for logging calls that sends log messages over pushbullet. 16 | 17 | Example: 18 | 19 | api_key = os.environ["PUSHBULLET_API_KEY"].strip() 20 | pb = AsyncPushbullet(api_key=api_key) 21 | handler = AsyncPushbulletLogHandler(pb, level=logging.WARNING) 22 | logger = logging.getLogger(__name__) 23 | logger.setLevel(logging.INFO) 24 | logger.addHandler(handler) 25 | 26 | logger.info("Normal stuff here.") 27 | logger.warning("Warning stuff here.") 28 | 29 | 30 | """ 31 | 32 | def __init__(self, pushbullet: Pushbullet, level=logging.NOTSET): 33 | """ 34 | Initialize the handler. 35 | """ 36 | super().__init__(level=level) 37 | self.pushbullet: Pushbullet = pushbullet 38 | formatter = logging.Formatter('%(asctime)s\n%(levelname)s\n%(message)s') 39 | self.setFormatter(formatter) 40 | 41 | def emit(self, record: logging.LogRecord): 42 | """ 43 | Emit a record. 44 | """ 45 | try: 46 | title = f"{record.name}:: {record.getMessage()}" 47 | body = self.format(record) 48 | self.pushbullet.push_note(title=title, body=body) 49 | 50 | except RecursionError: # See issue 36272 51 | raise 52 | except Exception: 53 | self.handleError(record) 54 | 55 | def __repr__(self): 56 | level = logging.getLevelName(self.level) 57 | return '<%s (%s)>' % (self.__class__.__name__, level) 58 | 59 | 60 | class AsyncPushbulletLogHandler(PushbulletLogHandler): 61 | 62 | def __init__(self, pushbullet: AsyncPushbullet, level=logging.NOTSET, use_first_available_loop: bool = True): 63 | """ 64 | Initialize the handler with the given AsyncPushbullet object and logging level. 65 | If use_first_available_loop is true (default), then the first time the log handler 66 | is invoked, if it is running from an active event loop, that event loop will be the 67 | one on which the AsyncPushbullet makes its connections. In the unusual case that you 68 | have more than one event loop running (different threads of course), then you may 69 | want to call the AsyncPushbullet aio_session() or connect() functions on the loop 70 | you intend it to run on. 71 | :param pushbullet: 72 | :param level: 73 | :param use_first_available_loop: 74 | """ 75 | super().__init__(pushbullet=pushbullet, level=level) 76 | self.pushbullet: AsyncPushbullet = pushbullet 77 | self.use_first_available_loop: bool = bool(use_first_available_loop) 78 | 79 | def emit(self, record: logging.LogRecord): 80 | """ 81 | Emit a record. 82 | """ 83 | try: 84 | # If there is no loop yet known for the AsyncPushbullet object, 85 | # then we may need to grab the current running loop if there is one. 86 | if self.pushbullet.loop is None: 87 | if self.use_first_available_loop and asyncio.get_event_loop().is_running(): 88 | fut = asyncio.get_event_loop().create_task(self.pushbullet.aio_session()) 89 | fut.add_done_callback(lambda f: self.emit(record)) 90 | else: 91 | super().emit(record) # synchronous version 92 | else: 93 | title = f"{record.name}: {record.getMessage()}" 94 | body = self.format(record) 95 | coro = self.pushbullet.async_push_note(title=title, body=body) 96 | asyncio.run_coroutine_threadsafe(coro, loop=self.pushbullet.loop) 97 | 98 | except RecursionError: # See issue 36272 99 | raise 100 | except Exception: 101 | self.handleError(record) 102 | 103 | def __repr__(self): 104 | level = logging.getLevelName(self.level) 105 | return '<%s (%s)>' % (self.__class__.__name__, level) 106 | 107 | 108 | def main(): 109 | # Just an example 110 | async def run(): 111 | api_key = os.environ["PUSHBULLET_API_KEY"].strip() 112 | # pb = await AsyncPushbullet(api_key=api_key).connect() 113 | pb = AsyncPushbullet(api_key=api_key) 114 | handler = AsyncPushbulletLogHandler(pb, level=logging.WARNING) 115 | logger = logging.getLogger(__name__) 116 | logger.setLevel(logging.INFO) 117 | logger.addHandler(handler) 118 | 119 | logger.info("Normal stuff here.") 120 | logger.warning("Warning stuff here.") 121 | logger.warning("Warning 2") 122 | 123 | print("Done") 124 | await asyncio.sleep(2) 125 | 126 | asyncio.get_event_loop().run_until_complete(run()) 127 | 128 | 129 | if __name__ == '__main__': 130 | main() 131 | -------------------------------------------------------------------------------- /asyncpushbullet/ephemeral_comm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import os 5 | import sys 6 | import traceback 7 | from asyncio import futures 8 | from collections import namedtuple 9 | from typing import TypeVar, Generic, AsyncIterator 10 | 11 | sys.path.append("..") # Since examples are buried one level into source tree 12 | from asyncpushbullet import AsyncPushbullet, LiveStreamListener 13 | 14 | __author__ = "Robert Harder" 15 | __email__ = "rob@iharder.net" 16 | 17 | T = TypeVar('T') 18 | 19 | 20 | class EphemeralComm(Generic[T]): 21 | 22 | def __init__(self, pb: AsyncPushbullet, t: namedtuple): 23 | self.pb: AsyncPushbullet = pb 24 | self.t: namedtuple = t 25 | self.lsl: LiveStreamListener = None 26 | self.queue: asyncio.Queue = None 27 | self.ephemeral_type: str = f"ephemeral:{self.t.__name__}" 28 | 29 | @property 30 | def closed(self): 31 | return self.lsl is None or self.lsl.closed 32 | 33 | async def close(self): 34 | if self.lsl: 35 | await self.lsl.close() 36 | 37 | async def __aenter__(self): 38 | self.queue = asyncio.Queue() 39 | ready = asyncio.Event() 40 | 41 | async def _listen(): 42 | try: 43 | async with LiveStreamListener(self.pb, types=self.ephemeral_type) as lsl: 44 | self.lsl = lsl 45 | ready.set() 46 | async for msg in lsl: 47 | del msg["push"]["type"] 48 | kmsg = self.t(**msg["push"]) 49 | await self.queue.put(kmsg) 50 | except Exception as ex: 51 | print("ERROR:", ex, file=sys.stderr, flush=True) 52 | traceback.print_tb(sys.exc_info()[2]) 53 | await self.queue.put(StopAsyncIteration(ex).with_traceback(sys.exc_info()[2])) 54 | else: 55 | await self.queue.put(StopAsyncIteration()) 56 | 57 | # print("LiveStreamListener closed:", self.lsl.closed) 58 | 59 | asyncio.get_event_loop().create_task(_listen()) 60 | await ready.wait() 61 | return self 62 | 63 | async def __aexit__(self, exc_type, exc_val, exc_tb): 64 | if self.lsl and not self.lsl.closed: 65 | await self.lsl.close() 66 | 67 | def __aiter__(self) -> AsyncIterator[T]: 68 | # return self 69 | return EphemeralComm._Iterator(self, break_on_timeout=True) 70 | 71 | # async def __anext__(self) -> T: 72 | # if self.closed: 73 | # raise StopAsyncIteration() 74 | # else: 75 | # kmsg = await self.queue.get() 76 | # if isinstance(kmsg, StopAsyncIteration): 77 | # raise kmsg 78 | # return kmsg 79 | 80 | async def next(self, timeout=None, break_on_timeout=False, timeout_val=None) -> T: 81 | """Returns the next message or None if the stream has closed or waiting times out.""" 82 | aiter = EphemeralComm._Iterator(self, timeout=timeout, timeout_val=timeout_val, 83 | break_on_timeout=break_on_timeout) 84 | return await aiter.__anext__() 85 | # try: 86 | # kmsg = await asyncio.wait_for(self.__anext__(), timeout=timeout) 87 | # # except futures.TimeoutError as te: 88 | # # return None 89 | # except StopAsyncIteration as sai: 90 | # return None 91 | # else: 92 | # return kmsg 93 | 94 | async def send(self, kmsg: T): 95 | if self.closed: 96 | raise RuntimeError("Unable to send -- underlying connection is closed.") 97 | try: 98 | d = kmsg._asdict() 99 | d["type"] = self.t.__name__ 100 | await self.pb.async_push_ephemeral(d) 101 | except Exception as ex: 102 | print("ERROR:", ex, file=sys.stderr, flush=True) 103 | traceback.print_tb(sys.exc_info()[2]) 104 | 105 | def with_timeout(self, timeout=None, break_on_timeout=True, timeout_val=None) -> AsyncIterator[T]: 106 | """Enables the async for loop to have a timeout. 107 | 108 | async for msg in ec.timeout(1): 109 | if msg is None: 110 | ... 111 | else: 112 | ... 113 | """ 114 | return EphemeralComm._Iterator(self, timeout=timeout, timeout_val=timeout_val, 115 | break_on_timeout=break_on_timeout) 116 | 117 | class _Iterator(AsyncIterator): 118 | def __init__(self, parent, timeout: float = None, timeout_val=None, break_on_timeout=True): 119 | self.timeout = timeout 120 | self.returnval = timeout_val 121 | self.break_on_timeout = break_on_timeout 122 | self.parent = parent 123 | 124 | def __aiter__(self) -> AsyncIterator[T]: 125 | return self 126 | 127 | async def __anext__(self) -> T: 128 | if self.parent.closed: 129 | raise StopAsyncIteration("EphemeralComm is closed.") 130 | else: 131 | try: 132 | kmsg = await asyncio.wait_for(self.parent.queue.get(), timeout=self.timeout) 133 | 134 | except futures.TimeoutError as te: 135 | if self.break_on_timeout: 136 | raise StopAsyncIteration(f"Timed out after {self.timeout} seconds.") \ 137 | .with_traceback(sys.exc_info()[2]) 138 | else: 139 | return None 140 | 141 | except futures.CancelledError as ce: 142 | raise StopAsyncIteration(f"Cancelled.").with_traceback(sys.exc_info()[2]) 143 | 144 | else: 145 | if isinstance(kmsg, StopAsyncIteration): 146 | raise kmsg 147 | else: 148 | return kmsg 149 | -------------------------------------------------------------------------------- /examples/tk_listener_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example tkinter app that shows pushes as they arrive. 5 | """ 6 | import asyncio 7 | import logging 8 | import os 9 | import sys 10 | import threading 11 | import tkinter as tk 12 | from functools import partial 13 | 14 | from tkinter_tools import BindableTextArea 15 | 16 | sys.path.append("..") # Since examples are buried one level into source tree 17 | from asyncpushbullet import AsyncPushbullet, oauth2 18 | from asyncpushbullet.async_listeners import LiveStreamListener 19 | 20 | __author__ = 'Robert Harder' 21 | __email__ = "rob@iharder.net" 22 | 23 | API_KEY = "" # YOUR API KEY 24 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 25 | 26 | # logging.basicConfig(level=logging.DEBUG) 27 | 28 | 29 | class PushApp(): 30 | def __init__(self, root): 31 | self.window = root 32 | root.title("Async Pushbullet Demo") 33 | self.log = logging.getLogger(__name__) 34 | 35 | # Data 36 | self.pushbullet = None # type: AsyncPushbullet 37 | self.pushbullet_listener = None # type: LiveStreamListener 38 | self.key_var = tk.StringVar() # API key 39 | self.pushes_var = tk.StringVar() 40 | self.ioloop = None # type: asyncio.BaseEventLoop 41 | self.proxy_var = tk.StringVar() 42 | 43 | # View / Control 44 | self.create_widgets() 45 | 46 | # Connections 47 | self.create_io_loop() 48 | self.key_var.set(API_KEY) 49 | self.proxy_var.set(PROXY) 50 | 51 | def create_widgets(self): 52 | """ 53 | API Key: [ ] 54 | 55 | Pushes: 56 | +----------------------------+ 57 | | | 58 | +----------------------------+ 59 | """ 60 | # API Key 61 | lbl_key = tk.Label(self.window, text="API Key:") 62 | lbl_key.grid(row=0, column=0, sticky=tk.W) 63 | txt_key = tk.Entry(self.window, textvariable=self.key_var) 64 | txt_key.grid(row=0, column=1, sticky=tk.W + tk.E) 65 | tk.Grid.grid_columnconfigure(self.window, 1, weight=1) 66 | txt_key.bind('', lambda x: self.connect_button_clicked()) 67 | 68 | btn_connect = tk.Button(self.window, text="Connect", command=self.connect_button_clicked) 69 | btn_connect.grid(row=1, column=1, sticky=tk.W) 70 | 71 | btn_disconnect = tk.Button(self.window, text="Disconnect", command=self.disconnect_button_clicked) 72 | btn_disconnect.grid(row=2, column=1, sticky=tk.W) 73 | 74 | lbl_data = tk.Label(self.window, text="Incoming Pushes...") 75 | lbl_data.grid(row=4, column=0, sticky=tk.W) 76 | txt_data = BindableTextArea(self.window, textvariable=self.pushes_var, width=80, height=10) 77 | txt_data.grid(row=5, column=0, columnspan=2, sticky="NSEW") 78 | tk.Grid.grid_rowconfigure(self.window, 5, weight=1) 79 | 80 | def connect_button_clicked(self): 81 | self.close() 82 | 83 | async def _listen(): 84 | try: 85 | self.pushbullet = AsyncPushbullet(self.key_var.get(), 86 | verify_ssl=False, 87 | proxy=self.proxy_var.get()) 88 | 89 | async with LiveStreamListener(self.pushbullet) as pl2: 90 | self.pushbullet_listener = pl2 91 | await self.connected(pl2) 92 | 93 | async for push in pl2: 94 | await self.push_received(push, pl2) 95 | 96 | except Exception as ex: 97 | print("Exception:", ex) 98 | finally: 99 | await self.disconnected(self.pushbullet_listener) 100 | 101 | asyncio.run_coroutine_threadsafe(_listen(), self.ioloop) 102 | 103 | def create_io_loop(self): 104 | """Creates a new thread to manage an asyncio event loop specifically for IO to/from Pushbullet.""" 105 | assert self.ioloop is None # This should only ever be run once 106 | 107 | def _run(loop): 108 | asyncio.set_event_loop(loop) 109 | loop.run_forever() 110 | 111 | self.ioloop = asyncio.new_event_loop() 112 | self.ioloop.set_exception_handler(self._ioloop_exc_handler) 113 | threading.Thread(target=partial(_run, self.ioloop), name="Thread-asyncio", daemon=True).start() 114 | 115 | def _ioloop_exc_handler(self, loop: asyncio.BaseEventLoop, context: dict): 116 | if "exception" in context: 117 | self.status = context["exception"] 118 | self.status = str(context) 119 | # Handle this more robustly in real-world code 120 | 121 | def close(self): 122 | 123 | if self.pushbullet is not None: 124 | self.pushbullet.close_all_threadsafe() 125 | self.pushbullet = None 126 | if self.pushbullet_listener is not None: 127 | assert self.ioloop is not None 128 | pl = self.pushbullet_listener 129 | asyncio.run_coroutine_threadsafe(pl.close(), self.ioloop) 130 | self.pushbullet_listener = None 131 | 132 | def disconnect_button_clicked(self): 133 | self.close() 134 | 135 | async def connected(self, listener: LiveStreamListener): 136 | print("Connected to websocket") 137 | 138 | async def disconnected(self, listener: LiveStreamListener): 139 | print("Disconnected from websocket") 140 | 141 | async def push_received(self, p: dict, listener: LiveStreamListener): 142 | print("Push received:", p) 143 | prev = self.pushes_var.get() 144 | prev += "{}\n\n".format(p) 145 | self.pushes_var.set(prev) 146 | 147 | 148 | def main(): 149 | tk1 = tk.Tk() 150 | program1 = PushApp(tk1) 151 | tk1.mainloop() 152 | 153 | 154 | if __name__ == '__main__': 155 | API_KEY = oauth2.get_oauth2_key() 156 | if not API_KEY: 157 | with open("../api_key.txt") as f: 158 | API_KEY = f.read().strip() 159 | 160 | try: 161 | main() 162 | except KeyboardInterrupt: 163 | print("Quitting") 164 | pass 165 | -------------------------------------------------------------------------------- /asyncpushbullet/oauth2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import asyncio 4 | import sys 5 | import webbrowser 6 | from concurrent import futures 7 | 8 | import aiohttp # pip install aiohttp 9 | from aiohttp import WSMsgType, web 10 | 11 | from asyncpushbullet import PushbulletError 12 | from asyncpushbullet.prefs import Prefs 13 | from asyncpushbullet.websocket_server import WebServer, WebsocketHandler, WebHandler 14 | 15 | OAUTH2_TOKEN_KEY = "oauth2_token" 16 | 17 | __ASYNCPUSHBULLET_OAUTH2_CLIENT_ID__ = "wS8zyC2gTU1WiROYlll60vAkpq7DTwjU" # This project is registered on Pushbullet 18 | __OAUTH2_URL_TEMPLATE__ = "https://www.pushbullet.com/authorize?client_id={client_id}&redirect_uri=http%3A%2F%2Flocalhost%3A{port}%2Fpb_oauth2&response_type=token&scope=everything" 19 | __OAUTH2_REDIRECT_PORT__ = 31772 # Thank you, random.org 20 | __PREFS_FOR_OAUTH2__ = Prefs("asyncpushbullet", "net.iharder.asyncpushbullet") 21 | 22 | 23 | def get_oauth2_key(): 24 | token = __PREFS_FOR_OAUTH2__.get(OAUTH2_TOKEN_KEY) 25 | return token 26 | 27 | def clear_oauth2_key(): 28 | __PREFS_FOR_OAUTH2__.set(OAUTH2_TOKEN_KEY, None) 29 | 30 | async def async_gain_oauth2_access(): 31 | __PREFS_FOR_OAUTH2__.set(OAUTH2_TOKEN_KEY, None) 32 | port = __OAUTH2_REDIRECT_PORT__ 33 | oauth2_url = __OAUTH2_URL_TEMPLATE__.format(client_id=__ASYNCPUSHBULLET_OAUTH2_CLIENT_ID__, port=port) 34 | 35 | server = WebServer(port=port) 36 | token = None 37 | try: 38 | queue = asyncio.Queue() 39 | oauth_handler = OauthResponseHandler(port=port) 40 | token_handler = RegisterTokenWebsocketHandler(queue) 41 | server.add_route("/pb_oauth2", oauth_handler) 42 | server.add_route("/register_token", token_handler) 43 | await server.start() 44 | 45 | # Open a web page for the user to authorize the app 46 | print("Experimental: Waiting for user to authenticate through their web browser...") 47 | webbrowser.open(oauth2_url) 48 | 49 | # Wait for token 50 | resp = await asyncio.wait_for(queue.get(), 120) 51 | ws = await queue.get() # type: web.WebSocketResponse 52 | 53 | if isinstance(resp, PushbulletError): 54 | await ws.send_json({"success": False}) 55 | raise resp 56 | else: 57 | token = resp # type: str 58 | __PREFS_FOR_OAUTH2__.set(OAUTH2_TOKEN_KEY, token) 59 | print("Oauth2 token successfully retrieved.") 60 | await ws.send_json({"success": True}) 61 | return token 62 | 63 | except futures.TimeoutError as te: 64 | print("Timed out. Did the user forget to authenticate?", file=sys.stderr) 65 | 66 | except Exception as ex: 67 | # print("Oauth2 token was not retrieved.", ex) 68 | # print(ex, file=sys.stderr) 69 | pass 70 | 71 | finally: 72 | await server.shutdown() 73 | return token 74 | 75 | 76 | def gain_oauth2_access(): 77 | loop = asyncio.get_event_loop() 78 | token = loop.run_until_complete(async_gain_oauth2_access()) 79 | return token 80 | 81 | 82 | class OauthResponseHandler(WebHandler): 83 | HTML = """ 84 | 85 | 86 | 87 | Title 88 | 89 | 90 | 91 | 92 | Pushbullet - Your devices working better together 93 | 94 | 95 | 96 | 97 | 147 | 148 | 149 |
150 |
151 |
152 | 158 |
159 |

AsyncPushbullet Command Line Tools on Your Computer

160 |

Grant https://github.com/rharder/asyncpushbullet 162 | access to your Pushbullet profile and data.

163 |

164 | 167 |

168 |

169 |
170 |
171 | 172 | 173 | """ 174 | 175 | def __init__(self, port: int, *kargs, **kwargs): 176 | super().__init__(*kargs, **kwargs) 177 | self.port = port 178 | self.html = self.HTML 179 | 180 | async def on_incoming_http(self, route: str, request: web.BaseRequest): 181 | return web.Response(text=self.html, content_type="text/html") 182 | 183 | 184 | class RegisterTokenWebsocketHandler(WebsocketHandler): 185 | 186 | def __init__(self, queue: asyncio.Queue, *kargs, **kwargs): 187 | super().__init__(*kargs, **kwargs) 188 | self.queue = queue 189 | 190 | async def on_message(self, route: str, ws: web.WebSocketResponse, ws_msg_from_client: aiohttp.WSMessage): 191 | 192 | if ws_msg_from_client.type == WSMsgType.TEXT: 193 | try: 194 | msg = ws_msg_from_client.json() 195 | except Exception as ex: 196 | print("Error trying to make json", ex, ws_msg_from_client) 197 | else: 198 | if OAUTH2_TOKEN_KEY in msg: 199 | token = msg.get(OAUTH2_TOKEN_KEY) 200 | await self.queue.put(token) 201 | else: 202 | err = msg.get("error") 203 | await self.queue.put(PushbulletError(err)) 204 | 205 | await self.queue.put(ws) 206 | -------------------------------------------------------------------------------- /asyncpushbullet/websocket_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Easy to use Websocket Server. 4 | 5 | Source: https://github.com/rharder/handy 6 | 7 | June 2018 - Updated for aiohttp v3.3 8 | August 2018 - Updated for Python 3.7, made WebServer support multiple routes on one port 9 | """ 10 | import asyncio 11 | import logging 12 | import weakref 13 | from functools import partial 14 | from typing import Dict, Set, List 15 | 16 | import aiohttp # pip install aiohttp 17 | from aiohttp import web 18 | 19 | __author__ = "Robert Harder" 20 | __email__ = "rob@iharder.net" 21 | __license__ = "Public Domain" 22 | 23 | 24 | class WebServer: 25 | """Hosts a web/websocket server on a given port and responds to multiple routes 26 | (relative urls) at that address. 27 | 28 | 29 | Source: https://github.com/rharder/handy 30 | Author: Robert Harder 31 | License: Public Domain 32 | 33 | """ 34 | 35 | def __init__(self, host: str = None, port: int = None, ssl_context=None): 36 | """ 37 | Create a new WebServer that will listen on the given port. 38 | 39 | :param port: The port on which to listen 40 | """ 41 | super().__init__() 42 | self.log = logging.getLogger(__name__ + '.' + self.__class__.__name__) 43 | 44 | # Passed parameters 45 | self.host: str = host 46 | self.port: int = port 47 | self.ssl_context = ssl_context 48 | 49 | # Internal use 50 | self.app: web.Application = None 51 | self.site: web.TCPSite = None 52 | self.runner: web.AppRunner = None 53 | self.route_handlers: Dict[str, WebHandler] = {} 54 | 55 | self._running: bool = False 56 | self._shutting_down: bool = False 57 | self._starting_up: bool = False 58 | 59 | def __str__(self): 60 | routes = ", ".join(self.route_handlers.keys()) 61 | return "{}({}:({})".format(self.__class__.__name__, self.port, routes) 62 | 63 | @property 64 | def running(self): 65 | return self._running 66 | 67 | @property 68 | def starting_up(self): 69 | return self._starting_up 70 | 71 | @property 72 | def shutting_down(self): 73 | return self._shutting_down 74 | 75 | async def start(self): 76 | """ 77 | Starts the websocket server and begins listening. This function returns 78 | with the server continuing to listen (non-blocking). 79 | 80 | :return: None 81 | """ 82 | if self.starting_up or self.running: 83 | raise Exception("Cannot start server when it is already running.") 84 | 85 | self._starting_up = True 86 | 87 | self.app = web.Application() 88 | self.app['requests'] = [] # type: List[web.BaseRequest] 89 | self.app.on_shutdown.append(self._on_shutdown) 90 | 91 | # Connect routes 92 | for route in self.route_handlers.keys(): 93 | self.app.router.add_get(route, partial(self.incoming_http_handler, route)) 94 | 95 | self.runner = web.AppRunner(self.app) 96 | await self.runner.setup() 97 | self.site = web.TCPSite(self.runner, port=self.port, host=self.host, ssl_context=self.ssl_context) 98 | await self.site.start() 99 | 100 | self._running = True 101 | self._starting_up = False 102 | 103 | async def shutdown(self): 104 | if not self.running: 105 | raise Exception("Cannot close server that is not running.") 106 | 107 | if self.shutting_down: 108 | pass 109 | else: 110 | self._shutting_down = True 111 | await self.runner.cleanup() 112 | 113 | async def _on_shutdown(self, app: web.Application): 114 | self.close_current_connections() 115 | self._running = False 116 | self._shutting_down = False 117 | 118 | def close_current_connections(self): 119 | for x in self.app["requests"]: 120 | if x is not None and x.transport is not None: 121 | x.transport.close() 122 | 123 | def add_route(self, route: str, handler): 124 | if self.running: 125 | raise RuntimeError("Cannot add a route after server is already running.") 126 | self.route_handlers[route] = handler 127 | 128 | async def incoming_http_handler(self, route: str, request: web.BaseRequest): 129 | self.app['requests'].append(request) 130 | try: 131 | resp = await self.route_handlers[route].on_incoming_http(route, request) 132 | finally: 133 | self.app['requests'].remove(request) 134 | return resp 135 | 136 | 137 | class WebHandler: 138 | 139 | async def on_incoming_http(self, route: str, request: web.BaseRequest): 140 | return web.Response(body=str(self.__class__.__name__)) 141 | 142 | 143 | class WebsocketHandler(WebHandler): 144 | 145 | def __init__(self, *kargs, **kwargs): 146 | super().__init__(*kargs, **kwargs) 147 | self.websockets: Set[web.WebSocketResponse] = weakref.WeakSet() 148 | 149 | async def broadcast_json(self, msg): 150 | """ Converts msg to json and broadcasts the json data to all connected clients. """ 151 | await self._broadcast(msg, web.WebSocketResponse.send_json) 152 | 153 | async def broadcast_text(self, msg: str): 154 | """ Broadcasts a string to all connected clients. """ 155 | await self._broadcast(msg, web.WebSocketResponse.send_str) 156 | 157 | async def broadcast_bytes(self, msg: bytes): 158 | """ Broadcasts bytes to all connected clients. """ 159 | await self._broadcast(msg, web.WebSocketResponse.send_bytes) 160 | 161 | async def _broadcast(self, msg, func: callable): 162 | for ws in set(self.websockets): # type: web.WebSocketResponse 163 | await func(ws, msg) 164 | 165 | async def close_websockets(self): 166 | """Closes all active websockets for this handler.""" 167 | ws_closers = [ws.close() for ws in set(self.websockets) if not ws.closed] 168 | ws_closers and await asyncio.gather(*ws_closers) 169 | 170 | async def on_incoming_http(self, route: str, request: web.BaseRequest): 171 | """Handles the incoming http(s) request and converts it to a WebSocketResponse. 172 | 173 | This method is not meant to be overridden when subclassed. 174 | """ 175 | ws = web.WebSocketResponse() 176 | self.websockets.add(ws) 177 | try: 178 | await ws.prepare(request) 179 | await self.on_websocket(route, ws) 180 | finally: 181 | self.websockets.discard(ws) 182 | return ws 183 | 184 | async def on_websocket(self, route: str, ws: web.WebSocketResponse): 185 | """ 186 | Override this function if you want to handle new incoming websocket clients. 187 | The default behavior is to listen indefinitely for incoming messages from clients 188 | and call on_message() with each one. 189 | 190 | If you override on_websocket and have your own loop to receive and process messages, 191 | you may also need an await asyncio.sleep(0) line to avoid an infinite loop with the 192 | websocket close message. 193 | 194 | Example: 195 | while not ws.closed: 196 | ws_msg = await ws.receive() 197 | await asyncio.sleep(0) 198 | ... 199 | 200 | """ 201 | try: 202 | while not ws.closed: 203 | ws_msg = await ws.receive() # type: aiohttp.WSMessage 204 | await self.on_message(route=route, ws=ws, ws_msg_from_client=ws_msg) 205 | 206 | # If you override on_websocket and have your own loop 207 | # to receive and process messages, you may also need 208 | # this await asyncio.sleep(0) line to avoid an infinite 209 | # loop with the websocket close message. 210 | await asyncio.sleep(0) # Need to yield control back to event loop 211 | 212 | except RuntimeError as e: # Socket closing throws RuntimeError 213 | print("RuntimeError - did socket close?", e, flush=True) 214 | pass 215 | finally: 216 | await self.on_close(route, ws) 217 | 218 | async def on_message(self, route: str, ws: web.WebSocketResponse, ws_msg_from_client: aiohttp.WSMessage): 219 | """ Override this function to handle incoming messages from websocket clients. """ 220 | pass 221 | 222 | async def on_close(self, route: str, ws: web.WebSocketResponse): 223 | """ Override this function to handle a websocket having closed. """ 224 | pass 225 | -------------------------------------------------------------------------------- /examples/tk_upload_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example tkinter app that uploads files and shows incoming pushes. 5 | 6 | It is not necessary to connect to a listener and listen for pushes in order to upload, 7 | but it makes the example more interesting. 8 | """ 9 | import asyncio 10 | import logging 11 | import os 12 | import sys 13 | import threading 14 | import tkinter as tk 15 | from functools import partial 16 | from tkinter import filedialog 17 | 18 | from tkinter_tools import BindableTextArea 19 | 20 | sys.path.append("..") # Since examples are buried one level into source tree 21 | from asyncpushbullet import AsyncPushbullet, LiveStreamListener, oauth2 22 | 23 | __author__ = 'Robert Harder' 24 | __email__ = "rob@iharder.net" 25 | 26 | API_KEY = "" # YOUR API KEY 27 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 28 | 29 | 30 | # logging.basicConfig(level=logging.DEBUG) 31 | 32 | 33 | class PushApp(): 34 | def __init__(self, root): 35 | self.window = root 36 | root.title("Async Pushbullet Upload Demo") 37 | self.log = logging.getLogger(__name__) 38 | 39 | # Data 40 | self.ioloop = None # type: asyncio.AbstractEventLoop 41 | self.pushbullet = None # type: AsyncPushbullet 42 | self.pushbullet_listener = None # type: LiveStreamListener 43 | self.key_var = tk.StringVar() # API key 44 | self.pushes_var = tk.StringVar() 45 | self.filename_var = tk.StringVar() 46 | self.btn_upload = None # type: tk.Button 47 | self.proxy_var = tk.StringVar() 48 | 49 | # View / Control 50 | self.create_widgets() 51 | 52 | # Connections 53 | self.create_io_loop() 54 | self.key_var.set(API_KEY) 55 | self.filename_var.set(__file__) 56 | self.proxy_var.set(PROXY) 57 | 58 | def create_widgets(self): 59 | """ 60 | API Key: [ ] 61 | 62 | Filename: [ ] 63 | 64 | Pushes: 65 | +----------------------------+ 66 | | | 67 | +----------------------------+ 68 | """ 69 | row = 0 70 | # API Key 71 | lbl_key = tk.Label(self.window, text="API Key:") 72 | lbl_key.grid(row=row, column=0, sticky=tk.W) 73 | txt_key = tk.Entry(self.window, textvariable=self.key_var) 74 | txt_key.grid(row=row, column=1, sticky=tk.W + tk.E) 75 | tk.Grid.grid_columnconfigure(self.window, 1, weight=1) 76 | txt_key.bind('', lambda x: self.connect_button_clicked()) 77 | row += 1 78 | btn_connect = tk.Button(self.window, text="Connect", command=self.connect_button_clicked) 79 | btn_connect.grid(row=row, column=1, sticky=tk.W) 80 | row += 1 81 | 82 | # Proxy, if we want to show it 83 | # lbl_proxy = tk.Label(self.window, text="Proxy") 84 | # lbl_proxy.grid(row=row, column=0, sticky=tk.W) 85 | # txt_proxy = tk.Entry(self.window, textvariable=self.proxy_var) 86 | # txt_proxy.grid(row=row, column=1, sticky=tk.W + tk.E) 87 | # row += 1 88 | 89 | # File: [ ] 90 | lbl_file = tk.Label(self.window, text="File:") 91 | lbl_file.grid(row=row, column=0, sticky=tk.W) 92 | txt_file = tk.Entry(self.window, textvariable=self.filename_var) 93 | txt_file.grid(row=row, column=1, sticky=tk.W + tk.E) 94 | row += 1 95 | 96 | # 97 | button_frame = tk.Frame(self.window) 98 | button_frame.grid(row=row, column=0, columnspan=2, sticky=tk.W + tk.E) 99 | row += 1 100 | btn_browse = tk.Button(button_frame, text="Browse...", command=self.browse_button_clicked) 101 | btn_browse.grid(row=0, column=0, sticky=tk.E) 102 | self.btn_upload = tk.Button(button_frame, text="Upload and Push", command=self.upload_button_clicked, 103 | state=tk.DISABLED) 104 | self.btn_upload.grid(row=0, column=1, sticky=tk.W) 105 | 106 | # Incoming pushes 107 | # +------------+ 108 | # | | 109 | # +------------+ 110 | lbl_data = tk.Label(self.window, text="Incoming Pushes...") 111 | lbl_data.grid(row=row, column=0, sticky=tk.W) 112 | row += 1 113 | txt_data = BindableTextArea(self.window, textvariable=self.pushes_var, width=80, height=10) 114 | txt_data.grid(row=row, column=0, columnspan=2) 115 | 116 | def create_io_loop(self): 117 | """Creates a new thread to manage an asyncio event loop specifically for IO to/from Pushbullet.""" 118 | assert self.ioloop is None # This should only ever be run once 119 | 120 | def _run(loop): 121 | asyncio.set_event_loop(loop) 122 | loop.run_forever() 123 | 124 | self.ioloop = asyncio.new_event_loop() 125 | self.ioloop.set_exception_handler(self._ioloop_exc_handler) 126 | threading.Thread(target=partial(_run, self.ioloop), name="Thread-asyncio", daemon=True).start() 127 | 128 | def _ioloop_exc_handler(self, loop: asyncio.BaseEventLoop, context: dict): 129 | if "exception" in context: 130 | self.status = context["exception"] 131 | self.status = str(context) 132 | # Handle this more robustly in real-world code 133 | 134 | def connect_button_clicked(self): 135 | self.pushes_var.set("Connecting...") 136 | self.close() 137 | 138 | async def _listen(): 139 | try: 140 | self.pushbullet = AsyncPushbullet(self.key_var.get(), 141 | verify_ssl=False, 142 | proxy=self.proxy_var.get()) 143 | 144 | async with LiveStreamListener(self.pushbullet) as pl2: 145 | self.pushbullet_listener = pl2 146 | await self.connected(pl2) 147 | 148 | async for push in pl2: 149 | await self.push_received(push, pl2) 150 | 151 | except Exception as ex: 152 | print("Exception:", ex) 153 | finally: 154 | await self.disconnected(self.pushbullet_listener) 155 | 156 | asyncio.run_coroutine_threadsafe(_listen(), self.ioloop) 157 | 158 | def close(self): 159 | 160 | if self.pushbullet is not None: 161 | self.pushbullet.close_all_threadsafe() 162 | self.pushbullet = None 163 | if self.pushbullet_listener is not None: 164 | assert self.ioloop is not None 165 | pl = self.pushbullet_listener 166 | asyncio.run_coroutine_threadsafe(pl.close(), self.ioloop) 167 | self.pushbullet_listener = None 168 | 169 | def browse_button_clicked(self): 170 | print("browse_button_clicked") 171 | resp = filedialog.askopenfilename(parent=self.window, title="Open a File to Push") 172 | if resp != "": 173 | self.filename_var.set(resp) 174 | 175 | def upload_button_clicked(self): 176 | self.pushes_var.set(self.pushes_var.get() + "Uploading...") 177 | self.btn_upload["state"] = tk.DISABLED 178 | filename = self.filename_var.get() 179 | asyncio.run_coroutine_threadsafe(self.upload_file(filename), loop=self.ioloop) 180 | 181 | async def upload_file(self, filename: str): 182 | # This is the actual upload command 183 | info = await self.pushbullet.async_upload_file(filename) 184 | 185 | # Push a notification of the upload "as a file": 186 | await self.pushbullet.async_push_file(info["file_name"], info["file_url"], info["file_type"], 187 | title="File Arrived!", body="Please enjoy your file") 188 | 189 | # Push a notification of the upload "as a link": 190 | await self.pushbullet.async_push_link("Link to File Arrived!", info["file_url"], body="Please enjoy your file") 191 | self.btn_upload["state"] = tk.NORMAL 192 | self.pushes_var.set(self.pushes_var.get() + "Uploaded\n") 193 | 194 | async def connected(self, listener: LiveStreamListener): 195 | self.btn_upload["state"] = tk.NORMAL 196 | self.pushes_var.set(self.pushes_var.get() + "Connected\n") 197 | 198 | async def disconnected(self, listener: LiveStreamListener): 199 | self.btn_upload["state"] = tk.DISABLED 200 | self.pushes_var.set(self.pushes_var.get() + "Disconnected\n") 201 | 202 | async def push_received(self, p: dict, listener: LiveStreamListener): 203 | print("Push received:", p) 204 | prev = self.pushes_var.get() 205 | prev += "{}\n\n".format(p) 206 | self.pushes_var.set(prev) 207 | 208 | 209 | def main(): 210 | tk1 = tk.Tk() 211 | program1 = PushApp(tk1) 212 | 213 | tk1.mainloop() 214 | 215 | 216 | if __name__ == '__main__': 217 | API_KEY = oauth2.get_oauth2_key() 218 | if not API_KEY: 219 | with open("../api_key.txt") as f: 220 | API_KEY = f.read().strip() 221 | 222 | try: 223 | main() 224 | except KeyboardInterrupt: 225 | print("Quitting") 226 | pass 227 | -------------------------------------------------------------------------------- /examples/retrieve_all_pushes_with_iterator_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Example retrieve pushes with iterator. 5 | """ 6 | import asyncio 7 | import datetime 8 | import os 9 | import pprint 10 | import sys 11 | import threading 12 | import tkinter as tk 13 | import traceback 14 | from functools import partial 15 | import queue 16 | from typing import List, Callable 17 | 18 | from asyncpushbullet.async_pushbullet import PushbulletAsyncIterator 19 | from tkinter_tools import BindableTextArea 20 | 21 | sys.path.append("..") # Since examples are buried one level into source tree 22 | from asyncpushbullet import AsyncPushbullet 23 | 24 | __author__ = 'Robert Harder' 25 | __email__ = "rob@iharder.net" 26 | 27 | API_KEY = "" # YOUR API KEY 28 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 29 | 30 | 31 | class RetrievingAllPushesApp(): 32 | def __init__(self, root): 33 | self.window = root # type: tk.Tk 34 | root.title("Retrieve All Pushes") 35 | 36 | # Data 37 | self.pushbullet = None # type: AsyncPushbullet 38 | self.push_count_var = tk.StringVar() # Current count as pushes keep coming in 39 | self.details_var = tk.StringVar() # Details about a push 40 | self.pushes = [] # type: List[dict] 41 | self.push_iterator = None # type: PushbulletAsyncIterator[dict] 42 | self.ioloop = None # type: asyncio.BaseEventLoop 43 | 44 | self._tk_queue = queue.Queue() # For managing inter-thread communication 45 | self._tk_after_id = None # For managing tk.x.after(...) 46 | 47 | # View / Control 48 | self.push_listbox = None # type: tk.Listbox 49 | self.retrieve_btn = None # type: tk.Button 50 | self.pause_btn = None # type: tk.Button 51 | self.create_widgets() 52 | 53 | # Connections 54 | self.create_io_loop() 55 | 56 | def create_io_loop(self): 57 | """Creates a new thread to manage an asyncio event loop specifically for IO to/from Pushbullet.""" 58 | assert self.ioloop is None # This should only ever be run once 59 | 60 | def _run(loop): 61 | asyncio.set_event_loop(loop) 62 | loop.run_forever() 63 | 64 | self.ioloop = asyncio.new_event_loop() 65 | threading.Thread(target=partial(_run, self.ioloop), name="Thread-asyncio", daemon=True).start() 66 | 67 | def tk_schedule(self, cmd: Callable, *kargs): 68 | """Schedule a command to be called on the main GUI event thread.""" 69 | 70 | def _process_tk_queue(): 71 | while not self._tk_queue.empty(): 72 | msg = self._tk_queue.get() # type: Callable 73 | msg() 74 | 75 | self._tk_queue.put(partial(cmd, *kargs)) 76 | if self._tk_after_id: 77 | self.window.after_cancel(self._tk_after_id) 78 | self._tk_after_id = self.window.after(5, _process_tk_queue) 79 | 80 | def create_widgets(self): 81 | 82 | # Buttons 83 | self.retrieve_btn = tk.Button(self.window, text="Retrieve Pushes!", command=self.retrieve_button_clicked) 84 | self.retrieve_btn.grid(row=0, column=0, sticky=tk.W, padx=10) 85 | self.pause_btn = tk.Button(self.window, text="Pause", command=self.pause_button_clicked) 86 | self.pause_btn.grid(row=0, column=1, sticky=tk.W, padx=10) 87 | self.pause_btn.configure(state=tk.DISABLED) 88 | 89 | # Count of pushes 90 | lbl_data = tk.Label(self.window, text="Number of Pushes:") 91 | lbl_data.grid(row=1, column=0, sticky=tk.W, padx=10) 92 | lbl_num_pushes = tk.Label(self.window, textvar=self.push_count_var) 93 | lbl_num_pushes.grid(row=1, column=1, padx=10, sticky=tk.W) 94 | 95 | # List of pushes 96 | scrollbar = tk.Scrollbar(self.window, orient=tk.VERTICAL) 97 | scrollbar.grid(row=2, column=3, sticky="NS") 98 | self.push_listbox = tk.Listbox(self.window, yscrollcommand=scrollbar.set, width=60, height=20) 99 | scrollbar.config(command=self.push_listbox.yview) 100 | self.push_listbox.grid(row=2, column=0, columnspan=2, sticky="NSEW") 101 | self.push_listbox.bind("", self.push_list_double_clicked) 102 | tk.Grid.columnconfigure(self.window, 0, weight=1) 103 | tk.Grid.columnconfigure(self.window, 1, weight=1) 104 | tk.Grid.rowconfigure(self.window, 2, weight=1) 105 | 106 | # Push details 107 | txt_details = BindableTextArea(self.window, textvariable=self.details_var, width=60, height=10) 108 | txt_details.grid(row=2, column=4, sticky="NSEW") 109 | tk.Grid.columnconfigure(self.window, 4, weight=1) 110 | 111 | def retrieve_button_clicked(self): 112 | if self.push_iterator is None: 113 | self.retrieve_btn.configure(state=tk.DISABLED) 114 | 115 | def _recv_push(x): # Run on main event thread 116 | pos = len(self.pushes) + 1 117 | title = x.get("title") 118 | body = x.get("body") 119 | creation = float(x.get("created", 0.0)) 120 | creation = datetime.datetime.fromtimestamp(creation).strftime('%c') 121 | push_str = "{}. {}: {}, {}".format(pos, creation, title, body) 122 | 123 | self.pushes.append(x) 124 | self.push_count_var.set("{:,}".format(len(self.pushes))) 125 | self.push_listbox.insert(tk.END, push_str) 126 | 127 | async def _listen(): 128 | asyncio.get_event_loop().set_debug(True) 129 | try: 130 | async with AsyncPushbullet(API_KEY, proxy=PROXY) as self.pushbullet: 131 | self.pause_btn.configure(state=tk.NORMAL) 132 | self.pushes.clear() 133 | 134 | # Two ways to handle GUI manipulation. Since these are 135 | # just two-off commands, not in a loop, it doesn't matter 136 | # very much which we use. 137 | # Technique 1 138 | # self.push_count_var.set(len(self.pushes)) 139 | # self.push_listbox.delete(0, tk.END) 140 | # await asyncio.sleep(0) 141 | # Technique 2 142 | self.tk_schedule(self.push_count_var.set, len(self.pushes)) 143 | self.tk_schedule(self.push_listbox.delete, 0, tk.END) 144 | 145 | self.push_iterator = self.pushbullet.pushes_asynciter(limit=None, 146 | modified_after=0.0, 147 | active_only=True 148 | ) 149 | async for push in self.push_iterator: 150 | # As pushes are retrieved -- it will take several calls to pushbullet 151 | # to retrieve the long history of pushes -- they are processed on 152 | # this async for loop. Although the for loop obviously only processes 153 | # one item at a time, they will come in bunches. When a network request 154 | # is processed, a batch of pushes maybe 1 to 20 will fire through quickly. 155 | # We immediately schedule processing on the main thread, since we'll 156 | # be updating the GUI. 157 | self.tk_schedule(_recv_push, push) 158 | 159 | except Exception as ex: 160 | print("Exception:", ex, file=sys.stderr, flush=True) 161 | tb = sys.exc_info()[2] 162 | traceback.print_tb(tb) 163 | 164 | self.retrieve_btn.configure(state=tk.NORMAL) 165 | self.pause_btn.configure(state=tk.DISABLED) 166 | else: 167 | self.retrieve_btn.configure(text="Completed") 168 | self.pause_btn.configure(text="Completed") 169 | self.retrieve_btn.configure(state=tk.DISABLED) 170 | self.pause_btn.configure(state=tk.DISABLED) 171 | self.tk_schedule(self.push_count_var.set, "{:,} (Completed)".format(len(self.pushes))) 172 | 173 | asyncio.run_coroutine_threadsafe(_listen(), self.ioloop) 174 | else: 175 | self.push_iterator.resume() 176 | 177 | def pause_button_clicked(self): 178 | if self.push_iterator: 179 | # self.push_iterator.stop() 180 | self.push_iterator.pause() 181 | self.retrieve_btn.configure(state=tk.NORMAL) 182 | 183 | def push_list_double_clicked(self, event): 184 | items = self.push_listbox.curselection() 185 | if len(items) == 0: 186 | print("No item selected") 187 | return 188 | 189 | push = self.pushes[int(items[0])] 190 | self.details_var.set(pprint.pformat(push)) 191 | 192 | 193 | def main(): 194 | tk1 = tk.Tk() 195 | _ = RetrievingAllPushesApp(tk1) 196 | tk1.mainloop() 197 | 198 | 199 | if __name__ == '__main__': 200 | if API_KEY == "": 201 | with open("../api_key.txt") as f: 202 | API_KEY = f.read().strip() 203 | 204 | try: 205 | main() 206 | except KeyboardInterrupt: 207 | print("Quitting") 208 | pass 209 | -------------------------------------------------------------------------------- /asyncpushbullet/websocket_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Handy class for consuming websockets as a client. 5 | 6 | Source: https://github.com/rharder/handy 7 | 8 | August 2018 - Initial creation 9 | """ 10 | 11 | import asyncio 12 | import logging 13 | import sys 14 | from typing import AsyncIterator, Callable 15 | 16 | import aiohttp # pip install aiohttp 17 | 18 | __author__ = "Robert Harder" 19 | __email__ = "rob@iharder.net" 20 | __license__ = "Public Domain" 21 | 22 | 23 | class WebsocketClient(): 24 | """A handy class for consuming websockets as a client. 25 | 26 | 27 | Source: https://github.com/rharder/handy 28 | Author: Robert Harder 29 | License: Public Domain 30 | 31 | """ 32 | 33 | def __init__(self, url, headers=None, verify_ssl=None, proxy=None, session=None): 34 | self.url = url 35 | self.headers = headers 36 | self.verify_ssl = verify_ssl 37 | self.proxy = None if proxy is None or str(proxy).strip() == "" else str(proxy) 38 | self._provided_session: aiohttp.ClientSession = session 39 | self._created_session: aiohttp.ClientSession = None 40 | self.socket: aiohttp.ClientWebSocketResponse = None 41 | self._queue: asyncio.Queue = None 42 | self.loop: asyncio.BaseEventLoop = None 43 | self.log = logging.getLogger(__name__) 44 | 45 | @staticmethod 46 | def connect(url, 47 | on_connect: Callable = None, 48 | on_message: Callable = None, 49 | on_close: Callable = None, 50 | loop=None, 51 | **kwargs): 52 | """Convenience method for connecting as a websocket client using callbacks.""" 53 | 54 | async def _run(): 55 | client = None 56 | try: 57 | async with WebsocketClient(url=url, **kwargs) as client: 58 | 59 | # Connect 60 | if asyncio.iscoroutinefunction(on_connect): 61 | await on_connect(client) 62 | elif callable(on_connect): 63 | on_connect(client) 64 | 65 | # Message 66 | async for msg in client: 67 | if asyncio.iscoroutinefunction(on_message): 68 | await on_message(client, msg) 69 | elif callable(on_message): 70 | on_message(client, msg) 71 | 72 | finally: 73 | 74 | # Close 75 | if on_close: 76 | if asyncio.iscoroutinefunction(on_close): 77 | await on_close(client) 78 | elif callable(on_close): 79 | on_close(client) 80 | 81 | asyncio.run_coroutine_threadsafe(_run(), loop or asyncio.get_event_loop()) 82 | 83 | async def _create_session(self) -> aiohttp.ClientSession: 84 | 85 | # TCP options 86 | aio_connector: aiohttp.TCPConnector = None 87 | if self.verify_ssl is not None and self.verify_ssl is False: 88 | aio_connector = aiohttp.TCPConnector(ssl=False) 89 | 90 | # Create session 91 | session = aiohttp.ClientSession(headers=self.headers, connector=aio_connector) 92 | self.log.debug("Created session {}".format(id(session))) 93 | 94 | return session 95 | 96 | async def close(self): 97 | if self.socket: 98 | if self.socket.closed: 99 | self.log.debug("Socket {} already closed".format(id(self.socket))) 100 | else: 101 | await self.socket.close() 102 | self.log.info("Closed socket {}".format(id(self.socket))) 103 | if self._created_session: # Only close session if we created it here 104 | if self._created_session.closed: 105 | self.log.debug("Session {} already closed".format(id(self._created_session))) 106 | else: 107 | await self._created_session.close() 108 | self.log.debug("Closed session {}".format(id(self._created_session))) 109 | 110 | @property 111 | def closed(self): 112 | if self.socket is None: 113 | raise Exception("No underlying websocket to close -- has this websocket connected yet?") 114 | return self.socket.closed 115 | 116 | async def send_str(self, data): 117 | """Sends a string to the websocket server.""" 118 | await self.socket.send_str(str(data)) 119 | await asyncio.sleep(0) 120 | 121 | async def send_bytes(self, data): 122 | """Sends raw bytes to the websocket server.""" 123 | await self.socket.send_bytes(data) 124 | await asyncio.sleep(0) 125 | 126 | async def send_json(self, data): 127 | """Converts data to a json message and sends to the websocket server.""" 128 | await self.socket.send_json(data) 129 | await asyncio.sleep(0) 130 | 131 | async def flush_incoming(self, timeout: float = None): 132 | """Flushes (throws away) all messages received to date but not yet consumed. 133 | 134 | The method will return silently if the timeout period is reached. 135 | 136 | :param float timeout: the optional timeout in seconds 137 | """ 138 | 139 | async def _flush_all(_timeout=None): 140 | if timeout: # Use await to dump anything that arrives in the timeout window 141 | while True: 142 | _ = await self._queue.get() 143 | if self.log.isEnabledFor(logging.DEBUG): 144 | self.log.debug("flushed: {}".format(_)) 145 | 146 | else: # Else empty the queue as fast as possible without awaiting 147 | try: 148 | while True: 149 | _ = self._queue.get_nowait() 150 | if self.log.isEnabledFor(logging.DEBUG): 151 | self.log.debug("Flushed: {}".format(_)) 152 | 153 | except asyncio.QueueEmpty: 154 | pass 155 | 156 | try: 157 | await asyncio.wait_for(_flush_all(timeout), timeout=timeout) 158 | except asyncio.futures.TimeoutError: 159 | pass 160 | 161 | def flush_incoming_threadsafe(self, timeout: float = None): 162 | """Flushes (throws away) all messages received to date but not yet consumed. 163 | 164 | The method will return silently if the timeout period is reached. 165 | 166 | This method is threadsafe, which also means it gets scheduled on the 167 | appropriate thread "sometime" in the future. Upon exiting this function, 168 | the queue may not yet be flushed or even have begun the flushing process. 169 | 170 | :param float timeout: the optional timeout in seconds 171 | """ 172 | asyncio.run_coroutine_threadsafe(self.flush_incoming(timeout=timeout), self.loop) 173 | 174 | async def next_msg(self, timeout: float = None) -> aiohttp.WSMessage: 175 | """Returns the next message from the websocket server. 176 | 177 | This method may throw a StopAsyncIteration exception if the socket 178 | closes or another similar event occurs. 179 | 180 | If a timeout is specified, this method may throw an 181 | asyncio.futures.TimeoutError (not the builtin version) if the timeout 182 | period is exceeded without a message being available from the server. 183 | """ 184 | if timeout is None: 185 | msg: aiohttp.WSMessage = await self._queue.get() 186 | if type(msg) == StopAsyncIteration: 187 | raise msg 188 | return msg 189 | else: 190 | msg: aiohttp.WSMessage = await asyncio.wait_for(self.next_msg(), timeout=timeout) 191 | return msg 192 | 193 | async def __aenter__(self): 194 | """ 195 | :rtype: WebsocketClient 196 | """ 197 | self.loop = asyncio.get_event_loop() 198 | self._queue: asyncio.Queue = asyncio.Queue() 199 | 200 | # Make connection 201 | try: 202 | session = self._provided_session 203 | if session is None: 204 | self._created_session = await self._create_session() 205 | session = self._created_session 206 | self.socket = await session.ws_connect(self.url, proxy=self.proxy) 207 | self.log.debug("Connected socket {} to {}".format(id(self.socket), self.url)) 208 | except Exception as ex: 209 | if self._provided_session: 210 | await self._provided_session.close() 211 | self._provided_session = None 212 | raise ex 213 | 214 | # Set up listener to receive messages and put them in a queue 215 | async def _listen_for_messages(): 216 | try: 217 | 218 | # Spend time here waiting for incoming messages 219 | msg: aiohttp.WSMessage 220 | async for msg in self.socket: 221 | if self.log.isEnabledFor(logging.DEBUG): 222 | self.log.debug("Received {}".format(msg)) 223 | await self._queue.put(msg) 224 | await asyncio.sleep(0) 225 | 226 | except Exception as e: 227 | sai = StopAsyncIteration(e).with_traceback(sys.exc_info()[2]) 228 | await self._queue.put(sai) 229 | else: 230 | sai = StopAsyncIteration() 231 | await self._queue.put(sai) 232 | 233 | asyncio.get_event_loop().create_task(_listen_for_messages()) 234 | await asyncio.sleep(0) 235 | 236 | return self 237 | 238 | async def __aexit__(self, exc_type, exc_val, exc_tb): 239 | await self.close() 240 | 241 | def __aiter__(self) -> AsyncIterator[aiohttp.WSMessage]: 242 | return WebsocketClient._Iterator(self) 243 | 244 | def with_timeout(self, timeout=None) -> AsyncIterator[aiohttp.WSMessage]: 245 | """Enables the async for loop to have a timeout. 246 | 247 | async for msg in client.timeout(1): 248 | ... 249 | """ 250 | return WebsocketClient._Iterator(self, timeout=timeout) 251 | 252 | class _Iterator(AsyncIterator): 253 | def __init__(self, ws_client, timeout: float = None): 254 | self.timeout = timeout 255 | self.ws_client: WebsocketClient = ws_client 256 | 257 | def __aiter__(self) -> AsyncIterator[aiohttp.WSMessage]: 258 | return self 259 | 260 | async def __anext__(self) -> aiohttp.WSMessage: 261 | if self.ws_client.socket.closed: 262 | raise StopAsyncIteration("The websocket has closed.") 263 | 264 | return await self.ws_client.next_msg(timeout=self.timeout) 265 | -------------------------------------------------------------------------------- /examples/tk_asyncio_base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | A base class for Tk apps that will use asyncio. 4 | """ 5 | import asyncio 6 | import queue 7 | import sys 8 | import threading 9 | import tkinter as tk 10 | import traceback 11 | import types 12 | from concurrent.futures import CancelledError 13 | from functools import partial 14 | from typing import Callable, Union, Coroutine 15 | 16 | __author__ = "Robert Harder" 17 | __email__ = "rob@iharder.net" 18 | __license__ = "Public Domain" 19 | 20 | 21 | class TkAsyncioBaseApp: 22 | """ 23 | A base app (or object you can instantiate) to help manage asyncio loops and 24 | tkinter event loop and executing tasks across their borders. 25 | 26 | For example if you are responding to a button click you might want to fire off 27 | some communication that uses asyncio functions. That might look like this: 28 | 29 | class MyGuiApp(TkAsyncioBaseApp): 30 | def __init__(self, tkroot): 31 | super().__init__(tkroot) 32 | ... 33 | 34 | def my_button_clicked(self): 35 | self.my_button.configure(state=tk.DISABLED) 36 | self.io(connect()) # Launch a coroutine on a thread managing an asyncio loop 37 | 38 | async def connect(self): 39 | await some_connection_stuff() 40 | self.tk(self.my_button.configure, state=tk.NORMAL) # Run GUI commands on main thread 41 | 42 | """ 43 | 44 | def __init__(self, base: tk.Misc): 45 | self.__tk_base: tk.Misc = base 46 | 47 | # Inter-thread communication 48 | self._ioloop: asyncio.BaseEventLoop = None 49 | self.__tk_queue: queue.Queue = queue.Queue() # Functions to call on tk loop 50 | self.__tk_after_ids: queue.Queue = queue.Queue() # Requests to process tk queue 51 | 52 | # IO Loop 53 | self.__create_io_loop() 54 | 55 | def __create_io_loop(self): 56 | """Creates a new thread to manage an asyncio event loop.""" 57 | if self._ioloop is not None: 58 | raise Exception("An IO loop is already running.") 59 | 60 | _ready = threading.Event() # Don't leave this function until thread is ready 61 | 62 | def _thread_run(loop: asyncio.BaseEventLoop): 63 | asyncio.set_event_loop(loop) 64 | loop.call_soon_threadsafe(_ready.set) 65 | loop.run_forever() 66 | 67 | self._ioloop: asyncio.BaseEventLoop = asyncio.new_event_loop() 68 | threading.Thread(target=partial(_thread_run, self._ioloop), name="Thread-asyncio", daemon=True).start() 69 | _ready.wait() 70 | 71 | async def ioloop_exception_happened(self, extype: type, ex: Exception, tb: types.TracebackType, coro: Coroutine): 72 | """Called when there is an unhandled exception in a job that was 73 | scheduled on the io event loop. 74 | 75 | The arguments are the three standard sys.exc_info() arguments 76 | plus the function that was called as a scheduled job and subsequently 77 | raised the Exception. 78 | 79 | :param extype: the exception type 80 | :param ex: the Exception object that was raised 81 | :param tb: the traceback associated with the exception 82 | :param coro: the scheduled function that caused the trouble 83 | """ 84 | pass 85 | print("TkAsyncioBaseApp.ioloop_exception_happened called. Override this function in your app.", 86 | file=sys.stderr, flush=True) 87 | print(f"coro: {coro}", file=sys.stderr, flush=True) 88 | traceback.print_tb(tb) 89 | 90 | def tkloop_exception_happened(self, extype: type, ex: Exception, tb: types.TracebackType, func: partial): 91 | """Called when there is an unhandled exception in a job that was 92 | scheduled on the tk event loop. 93 | 94 | The arguments are the three standard sys.exc_info() arguments 95 | plus the function that was called as a scheduled job and subsequently 96 | raised the Exception. 97 | 98 | :param extype: the exception type 99 | :param ex: the Exception object that was raised 100 | :param tb: the traceback associated with the exception 101 | :param func: the scheduled function that caused the trouble 102 | """ 103 | pass 104 | print("TkAsyncioBaseApp.tkloop_exception_happened was called. Override this function in your app.", 105 | file=sys.stderr, flush=True) 106 | print("Exception:", extype, ex, func, file=sys.stderr, flush=True) 107 | print(f"func: {func}", file=sys.stderr, flush=True) 108 | print(f"kargs: {func.args}", file=sys.stderr, flush=True) 109 | print(f"kwargs: {func.keywords}", file=sys.stderr, flush=True) 110 | traceback.print_tb(tb) 111 | 112 | def io(self, func: Union[Coroutine, Callable], *kargs, **kwargs) -> Union[asyncio.Future, asyncio.Handle]: 113 | """ 114 | Schedule a coroutine or regular function to be called on the io event loop. 115 | 116 | self.io(some_coroutine()) 117 | self.io(some_func, "some arg") 118 | 119 | The positional *kargs and named **kwargs arguments only apply when 120 | a non-coroutine function is passed such as: 121 | 122 | self.io(print, "hello world", file=sys.stderr) 123 | 124 | Returns a Future (for coroutines) or Handle (for regular functions) that can 125 | be used to cancel or otherwise inspect the scheduled job. 126 | 127 | This is threadsafe. 128 | 129 | :param func: the coroutine or function 130 | :param kargs: optional positional arguments for the function 131 | :param kwargs: optional named arguments for the function 132 | :return: 133 | """ 134 | 135 | async def _coro_run(): 136 | # noinspection PyBroadException 137 | try: 138 | await func 139 | except Exception: 140 | await self.ioloop_exception_happened(*sys.exc_info(), func) 141 | 142 | def _func_run(): 143 | # noinspection PyBroadException 144 | try: 145 | func(*kargs, **kwargs) 146 | except Exception: 147 | self.tkloop_exception_happened(*sys.exc_info(), func, *kargs, **kwargs) 148 | 149 | if asyncio.iscoroutine(func): 150 | return asyncio.run_coroutine_threadsafe(_coro_run(), self._ioloop) 151 | else: 152 | return self._ioloop.call_soon_threadsafe(_func_run) 153 | 154 | def tk(self, func: Callable, *kargs, **kwargs): 155 | """ 156 | Schedule a function to be called on the Tk GUI event loop. 157 | 158 | This is threadsafe. 159 | 160 | :param func: The function to call 161 | :param kargs: optional positional arguments 162 | :param kwargs: optional named arguments 163 | :return: 164 | """ 165 | 166 | # Put the command in a thread-safe queue and schedule the tk thread 167 | # to retrieve it in a few milliseconds. The few milliseconds delay 168 | # helps if there's a flood of calls all at once. 169 | 170 | x = TkTask(func, *kargs, **kwargs) 171 | self.__tk_queue.put(x) 172 | 173 | # Now throw away old requests to process the tk queue 174 | while True: 175 | try: 176 | old_scheduled_timer = self.__tk_after_ids.get(block=False) 177 | self.__tk_base.after_cancel(old_scheduled_timer) 178 | except queue.Empty: 179 | break 180 | self.__tk_after_ids.put(self.__tk_base.after(5, self._tk_process_queue)) 181 | 182 | return x 183 | 184 | def _tk_process_queue(self): 185 | """Used internally to actually process the tk task queue.""" 186 | while True: 187 | try: 188 | x: TkTask = self.__tk_queue.get(block=False) 189 | except queue.Empty: 190 | break # empty queue - we're done! 191 | else: 192 | # noinspection PyBroadException 193 | try: 194 | x.run() # Will skip if task is cancelled 195 | except Exception as ex: 196 | extype, ex, tb = sys.exc_info() 197 | self.tkloop_exception_happened(extype, ex, tb, x.job()) 198 | 199 | 200 | class TkTask: 201 | """A task-like object shceduled from, presumably, an asyncio loop or other thread.""" 202 | 203 | def __init__(self, func: Callable, *kargs, **kwargs): 204 | self._job: partial = partial(func, *kargs, **kwargs) 205 | self._cancelled: bool = False 206 | self._result = None 207 | self._result_ready: threading.Event = threading.Event() 208 | 209 | self._run_has_been_attempted: bool = False 210 | self._exception: Exception = None 211 | 212 | self._host_loop: asyncio.BaseEventLoop = None 213 | self._host_loop_result_ready: asyncio.Event = None 214 | try: 215 | self._host_loop = asyncio.get_event_loop() 216 | self._host_loop_result_ready: asyncio.Event = asyncio.Event() 217 | except RuntimeError: 218 | # Whichever thread called this isn't using asyncio--that's OK 219 | pass 220 | 221 | def run(self): 222 | if self._run_has_been_attempted: 223 | raise RuntimeError(f"Job has already been run: {self._job}") 224 | 225 | elif not self.cancelled(): 226 | try: 227 | x = self._job() 228 | except Exception as ex: 229 | self._exception = ex 230 | raise ex 231 | else: 232 | self._set_result(x) 233 | finally: 234 | self._run_has_been_attempted = True 235 | 236 | def _set_result(self, val): 237 | self._result = val 238 | self._result_ready.set() 239 | if self._host_loop: 240 | self._host_loop.call_soon_threadsafe(self._host_loop_result_ready.set) 241 | 242 | def cancel(self): 243 | self._cancelled = True 244 | 245 | def cancelled(self) -> bool: 246 | return self._cancelled 247 | 248 | def done(self) -> bool: 249 | return self._run_has_been_attempted or self._cancelled 250 | 251 | def job(self) -> partial: 252 | return self._job 253 | 254 | async def async_result(self, timeout=None): 255 | """ 256 | Await a result without blocking an asyncio loop. 257 | 258 | Raises concurrent.futures.TimeoutError if times out. 259 | :param timeout: 260 | :return: 261 | """ 262 | if timeout is None: 263 | await self._host_loop_result_ready.wait() 264 | else: 265 | await asyncio.wait_for(self._host_loop_result_ready.wait(), timeout=timeout) 266 | 267 | return self.result() 268 | 269 | def result(self, timeout=None): 270 | if self._exception is not None: 271 | raise self._exception 272 | elif self._cancelled: 273 | raise CancelledError() 274 | else: 275 | if self._result_ready.wait(timeout): 276 | return self._result 277 | else: 278 | raise TimeoutError(f"Timeout of {timeout} seconds exceeded.") 279 | -------------------------------------------------------------------------------- /examples/push_console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Horribly dangerous. Do not use. 5 | """ 6 | import asyncio 7 | import os 8 | import sys 9 | import traceback 10 | from collections import namedtuple 11 | from typing import List, AsyncIterator 12 | 13 | # import asyncpushbullet # pip install asyncpushbullet 14 | from async_command import AsyncReadConsole, async_execute_command 15 | from asyncpushbullet import AsyncPushbullet, EphemeralComm, oauth2 # pip install asyncpushbullet 16 | 17 | __author__ = "Robert Harder" 18 | __email__ = "rob@iharder.net" 19 | __license__ = "Public Domain" 20 | 21 | PROXY = os.environ.get("https_proxy") or os.environ.get("http_proxy") 22 | # asyncpushbullet.oauth2.gain_oauth2_access() 23 | 24 | CMsg = namedtuple("console", ["from_stdout", "from_stderr", "for_stdin", "status"], 25 | defaults=([], [], [], None)) 26 | 27 | 28 | def main(): 29 | # An example 30 | loop = asyncio.get_event_loop() 31 | 32 | sys.argv.append("console") 33 | sys.argv.append("server") 34 | 35 | if "server" in sys.argv and "console" in sys.argv: 36 | if sys.platform == 'win32': 37 | cmd = "cmd" 38 | else: 39 | cmd = "bash" 40 | t1 = loop.create_task(run_console()) 41 | t2 = loop.create_task(run_cmd_server(cmd)) 42 | loop.run_until_complete(asyncio.gather(t1, t2)) 43 | loop.run_until_complete(asyncio.sleep(1)) 44 | print("both closed") 45 | return 46 | 47 | if sys.argv[-1] == "console": 48 | loop.run_until_complete(run_console()) 49 | 50 | if sys.argv[-1] == "server": 51 | loop.run_until_complete(run_cmd_server("dir")) 52 | 53 | 54 | async def run_console(): 55 | # Read console input from input() and write to pushbullet 56 | # Echo pushbullet from_stdout through print() 57 | print("Starting console for connecting with remote server", flush=True) 58 | stdout_task = None # type: asyncio.Task 59 | try: 60 | key = oauth2.get_oauth2_key() 61 | 62 | async with AsyncPushbullet(key, proxy=PROXY) as pb: 63 | async with EphemeralComm(pb, CMsg) as ec: # type: EphemeralComm[CMsg] 64 | # msg = {"type": "console", "status": "Console input connected to pushbullet"} 65 | # await pb.async_push_ephemeral(msg) 66 | kmsg = CMsg(status="Console input connected to pushbullet") 67 | await ec.send(kmsg) 68 | 69 | async with AsyncReadConsole("cmd input: ") as arc: 70 | async def _dump_stdout(): 71 | try: 72 | remote_stdout_closed = False 73 | remote_stderr_closed = False 74 | async for kmsg in ec.with_timeout(10, break_on_timeout=False): 75 | 76 | if kmsg is None: 77 | # print("TIMEDOUT") 78 | if remote_stderr_closed or remote_stdout_closed: 79 | # We received a close from one but not the other. Just quit. 80 | print("Remote command exited.", flush=True) 81 | await ec.close() # TODO: error here 82 | # break 83 | continue 84 | 85 | for line in kmsg.from_stdout: 86 | if line is None: 87 | # print("stdout closed.") 88 | remote_stdout_closed = True 89 | else: 90 | print(line, flush=True) 91 | 92 | for line in kmsg.from_stderr: 93 | if line is None: 94 | # print("stderr closed.") 95 | remote_stderr_closed = True 96 | else: 97 | print(line, file=sys.stderr, flush=True) 98 | 99 | if remote_stdout_closed and remote_stderr_closed: 100 | print("Remote command exited.", flush=True) 101 | await ec.close() 102 | # print("end: async for kmsg in ec") 103 | 104 | except Exception as ex: 105 | print("ERROR in _dump_stdout:", ex, file=sys.stderr, flush=True) 106 | traceback.print_tb(sys.exc_info()[2]) 107 | finally: 108 | print('FINALLY: closing arc') 109 | await arc.close() 110 | print("arc.close() returned") 111 | 112 | stdout_task = asyncio.get_event_loop().create_task(_dump_stdout()) 113 | 114 | async for line in arc: 115 | if line is None: 116 | assert line is not None, "This should never happen" 117 | break 118 | else: 119 | # msg = {"type": "console", "for_stdin": line} 120 | # await pb.async_push_ephemeral(msg) 121 | # print("Sending command: " + line) 122 | kmsg = CMsg(for_stdin=[line]) 123 | await ec.send(kmsg) 124 | print("exited async for line in arc:") 125 | 126 | except Exception as ex: 127 | print("ERROR in run_console:", ex, file=sys.stderr, flush=True) 128 | traceback.print_tb(sys.exc_info()[2]) 129 | finally: 130 | print("Console tool closing ... ", end="", flush=True) 131 | if stdout_task: 132 | stdout_task.cancel() 133 | print("Closed.", flush=True) 134 | 135 | 136 | class LiveStreamCommandListener: 137 | def __init__(self, ec): 138 | self.ec: EphemeralComm[CMsg] = ec 139 | self.lines: List[str] = [] 140 | 141 | def __aiter__(self) -> AsyncIterator[str]: 142 | return self 143 | 144 | async def __anext__(self) -> str: 145 | ec_iter = self.ec.__aiter__() 146 | while len(self.lines) == 0: 147 | kmsg = await ec_iter.__anext__() 148 | self.lines += kmsg.for_stdin 149 | line = self.lines.pop(0) 150 | print(f"Received command: {line}") 151 | return line 152 | 153 | 154 | async def run_cmd_server(cmd, args: List = None): 155 | print("Remote command server.", flush=True) 156 | loop = asyncio.get_event_loop() 157 | args = args or [] 158 | 159 | try: 160 | key = oauth2.get_oauth2_key() 161 | 162 | async with AsyncPushbullet(key, proxy=PROXY) as pb: 163 | stdout_queue = asyncio.Queue() 164 | stderr_queue = asyncio.Queue() 165 | async with EphemeralComm(pb, CMsg) as ec: # type: EphemeralComm[CMsg] 166 | 167 | # msg = {"type": "console", "status": "command server connected to pushbullet"} 168 | # await pb.async_push_ephemeral(msg) 169 | kmsg = CMsg(status="command server connected to pushbullet") 170 | await ec.send(kmsg) 171 | 172 | async def _output_flusher(_q, name): 173 | # name is from_stdout or from_stderr 174 | while True: 175 | lines = [] 176 | while len(lines) < 20: 177 | line: bytes 178 | try: 179 | if lines: 180 | # If we have something to send, wait only a moment 181 | # to see if there's anything else coming. 182 | # print("Waiting with timeout", name) 183 | line = await asyncio.wait_for(_q.get(), timeout=0.25) 184 | else: 185 | # print("Waiting without timeout", name) 186 | # If we have an empty queue, no need for the timeout 187 | line = await _q.get() 188 | 189 | except asyncio.TimeoutError: 190 | # print("TE") 191 | break 192 | # break # while loop for length of lines 193 | else: 194 | # print(f"{name}: {line}") 195 | if line is None: 196 | # print(f"{name} output flusher on server done!") 197 | # return # We're done! 198 | lines.append(None) 199 | break 200 | else: 201 | line = line.decode().rstrip() 202 | lines.append(line) 203 | 204 | # print(f"{name} server LINES:", lines) 205 | if lines: 206 | try: 207 | # msg = {"type": "console", name: lines} 208 | # await pb.async_push_ephemeral(msg) 209 | if name == "from_stdout": 210 | kmsg = CMsg(from_stdout=lines) 211 | await ec.send(kmsg) 212 | elif name == "from_stderr": 213 | kmsg = CMsg(from_stderr=lines) 214 | await ec.send(kmsg) 215 | 216 | if lines[-1] is None: 217 | # print(f"{name} found None - output flusher is returning") 218 | return # We're done 219 | except Exception as ex: 220 | print("ERROR:", ex, file=sys.stderr, flush=True) 221 | traceback.print_tb(sys.exc_info()[2]) 222 | 223 | t1 = loop.create_task(_output_flusher(stdout_queue, "from_stdout")) 224 | t2 = loop.create_task(_output_flusher(stderr_queue, "from_stderr")) 225 | 226 | # async with LiveStreamListener(pb, types="ephemeral:console") as lsl: 227 | 228 | await async_execute_command(cmd, args, 229 | provide_stdin=LiveStreamCommandListener(ec), 230 | handle_stderr=stderr_queue.put, 231 | # handle_stdout=print) 232 | handle_stdout=stdout_queue.put) 233 | 234 | # print("ADDING None TO BOTH OUTPUT QUEUES") 235 | await stdout_queue.put(None) # mark that we're done for the output flushers 236 | await stderr_queue.put(None) # mark that we're done 237 | 238 | await asyncio.gather(t1, t2) 239 | 240 | # print("SERVER asyncpush WITH BLOCK EXITED") 241 | 242 | 243 | except Exception as ex: 244 | print("ERROR:", ex, file=sys.stderr, flush=True) 245 | traceback.print_tb(sys.exc_info()[2]) 246 | 247 | finally: 248 | print("Server tool closing ... ", end="", flush=True) 249 | print("Closed.", flush=True) 250 | 251 | 252 | if __name__ == "__main__": 253 | main() 254 | -------------------------------------------------------------------------------- /examples/tkinter_tools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | A collection of functions and classes to help with tkinter. 5 | Source: https://github.com/rharder/handy 6 | """ 7 | import tkinter as tk 8 | import tkinter.ttk as ttk 9 | from tkinter import scrolledtext 10 | 11 | __author__ = "Robert Harder" 12 | __email__ = "rob@iharder.net" 13 | __date__ = "10 Oct 2017" 14 | __license__ = "Public Domain" 15 | 16 | 17 | def bind_tk_var_to_tk_attribute(widget, attr_name, tkvar): 18 | """ 19 | Helper function to bind an arbitrary tk widget attribute to a tk.xxxVar. 20 | 21 | Example: 22 | 23 | window = tk.Tk() 24 | var = tk.StringVar() 25 | label = tk.Label(window, text="Change My Background") 26 | label.pack() 27 | bind_tk_var_to_tk_attribute(label, "bg", var) 28 | var.set("light blue") 29 | window.mainloop() 30 | 31 | Equivalently calls the function: 32 | 33 | label.configure({"bg": "light blue"}) 34 | 35 | :param widget: the tk widget to be affected 36 | :param attr_name: the name of the attribute to bind 37 | :param tkvar: the variable to bind to 38 | """ 39 | tkvar.trace("w", lambda _, __, ___, v=tkvar: widget.configure({attr_name: v.get()})) 40 | 41 | 42 | def bind_tk_var_to_method(func, tkvar): 43 | """ 44 | Helper function to bind an arbitrary method to a tkvar value. 45 | 46 | Example: 47 | 48 | window = tk.Tk() 49 | var = tk.StringVar() 50 | bind_tk_method(window.title, var) 51 | var.set("My New Title") 52 | window.mainloop() 53 | 54 | Equivalently calls the function: 55 | 56 | window.title("My New Title") 57 | 58 | :param func: the function to call expecting a single argument 59 | :param tkvar: the variable to bind to 60 | """ 61 | tkvar.trace("w", lambda _, __, ___, v=tkvar: func(tkvar.get())) 62 | 63 | 64 | def bind_tk_var_to_property(obj, prop_name, tkvar): 65 | """ 66 | Helper function to bind an arbitrary property to a tkvar value. 67 | 68 | Example: 69 | 70 | class Cat: 71 | def __init__(self): 72 | self.name = "Tiger" 73 | 74 | window = tk.Tk() 75 | var = tk.StringVar() 76 | cat = Cat() 77 | bind_tk_var_to_property(cat, "name", var) 78 | var.set("Basement Cat") 79 | window.mainloop() 80 | 81 | Equivalently sets the property: 82 | 83 | cat.name = "Basement Cat" 84 | 85 | :param obj: the object whose property will be changed 86 | :param str prop_name: name of the property to change 87 | :param tk.Variable tkvar: the tk variable from which to get a value 88 | """ 89 | tkvar.trace("w", lambda _, __, ___, v=tkvar: setattr(obj, prop_name, v.get())) 90 | 91 | 92 | class FormattableTkStringVar(tk.StringVar): 93 | """ 94 | An extension of the tk.StringVar that takes a formattable string, eg, "Age: {}" and a list 95 | of tk.xxxVar objects, and updates the formatted string whenever one of the underlying vars 96 | is changed. 97 | 98 | Example: 99 | 100 | self.red = tk.IntVar() 101 | self.green = tk.IntVar() 102 | self.blue = tk.IntVar() 103 | self.hex = FormattableTkStringVar("#{:02X}{:02X}{:02X}", [self.red, self.green, self.blue]) 104 | ... 105 | hex_label = tk.Label(frame, textvariable=self.hex) 106 | """ 107 | 108 | def __init__(self, str_format: str, vars, **kwargs): 109 | """ 110 | Creates a bindable variable whose string format is up to date with the underlying variables. 111 | :param str str_format: the string format, eg, "Age: {}" 112 | :param var_list: the list of tk.xxxVar objects that feed into the string format 113 | """ 114 | super().__init__(**kwargs) 115 | self._format = str_format 116 | self._vars = list(vars) 117 | 118 | for v in self._vars: 119 | v.trace("w", self.var_changed) # Bind to all underlying vars 120 | 121 | self.update_format() # Set initial value of formatted string 122 | 123 | def var_changed(self, _, __, ___): 124 | self.update_format() # Update with new value 125 | 126 | def update_format(self): 127 | var_vals = [v.get() for v in self._vars] # Collect values for all vars 128 | self.set(self._format.format(*var_vals)) # Format string, unpacking the format(..) arguments 129 | 130 | 131 | class BindableTextArea(tk.scrolledtext.ScrolledText): 132 | """ 133 | A multi-line tk widget that is bindable to a tk.StringVar. 134 | 135 | You will need to import ScrolledText like so: 136 | 137 | from tkinter import scrolledtext 138 | """ 139 | 140 | class _SuspendTrace: 141 | """ Used internally to suspend a trace during some particular operation. """ 142 | 143 | def __init__(self, parent): 144 | self.__parent = parent # type: BindableTextArea 145 | 146 | def __enter__(self): 147 | """ At beginning of operation, stop the trace. """ 148 | if self.__parent._trace_id is not None and self.__parent._textvariable is not None: 149 | self.__parent._textvariable.trace_vdelete("w", self.__parent._trace_id) 150 | 151 | def __exit__(self, exc_type, exc_val, exc_tb): 152 | """ At conclusion of operation, resume the trace. """ 153 | self.__parent._trace_id = self.__parent._textvariable.trace("w", self.__parent._variable_value_changed) 154 | 155 | def __init__(self, parent, textvariable: tk.StringVar = None, autoscroll=True, **kwargs): 156 | tk.scrolledtext.ScrolledText.__init__(self, parent, **kwargs) 157 | self._textvariable = None # type: tk.StringVar 158 | self._trace_id = None 159 | if textvariable is None: 160 | self.textvariable = tk.StringVar() 161 | else: 162 | self.textvariable = textvariable 163 | self.bind("", self._key_released) 164 | self.autoscroll = autoscroll 165 | 166 | @property 167 | def textvariable(self): 168 | return self._textvariable 169 | 170 | @textvariable.setter 171 | def textvariable(self, new_var): 172 | # Delete old trace if we already had a bound textvariable 173 | if self._trace_id is not None and self._textvariable is not None: 174 | self._textvariable.trace_vdelete("w", self._trace_id) 175 | 176 | # Set up new textvariable binding 177 | self._textvariable = new_var 178 | self._trace_id = self._textvariable.trace("w", self._variable_value_changed) 179 | 180 | def _variable_value_changed(self, _, __, ___): 181 | 182 | # Must be in NORMAL state to respond to delete/insert methods 183 | prev_state = self["state"] 184 | self["state"] = tk.NORMAL 185 | 186 | # Replace text 187 | text = self._textvariable.get() 188 | self.delete("1.0", tk.END) 189 | self.insert(tk.END, text) 190 | 191 | if self.autoscroll: 192 | self.see(tk.END) 193 | 194 | # Restore previous state, whatever that was 195 | self["state"] = prev_state 196 | 197 | def _key_released(self, evt): 198 | """ When someone types a key, update the bound text variable. """ 199 | text = self.get("1.0", tk.END) 200 | with BindableTextArea._SuspendTrace(self): # Suspend trace to avoid infinite recursion 201 | if self.textvariable: 202 | self.textvariable.set(text) 203 | 204 | 205 | class ToggledFrame(tk.LabelFrame): 206 | """ 207 | Heavily modified from 208 | http://stackoverflow.com/questions/13141259/expandable-and-contracting-frame-in-tkinter 209 | """ 210 | 211 | def __init__(self, parent, text="", prefs=None, *args, **options): 212 | try: 213 | from .prefs import Prefs 214 | except ImportError: 215 | print("The Prefs class did not import. Frame states will not be saved.") 216 | tk.LabelFrame.__init__(self, parent, text=text, *args, **options) 217 | 218 | self.__title = text 219 | self.__hidden_text = None 220 | self.__prefs = prefs # type: Prefs 221 | 222 | # Data mechanism for show/hide 223 | name = "ToggledFrame_{}".format(text) 224 | self.show = tk.IntVar(name=name) 225 | if self.__prefs: 226 | self.show.set(self.__prefs.get(name, 1)) # Retrieve from prefs 227 | else: 228 | self.show.set(1) # Default is show 229 | 230 | def __update_show(name, value): 231 | if self.__prefs: 232 | self.__prefs.set(name, value) # Save in prefs 233 | self.update_gui_based_on_show() # Update gui 234 | 235 | self.show.trace("w", lambda name, index, mode, v=self.show: __update_show(name, v.get())) 236 | 237 | # This will respond to a click in the frame and toggle the underlying variable 238 | def frame_clicked(event): 239 | # print(event) 240 | self.show.set(int(not bool(self.show.get()))) 241 | 242 | self.bind("", frame_clicked) 243 | 244 | # GUI elements 245 | self.title_frame = ttk.Frame(self) 246 | self.title_frame.pack(fill="x", expand=1) 247 | self.subframe = tk.Frame(self, borderwidth=1) 248 | self.update_gui_based_on_show() 249 | 250 | def clear_subframe(self): 251 | if self.subframe is not None: 252 | for widget in self.subframe.winfo_children(): 253 | widget.destroy() 254 | self.subframe.pack(fill="x", expand=1) 255 | self.update_gui_based_on_show() 256 | 257 | @property 258 | def hidden_text(self): 259 | return self.__hidden_text 260 | 261 | @hidden_text.setter 262 | def hidden_text(self, value): 263 | self.__hidden_text = value 264 | self.update_gui_based_on_show() 265 | 266 | def update_gui_based_on_show(self): 267 | if bool(self.show.get()): # Show 268 | # print("show", self.__title) 269 | self.subframe.pack(fill="x", expand=1) 270 | self.config(text=self.__title + " [-]") 271 | else: # Hide 272 | # print("hide", self.__title) 273 | resp = "" 274 | if self.hidden_text is not None and self.hidden_text != "": 275 | resp = " ({})".format(self.hidden_text) 276 | self.config(text=self.__title + resp) 277 | self.subframe.forget() 278 | 279 | 280 | class ToolTip: 281 | """ 282 | create a tooltip for a given widget 283 | 284 | Author: Wayne Brown 285 | """ 286 | 287 | def __init__(self, widget, text='widget info', textvariable=None): 288 | self.wait_time = 500 # miliseconds 289 | self.wrap_length = 300 # pixels 290 | self.widget = widget 291 | self.text = text 292 | self.widget.bind("", self.enter) 293 | self.widget.bind("", self.leave) 294 | self.widget.bind("", self.leave) 295 | self.id = None 296 | self.tw = None 297 | self.textvariable = textvariable # type: tk.Variable 298 | if self.textvariable is not None: 299 | self.textvariable.trace("w", lambda _, __, ___, v=self.textvariable: setattr(self, "text", str(v.get()))) 300 | self.text = str(self.textvariable.get()) 301 | 302 | def enter(self, event=None): 303 | self.schedule() 304 | 305 | def leave(self, event=None): 306 | self.unschedule() 307 | self.hidetip() 308 | 309 | def schedule(self): 310 | self.unschedule() 311 | self.id = self.widget.after(self.wait_time, self.showtip) 312 | 313 | def unschedule(self): 314 | my_id = self.id 315 | self.id = None 316 | if my_id: 317 | self.widget.after_cancel(my_id) 318 | 319 | def showtip(self, event=None): 320 | # x = y = 0 321 | x, y, cx, cy = self.widget.bbox("insert") 322 | x += self.widget.winfo_rootx() + 25 323 | y += self.widget.winfo_rooty() + 20 324 | # creates a toplevel window 325 | self.tw = tk.Toplevel(self.widget) 326 | # Leaves only the label and removes the app window 327 | self.tw.wm_overrideredirect(True) 328 | self.tw.wm_geometry("+%d+%d" % (x, y)) 329 | label = tk.Label(self.tw, text=self.text, justify='left', 330 | background="#ffffff", relief='solid', borderwidth=1, 331 | wraplength=self.wrap_length) 332 | label.pack(ipadx=1) 333 | 334 | def hidetip(self): 335 | tw = self.tw 336 | self.tw = None 337 | if tw: 338 | tw.destroy() 339 | -------------------------------------------------------------------------------- /asyncpushbullet/async_listeners.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Asynchronous listeners for connecting to Pushbullet's realtime event stream. 4 | 5 | Pushbullet's API: https://docs.pushbullet.com/#realtime-event-stream 6 | """ 7 | import asyncio 8 | import json 9 | import logging 10 | import sys 11 | import time 12 | from typing import AsyncIterator, Set, Iterable, Callable, Dict 13 | 14 | import aiohttp # pip install aiohttp 15 | 16 | from .async_pushbullet import AsyncPushbullet 17 | from .errors import PushbulletError 18 | from .websocket_client import WebsocketClient 19 | 20 | __author__ = 'Robert Harder' 21 | __email__ = "rob@iharder.net" 22 | 23 | 24 | class LiveStreamListener: 25 | PUSHBULLET_WEBSOCKET_URL = 'wss://stream.pushbullet.com/websocket/' 26 | 27 | def __init__(self, account: AsyncPushbullet, 28 | active_only: bool = True, 29 | ignore_dismissed: bool = True, 30 | only_this_device_nickname: str = None, 31 | types: Iterable[str] = None, 32 | post_process: Callable = None): 33 | """Listens for events on the pushbullet live stream websocket. 34 | 35 | The types parameter can be used to limit which kinds of pushes 36 | are returned in an async for loop or the next_push() call. 37 | The default is to show actual pushes only, not ephemerals. 38 | Some possible values in the tuple at this time are 39 | push, ephemeral, and ephemeral:xxx where xxx is matched to 40 | the "type" parameter if the ephemeral payload has that. 41 | For instance to listen only for the universal copy/paste 42 | pushes, you could pass in the tuple ("ephemeral:clip",). 43 | 44 | :param account: the AsyncPushbullet object that represents the account 45 | :param active_only: ignore inactive pushes, defaults to true 46 | :param ignore_dismissed: ignore dismissed pushes, defaults to true 47 | :param only_this_device_nickname: only show pushes from this device 48 | :param types: the types of pushes to show 49 | """ 50 | self.log = logging.getLogger(__name__ + "." + self.__class__.__name__) 51 | 52 | self.pb: AsyncPushbullet = account 53 | self._last_update: float = 0 54 | self._active_only: bool = active_only 55 | self._ignore_dismissed: bool = ignore_dismissed 56 | self._only_this_device_nickname: str = only_this_device_nickname 57 | self._ws_client: WebsocketClient = None 58 | self._loop: asyncio.BaseEventLoop = None 59 | self._queue: asyncio.Queue = None 60 | self._post_process: Callable = post_process 61 | 62 | # Push types are what should be allowed through. 63 | # Ephemerals can be sub-typed like so: ephemeral:clip 64 | # The ephemeral_types variable contains the post-colon words 65 | if type(types) == str: 66 | types = (types,) # Convert to tuple of one 67 | self.push_types = set(types) if types is not None else ("push",) # type: Set[str] 68 | 69 | # In the form ephemeral:foo 70 | eph_types = [] 71 | for x in self.push_types: 72 | compound = x.split(":") 73 | if len(compound) >= 2 and compound[0] == "ephemeral": 74 | eph_types.append(compound[1]) 75 | self.ephemeral_types = tuple(eph_types) 76 | 77 | @property 78 | def closed(self): 79 | if self._ws_client is None: 80 | raise PushbulletError("No underlying websocket to close -- has this websocket connected yet?") 81 | return self._ws_client.closed 82 | 83 | async def close(self): 84 | await self._ws_client.close() 85 | 86 | async def __aenter__(self): 87 | self._queue = asyncio.Queue() 88 | 89 | # Are we filtering on device? 90 | if self._only_this_device_nickname is not None: 91 | device = await self.pb.async_get_device(nickname=self._only_this_device_nickname) 92 | if device is None: 93 | self.log.warning( 94 | "Filtering on device name that does not yet exist: {}".format(self._only_this_device_nickname)) 95 | del device 96 | 97 | # Load pushes that arrived since parent AsyncPushbullet was connected 98 | await self._process_pushbullet_message_tickle_push() 99 | 100 | async def _listen_for_websocket_messages(_wc: WebsocketClient): 101 | try: 102 | 103 | # Stay here for a while receiving messages 104 | async for msg in _wc: 105 | await self._process_websocket_message(msg) 106 | 107 | except Exception as e: 108 | # raise e 109 | sai = StopAsyncIteration(e).with_traceback(sys.exc_info()[2]) 110 | await self._queue.put(sai) 111 | else: 112 | msg = "Websocket closed" if _wc.closed else None 113 | sai = StopAsyncIteration(msg) 114 | await self._queue.put(sai) 115 | 116 | _ = await self.pb.aio_session() 117 | wc = WebsocketClient(url=self.PUSHBULLET_WEBSOCKET_URL + self.pb.api_key, 118 | proxy=self.pb.proxy, 119 | verify_ssl=self.pb.verify_ssl) 120 | self._ws_client = await wc.__aenter__() 121 | asyncio.get_event_loop().create_task(_listen_for_websocket_messages(wc)) 122 | await asyncio.sleep(0) 123 | 124 | return self 125 | 126 | async def _process_websocket_message(self, msg: aiohttp.WSMessage): 127 | 128 | # Process websocket message 129 | self._last_update = time.time() 130 | 131 | if msg.type == aiohttp.WSMsgType.CLOSED: 132 | err_msg = "Websocket closed: {}".format(msg) 133 | self.log.warning(err_msg) 134 | await self._queue.put(StopAsyncIteration(err_msg)) 135 | 136 | elif msg.type == aiohttp.WSMsgType.ERROR: 137 | err_msg = "Websocket error: {}".format(msg) 138 | self.log.debug(err_msg) 139 | await self._queue.put(StopAsyncIteration(err_msg)) 140 | 141 | else: 142 | self.log.debug("WebSocket message: {}".format(msg.data)) 143 | await self._process_pushbullet_message(json.loads(msg.data)) 144 | 145 | async def _process_pushbullet_message(self, msg: dict): 146 | 147 | # If everything is requested, then immediately post the message. 148 | # It might still require some follow-up processing though. 149 | if not self.push_types: 150 | await self._queue.put(msg) 151 | 152 | # Look for ephemeral messages 153 | # Takes special processing to sort through ephemerals. 154 | # Example values in self.push_types: ephemeral, ephemeral:clip 155 | if "type" in msg and "push" in msg: # Ephemeral 156 | 157 | # If we're looking for ALL ephemerals, that's easy 158 | if "ephemeral" in self.push_types: 159 | await self._queue.put(msg) 160 | 161 | elif self.ephemeral_types: # If items in the list 162 | 163 | # See if there is a sub-type in the ephemeral 164 | sub_push = msg.get("push") 165 | if type(sub_push) is dict: 166 | sub_type = sub_push.get("type") 167 | if sub_type is not None and sub_type in self.ephemeral_types: 168 | await self._queue.put(msg) 169 | 170 | # Tickles requested or all messages requested? 171 | if msg.get("type") == "tickle": 172 | 173 | if "tickle" in self.push_types: # All tickles have been requested 174 | await self._queue.put(msg) 175 | 176 | # If we got a push tickle, retrieve pushes 177 | if msg.get("subtype") == "push" and ("push" in self.push_types or not self.push_types): 178 | await self._process_pushbullet_message_tickle_push() 179 | 180 | elif msg.get("subtype") == "device": 181 | self.pb._devices = None 182 | 183 | elif msg.get("subtype") == "chat": 184 | self.pb._chats = None 185 | 186 | elif msg.get("subtype") == "channel": 187 | self.pb._channels = None 188 | 189 | elif "type" in msg and msg["type"] in self.push_types: 190 | # Not sure what "type" this would be, but let's put it there 191 | await self._queue.put(msg) 192 | 193 | else: 194 | pass 195 | # raise Exception("Didn't expect any 'else' code here', msg: {}".format(msg)) 196 | 197 | async def _process_pushbullet_message_tickle_push(self): # , msg: dict): 198 | """When we received a tickle regarding a push.""" 199 | self.log.debug("Received a push tickle. Looking for new pushes...") 200 | await self.pb.async_verify_key() 201 | pushes = await self.pb.async_get_pushes(modified_after=self.pb.most_recent_timestamp, 202 | active_only=self._active_only) 203 | self.log.debug("After a push tickle, retrieved {} pushes".format(len(pushes))) 204 | 205 | # Update timestamp for most recent push so we only get "new" pushes 206 | # if len(pushes) > 0 and pushes[0].get('modified', 0) > self.pb.most_recent_timestamp: 207 | # self.pb.most_recent_timestamp = pushes[0]['modified'] 208 | 209 | # Process each push 210 | for push in pushes: 211 | 212 | # Filter dismissed pushes if requested 213 | if self._ignore_dismissed is not None and bool(push.get("dismissed")): 214 | self.log.debug("Skipped push because it was dismissed: {}".format(push)) 215 | continue # skip this push 216 | 217 | # Filter on device if requested 218 | if self._only_this_device_nickname is not None: 219 | 220 | # Does push have no target at all? 221 | target_iden = push.get("target_device_iden") 222 | if target_iden is None: 223 | self.log.info("Skipped push because it had no target device: {}".format(push)) 224 | continue # skip push 225 | 226 | # Does target device not exist? 227 | # This would be a strange problem but could happen if 228 | # clients have cached devices. 229 | target_dev = await self.pb.async_get_device(iden=target_iden) 230 | if target_dev is None: 231 | self.log.warning( 232 | "Skipped push because the target_device_iden did not map to any known device: {}" 233 | .format(push)) 234 | continue # skip this push 235 | 236 | # Does target device have the wrong name? 237 | if target_dev.nickname != self._only_this_device_nickname: 238 | self.log.debug("Skipped push that was not to target device {}: {}".format( 239 | self._only_this_device_nickname, push 240 | )) 241 | continue # skip push - wrong device 242 | 243 | # Passed all filters - accept push 244 | self.log.debug("Adding to push queue: {}".format(push)) 245 | await self._queue.put(push) 246 | 247 | async def __aexit__(self, exc_type, exc_val, exc_tb): 248 | await self._ws_client.__aexit__(exc_type, exc_val, exc_tb) 249 | await self._ws_client.close() 250 | 251 | def __aiter__(self) -> AsyncIterator[dict]: 252 | return LiveStreamListener._Iterator(self) 253 | 254 | def timeout(self, timeout=None) -> AsyncIterator[dict]: 255 | """Enables the async for loop to have a timeout. 256 | 257 | async for push in listener.timeout(1): 258 | ... 259 | """ 260 | return LiveStreamListener._Iterator(self, timeout=timeout) 261 | 262 | async def next_push(self, timeout: float = None) -> Dict: 263 | if timeout is None: 264 | if self._queue is None: # __aenter__ never called -- call it now 265 | await self.__aenter__() 266 | push = await self._queue.get() 267 | if type(push) == StopAsyncIteration: 268 | raise push 269 | else: 270 | push = await asyncio.wait_for(self.next_push(), timeout=timeout) 271 | 272 | if asyncio.iscoroutinefunction(self._post_process): 273 | return await self._post_process(push) 274 | elif callable(self._post_process): 275 | return self._post_process(push) 276 | else: 277 | return push 278 | 279 | class _Iterator(AsyncIterator): 280 | def __init__(self, pushlistener, timeout: float = None): 281 | self.timeout = timeout 282 | self.parent = pushlistener # type: LiveStreamListener 283 | 284 | def __aiter__(self) -> AsyncIterator[dict]: 285 | return self 286 | 287 | async def __anext__(self) -> dict: 288 | if self.parent.closed: 289 | raise StopAsyncIteration("The websocket has closed.") 290 | 291 | try: 292 | push = await self.parent.next_push(timeout=self.timeout) # Wait here for another push 293 | 294 | except Exception as e: 295 | raise StopAsyncIteration(e) 296 | 297 | if type(push) == StopAsyncIteration: 298 | raise push 299 | 300 | return push 301 | -------------------------------------------------------------------------------- /asyncpushbullet/command_line_push.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | A command line script for sending pushes. 5 | 6 | This file contains entry points for both pbpush and pbtransfer. 7 | 8 | usage: command_line_push.py [-h] [-k KEY] [--key-file KEY_FILE] 9 | [--proxy PROXY] [-t TITLE] [-b BODY] [-d DEVICE] 10 | [--list-devices] [-u URL] [-f FILE] 11 | [--transfer.sh] [-q] [--oauth2] [--debug] [-v] 12 | [--version] 13 | 14 | optional arguments: 15 | -h, --help show this help message and exit 16 | -k KEY, --key KEY Your Pushbullet.com API key 17 | --key-file KEY_FILE Text file containing your Pushbullet.com API key 18 | --proxy PROXY Optional web proxy 19 | -t TITLE, --title TITLE 20 | Title of your push 21 | -b BODY, --body BODY Body of your push (- means read from stdin) 22 | -d DEVICE, --device DEVICE 23 | Destination device nickname 24 | --list-devices List registered device names 25 | -u URL, --url URL URL of link being pushed 26 | -f FILE, --file FILE Pathname to file to push 27 | --transfer.sh Use www.transfer.sh website for uploading files (use 28 | with --file) 29 | -q, --quiet Suppress all output 30 | --oauth2 Register your command line tool using OAuth2 31 | --debug Turn on debug logging 32 | -v, --verbose Turn on verbose logging (INFO messages) 33 | --version show program's version number and exit 34 | 35 | 36 | """ 37 | import argparse 38 | import asyncio 39 | import logging 40 | import os 41 | import sys 42 | from typing import Dict 43 | 44 | from asyncpushbullet import AsyncPushbullet, __version__ 45 | from asyncpushbullet import Device 46 | from asyncpushbullet import InvalidKeyError, PushbulletError 47 | from asyncpushbullet import errors 48 | from asyncpushbullet import oauth2 49 | from asyncpushbullet.command_line_listen import try_to_find_key 50 | 51 | 52 | def main(): 53 | # sys.argv.append("--help") 54 | # sys.argv.append("--quiet") 55 | # sys.argv += ["--key", "badkey"] 56 | # sys.argv += ["--key-file", "../api_key.txt"] 57 | 58 | # sys.argv.append("--oauth2") 59 | # sys.argv.append("--version") 60 | # sys.argv.append("--list-devices") 61 | # sys.argv += ["-t", "test to device", "--device", "baddevice"] 62 | # sys.argv += ["-d", "Kanga"] 63 | # sys.argv += ["-t", "my title", "-b", "foo"] 64 | # sys.argv += ["-t", "Read stdin", "-b", "-"] 65 | 66 | # sys.argv += [ "--file", __file__] 67 | # sys.argv += [__file__] 68 | 69 | # sys.argv += ["--file", "badfile.txt"] 70 | # sys.argv.append("--transfer.sh") 71 | 72 | # sys.argv += ["-t", "foo"] 73 | 74 | main_pbpush() # Default 75 | # main_pbtransfer() 76 | 77 | 78 | def main_pbpush(): 79 | """Intended entry point for pbpush""" 80 | args = parse_args_pbpush() 81 | do_main(args) 82 | 83 | 84 | def main_pbtransfer(): 85 | """Intended entry point for pbtransfer""" 86 | args = parse_args_pbtransfer() 87 | do_main(args) 88 | 89 | 90 | def do_main(args): 91 | loop = asyncio.get_event_loop() 92 | exit_code = None 93 | try: 94 | exit_code = loop.run_until_complete(_run(args)) 95 | except KeyboardInterrupt: 96 | exit_code = errors.__ERR_KEYBOARD_INTERRUPT__ 97 | except Exception as ex: 98 | print("Error:", ex, file=sys.stderr) 99 | exit_code = errors.__ERR_UNKNOWN__ 100 | finally: 101 | return exit_code or 0 102 | 103 | 104 | async def _run(args): 105 | # Logging levels 106 | if args.debug: # Debug? 107 | print("Log level: DEBUG") 108 | logging.basicConfig(level=logging.DEBUG) 109 | elif args.verbose: # Verbose? 110 | print("Log level: INFO") 111 | logging.basicConfig(level=logging.INFO) 112 | 113 | # Request setting up oauth2 access? 114 | if args.oauth2: 115 | token = await oauth2.async_gain_oauth2_access() 116 | if token: 117 | print("Successfully authenticated using OAuth2.") 118 | print("You should now be able to use the command line tools without specifying an API key.") 119 | sys.exit(0) 120 | else: 121 | print("There was a problem authenticating.") 122 | sys.exit(1) 123 | 124 | # Find a valid API key 125 | api_key = try_to_find_key(args, not args.quiet) 126 | if api_key is None: 127 | print("You must specify an API key.", file=sys.stderr) 128 | sys.exit(errors.__ERR_API_KEY_NOT_GIVEN__) 129 | 130 | # Proxy 131 | proxy = lambda: args.proxy or os.environ.get("https_proxy") or os.environ.get("http_proxy") 132 | 133 | try: 134 | # List devices? 135 | if args.list_devices: 136 | print("Devices:") 137 | async with AsyncPushbullet(api_key, proxy=proxy()) as pb: 138 | async for dev in pb.devices_asynciter(): 139 | print("\t", dev.nickname) 140 | return errors.__EXIT_NO_ERROR__ 141 | 142 | # Specify a device? 143 | target_device = None # type: Device 144 | if args.device: 145 | async with AsyncPushbullet(api_key, proxy=proxy()) as pb: 146 | target_device = await pb.async_get_device(nickname=args.device) 147 | 148 | if target_device is None: 149 | print("Device not found:", args.device, file=sys.stderr) 150 | return errors.__ERR_DEVICE_NOT_FOUND__ 151 | else: 152 | print("Target device:", target_device.nickname) 153 | 154 | # Transfer single file? 155 | if getattr(args, "file", False): 156 | async with AsyncPushbullet(api_key, proxy=proxy()) as pb: 157 | return await _transfer_file(pb=pb, 158 | file_path=args.file, 159 | use_transfer_sh=args.transfer_sh, 160 | quiet=args.quiet, 161 | title=args.title, 162 | body=args.body, 163 | target_device=target_device) 164 | 165 | elif getattr(args, "files", False): 166 | async with AsyncPushbullet(api_key, proxy=proxy()) as pb: 167 | for file_path in args.files: # type str 168 | _ = await _transfer_file(pb=pb, 169 | file_path=file_path, 170 | use_transfer_sh=args.transfer_sh, 171 | quiet=args.quiet, 172 | title=args.title, 173 | body=args.body, 174 | target_device=target_device) 175 | 176 | # Push note 177 | elif args.title or args.body: 178 | 179 | async with AsyncPushbullet(api_key, proxy=proxy()) as pb: 180 | if args.body is not None and args.body == "-": 181 | body = sys.stdin.read().rstrip() 182 | else: 183 | body = args.body 184 | url = args.url 185 | if url is None: 186 | if not args.quiet: 187 | print("Pushing note...", end="", flush=True) 188 | _ = await pb.async_push_note(title=args.title, body=body, device=target_device) 189 | if not args.quiet: 190 | print("Done.", flush=True) 191 | else: 192 | if not args.quiet: 193 | print("Pushing link...") 194 | _ = await pb.async_push_link(title=args.title, url=url, body=body, device=target_device) 195 | if not args.quiet: 196 | print("Done.", flush=True) 197 | 198 | else: 199 | print("Nothing to do.") 200 | return errors.__ERR_NOTHING_TO_DO__ 201 | 202 | except InvalidKeyError as exc: 203 | print(exc, file=sys.stderr) 204 | return errors.__ERR_INVALID_API_KEY__ 205 | 206 | except PushbulletError as exc: 207 | print(exc, file=sys.stderr) 208 | return errors.__ERR_CONNECTING_TO_PB__ 209 | 210 | 211 | # Transfer file sub-function 212 | async def _transfer_file(pb: AsyncPushbullet, 213 | file_path: str, 214 | use_transfer_sh: bool = True, 215 | quiet: bool = False, 216 | title: str = None, 217 | body: str = None, 218 | target_device: Device = None): 219 | if not os.path.isfile(file_path): 220 | print("File not found:", file_path, file=sys.stderr) 221 | return errors.__ERR_FILE_NOT_FOUND__ 222 | 223 | show_progress = not quiet 224 | if use_transfer_sh: 225 | if not quiet: 226 | print("Uploading file to transfer.sh ... {}".format(file_path)) 227 | 228 | info: Dict = await pb.async_upload_file_to_transfer_sh(file_path=file_path, 229 | show_progress=show_progress) 230 | 231 | else: 232 | if not quiet: 233 | print("Uploading file to Pushbullet ... {}".format(file_path)) 234 | info: Dict = await pb.async_upload_file(file_path=file_path, 235 | show_progress=show_progress) 236 | 237 | file_url: str = info["file_url"] 238 | file_type: str = info["file_type"] 239 | file_name: str = info["file_name"] 240 | if not quiet: 241 | print("Pushing file ... {}".format(file_url)) 242 | 243 | title = title or "File: {}".format(file_name) 244 | await pb.async_push_file(file_name=file_name, 245 | file_type=file_type, 246 | file_url=file_url, 247 | title=title, 248 | body=body or file_url, 249 | device=target_device) 250 | 251 | 252 | def parse_args_pbpush(): 253 | parser = argparse.ArgumentParser() 254 | parser.add_argument("-k", "--key", help="Your Pushbullet.com API key") 255 | parser.add_argument("--key-file", help="Text file containing your Pushbullet.com API key") 256 | parser.add_argument("--proxy", help="Optional web proxy") 257 | parser.add_argument("-t", "--title", help="Title of your push") 258 | parser.add_argument("-b", "--body", help="Body of your push (- means read from stdin)") 259 | parser.add_argument("-d", "--device", help="Destination device nickname") 260 | parser.add_argument("--list-devices", action="store_true", help="List registered device names") 261 | parser.add_argument("-u", "--url", help="URL of link being pushed") 262 | parser.add_argument("-f", "--file", help="Pathname to file to push") 263 | parser.add_argument("--transfer.sh", dest="transfer_sh", action="store_true", 264 | help="Use www.transfer.sh website for uploading files (use with --file)") 265 | parser.add_argument("-q", "--quiet", action="store_true", help="Suppress all output") 266 | parser.add_argument("--oauth2", action="store_true", help="Register your command line tool using OAuth2") 267 | parser.add_argument("--debug", action="store_true", help="Turn on debug logging") 268 | parser.add_argument("-v", "--verbose", action="store_true", help="Turn on verbose logging (INFO messages)") 269 | parser.add_argument("--version", action="version", version='%(prog)s ' + __version__) 270 | 271 | args = parser.parse_args() 272 | 273 | if len(sys.argv) == 1: 274 | parser.print_help() 275 | sys.exit(errors.__ERR_NOTHING_TO_DO__) 276 | 277 | return args 278 | 279 | 280 | def parse_args_pbtransfer(): 281 | parser = argparse.ArgumentParser() 282 | parser.add_argument("-k", "--key", help="Your Pushbullet.com API key") 283 | parser.add_argument("--key-file", help="Text file containing your Pushbullet.com API key") 284 | parser.add_argument("--proxy", help="Optional web proxy") 285 | parser.add_argument("-d", "--device", help="Destination device nickname") 286 | parser.add_argument("--list-devices", action="store_true", help="List registered device names") 287 | parser.add_argument("-f", "--file", help="Pathname to file to push") 288 | parser.add_argument('files', nargs='*', help="Remaining arguments will be files to push") 289 | parser.add_argument("-q", "--quiet", action="store_true", help="Suppress all output") 290 | parser.add_argument("--oauth2", action="store_true", help="Register your command line tool using OAuth2") 291 | parser.add_argument("--debug", action="store_true", help="Turn on debug logging") 292 | parser.add_argument("-v", "--verbose", action="store_true", help="Turn on verbose logging (INFO messages)") 293 | parser.add_argument("--version", action="version", version='%(prog)s ' + __version__) 294 | 295 | args = parser.parse_args() 296 | 297 | if len(sys.argv) == 1: 298 | parser.print_help() 299 | sys.exit(errors.__ERR_NOTHING_TO_DO__) 300 | 301 | # Emulate the arguments used in the regular push function 302 | setattr(args, "transfer_sh", True) 303 | setattr(args, "title", None) 304 | setattr(args, "body", None) 305 | 306 | return args 307 | 308 | 309 | if __name__ == "__main__": 310 | main() 311 | -------------------------------------------------------------------------------- /examples/guitoolapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tool for managing Pushbullet account 5 | """ 6 | import asyncio 7 | import io 8 | import logging 9 | import os 10 | import pprint 11 | import sys 12 | import tkinter as tk 13 | from functools import partial 14 | from tkinter import ttk 15 | from typing import Tuple 16 | 17 | from tk_asyncio_base import TkAsyncioBaseApp 18 | 19 | try: 20 | from PIL import Image 21 | from PIL.ImageTk import PhotoImage 22 | except ImportError as ie: 23 | print("To include image support: pip install pillow") 24 | 25 | import tkinter_tools 26 | 27 | sys.path.append("..") # Since examples are buried one level into source tree 28 | from asyncpushbullet import Device, oauth2 29 | from asyncpushbullet import AsyncPushbullet 30 | from asyncpushbullet import LiveStreamListener 31 | from asyncpushbullet.prefs import Prefs 32 | 33 | __author__ = 'Robert Harder' 34 | __email__ = "rob@iharder.net" 35 | 36 | PREFS = Prefs("asyncpushbullet.guitoolapp", "net.iharder.asyncpushbullet") 37 | API_KEY = PREFS.get("api_key") 38 | 39 | 40 | class GuiToolApp(TkAsyncioBaseApp): 41 | def __init__(self, root): 42 | super().__init__(root) 43 | self.window = root 44 | root.title("Pushbullet Account Management") 45 | self.log = logging.getLogger(__name__) 46 | 47 | # Data 48 | self._pushbullet = None # type: AsyncPushbullet 49 | self.pushbullet_listener = None # type: LiveStreamListener 50 | self.key_var = tk.StringVar() # type: tk.StringVar # API key 51 | self.pushes_var = tk.StringVar() # type: tk.StringVar # Used in text box to display pushes received 52 | self.status_var = tk.StringVar() # type: tk.StringVar # Bound to bottom of window status bar 53 | self.proxy = os.environ.get("https_proxy") or os.environ.get("http_proxy") # type: str 54 | 55 | # Related to Devices 56 | self.device_detail_var = tk.StringVar() # type: tk.StringVar # Used in text box to display device details 57 | self.devices_in_listbox = None # type: Tuple[Device] # Cached devices that were retrieved 58 | self.device_tab_index = None # type: int # The index for the Devices tab 59 | 60 | # View / Control 61 | self.btn_connect = None # type: tk.Button 62 | self.btn_disconnect = None # type: tk.Button 63 | self.lb_device = None # type: tk.Listbox 64 | self.btn_load_devices = None # type: tk.Button 65 | self.lbl_photo = None # type: tk.Label 66 | self.lbl_status = None # type: tk.Label 67 | self.create_widgets() 68 | 69 | # Connections / Bindings 70 | tkinter_tools.bind_tk_var_to_method(partial(PREFS.set, "api_key"), self.key_var) 71 | self.key_var.set(API_KEY) 72 | 73 | @property 74 | def status(self): 75 | return str(self.status_var.get()) 76 | 77 | @status.setter 78 | def status(self, val): 79 | self.tk(self.status_var.set, val) 80 | 81 | @property 82 | def pushbullet(self) -> AsyncPushbullet: 83 | current_key = self.key_var.get() 84 | if self._pushbullet is not None: 85 | if current_key != self._pushbullet.api_key: 86 | self._pushbullet.close_all_threadsafe() 87 | self._pushbullet = None 88 | if self._pushbullet is None: 89 | self._pushbullet = AsyncPushbullet(api_key=current_key, 90 | # loop=self.ioloop, 91 | verify_ssl=False, 92 | proxy=self.proxy) 93 | 94 | return self._pushbullet 95 | 96 | @pushbullet.setter 97 | def pushbullet(self, val: AsyncPushbullet): 98 | if val is None and self._pushbullet is not None: 99 | self._pushbullet.close_all_threadsafe() 100 | self._pushbullet = val 101 | 102 | def ioloop_exception_happened(self, extype, ex, tb, func): 103 | self.status = ex 104 | 105 | def create_widgets(self): 106 | parent = self.window 107 | 108 | # API Key 109 | frm_key = tk.Frame(parent) 110 | frm_key.grid(row=0, column=0, sticky="NSEW") 111 | lbl_key = tk.Label(frm_key, text="API Key:") 112 | lbl_key.grid(row=0, column=0, sticky=tk.W) 113 | txt_key = tk.Entry(frm_key, textvariable=self.key_var) 114 | txt_key.grid(row=0, column=1, sticky=tk.W + tk.E, columnspan=2) 115 | btn_oauth2 = tk.Button(frm_key, text="Authenticate online...") 116 | btn_oauth2.configure(command=partial(self.oauth2_clicked, btn_oauth2)) 117 | btn_oauth2.grid(row=0, column=2, sticky=tk.W) 118 | tk.Grid.grid_columnconfigure(frm_key, 1, weight=1) 119 | tk.Grid.grid_columnconfigure(parent, 0, weight=1) 120 | 121 | # Top level notebook 122 | notebook = ttk.Notebook(parent) 123 | notebook.grid(row=1, column=0, sticky="NSEW", columnspan=2) 124 | # tk.Grid.grid_columnconfigure(parent, 0, weight=1) 125 | tk.Grid.grid_rowconfigure(parent, 1, weight=1) 126 | 127 | notebook.bind("<>", self.notebook_tab_changed) 128 | 129 | # Status line 130 | status_line = tk.Frame(parent, borderwidth=2, relief=tk.GROOVE) 131 | status_line.grid(row=999, column=0, sticky="EW", columnspan=2) 132 | self.lbl_photo = tk.Label(status_line) # , text="", width=16, height=16) 133 | self.lbl_photo.grid(row=0, column=0, sticky=tk.W) 134 | self.lbl_status = tk.Label(status_line, textvar=self.status_var) 135 | self.lbl_status.grid(row=0, column=1, sticky=tk.W) 136 | 137 | # Tab: Pushes 138 | pushes_frame = tk.Frame(notebook) 139 | notebook.add(pushes_frame, text="Pushes") 140 | self.create_widgets_pushes(pushes_frame) 141 | 142 | # Tab: Devices 143 | devices_frame = tk.Frame(notebook) 144 | notebook.add(devices_frame, text="Devices") 145 | self.device_tab_index = notebook.index(tk.END) - 1 # save tab pos for later 146 | self.create_widgets_devices(devices_frame) 147 | 148 | def create_widgets_pushes(self, parent: tk.Frame): 149 | 150 | self.btn_connect = tk.Button(parent, text="Connect", command=self.connect_button_clicked) 151 | self.btn_connect.grid(row=0, column=0, sticky=tk.W) 152 | 153 | self.btn_disconnect = tk.Button(parent, text="Disconnect", command=self.disconnect_button_clicked) 154 | self.btn_disconnect.grid(row=0, column=1, sticky=tk.W) 155 | self.btn_disconnect.configure(state=tk.DISABLED) 156 | 157 | btn_clear = tk.Button(parent, text="Clear", command=partial(self.pushes_var.set, "")) 158 | btn_clear.grid(row=0, column=2) 159 | 160 | txt_data = tkinter_tools.BindableTextArea(parent, textvariable=self.pushes_var, width=60, height=20) 161 | txt_data.grid(row=1, column=0, sticky="NSEW", columnspan=3) 162 | tk.Grid.grid_columnconfigure(parent, 0, weight=1) 163 | tk.Grid.grid_columnconfigure(parent, 1, weight=1) 164 | tk.Grid.grid_rowconfigure(parent, 1, weight=1) 165 | 166 | def create_widgets_devices(self, parent: tk.Frame): 167 | 168 | scrollbar = tk.Scrollbar(parent, orient=tk.VERTICAL) 169 | self.lb_device = tk.Listbox(parent, yscrollcommand=scrollbar.set) 170 | scrollbar.config(command=self.lb_device.yview) 171 | self.lb_device.grid(row=0, column=0, sticky="NSEW") 172 | self.lb_device.bind("", self.device_list_double_clicked) 173 | 174 | self.btn_load_devices = tk.Button(parent, text="Load Devices", command=self.load_devices_clicked) 175 | self.btn_load_devices.grid(row=1, column=0, sticky="EW") 176 | 177 | txt_device_details = tkinter_tools.BindableTextArea(parent, textvariable=self.device_detail_var, width=80, 178 | height=10) 179 | txt_device_details.grid(row=0, column=1, sticky="NSEW") 180 | tk.Grid.grid_columnconfigure(parent, 1, weight=1) 181 | tk.Grid.grid_rowconfigure(parent, 0, weight=1) 182 | 183 | # ######## G U I E V E N T S ######## 184 | 185 | def notebook_tab_changed(self, event): 186 | nb = event.widget # type: ttk.Notebook 187 | index = nb.index("current") 188 | if index == self.device_tab_index: 189 | # If there are no devices loaded, go ahead and try 190 | if self.devices_in_listbox is None: 191 | self.load_devices_clicked() 192 | 193 | def oauth2_clicked(self, btn: tk.Button): 194 | btn.configure(state=tk.DISABLED) 195 | self.status = "Authenticating online using OAuth2..." 196 | 197 | async def _auth(): 198 | token = await oauth2.async_gain_oauth2_access() 199 | if token: 200 | self.tk(self.key_var.set, token) 201 | self.status = "Authentication using OAuth2 succeeded." 202 | else: 203 | self.status = "Authentication using OAuth2 failed." 204 | btn.configure(state=tk.NORMAL) 205 | 206 | self.io(_auth()) 207 | 208 | def connect_button_clicked(self): 209 | self.status = "Connecting to Pushbullet..." 210 | self.btn_connect.configure(state=tk.DISABLED) 211 | self.btn_disconnect.configure(state=tk.DISABLED) 212 | 213 | if self.pushbullet is not None: 214 | self.pushbullet = None 215 | if self.pushbullet_listener is not None: 216 | pl = self.pushbullet_listener # type: LiveStreamListener 217 | if pl is not None: 218 | self.io(pl.close()) 219 | self.pushbullet_listener = None 220 | 221 | async def _listen(): 222 | pl2 = None # type: LiveStreamListener 223 | try: 224 | await self.verify_key() 225 | async with LiveStreamListener(self.pushbullet, types=()) as pl2: 226 | self.pushbullet_listener = pl2 227 | await self.pushlistener_connected(pl2) 228 | 229 | async for push in pl2: 230 | await self.push_received(push, pl2) 231 | 232 | except Exception as ex: 233 | pass 234 | print("guitool _listen caught exception", ex) 235 | finally: 236 | # if pl2 is not None: 237 | await self.pushlistener_closed(pl2) 238 | 239 | self.io(_listen()) 240 | 241 | def disconnect_button_clicked(self): 242 | self.status = "Disconnecting from Pushbullet..." 243 | self.io(self.pushbullet_listener.close()) 244 | 245 | def load_devices_clicked(self): 246 | self.btn_load_devices.configure(state=tk.DISABLED) 247 | self.status = "Loading devices..." 248 | self.lb_device.delete(0, tk.END) 249 | self.lb_device.insert(tk.END, "Loading...") 250 | self.devices_in_listbox = None 251 | 252 | async def _load(): 253 | try: 254 | await self.verify_key() 255 | self.devices_in_listbox = tuple(await self.pushbullet.async_get_devices()) 256 | self.tk(self.lb_device.delete, 0, tk.END) 257 | for dev in self.devices_in_listbox: 258 | self.tk(self.lb_device.insert, tk.END, str(dev.nickname)) 259 | self.status = "Loaded {} devices".format(len(self.devices_in_listbox)) 260 | 261 | except Exception as ex: 262 | self.tk(self.lb_device.delete, 0, tk.END) 263 | self.status = "Error retrieving devices: {}".format(ex) 264 | raise ex 265 | finally: 266 | self.tk(self.btn_load_devices.configure, state=tk.NORMAL) 267 | 268 | # asyncio.run_coroutine_threadsafe(_load(), self.ioloop) 269 | self.io(_load()) 270 | 271 | def device_list_double_clicked(self, event): 272 | items = self.lb_device.curselection() 273 | if len(items) == 0: 274 | print("No item selected") 275 | return 276 | 277 | if self.devices_in_listbox is None: 278 | print("No devices have been loaded") 279 | 280 | device = self.devices_in_listbox[int(items[0])] 281 | self.device_detail_var.set(repr(device)) 282 | 283 | # ######## C A L L B A C K S ######## 284 | 285 | async def pushlistener_connected(self, listener: LiveStreamListener): 286 | self.status = "Connected to Pushbullet" 287 | try: 288 | me = await self.pushbullet.async_get_user() 289 | self.status = "Connected to Pushbullet: {}".format(me.get("name")) 290 | 291 | except Exception as ex: 292 | # print("To include image support: pip install pillow") 293 | pass 294 | finally: 295 | self.tk(self.btn_connect.configure, state=tk.DISABLED) 296 | self.tk(self.btn_disconnect.configure, state=tk.NORMAL) 297 | 298 | async def pushlistener_closed(self, listener: LiveStreamListener): 299 | # print_function_name() 300 | self.status = "Disconnected from Pushbullet" 301 | self.tk(self.btn_connect.configure, state=tk.NORMAL) 302 | self.tk(self.btn_disconnect.configure, state=tk.DISABLED) 303 | 304 | async def push_received(self, p: dict, listener: LiveStreamListener): 305 | # print("Push received:", p) 306 | push_type = p.get("type") 307 | if push_type == "push": 308 | push_type = "ephemeral" 309 | prev = self.pushes_var.get() 310 | prev += "Type: {}\n{}\n\n".format(push_type, pprint.pformat(p)) 311 | self.tk(self.pushes_var.set, prev) 312 | 313 | # ######## O T H E R ######## 314 | 315 | async def verify_key(self): 316 | self.status = "Verifying API key..." 317 | api_key = self.key_var.get() 318 | 319 | try: 320 | await self.pushbullet.async_verify_key() 321 | self.status = "Valid API key: {}".format(api_key) 322 | 323 | if getattr(self.lbl_photo, "image_ref", None) is None: 324 | async def _load_pic(): 325 | try: 326 | me = await self.pushbullet.async_get_user() 327 | if "image_url" in me: 328 | image_url = me.get("image_url") 329 | try: 330 | msg = await self.pushbullet._async_get_data(image_url) 331 | except Exception as ex_get: 332 | self.log.info("Could not retrieve user photo from url {}".format(image_url)) 333 | else: 334 | photo_bytes = io.BytesIO(msg.get("raw")) 335 | img = Image.open(photo_bytes) 336 | label_size = self.lbl_photo.winfo_height() 337 | img = img.resize((label_size, label_size), Image.ANTIALIAS) 338 | photo = PhotoImage(img) 339 | self.tk(self.lbl_photo.configure, image=photo) 340 | self.lbl_photo.image_ref = photo # Save for garbage collection protection 341 | self.log.info("Loaded user image from url {}".format(image_url)) 342 | 343 | except Exception as ex: 344 | # print(ex) 345 | print("To include image support: pip install pillow") 346 | # ex.with_traceback() 347 | # raise ex 348 | 349 | asyncio.get_event_loop().create_task(_load_pic()) 350 | 351 | except Exception as e: 352 | self.status = "Invalid API key: {}".format(api_key) 353 | self.tk(self.lbl_photo.configure, image="") 354 | self.lbl_photo.image_ref = None 355 | raise e 356 | 357 | 358 | def main(): 359 | tk1 = tk.Tk() 360 | _ = GuiToolApp(tk1) 361 | tk1.mainloop() 362 | 363 | 364 | if __name__ == '__main__': 365 | try: 366 | main() 367 | except KeyboardInterrupt: 368 | print("Quitting") 369 | pass 370 | --------------------------------------------------------------------------------