pythonhere 0.1.4__tar.gz → 0.2.0__tar.gz

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.
Files changed (58) hide show
  1. pythonhere-0.2.0/LICENSE +22 -0
  2. pythonhere-0.2.0/PKG-INFO +125 -0
  3. pythonhere-0.2.0/README.rst +101 -0
  4. pythonhere-0.2.0/pyproject.toml +151 -0
  5. pythonhere-0.2.0/pythonhere/android_here.py +100 -0
  6. pythonhere-0.2.0/pythonhere/data/logo/logo-128.png +0 -0
  7. pythonhere-0.2.0/pythonhere/data/logo/logo-32.png +0 -0
  8. pythonhere-0.2.0/pythonhere/data/logo/logo-splash.png +0 -0
  9. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/enum_here.py +1 -0
  10. pythonhere-0.2.0/pythonhere/exception_manager_here.kv +22 -0
  11. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/exception_manager_here.py +14 -30
  12. pythonhere-0.2.0/pythonhere/launcher_here.py +46 -0
  13. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/magic_here/shortcuts.py +21 -5
  14. pythonhere-0.2.0/pythonhere/main.py +225 -0
  15. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/network_here.py +4 -3
  16. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/patches_here.py +1 -2
  17. pythonhere-0.2.0/pythonhere/pythonhere.kv +49 -0
  18. pythonhere-0.2.0/pythonhere/server_here.py +59 -0
  19. pythonhere-0.2.0/pythonhere/ui_here/actionbar_here.kv +8 -0
  20. pythonhere-0.2.0/pythonhere/ui_here/common_here.kv +14 -0
  21. pythonhere-0.2.0/pythonhere/ui_here/connection_address_here.kv +32 -0
  22. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/ui_here/layout_here.py +1 -0
  23. pythonhere-0.2.0/pythonhere/ui_here/server_screen_here.kv +57 -0
  24. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/ui_here/server_screen_here.py +4 -3
  25. pythonhere-0.2.0/pythonhere/ui_here/settings_here.kv +31 -0
  26. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/ui_here/settings_here.py +15 -6
  27. pythonhere-0.2.0/pythonhere/version_here.py +1 -0
  28. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/window_here.py +10 -3
  29. pythonhere-0.2.0/pythonhere.egg-info/PKG-INFO +125 -0
  30. pythonhere-0.2.0/pythonhere.egg-info/SOURCES.txt +47 -0
  31. pythonhere-0.2.0/pythonhere.egg-info/requires.txt +4 -0
  32. {pythonhere-0.1.4 → pythonhere-0.2.0}/setup.cfg +0 -3
  33. pythonhere-0.2.0/setup.py +3 -0
  34. pythonhere-0.2.0/tests/test_android_here.py +32 -0
  35. pythonhere-0.2.0/tests/test_exception_manager_here.py +20 -0
  36. pythonhere-0.2.0/tests/test_launcher_here.py +50 -0
  37. pythonhere-0.2.0/tests/test_magic.py +73 -0
  38. pythonhere-0.2.0/tests/test_main.py +271 -0
  39. pythonhere-0.2.0/tests/test_network.py +8 -0
  40. pythonhere-0.2.0/tests/test_patches.py +37 -0
  41. pythonhere-0.2.0/tests/test_server_here.py +86 -0
  42. pythonhere-0.2.0/tests/test_settings.py +33 -0
  43. pythonhere-0.2.0/tests/test_window_here.py +14 -0
  44. pythonhere-0.1.4/PKG-INFO +0 -119
  45. pythonhere-0.1.4/README.rst +0 -101
  46. pythonhere-0.1.4/pythonhere/main.py +0 -128
  47. pythonhere-0.1.4/pythonhere/server_here.py +0 -56
  48. pythonhere-0.1.4/pythonhere/version_here.py +0 -1
  49. pythonhere-0.1.4/pythonhere.egg-info/PKG-INFO +0 -119
  50. pythonhere-0.1.4/pythonhere.egg-info/SOURCES.txt +0 -24
  51. pythonhere-0.1.4/pythonhere.egg-info/requires.txt +0 -22
  52. pythonhere-0.1.4/setup.py +0 -63
  53. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/__init__.py +1 -1
  54. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/magic_here/__init__.py +0 -0
  55. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/ui_here/__init__.py +0 -0
  56. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere/ui_here/connection_address_here.py +2 -2
  57. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere.egg-info/dependency_links.txt +0 -0
  58. {pythonhere-0.1.4 → pythonhere-0.2.0}/pythonhere.egg-info/top_level.txt +0 -0
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 b3b
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonhere
3
+ Version: 0.2.0
4
+ Summary: Here is the Kivy based app to run code from the Jupyter magic %there
5
+ Author-email: b3b <ash.b3b@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/b3b/ipython-pythonhere
8
+ Project-URL: Changelog, https://github.com/b3b/pythonhere/blob/master/CHANGELOG.rst
9
+ Keywords: android,ipython,jupyter,magic,kivy
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/x-rst
18
+ License-File: LICENSE
19
+ Requires-Dist: herethere[magic]>=0.1.0
20
+ Requires-Dist: ipython
21
+ Requires-Dist: ipywidgets
22
+ Requires-Dist: Pillow
23
+ Dynamic: license-file
24
+
25
+ PythonHere
26
+ ==========
27
+
28
+ .. start-badges
29
+ .. image:: https://img.shields.io/pypi/status/pythonhere
30
+ :target: https://pypi.python.org/pypi/pythonhere
31
+ :alt: Status
32
+ .. image:: https://img.shields.io/pypi/v/pythonhere.svg
33
+ :target: https://pypi.python.org/pypi/pythonhere
34
+ :alt: Latest version on PyPi
35
+ .. image:: https://img.shields.io/docker/v/herethere/pythonhere?color=%23FFD43B&label=Docker%20Image
36
+ :target: https://hub.docker.com/r/herethere/pythonhere
37
+ :alt: Docker Image Version (latest by date)
38
+ .. image:: https://img.shields.io/pypi/pyversions/pythonhere.svg
39
+ :target: https://pypi.python.org/pypi/pythonhere
40
+ :alt: Supported Python versions
41
+ .. image:: https://github.com/b3b/pythonhere/actions/workflows/tests.yml/badge.svg?branch=master
42
+ :target: https://github.com/b3b/pythonhere/actions/workflows/tests.yml?query=branch%3Amaster
43
+ :alt: CI Status
44
+ .. image:: https://codecov.io/github/b3b/pythonhere/coverage.svg?branch=master
45
+ :target: https://codecov.io/github/b3b/pythonhere?branch=master
46
+ :alt: Code coverage Status
47
+ .. end-badges
48
+
49
+ *PythonHere* lets you run Python code from a local `Jupyter <https://jupyter.org/>`_
50
+ notebook inside a remote `Kivy <https://kivy.org>`_ app.
51
+
52
+ PythonHere has two parts:
53
+
54
+ * *Here* is the remote/server side. It runs a Python environment with a Kivy GUI,
55
+ for example on Android, Raspberry Pi, or another machine.
56
+ * *%there* is the local/client side. It is a Jupyter magic command for running
57
+ code interactively in the remote PythonHere environment.
58
+
59
+ This makes PythonHere useful as a live Python/Kivy playground, and as a way to
60
+ inspect or control a Python app running remotely.
61
+
62
+ Project documentation: https://herethere.me/pythonhere
63
+
64
+ .. image:: https://raw.githubusercontent.com/b3b/pythonhere/master/docs/description.png
65
+ :alt: Project description
66
+
67
+
68
+ Install the Android app
69
+ -----------------------
70
+
71
+ Ready-to-use *PythonHere* APKs are available from the `GitHub Releases <https://github.com/b3b/pythonhere/releases>`_ page.
72
+
73
+ For APK provenance and signing checks, see `Android APK verification <https://github.com/b3b/pythonhere/blob/master/docs/android-apk-verification.rst>`_.
74
+ For a list of Python packages included in the Android build, see `buildozer.spec <https://github.com/b3b/pythonhere/blob/master/buildozer.spec>`_.
75
+
76
+ Start a local Jupyter environment with Docker
77
+ ---------------------------------------------
78
+
79
+ The Docker image is based on `Jupyter Docker Stacks <https://jupyter-docker-stacks.readthedocs.io/en/latest/>`_
80
+ and includes *PythonHere* with usage examples.
81
+
82
+ Example command to start the Docker container::
83
+
84
+ docker run \
85
+ --rm \
86
+ -p 8888:8888 \
87
+ --user root \
88
+ -e CHOWN_EXTRA=/home/jovyan/work \
89
+ -e CHOWN_EXTRA_OPTS='-R' \
90
+ -v "$(pwd)/work":/home/jovyan/work \
91
+ herethere/pythonhere:latest
92
+
93
+ The command exposes the Jupyter server on host port ``8888``. Jupyter logs are
94
+ printed in the terminal and include a URL such as
95
+ ``http://127.0.0.1:8888/?token=...``. Open this URL in a browser to use the
96
+ local Jupyter environment.
97
+
98
+ Files in ``/home/jovyan/work`` inside the container are stored in the local
99
+ ``work`` directory.
100
+
101
+
102
+ Run a local Jupyter environment without Docker
103
+ ----------------------------------------------
104
+
105
+ Commands to run locally::
106
+
107
+ pip install pythonhere jupyter
108
+ jupyter notebook
109
+
110
+
111
+ Build Android app
112
+ -----------------
113
+
114
+ To build with `Buildozer <https://github.com/kivy/buildozer>`_, run in the source directory::
115
+
116
+
117
+ buildozer android debug
118
+
119
+
120
+ Related resources
121
+ -----------------
122
+
123
+ * `Kivy Remote Shell <https://github.com/kivy/kivy-remote-shell>`_ : Remote SSH+Python interactive shell application
124
+ * `herethere <https://github.com/b3b/herethere>`_ : Library for interactive code execution, based on AsyncSSH
125
+ * `AsyncSSH <https://github.com/ronf/asyncssh>`_ : Asynchronous SSH for Python
@@ -0,0 +1,101 @@
1
+ PythonHere
2
+ ==========
3
+
4
+ .. start-badges
5
+ .. image:: https://img.shields.io/pypi/status/pythonhere
6
+ :target: https://pypi.python.org/pypi/pythonhere
7
+ :alt: Status
8
+ .. image:: https://img.shields.io/pypi/v/pythonhere.svg
9
+ :target: https://pypi.python.org/pypi/pythonhere
10
+ :alt: Latest version on PyPi
11
+ .. image:: https://img.shields.io/docker/v/herethere/pythonhere?color=%23FFD43B&label=Docker%20Image
12
+ :target: https://hub.docker.com/r/herethere/pythonhere
13
+ :alt: Docker Image Version (latest by date)
14
+ .. image:: https://img.shields.io/pypi/pyversions/pythonhere.svg
15
+ :target: https://pypi.python.org/pypi/pythonhere
16
+ :alt: Supported Python versions
17
+ .. image:: https://github.com/b3b/pythonhere/actions/workflows/tests.yml/badge.svg?branch=master
18
+ :target: https://github.com/b3b/pythonhere/actions/workflows/tests.yml?query=branch%3Amaster
19
+ :alt: CI Status
20
+ .. image:: https://codecov.io/github/b3b/pythonhere/coverage.svg?branch=master
21
+ :target: https://codecov.io/github/b3b/pythonhere?branch=master
22
+ :alt: Code coverage Status
23
+ .. end-badges
24
+
25
+ *PythonHere* lets you run Python code from a local `Jupyter <https://jupyter.org/>`_
26
+ notebook inside a remote `Kivy <https://kivy.org>`_ app.
27
+
28
+ PythonHere has two parts:
29
+
30
+ * *Here* is the remote/server side. It runs a Python environment with a Kivy GUI,
31
+ for example on Android, Raspberry Pi, or another machine.
32
+ * *%there* is the local/client side. It is a Jupyter magic command for running
33
+ code interactively in the remote PythonHere environment.
34
+
35
+ This makes PythonHere useful as a live Python/Kivy playground, and as a way to
36
+ inspect or control a Python app running remotely.
37
+
38
+ Project documentation: https://herethere.me/pythonhere
39
+
40
+ .. image:: https://raw.githubusercontent.com/b3b/pythonhere/master/docs/description.png
41
+ :alt: Project description
42
+
43
+
44
+ Install the Android app
45
+ -----------------------
46
+
47
+ Ready-to-use *PythonHere* APKs are available from the `GitHub Releases <https://github.com/b3b/pythonhere/releases>`_ page.
48
+
49
+ For APK provenance and signing checks, see `Android APK verification <https://github.com/b3b/pythonhere/blob/master/docs/android-apk-verification.rst>`_.
50
+ For a list of Python packages included in the Android build, see `buildozer.spec <https://github.com/b3b/pythonhere/blob/master/buildozer.spec>`_.
51
+
52
+ Start a local Jupyter environment with Docker
53
+ ---------------------------------------------
54
+
55
+ The Docker image is based on `Jupyter Docker Stacks <https://jupyter-docker-stacks.readthedocs.io/en/latest/>`_
56
+ and includes *PythonHere* with usage examples.
57
+
58
+ Example command to start the Docker container::
59
+
60
+ docker run \
61
+ --rm \
62
+ -p 8888:8888 \
63
+ --user root \
64
+ -e CHOWN_EXTRA=/home/jovyan/work \
65
+ -e CHOWN_EXTRA_OPTS='-R' \
66
+ -v "$(pwd)/work":/home/jovyan/work \
67
+ herethere/pythonhere:latest
68
+
69
+ The command exposes the Jupyter server on host port ``8888``. Jupyter logs are
70
+ printed in the terminal and include a URL such as
71
+ ``http://127.0.0.1:8888/?token=...``. Open this URL in a browser to use the
72
+ local Jupyter environment.
73
+
74
+ Files in ``/home/jovyan/work`` inside the container are stored in the local
75
+ ``work`` directory.
76
+
77
+
78
+ Run a local Jupyter environment without Docker
79
+ ----------------------------------------------
80
+
81
+ Commands to run locally::
82
+
83
+ pip install pythonhere jupyter
84
+ jupyter notebook
85
+
86
+
87
+ Build Android app
88
+ -----------------
89
+
90
+ To build with `Buildozer <https://github.com/kivy/buildozer>`_, run in the source directory::
91
+
92
+
93
+ buildozer android debug
94
+
95
+
96
+ Related resources
97
+ -----------------
98
+
99
+ * `Kivy Remote Shell <https://github.com/kivy/kivy-remote-shell>`_ : Remote SSH+Python interactive shell application
100
+ * `herethere <https://github.com/b3b/herethere>`_ : Library for interactive code execution, based on AsyncSSH
101
+ * `AsyncSSH <https://github.com/ronf/asyncssh>`_ : Asynchronous SSH for Python
@@ -0,0 +1,151 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pythonhere"
7
+ dynamic = ["version"]
8
+ description = "Here is the Kivy based app to run code from the Jupyter magic %there"
9
+ readme = { file = "README.rst", content-type = "text/x-rst" }
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "b3b", email = "ash.b3b@gmail.com" },
14
+ ]
15
+ keywords = ["android", "ipython", "jupyter", "magic", "kivy"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ ]
24
+ dependencies = [
25
+ "herethere[magic]>=0.1.0",
26
+ "ipython",
27
+ "ipywidgets",
28
+ "Pillow",
29
+ ]
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ { include-group = "docker" },
34
+ "build",
35
+ "codecov",
36
+ "docutils",
37
+ "ifaddr",
38
+ "kivy==2.3.1",
39
+ "nest-asyncio2==1.7.2",
40
+ "pylint",
41
+ "pytest",
42
+ "pytest-asyncio",
43
+ "pytest-cov",
44
+ "pytest-mock",
45
+ "ruff",
46
+ "twine",
47
+ ]
48
+ docker = [
49
+ "jupytext==1.19.3",
50
+ ]
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/b3b/ipython-pythonhere"
54
+ Changelog = "https://github.com/b3b/pythonhere/blob/master/CHANGELOG.rst"
55
+
56
+ [tool.uv]
57
+ environments = [
58
+ "sys_platform == 'linux'",
59
+ ]
60
+
61
+ [tool.pytest.ini_options]
62
+ asyncio_mode = "auto"
63
+
64
+ [tool.setuptools]
65
+ packages = [
66
+ "pythonhere",
67
+ "pythonhere.magic_here",
68
+ "pythonhere.ui_here",
69
+ ]
70
+
71
+ [tool.setuptools.dynamic]
72
+ version = { attr = "pythonhere.version_here.__version__" }
73
+
74
+ [tool.setuptools.package-data]
75
+ pythonhere = [
76
+ "*.kv",
77
+ "data/logo/*.png",
78
+ "ui_here/*.kv",
79
+ ]
80
+
81
+ [tool.ruff]
82
+ line-length = 88
83
+ target-version = "py310"
84
+
85
+ [tool.ruff.lint]
86
+ select = [
87
+ "E", # pycodestyle errors
88
+ "F", # pyflakes
89
+ "W", # pycodestyle warnings
90
+ "I", # import sorting
91
+ "UP", # pyupgrade
92
+ "B", # flake8-bugbear
93
+ "TRY", # exception handling checks
94
+ "PL", # pylint-style checks supported by Ruff
95
+ ]
96
+ ignore = [
97
+ "PLC0207", # keep simple last-part string splits when they are clearer
98
+ "PLC0415", # allow delayed Kivy/Android imports that control side effects
99
+ "TRY002", # keep existing broad app-facing exceptions for now
100
+ "TRY003", # allow direct exception messages in this small app
101
+ ]
102
+
103
+ [tool.ruff.lint.per-file-ignores]
104
+ "tests/**/*.py" = [
105
+ "PLR0913", # parametrized tests naturally pass several fixtures/values
106
+ "PLR2004", # literal expected values are readable in tests
107
+ "PLW0603", # tests may intentionally exercise global namespace behavior
108
+ ]
109
+
110
+ [tool.ruff.format]
111
+ quote-style = "double"
112
+ indent-style = "space"
113
+
114
+ [tool.pylint.main]
115
+ fail-under = 10.0
116
+ ignore = ["migrations"]
117
+ py-version = "3.10"
118
+
119
+ [tool.pylint."messages control"]
120
+ disable = [
121
+ "attribute-defined-outside-init",
122
+ "bad-inline-option",
123
+ "broad-exception-caught",
124
+ "broad-exception-raised",
125
+ "deprecated-pragma",
126
+ "duplicate-code",
127
+ "file-ignored",
128
+ "import-outside-toplevel",
129
+ "import-error",
130
+ "inconsistent-return-statements",
131
+ "invalid-name",
132
+ "locally-disabled",
133
+ "logging-too-few-args",
134
+ "missing-function-docstring",
135
+ "missing-module-docstring",
136
+ "no-member",
137
+ "possibly-used-before-assignment",
138
+ "protected-access",
139
+ "raw-checker-failed",
140
+ "suppressed-message",
141
+ "too-few-public-methods",
142
+ "too-many-instance-attributes",
143
+ "unnecessary-ellipsis",
144
+ "unused-argument",
145
+ "unused-wildcard-import",
146
+ "use-symbolic-message-instead",
147
+ "useless-suppression",
148
+ "wildcard-import",
149
+ "wrong-import-order",
150
+ ]
151
+ enable = ["c-extension-no-member"]
@@ -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)
@@ -1,4 +1,5 @@
1
1
  """Enums."""
2
+
2
3
  from enum import Enum
3
4
 
4
5
 
@@ -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 typing import Optional
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
- Builder.load_string(
20
- """<-UnhandledExceptionPopupHere>:
21
- title: "Unhandled Exception catched"
22
- BoxLayout:
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
- ExceptionManager.add_handler(ErrorMessageOnException())
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: Optional[Exception] = None):
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