pythonhere 0.1.4__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonhere/__init__.py +1 -1
- pythonhere/android_here.py +100 -0
- pythonhere/data/logo/logo-128.png +0 -0
- pythonhere/data/logo/logo-32.png +0 -0
- pythonhere/data/logo/logo-splash.png +0 -0
- pythonhere/enum_here.py +1 -0
- pythonhere/exception_manager_here.kv +22 -0
- pythonhere/exception_manager_here.py +14 -30
- pythonhere/launcher_here.py +46 -0
- pythonhere/magic_here/shortcuts.py +21 -5
- pythonhere/main.py +131 -34
- pythonhere/network_here.py +4 -3
- pythonhere/patches_here.py +1 -2
- pythonhere/pythonhere.kv +49 -0
- pythonhere/server_here.py +29 -26
- pythonhere/ui_here/actionbar_here.kv +8 -0
- pythonhere/ui_here/common_here.kv +14 -0
- pythonhere/ui_here/connection_address_here.kv +32 -0
- pythonhere/ui_here/connection_address_here.py +2 -2
- pythonhere/ui_here/layout_here.py +1 -0
- pythonhere/ui_here/server_screen_here.kv +57 -0
- pythonhere/ui_here/server_screen_here.py +4 -3
- pythonhere/ui_here/settings_here.kv +31 -0
- pythonhere/ui_here/settings_here.py +15 -6
- pythonhere/version_here.py +1 -1
- pythonhere/window_here.py +10 -3
- pythonhere-0.2.0.dist-info/METADATA +125 -0
- pythonhere-0.2.0.dist-info/RECORD +33 -0
- {pythonhere-0.1.4.dist-info → pythonhere-0.2.0.dist-info}/WHEEL +1 -1
- pythonhere-0.1.4.dist-info/METADATA +0 -139
- pythonhere-0.1.4.dist-info/RECORD +0 -21
- {pythonhere-0.1.4.dist-info → pythonhere-0.2.0.dist-info/licenses}/LICENSE +0 -0
- {pythonhere-0.1.4.dist-info → pythonhere-0.2.0.dist-info}/top_level.txt +0 -0
pythonhere/__init__.py
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Android specific functions."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=invalid-name,import-error,import-outside-toplevel
|
|
4
|
+
import uuid
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from android import activity as android_activity
|
|
8
|
+
from jnius import autoclass, cast
|
|
9
|
+
from kivy.logger import Logger
|
|
10
|
+
|
|
11
|
+
Context = autoclass("android.content.Context")
|
|
12
|
+
Icon = autoclass("android.graphics.drawable.Icon")
|
|
13
|
+
Intent = autoclass("android.content.Intent")
|
|
14
|
+
PythonActivity = autoclass("org.kivy.android.PythonActivity")
|
|
15
|
+
ShortcutInfoBuilder = autoclass("android.content.pm.ShortcutInfo$Builder")
|
|
16
|
+
System = autoclass("java.lang.System")
|
|
17
|
+
Uri = autoclass("android.net.Uri")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_current_intent() -> Intent:
|
|
21
|
+
"""Return the intent that started Python activity."""
|
|
22
|
+
return PythonActivity.mActivity.getIntent()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_startup_script(intent: None = None) -> str | None:
|
|
26
|
+
"""Return script entrypoint that was passed to a given, or current, intent."""
|
|
27
|
+
if not intent:
|
|
28
|
+
intent = get_current_intent()
|
|
29
|
+
data = intent.getData()
|
|
30
|
+
return data and data.toString()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def restart_app(script: str = None):
|
|
34
|
+
"""Restart app, with a script as a starting point if provided."""
|
|
35
|
+
Logger.info("PythonHere: restart requested with a script: %s", script)
|
|
36
|
+
activity = PythonActivity.mActivity
|
|
37
|
+
intent = Intent(activity.getApplicationContext(), PythonActivity)
|
|
38
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
39
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
40
|
+
|
|
41
|
+
if script:
|
|
42
|
+
intent.setData(Uri.parse(script))
|
|
43
|
+
|
|
44
|
+
activity.startActivity(intent)
|
|
45
|
+
System.exit(0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bind_run_script_on_new_intent():
|
|
49
|
+
"""Add handler for new intent event:
|
|
50
|
+
restart app with entrypoint of a new intent.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def on_new_intent(intent):
|
|
54
|
+
Logger.info("PythonHere: on_new_intent")
|
|
55
|
+
restart_app(get_startup_script(intent))
|
|
56
|
+
Logger.error("PythonHere: app was not restarted")
|
|
57
|
+
|
|
58
|
+
android_activity.bind(on_new_intent=on_new_intent)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_shortcut_icon() -> Icon:
|
|
62
|
+
"""Create icon to use for a shurtcut."""
|
|
63
|
+
activity = PythonActivity.mActivity
|
|
64
|
+
Drawable = autoclass(f"{activity.getPackageName()}.R$drawable")
|
|
65
|
+
context = cast("android.content.Context", activity.getApplicationContext())
|
|
66
|
+
return Icon.createWithResource(context, Drawable.icon)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resolve_script_path(script: str) -> str:
|
|
70
|
+
"""Resolve path against upload directory."""
|
|
71
|
+
from kivy.app import App
|
|
72
|
+
|
|
73
|
+
if script.startswith("/"):
|
|
74
|
+
path = Path(script)
|
|
75
|
+
else:
|
|
76
|
+
app = App.get_running_app()
|
|
77
|
+
path = Path(app.upload_dir) / script
|
|
78
|
+
return str(path.resolve(strict=True))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def pin_shortcut(script: str, label: str):
|
|
82
|
+
"""Request a pinned shortcut creation to run a Python script."""
|
|
83
|
+
activity = PythonActivity.mActivity
|
|
84
|
+
context = cast("android.content.Context", activity.getApplicationContext())
|
|
85
|
+
|
|
86
|
+
intent = Intent(activity.getApplicationContext(), PythonActivity)
|
|
87
|
+
intent.setAction(Intent.ACTION_MAIN)
|
|
88
|
+
intent.setData(Uri.parse(resolve_script_path(script)))
|
|
89
|
+
|
|
90
|
+
shortcut = (
|
|
91
|
+
ShortcutInfoBuilder(context, f"pythonhere-{uuid.uuid4().hex}")
|
|
92
|
+
.setShortLabel(label)
|
|
93
|
+
.setLongLabel(label)
|
|
94
|
+
.setIntent(intent)
|
|
95
|
+
.setIcon(create_shortcut_icon())
|
|
96
|
+
.build()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
manager = activity.getSystemService(Context.SHORTCUT_SERVICE)
|
|
100
|
+
manager.requestPinShortcut(shortcut, None)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
pythonhere/enum_here.py
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<-UnhandledExceptionPopupHere>:
|
|
2
|
+
title: "Unhandled Exception catched"
|
|
3
|
+
BoxLayout:
|
|
4
|
+
orientation: 'vertical'
|
|
5
|
+
padding: 10
|
|
6
|
+
spacing: 20
|
|
7
|
+
Label:
|
|
8
|
+
size_hint_y: None
|
|
9
|
+
font_size: '18sp'
|
|
10
|
+
height: '24sp'
|
|
11
|
+
text: 'Exception details: '
|
|
12
|
+
ScrollView:
|
|
13
|
+
CodeInput:
|
|
14
|
+
id: catched_exception_code_input_here
|
|
15
|
+
text: root.message
|
|
16
|
+
size_hint: 1, None
|
|
17
|
+
height: self.minimum_height
|
|
18
|
+
Button:
|
|
19
|
+
size_hint_y: None
|
|
20
|
+
height: '40sp'
|
|
21
|
+
text: 'OK, continue'
|
|
22
|
+
on_press: root.dismiss()
|
|
@@ -1,46 +1,26 @@
|
|
|
1
1
|
"""App exceptions manager."""
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import traceback
|
|
4
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from kivy.base import (
|
|
7
8
|
ExceptionHandler,
|
|
8
9
|
ExceptionManager,
|
|
9
10
|
)
|
|
10
11
|
from kivy.clock import Clock
|
|
11
|
-
from kivy.properties import StringProperty # pylint: disable=no-name-in-module
|
|
12
12
|
from kivy.lang import Builder
|
|
13
13
|
from kivy.logger import Logger
|
|
14
|
+
from kivy.properties import StringProperty # pylint: disable=no-name-in-module
|
|
14
15
|
from kivy.uix.popup import Popup
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def load_exception_popup_style():
|
|
18
19
|
"""Load KV rules for `UnhandledExceptionPopupHere`."""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
orientation: 'vertical'
|
|
24
|
-
padding: 10
|
|
25
|
-
spacing: 20
|
|
26
|
-
Label:
|
|
27
|
-
size_hint_y: None
|
|
28
|
-
font_size: '18sp'
|
|
29
|
-
height: '24sp'
|
|
30
|
-
text: 'Exception details: '
|
|
31
|
-
ScrollView:
|
|
32
|
-
CodeInput:
|
|
33
|
-
id: catched_exception_code_input_here
|
|
34
|
-
text: root.message
|
|
35
|
-
size_hint: 1, None
|
|
36
|
-
height: self.minimum_height
|
|
37
|
-
Button:
|
|
38
|
-
size_hint_y: None
|
|
39
|
-
height: '40sp'
|
|
40
|
-
text: 'OK, continue'
|
|
41
|
-
on_press: root.dismiss()
|
|
42
|
-
"""
|
|
43
|
-
)
|
|
20
|
+
kv_path = str(Path(__file__).with_suffix(".kv"))
|
|
21
|
+
|
|
22
|
+
if kv_path not in Builder.files:
|
|
23
|
+
Builder.load_file(kv_path)
|
|
44
24
|
|
|
45
25
|
|
|
46
26
|
class ErrorMessageOnException(ExceptionHandler):
|
|
@@ -48,9 +28,9 @@ class ErrorMessageOnException(ExceptionHandler):
|
|
|
48
28
|
|
|
49
29
|
def handle_exception(self, exception) -> int:
|
|
50
30
|
"""Handle a exception."""
|
|
51
|
-
Logger.exception("Unhandled Exception catched")
|
|
52
31
|
if isinstance(exception, (asyncio.CancelledError, KeyboardInterrupt)):
|
|
53
32
|
return ExceptionManager.RAISE
|
|
33
|
+
Logger.exception("Unhandled Exception catched")
|
|
54
34
|
show_exception_popup()
|
|
55
35
|
return ExceptionManager.PASS
|
|
56
36
|
|
|
@@ -63,10 +43,14 @@ class UnhandledExceptionPopupHere(Popup):
|
|
|
63
43
|
|
|
64
44
|
def install_exception_handler():
|
|
65
45
|
"""Install `ErrorMessageOnException` exception handler."""
|
|
66
|
-
|
|
46
|
+
if not any(
|
|
47
|
+
isinstance(handler, ErrorMessageOnException)
|
|
48
|
+
for handler in ExceptionManager.handlers
|
|
49
|
+
):
|
|
50
|
+
ExceptionManager.add_handler(ErrorMessageOnException())
|
|
67
51
|
|
|
68
52
|
|
|
69
|
-
def show_exception_popup(exc:
|
|
53
|
+
def show_exception_popup(exc: Exception | None = None):
|
|
70
54
|
"""Show exception popup."""
|
|
71
55
|
load_exception_popup_style()
|
|
72
56
|
if exc:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Utilities for launching scripts."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import runpy
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from kivy import platform
|
|
9
|
+
from kivy.logger import Logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_script(script: str):
|
|
13
|
+
"""Execute given script."""
|
|
14
|
+
Logger.info("PythonHere: Run script %s", script)
|
|
15
|
+
try:
|
|
16
|
+
path = Path(script).resolve(strict=True)
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
Logger.error("Script not found: %s", script)
|
|
19
|
+
raise Exception(f"Script not found: {script}") from None
|
|
20
|
+
|
|
21
|
+
original_cwd = str(Path.cwd())
|
|
22
|
+
original_sys_path = sys.path[:]
|
|
23
|
+
try:
|
|
24
|
+
script_dir = path.parent
|
|
25
|
+
os.chdir(str(script_dir))
|
|
26
|
+
sys.path.insert(0, str(script_dir))
|
|
27
|
+
runpy.run_path(str(path), run_name="__main__")
|
|
28
|
+
finally:
|
|
29
|
+
os.chdir(original_cwd)
|
|
30
|
+
sys.path = original_sys_path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def try_startup_script():
|
|
34
|
+
"""Execute startup script, if it was passed to app."""
|
|
35
|
+
if platform != "android":
|
|
36
|
+
return
|
|
37
|
+
import android_here # pylint: disable=import-outside-toplevel
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
android_here.bind_run_script_on_new_intent()
|
|
41
|
+
script = android_here.get_startup_script()
|
|
42
|
+
if script:
|
|
43
|
+
run_script(script)
|
|
44
|
+
except Exception:
|
|
45
|
+
Logger.exception("PythonHere: Error while starting script")
|
|
46
|
+
raise
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
from base64 import b64decode
|
|
5
5
|
from io import BytesIO, StringIO
|
|
6
6
|
|
|
7
|
-
from PIL import Image as PILImage
|
|
8
|
-
from IPython.display import display
|
|
9
7
|
import click
|
|
10
|
-
from herethere.there.commands import
|
|
11
|
-
|
|
8
|
+
from herethere.there.commands import there_code_shortcut, there_group
|
|
9
|
+
from IPython.display import display
|
|
10
|
+
from PIL import Image as PILImage
|
|
12
11
|
|
|
13
12
|
KV_COMMAND_TEMPLATE = r"""
|
|
14
13
|
from window_here import load_kv_string
|
|
@@ -21,6 +20,11 @@ from window_here import encoded_screenshot
|
|
|
21
20
|
sys.stderr.write(encoded_screenshot())
|
|
22
21
|
"""
|
|
23
22
|
|
|
23
|
+
PIN_COMMAND_TEMPLATE = """
|
|
24
|
+
from android_here import pin_shortcut
|
|
25
|
+
pin_shortcut(script="{script}", label="{label}")
|
|
26
|
+
"""
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
@there_code_shortcut
|
|
26
30
|
@click.option(
|
|
@@ -62,10 +66,22 @@ def screenshot(ctx, width, output):
|
|
|
62
66
|
img = PILImage.open(BytesIO(data)).convert("RGB")
|
|
63
67
|
if width:
|
|
64
68
|
height = int(width * img.size[1] // img.size[0])
|
|
65
|
-
|
|
69
|
+
resampling = getattr(PILImage, "Resampling", PILImage)
|
|
70
|
+
resample_filter = getattr(resampling, "LANCZOS", PILImage.LANCZOS)
|
|
71
|
+
img = img.resize((width, height), resample_filter)
|
|
66
72
|
|
|
67
73
|
if output:
|
|
68
74
|
img.save(output)
|
|
69
75
|
output.close()
|
|
70
76
|
|
|
71
77
|
display(img)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@there_code_shortcut
|
|
81
|
+
@click.option("-l", "--label", type=str, default="", help="Label for shortcut")
|
|
82
|
+
@click.argument("script", nargs=1)
|
|
83
|
+
def pin(code: str, script: str, label: str) -> str: # pylint: disable=unused-argument
|
|
84
|
+
"""Create pinned shortcut to run a Python script from Android home screen."""
|
|
85
|
+
if not label:
|
|
86
|
+
label = script.split("/")[-1]
|
|
87
|
+
return PIN_COMMAND_TEMPLATE.format(script=script, label=label)
|
pythonhere/main.py
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
"""PythonHere app."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=wrong-import-order,wrong-import-position
|
|
4
|
+
|
|
5
|
+
from launcher_here import try_startup_script
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
try_startup_script() # run script entrypoint, if it was passed
|
|
9
|
+
except Exception as exc:
|
|
10
|
+
startup_script_exception = exc # pylint: disable=invalid-name
|
|
11
|
+
else:
|
|
12
|
+
startup_script_exception = None # pylint: disable=invalid-name
|
|
13
|
+
|
|
2
14
|
import asyncio
|
|
3
15
|
import os
|
|
4
|
-
from pathlib import Path
|
|
5
16
|
import sys
|
|
6
|
-
|
|
17
|
+
import threading
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
7
20
|
|
|
21
|
+
from enum_here import ScreenName, ServerState
|
|
22
|
+
from exception_manager_here import install_exception_handler, show_exception_popup
|
|
8
23
|
from kivy.app import App
|
|
24
|
+
from kivy.clock import Clock
|
|
25
|
+
from kivy.config import Config, ConfigParser
|
|
9
26
|
from kivy.logger import Logger
|
|
10
|
-
|
|
11
|
-
from enum_here import ScreenName
|
|
12
|
-
from exception_manager_here import install_exception_handler
|
|
13
27
|
from patches_here import monkeypatch_kivy
|
|
14
28
|
from server_here import run_ssh_server
|
|
15
29
|
from window_here import reset_window_environment
|
|
@@ -22,13 +36,25 @@ class PythonHereApp(App):
|
|
|
22
36
|
|
|
23
37
|
def __init__(self):
|
|
24
38
|
super().__init__()
|
|
25
|
-
self.server_task = None
|
|
39
|
+
self.server_task: asyncio.Task | None = None
|
|
40
|
+
self.app_task: asyncio.Task | None = None
|
|
41
|
+
self.asyncio_loop: asyncio.AbstractEventLoop | None = None
|
|
26
42
|
self.settings = None
|
|
43
|
+
|
|
44
|
+
# Created once a running asyncio loop exists.
|
|
45
|
+
self.ssh_server_config_ready: asyncio.Event | None = None
|
|
46
|
+
self.ssh_server_started: asyncio.Event | None = None
|
|
47
|
+
self.ssh_server_connected: asyncio.Event | None = None
|
|
48
|
+
|
|
49
|
+
self.ssh_server_namespace = {}
|
|
50
|
+
self.icon = "data/logo/logo-32.png"
|
|
51
|
+
|
|
52
|
+
def init_asyncio_state(self):
|
|
53
|
+
"""Initialize asyncio-owned state after the event loop is running."""
|
|
54
|
+
self.asyncio_loop = asyncio.get_running_loop()
|
|
27
55
|
self.ssh_server_config_ready = asyncio.Event()
|
|
28
56
|
self.ssh_server_started = asyncio.Event()
|
|
29
57
|
self.ssh_server_connected = asyncio.Event()
|
|
30
|
-
self.ssh_server_namespace = {}
|
|
31
|
-
self.icon = "data/logo/logo-32.png"
|
|
32
58
|
|
|
33
59
|
@property
|
|
34
60
|
def upload_dir(self) -> str:
|
|
@@ -38,59 +64,120 @@ class PythonHereApp(App):
|
|
|
38
64
|
upload_dir.mkdir(exist_ok=True)
|
|
39
65
|
return str(upload_dir)
|
|
40
66
|
|
|
67
|
+
@property
|
|
68
|
+
def config_path(self) -> str:
|
|
69
|
+
"""Path to the application config file."""
|
|
70
|
+
root_dir = Path(self.user_data_dir or ".").resolve()
|
|
71
|
+
return str(root_dir / "config.ini")
|
|
72
|
+
|
|
73
|
+
def load_config(self) -> ConfigParser:
|
|
74
|
+
"""Returning the application configuration."""
|
|
75
|
+
Config.read(self.config_path) # Override the configuration file location
|
|
76
|
+
return super().load_config()
|
|
77
|
+
|
|
41
78
|
def build(self):
|
|
42
79
|
"""Initialize application UI."""
|
|
43
80
|
super().build()
|
|
44
81
|
install_exception_handler()
|
|
45
|
-
|
|
46
82
|
self.settings = self.root.ids.settings
|
|
47
|
-
|
|
48
83
|
self.ssh_server_namespace.update(
|
|
49
84
|
{
|
|
50
85
|
"app": self,
|
|
51
86
|
"root": self.root,
|
|
52
87
|
}
|
|
53
88
|
)
|
|
54
|
-
|
|
55
89
|
self.update_server_config_status()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
90
|
+
if startup_script_exception:
|
|
91
|
+
Clock.schedule_once(
|
|
92
|
+
lambda _: show_exception_popup(startup_script_exception), 0
|
|
93
|
+
)
|
|
61
94
|
|
|
62
|
-
def run_app(self):
|
|
63
|
-
"""Run application and SSH server
|
|
64
|
-
self.
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
async def run_app(self):
|
|
96
|
+
"""Run application and SSH server until either main task exits."""
|
|
97
|
+
self.init_asyncio_state()
|
|
98
|
+
|
|
99
|
+
self.server_task = asyncio.create_task(run_ssh_server(self))
|
|
100
|
+
self.app_task = asyncio.create_task(self.async_run_app())
|
|
101
|
+
|
|
102
|
+
tasks = (self.server_task, self.app_task)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
done, pending = await asyncio.wait(
|
|
106
|
+
tasks,
|
|
107
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for task in pending:
|
|
111
|
+
task.cancel()
|
|
112
|
+
|
|
113
|
+
if pending:
|
|
114
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
115
|
+
|
|
116
|
+
for task in done:
|
|
117
|
+
if task.cancelled():
|
|
118
|
+
continue
|
|
119
|
+
exc = task.exception()
|
|
120
|
+
if exc is not None:
|
|
121
|
+
raise exc
|
|
122
|
+
finally:
|
|
123
|
+
await self.cancel_app_tasks()
|
|
67
124
|
|
|
68
125
|
async def async_run_app(self):
|
|
69
126
|
"""Run app asynchronously."""
|
|
70
127
|
try:
|
|
71
|
-
await self.async_run(
|
|
128
|
+
await self.async_run()
|
|
72
129
|
Logger.info("PythonHere: async run completed")
|
|
73
130
|
except asyncio.CancelledError:
|
|
74
131
|
Logger.info("PythonHere: app main task canceled")
|
|
132
|
+
raise
|
|
75
133
|
except Exception as exc:
|
|
76
134
|
Logger.exception(exc)
|
|
135
|
+
raise
|
|
136
|
+
finally:
|
|
137
|
+
if self.get_running_app():
|
|
138
|
+
self.stop()
|
|
139
|
+
|
|
140
|
+
async def cancel_app_tasks(self):
|
|
141
|
+
"""Cancel tasks owned by this app."""
|
|
142
|
+
tasks = [
|
|
143
|
+
task
|
|
144
|
+
for task in (self.server_task, self.app_task)
|
|
145
|
+
if task is not None
|
|
146
|
+
and task is not asyncio.current_task()
|
|
147
|
+
and not task.done()
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
for task in tasks:
|
|
151
|
+
task.cancel()
|
|
152
|
+
|
|
153
|
+
if tasks:
|
|
154
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
77
155
|
|
|
78
|
-
|
|
79
|
-
|
|
156
|
+
def update_server_config_status(self):
|
|
157
|
+
"""Check and update value of the `ssh_server_config_ready`, update screen."""
|
|
80
158
|
|
|
81
|
-
|
|
82
|
-
self.
|
|
159
|
+
def update():
|
|
160
|
+
if all(self.get_pythonhere_config().values()):
|
|
161
|
+
if (
|
|
162
|
+
self.asyncio_loop is not None
|
|
163
|
+
and self.ssh_server_config_ready is not None
|
|
164
|
+
):
|
|
165
|
+
self.asyncio_loop.call_soon_threadsafe(
|
|
166
|
+
self.ssh_server_config_ready.set
|
|
167
|
+
)
|
|
83
168
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
169
|
+
Clock.schedule_once(lambda _: screen.update(), 0)
|
|
170
|
+
|
|
171
|
+
screen = self.root.ids.here_screen_manager
|
|
172
|
+
screen.current = ServerState.starting_server
|
|
173
|
+
self.root.switch_screen(ScreenName.here)
|
|
174
|
+
threading.Thread(name="update_server_config_status", target=update).start()
|
|
88
175
|
|
|
89
176
|
def get_pythonhere_config(self):
|
|
90
177
|
"""Return user settings for SSH server."""
|
|
91
178
|
return self.settings.get_pythonhere_config()
|
|
92
179
|
|
|
93
|
-
def update_ssh_server_namespace(self, namespace:
|
|
180
|
+
def update_ssh_server_namespace(self, namespace: dict[str, Any]):
|
|
94
181
|
"""Update SSH server namespace."""
|
|
95
182
|
self.ssh_server_namespace.update(namespace)
|
|
96
183
|
|
|
@@ -109,8 +196,14 @@ class PythonHereApp(App):
|
|
|
109
196
|
def on_ssh_connection_made(self):
|
|
110
197
|
"""New authenticated SSH client connected handler."""
|
|
111
198
|
Logger.info("PythonHere: new SSH client connected")
|
|
199
|
+
|
|
200
|
+
if self.ssh_server_connected is None:
|
|
201
|
+
Logger.warning("PythonHere: SSH connected before asyncio state was ready")
|
|
202
|
+
return
|
|
203
|
+
|
|
112
204
|
if not self.ssh_server_connected.is_set():
|
|
113
205
|
self.ssh_server_connected.set()
|
|
206
|
+
|
|
114
207
|
Logger.info("PythonHere: reset window environment")
|
|
115
208
|
self.ssh_server_namespace["root"] = reset_window_environment()
|
|
116
209
|
self.chdir(self.upload_dir)
|
|
@@ -122,7 +215,11 @@ class PythonHereApp(App):
|
|
|
122
215
|
sys.path.insert(0, path)
|
|
123
216
|
|
|
124
217
|
|
|
218
|
+
async def main():
|
|
219
|
+
"""Run PythonHere."""
|
|
220
|
+
app = PythonHereApp()
|
|
221
|
+
await app.run_app()
|
|
222
|
+
|
|
223
|
+
|
|
125
224
|
if __name__ == "__main__":
|
|
126
|
-
|
|
127
|
-
loop.run_until_complete(PythonHereApp().run_app())
|
|
128
|
-
loop.close()
|
|
225
|
+
asyncio.run(main())
|
pythonhere/network_here.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Network addresses discovering."""
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
3
4
|
|
|
4
5
|
from kivy import platform
|
|
5
6
|
|
|
@@ -14,7 +15,7 @@ else:
|
|
|
14
15
|
|
|
15
16
|
def get_android_interface_addresses(
|
|
16
17
|
interface: "NetworkInterface",
|
|
17
|
-
) -> Iterator[
|
|
18
|
+
) -> Iterator[tuple[str, str]]:
|
|
18
19
|
"""Yields active IPv4 addresses for given network interface."""
|
|
19
20
|
if interface.isUp():
|
|
20
21
|
addresses = interface.getInetAddresses()
|
|
@@ -24,7 +25,7 @@ def get_android_interface_addresses(
|
|
|
24
25
|
yield interface.getDisplayName(), address.getHostAddress()
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def get_all_available_ipv4_adrresses() -> Iterator[
|
|
28
|
+
def get_all_available_ipv4_adrresses() -> Iterator[tuple[str, str]]:
|
|
28
29
|
"""Yields available interfaces with IPv4 addresses
|
|
29
30
|
available for connections from there.
|
|
30
31
|
"""
|
pythonhere/patches_here.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"""Monkey patching Kivy @('_')@."""
|
|
2
2
|
|
|
3
|
+
import kivy.uix.widget
|
|
3
4
|
from kivy.factory import Factory
|
|
4
5
|
from kivy.lang.builder import BuilderBase
|
|
5
|
-
import kivy.uix.widget
|
|
6
|
-
|
|
7
6
|
|
|
8
7
|
_original_factory_register = Factory.register
|
|
9
8
|
_original_builderbase_match = BuilderBase.match
|
pythonhere/pythonhere.kv
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#:kivy 1.0
|
|
2
|
+
#:import RootLayout ui_here.layout_here.RootLayout
|
|
3
|
+
#:import ScreenName enum_here.ScreenName
|
|
4
|
+
#:import ServerState enum_here.ServerState
|
|
5
|
+
#:import ServerScreenManager ui_here.server_screen_here.ServerScreenManager
|
|
6
|
+
#:include ui_here/actionbar_here.kv
|
|
7
|
+
#:include ui_here/common_here.kv
|
|
8
|
+
#:include ui_here/connection_address_here.kv
|
|
9
|
+
#:include ui_here/server_screen_here.kv
|
|
10
|
+
#:include ui_here/settings_here.kv
|
|
11
|
+
|
|
12
|
+
RootLayout:
|
|
13
|
+
orientation: "vertical"
|
|
14
|
+
|
|
15
|
+
ActionBar:
|
|
16
|
+
pos_hint: {"top": 1}
|
|
17
|
+
|
|
18
|
+
ActionView:
|
|
19
|
+
ActionPrevious:
|
|
20
|
+
title: ""
|
|
21
|
+
with_previous: False
|
|
22
|
+
app_icon: "data/logo/logo-32.png"
|
|
23
|
+
|
|
24
|
+
ScreenActionButton:
|
|
25
|
+
id: open_here_action
|
|
26
|
+
text: f"%{tc('H', 3)}ere"
|
|
27
|
+
screen: ScreenName.here
|
|
28
|
+
ScreenActionButton:
|
|
29
|
+
id: open_settings_action
|
|
30
|
+
text: f"{tc('S', 3)}ettings"
|
|
31
|
+
screen: ScreenName.settings
|
|
32
|
+
|
|
33
|
+
ActionButton:
|
|
34
|
+
text: f"{tc('Q', 3)}uit"
|
|
35
|
+
on_release: app.get_running_app().stop()
|
|
36
|
+
|
|
37
|
+
ScreenManager:
|
|
38
|
+
id: screen_manager
|
|
39
|
+
Screen:
|
|
40
|
+
Screen:
|
|
41
|
+
id: here_screen
|
|
42
|
+
name: ScreenName.here
|
|
43
|
+
ServerScreenManager:
|
|
44
|
+
id: here_screen_manager
|
|
45
|
+
Screen:
|
|
46
|
+
id: settings_screen
|
|
47
|
+
name: ScreenName.settings
|
|
48
|
+
SettingsHere:
|
|
49
|
+
id: settings
|