pythonhere 0.1.5.2__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 CHANGED
@@ -1,8 +1,8 @@
1
1
  """PythonHere Jupyter magic."""
2
+
2
3
  from herethere.magic import load_ipython_extension
3
4
 
4
5
  from .magic_here import shortcuts # noqa
5
6
  from .version_here import __version__ # noqa
6
7
 
7
-
8
8
  __all__ = ("load_ipython_extension",)
@@ -1,14 +1,13 @@
1
1
  """Android specific functions."""
2
+
2
3
  # pylint: disable=invalid-name,import-error,import-outside-toplevel
3
- from pathlib import Path
4
- from typing import Optional
5
4
  import uuid
5
+ from pathlib import Path
6
6
 
7
7
  from android import activity as android_activity
8
8
  from jnius import autoclass, cast
9
9
  from kivy.logger import Logger
10
10
 
11
-
12
11
  Context = autoclass("android.content.Context")
13
12
  Icon = autoclass("android.graphics.drawable.Icon")
14
13
  Intent = autoclass("android.content.Intent")
@@ -23,7 +22,7 @@ def get_current_intent() -> Intent:
23
22
  return PythonActivity.mActivity.getIntent()
24
23
 
25
24
 
26
- def get_startup_script(intent: None = None) -> Optional[str]:
25
+ def get_startup_script(intent: None = None) -> str | None:
27
26
  """Return script entrypoint that was passed to a given, or current, intent."""
28
27
  if not intent:
29
28
  intent = get_current_intent()
@@ -62,7 +61,7 @@ def bind_run_script_on_new_intent():
62
61
  def create_shortcut_icon() -> Icon:
63
62
  """Create icon to use for a shurtcut."""
64
63
  activity = PythonActivity.mActivity
65
- Drawable = autoclass("{}.R$drawable".format(activity.getPackageName()))
64
+ Drawable = autoclass(f"{activity.getPackageName()}.R$drawable")
66
65
  context = cast("android.content.Context", activity.getApplicationContext())
67
66
  return Icon.createWithResource(context, Drawable.icon)
68
67
 
Binary file
Binary file
Binary file
pythonhere/enum_here.py CHANGED
@@ -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:
@@ -1,12 +1,12 @@
1
1
  """Utilities for launching scripts."""
2
+
2
3
  import os
3
- from pathlib import Path
4
4
  import runpy
5
5
  import sys
6
+ from pathlib import Path
6
7
 
7
-
8
- from kivy.logger import Logger
9
8
  from kivy import platform
9
+ from kivy.logger import Logger
10
10
 
11
11
 
12
12
  def run_script(script: str):
@@ -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 there_group, there_code_shortcut
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
@@ -67,7 +66,9 @@ def screenshot(ctx, width, output):
67
66
  img = PILImage.open(BytesIO(data)).convert("RGB")
68
67
  if width:
69
68
  height = int(width * img.size[1] // img.size[0])
70
- img = img.resize((width, height), PILImage.ANTIALIAS)
69
+ resampling = getattr(PILImage, "Resampling", PILImage)
70
+ resample_filter = getattr(resampling, "LANCZOS", PILImage.LANCZOS)
71
+ img = img.resize((width, height), resample_filter)
71
72
 
72
73
  if output:
73
74
  img.save(output)
pythonhere/main.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """PythonHere app."""
2
+
2
3
  # pylint: disable=wrong-import-order,wrong-import-position
3
4
 
4
5
  from launcher_here import try_startup_script
@@ -12,18 +13,17 @@ else:
12
13
 
13
14
  import asyncio
14
15
  import os
15
- from pathlib import Path
16
16
  import sys
17
17
  import threading
18
- from typing import Any, Dict
18
+ from pathlib import Path
19
+ from typing import Any
19
20
 
21
+ from enum_here import ScreenName, ServerState
22
+ from exception_manager_here import install_exception_handler, show_exception_popup
20
23
  from kivy.app import App
21
24
  from kivy.clock import Clock
22
25
  from kivy.config import Config, ConfigParser
23
26
  from kivy.logger import Logger
24
-
25
- from enum_here import ScreenName, ServerState
26
- from exception_manager_here import install_exception_handler, show_exception_popup
27
27
  from patches_here import monkeypatch_kivy
28
28
  from server_here import run_ssh_server
29
29
  from window_here import reset_window_environment
@@ -36,13 +36,25 @@ class PythonHereApp(App):
36
36
 
37
37
  def __init__(self):
38
38
  super().__init__()
39
- 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
40
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()
41
55
  self.ssh_server_config_ready = asyncio.Event()
42
56
  self.ssh_server_started = asyncio.Event()
43
57
  self.ssh_server_connected = asyncio.Event()
44
- self.ssh_server_namespace = {}
45
- self.icon = "data/logo/logo-32.png"
46
58
 
47
59
  @property
48
60
  def upload_dir(self) -> str:
@@ -67,9 +79,7 @@ class PythonHereApp(App):
67
79
  """Initialize application UI."""
68
80
  super().build()
69
81
  install_exception_handler()
70
-
71
82
  self.settings = self.root.ids.settings
72
-
73
83
  self.ssh_server_namespace.update(
74
84
  {
75
85
  "app": self,
@@ -82,47 +92,81 @@ class PythonHereApp(App):
82
92
  lambda _: show_exception_popup(startup_script_exception), 0
83
93
  )
84
94
 
85
- def run_app(self):
86
- """Run application and SSH server tasks."""
87
- self.ssh_server_started = asyncio.Event()
88
- self.server_task = asyncio.ensure_future(run_ssh_server(self))
89
- return asyncio.gather(self.async_run_app(), self.server_task)
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()
90
124
 
91
125
  async def async_run_app(self):
92
126
  """Run app asynchronously."""
93
127
  try:
94
- await self.async_run(async_lib="asyncio")
128
+ await self.async_run()
95
129
  Logger.info("PythonHere: async run completed")
96
130
  except asyncio.CancelledError:
97
131
  Logger.info("PythonHere: app main task canceled")
132
+ raise
98
133
  except Exception as exc:
99
134
  Logger.exception(exc)
135
+ raise
136
+ finally:
137
+ if self.get_running_app():
138
+ self.stop()
100
139
 
101
- if self.server_task:
102
- self.server_task.cancel()
103
-
104
- if self.get_running_app():
105
- self.stop()
106
-
107
- await self.cancel_asyncio_tasks()
108
-
109
- async def cancel_asyncio_tasks(self):
110
- """Cancel all asyncio tasks."""
140
+ async def cancel_app_tasks(self):
141
+ """Cancel tasks owned by this app."""
111
142
  tasks = [
112
- task for task in asyncio.all_tasks() if task is not asyncio.current_task()
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()
113
148
  ]
149
+
150
+ for task in tasks:
151
+ task.cancel()
152
+
114
153
  if tasks:
115
- for task in tasks:
116
- task.cancel()
117
- await asyncio.wait(tasks, timeout=1)
154
+ await asyncio.gather(*tasks, return_exceptions=True)
118
155
 
119
156
  def update_server_config_status(self):
120
157
  """Check and update value of the `ssh_server_config_ready`, update screen."""
121
158
 
122
159
  def update():
123
160
  if all(self.get_pythonhere_config().values()):
124
- self.ssh_server_config_ready.set()
125
- screen.update()
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
+ )
168
+
169
+ Clock.schedule_once(lambda _: screen.update(), 0)
126
170
 
127
171
  screen = self.root.ids.here_screen_manager
128
172
  screen.current = ServerState.starting_server
@@ -133,7 +177,7 @@ class PythonHereApp(App):
133
177
  """Return user settings for SSH server."""
134
178
  return self.settings.get_pythonhere_config()
135
179
 
136
- def update_ssh_server_namespace(self, namespace: Dict[str, Any]):
180
+ def update_ssh_server_namespace(self, namespace: dict[str, Any]):
137
181
  """Update SSH server namespace."""
138
182
  self.ssh_server_namespace.update(namespace)
139
183
 
@@ -152,8 +196,14 @@ class PythonHereApp(App):
152
196
  def on_ssh_connection_made(self):
153
197
  """New authenticated SSH client connected handler."""
154
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
+
155
204
  if not self.ssh_server_connected.is_set():
156
205
  self.ssh_server_connected.set()
206
+
157
207
  Logger.info("PythonHere: reset window environment")
158
208
  self.ssh_server_namespace["root"] = reset_window_environment()
159
209
  self.chdir(self.upload_dir)
@@ -165,7 +215,11 @@ class PythonHereApp(App):
165
215
  sys.path.insert(0, path)
166
216
 
167
217
 
218
+ async def main():
219
+ """Run PythonHere."""
220
+ app = PythonHereApp()
221
+ await app.run_app()
222
+
223
+
168
224
  if __name__ == "__main__":
169
- loop = asyncio.get_event_loop()
170
- loop.run_until_complete(PythonHereApp().run_app())
171
- loop.close()
225
+ asyncio.run(main())
@@ -1,5 +1,6 @@
1
1
  """Network addresses discovering."""
2
- from typing import Iterator, Tuple
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[Tuple[str, str]]:
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[Tuple[str, str]]:
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
  """
@@ -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
@@ -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
pythonhere/server_here.py CHANGED
@@ -1,12 +1,12 @@
1
1
  """SSH server."""
2
+
2
3
  import asyncio
3
4
  from pathlib import Path
4
5
 
6
+ from exception_manager_here import show_exception_popup
7
+ from herethere.here.server import ServerConfig, SSHServerHere, start_server
5
8
  from kivy.app import App
6
9
  from kivy.logger import Logger
7
- from herethere.here.server import SSHServerHere, ServerConfig, start_server
8
-
9
- from exception_manager_here import show_exception_popup
10
10
 
11
11
 
12
12
  class PythonHereServer(SSHServerHere):
@@ -53,7 +53,7 @@ async def run_ssh_server(app):
53
53
  Logger.info("PythonHere: SSH server task canceled")
54
54
  await server.stop()
55
55
  except Exception as exc:
56
- Logger.errror("PythonHere: SSH server stop by exception")
56
+ Logger.error("PythonHere: SSH server stop by exception")
57
57
  Logger.exception(exc)
58
58
  show_exception_popup(exc)
59
59
  Logger.info("PythonHere: SSH server closed")
@@ -0,0 +1,8 @@
1
+ #:kivy 1.0
2
+ #:import App kivy.app.App
3
+
4
+ <ScreenActionButton@ActionToggleButton>:
5
+ group: "screen"
6
+ screen: ""
7
+ allow_no_selection: False
8
+ on_release: App.get_running_app().root.switch_screen(self.screen)
@@ -0,0 +1,14 @@
1
+ #:kivy 1.0
2
+ # Common settings
3
+
4
+ #:import hex kivy.utils.get_color_from_hex
5
+ #:import sp kivy.metrics.sp
6
+
7
+ #:set bg_color "#EEEEEE"
8
+ #:set pallete ["#646464", "#306998", "#4B8BBE", "#FFD43B", "#FFE873"]
9
+ #:set tc lambda text, index: f"[color={pallete[index]}]{text}[/color]"
10
+
11
+
12
+ <Label>:
13
+ font_name: "DejaVuSans"
14
+ markup: True
@@ -0,0 +1,32 @@
1
+ #:kivy 1.0
2
+ #:import ConnectionAddressLabel ui_here.connection_address_here.ConnectionAddressLabel
3
+ #:import ConnectionAddressInfoBox ui_here.connection_address_here.ConnectionAddressInfoBox
4
+
5
+ <ConnectionAddressLabel>:
6
+ text: f"{tc('↳', 3)} {tc(self.address, 2)} ({self.interface})"
7
+ size_hint: 1, None
8
+ text_size: self.size
9
+ height: self.texture_size[1]
10
+ font_size: "20sp"
11
+
12
+ <ConnectionAddressInfoBox>:
13
+ cols: 1
14
+ size_hint: 1, 1
15
+ padding: '20sp', 0, 0, 0
16
+
17
+ Label:
18
+ text: f"Server is running.\nConnect {tc('here', 3)} via"
19
+ size_hint: 1, None
20
+ text_size: self.size
21
+ height: "80sp"
22
+ font_size: "30sp"
23
+
24
+ ScrollView:
25
+ do_scroll_x: False
26
+ size_hint: 1, 1
27
+
28
+ BoxLayout:
29
+ id: address_list
30
+ orientation: "vertical"
31
+ size_hint: 1, None
32
+ height: self.minimum_size[1]
@@ -1,10 +1,10 @@
1
1
  """Connection information widgets."""
2
+
2
3
  from kivy.clock import Clock, mainthread
3
4
  from kivy.logger import Logger
4
5
  from kivy.properties import StringProperty # pylint: disable=no-name-in-module
5
- from kivy.uix.label import Label
6
6
  from kivy.uix.gridlayout import GridLayout
7
-
7
+ from kivy.uix.label import Label
8
8
  from network_here import get_all_available_ipv4_adrresses
9
9
 
10
10
 
@@ -1,4 +1,5 @@
1
1
  """Layouts."""
2
+
2
3
  from kivy.uix.boxlayout import BoxLayout
3
4
  from kivy.uix.togglebutton import ToggleButton
4
5
 
@@ -0,0 +1,57 @@
1
+ #:import FallOutTransition kivy.uix.screenmanager.FallOutTransition
2
+ #:import App kivy.app.App
3
+
4
+ <LoadingImage@Image>:
5
+ source: "data/images/image-loading.zip"
6
+ allow_stretch: True
7
+ pos_hint: {'center_x': 0.5, 'center_y': 0.4}
8
+ size_hint: None, None
9
+ size: sp(100), sp(100)
10
+
11
+ <StackLabel@Label>:
12
+ size_hint: None, None
13
+ font_size: "20sp"
14
+ size: self.texture_size
15
+
16
+ <ServerNotConfigured@Screen>:
17
+ StackLayout:
18
+ orientation: "lr-tb"
19
+ padding: sp(20), sp(20), 0, 0
20
+ StackLabel:
21
+ text: "To start "
22
+ StackLabel:
23
+ text: f"{tc('Python', 1)}{tc('here', 3)}, "
24
+ StackLabel:
25
+ text: "you "
26
+ StackLabel:
27
+ text: "need to "
28
+ StackLabel:
29
+ text: "edit the "
30
+ Button:
31
+ text: "Settings"
32
+ font_size: "20sp"
33
+ size_hint: None, None
34
+ width: self.texture_size[0] + sp(20)
35
+ height: sp(30)
36
+ on_release: App.get_running_app().root.switch_screen(ScreenName.settings)
37
+ StackLabel:
38
+ text: " section."
39
+
40
+ <StartingServerScreen@Screen>:
41
+ Label:
42
+ text: f"Waiting {tc('Python', 1)}{tc('here', 3)} to start"
43
+ font_size: "30sp"
44
+ LoadingImage:
45
+
46
+ <ServerScreenManager>:
47
+ transition: FallOutTransition()
48
+ Screen:
49
+ LoadingImage:
50
+ ServerNotConfigured:
51
+ name: ServerState.not_configured
52
+ Screen:
53
+ StartingServerScreen:
54
+ name: ServerState.starting_server
55
+ Screen:
56
+ name: ServerState.ready
57
+ ConnectionAddressInfoBox:
@@ -1,10 +1,10 @@
1
1
  """%here server screen."""
2
+
3
+ from enum_here import ServerState
2
4
  from kivy.app import App
3
5
  from kivy.clock import Clock, mainthread
4
6
  from kivy.uix.screenmanager import ScreenManager
5
7
 
6
- from enum_here import ServerState
7
-
8
8
 
9
9
  class ServerScreenManager(ScreenManager):
10
10
  """Screen manager for server %here section."""
@@ -0,0 +1,31 @@
1
+ #:kivy 1.0
2
+ #:import App kivy.app.App
3
+ #:import SettingPassword ui_here.settings_here.SettingPassword
4
+
5
+ <SettingPassword>:
6
+ PasswordLabel:
7
+ pos: root.pos
8
+ font_size: "15sp"
9
+ text: '*' * len(root.value or '')
10
+
11
+ <SettingButton>:
12
+ size_hint: 1, None
13
+ height: sp(60)
14
+ opacity: 1 if self.active else 0
15
+
16
+ Button:
17
+ size_hint: None, None
18
+ width: self.texture_size[0] + sp(20)
19
+ height: sp(40)
20
+ halign: "center"
21
+ text: root.title
22
+ disabled: not root.active
23
+ on_release: root.on_release()
24
+
25
+
26
+ <StartServersettingButton>:
27
+ title: f"{tc('▶', 3)} Start the server"
28
+
29
+
30
+ <ShowPolicySettingButton>:
31
+ title: "Open privacy\n policy in browser"
@@ -1,6 +1,7 @@
1
1
  """Settings panel widgets."""
2
- from typing import Any, Dict
2
+
3
3
  import webbrowser
4
+ from typing import Any
4
5
 
5
6
  from kivy.app import App
6
7
  from kivy.config import Config
@@ -14,7 +15,6 @@ from kivy.uix.label import Label
14
15
  from kivy.uix.popup import Popup
15
16
  from kivy.uix.settings import Settings, SettingString
16
17
 
17
-
18
18
  SETTINGS_HERE = """
19
19
  [
20
20
  {
@@ -137,7 +137,7 @@ class SettingsHere(Settings):
137
137
  self.add_kivy_panel()
138
138
  self.add_json_panel("Privacy Policy", Config, data=SETTINGS_PRIVACY)
139
139
 
140
- def get_pythonhere_config(self) -> Dict[str, Any]:
140
+ def get_pythonhere_config(self) -> dict[str, Any]:
141
141
  """Extract server parts of the config."""
142
142
  return {
143
143
  "username": Config.get("pythonhere", "username"),
@@ -1 +1 @@
1
- __version__ = "0.1.5.2"
1
+ __version__ = "0.2.0"
pythonhere/window_here.py CHANGED
@@ -1,8 +1,9 @@
1
1
  """Utilities for working with Kivy window."""
2
- from base64 import b64encode
2
+
3
3
  import os
4
- from pathlib import Path
5
4
  import time
5
+ from base64 import b64encode
6
+ from pathlib import Path
6
7
 
7
8
  from kivy.app import App
8
9
  from kivy.lang import Builder
@@ -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,33 @@
1
+ pythonhere/__init__.py,sha256=gkVrYFkF3klE1fripHpmTuZx8_BaVHWRRvvr6PK6XWE,212
2
+ pythonhere/android_here.py,sha256=imQCNkhAJYgT3gMeX25kd9cQzH5ZbeF5PlgP8e9sbl0,3316
3
+ pythonhere/enum_here.py,sha256=FPj3pBeXfpPwl4-LDdC-wmbaoiv4QdbKHkGoBfTXnTE,449
4
+ pythonhere/exception_manager_here.kv,sha256=FZNhQgVojSLcVxZ0vjAxAF915xCBP-3nldPXVczRzCE,656
5
+ pythonhere/exception_manager_here.py,sha256=-UYqGRbzkoyfF0-pLWbxaFGEAvAdaw6b8hkm_bMIKx8,1943
6
+ pythonhere/launcher_here.py,sha256=I5zBpN0P3hmhsZFOJemxQ9Q2k2am5zkfS6Vg0Svnl98,1274
7
+ pythonhere/main.py,sha256=7vAF6mznCr2JFxeXjm3-a6PN1abPt-loI8ZHncAAPp0,7237
8
+ pythonhere/network_here.py,sha256=e3FDlBcXPaTFlWaqW2_bcWctht-ifpFIvtOj0lO4S9w,1558
9
+ pythonhere/patches_here.py,sha256=zB7Ohbk5PIXt0imnnrV5Mtl-Y7jpD_p5IBPlijQcCCQ,1727
10
+ pythonhere/pythonhere.kv,sha256=x1xwiKvJAevqBhtXGLb_axIf-VRos4V_jM9Q2i44f2U,1453
11
+ pythonhere/server_here.py,sha256=ss4Alg4ySkI0aG58l8D2SQMXiGHJeK-hGUIIBrbQ6nE,1854
12
+ pythonhere/version_here.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
13
+ pythonhere/window_here.py,sha256=na47v7bgomAjb6VC_g35RwhzEtm5RCInQIuQK27zY8U,2023
14
+ pythonhere/data/logo/logo-128.png,sha256=4sitDJcVBHOU5UPxicmrzc6YqFY7_xtYrc_ULyrw9ks,4161
15
+ pythonhere/data/logo/logo-32.png,sha256=qanrIAt6vK_2BsCjCxth2B4vxBa0ONnpDlils7syUWE,1247
16
+ pythonhere/data/logo/logo-splash.png,sha256=-edmwKnkkpGP4RZ0AJYto5_BOy8NoISt5ftOEI8BQOY,4663
17
+ pythonhere/magic_here/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ pythonhere/magic_here/shortcuts.py,sha256=pdB_ET6Snl4DxusbyZPI1_G8jGzA8d35er60qQb_2cY,2499
19
+ pythonhere/ui_here/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ pythonhere/ui_here/actionbar_here.kv,sha256=cOjY4ByOyBDUbeS9kbI3JAhi_X3kJ3Pb5h4RL58urwU,214
21
+ pythonhere/ui_here/common_here.kv,sha256=vnNyDwAKKNbRKvh0QKMbTvR0jNDKiY8DdGRnoEEexSc,324
22
+ pythonhere/ui_here/connection_address_here.kv,sha256=T0E8WX6QVvFLB2QL4GP7g9uDwQt5ja7aAODUe8x1NUY,885
23
+ pythonhere/ui_here/connection_address_here.py,sha256=_NM2mJuXM2jgk9eXH8NEV6IxGvJX_fc3wuEq-n7BoLs,1160
24
+ pythonhere/ui_here/layout_here.py,sha256=4sOf4ELSeaJJQkBfgrkM0Cmf6A1bpYTYtZl5GDXGJZE,485
25
+ pythonhere/ui_here/server_screen_here.kv,sha256=x0Se8hmgjS57gFIPa1r2lf6yJchs3lXOrAAtWf6M5Io,1562
26
+ pythonhere/ui_here/server_screen_here.py,sha256=2zQv1YFmiKNxa__7UZDYbAAAwYFLoZPSZPhr7vjCAkU,925
27
+ pythonhere/ui_here/settings_here.kv,sha256=stMcDq8dCYjay4idK30_7EUu5edkMDAQVPv0H0GuAcM,710
28
+ pythonhere/ui_here/settings_here.py,sha256=7NflZBUqY2ANZaSEuubaX9wIKniqbekcNQkUbqhhmto,4093
29
+ pythonhere-0.2.0.dist-info/licenses/LICENSE,sha256=nW9_eVi3dSMOkr6xghsobAovBEJiffjl_5WjujJTa74,1071
30
+ pythonhere-0.2.0.dist-info/METADATA,sha256=s6EVWNzwY9n6PDwLpNX6kVrd55fGxGpba3j26LYz7cQ,4790
31
+ pythonhere-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
+ pythonhere-0.2.0.dist-info/top_level.txt,sha256=dvkfRGF1tFbkjXzD9vwqXTge1Znkv7ga5fRJy5yhJsE,11
33
+ pythonhere-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.36.2)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,139 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: pythonhere
3
- Version: 0.1.5.2
4
- Summary: Here is the Kivy based app to run code from the Jupyter magic %there
5
- Home-page: https://github.com/b3b/ipython-pythonhere
6
- Author: b3b
7
- Author-email: ash.b3b@gmail.com
8
- License: MIT
9
- Project-URL: Changelog, https://github.com/b3b/pythonhere/blob/master/CHANGELOG.rst
10
- Keywords: android ipython jupyter magic kivy
11
- Platform: UNKNOWN
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Programming Language :: Python :: 3.7
14
- Classifier: Programming Language :: Python :: 3.8
15
- Description-Content-Type: text/x-rst
16
- Requires-Dist: kivy (>=2.0.0)
17
- Requires-Dist: herethere (<0.2.0,>=0.1.0)
18
- Requires-Dist: ifaddr
19
- Requires-Dist: ipython
20
- Requires-Dist: ipywidgets
21
- Requires-Dist: nest-asyncio
22
- Requires-Dist: Pillow
23
- Provides-Extra: dev
24
- Requires-Dist: black ; extra == 'dev'
25
- Requires-Dist: codecov ; extra == 'dev'
26
- Requires-Dist: docutils ; extra == 'dev'
27
- Requires-Dist: flake8 ; extra == 'dev'
28
- Requires-Dist: jupytext ; extra == 'dev'
29
- Requires-Dist: pylint ; extra == 'dev'
30
- Requires-Dist: pytest ; extra == 'dev'
31
- Requires-Dist: pytest-asyncio ; extra == 'dev'
32
- Requires-Dist: pytest-cov ; extra == 'dev'
33
- Requires-Dist: pytest-mock ; extra == 'dev'
34
- Provides-Extra: docker
35
- Requires-Dist: jupytext (==1.7.1) ; extra == 'docker'
36
-
37
- PythonHere
38
- ==========
39
-
40
- .. start-badges
41
- .. image:: https://img.shields.io/pypi/status/pythonhere
42
- :target: https://pypi.python.org/pypi/pythonhere
43
- :alt: Status
44
- .. image:: https://img.shields.io/pypi/v/pythonhere.svg
45
- :target: https://pypi.python.org/pypi/pythonhere
46
- :alt: Latest version on PyPi
47
- .. image:: https://img.shields.io/docker/v/herethere/pythonhere?color=%23FFD43B&label=Docker%20Image
48
- :target: https://hub.docker.com/r/herethere/pythonhere
49
- :alt: Docker Image Version (latest by date)
50
- .. image:: https://img.shields.io/pypi/pyversions/pythonhere.svg
51
- :target: https://pypi.python.org/pypi/pythonhere
52
- :alt: Supported Python versions
53
- .. image:: https://github.com/b3b/pythonhere/workflows/ci/badge.svg?branch=master
54
- :target: https://github.com/b3b/pythonhere/actions?workflow=CI
55
- :alt: CI Status
56
- .. image:: https://codecov.io/github/b3b/pythonhere/coverage.svg?branch=master
57
- :target: https://codecov.io/github/b3b/pythonhere?branch=master
58
- :alt: Code coverage Status
59
- .. end-badges
60
-
61
- *Here* is the `Kivy <https://kivy.org>`_ based app to run Python code from the `Jupyter <https://jupyter.org/>`_ magic %there.
62
-
63
- - *Here* is a server part with the GUI interface. It could be Android, Raspberry Pi, some other remote device that being debugged.
64
- - And *%there* is a client - Jupyter magic command to run code interactively on remote device.
65
-
66
- This app could serve as a Python Kivy playground, for dynamic code execution from the PC.
67
-
68
- Project documentation: https://herethere.me
69
-
70
- .. image:: https://raw.githubusercontent.com/b3b/pythonhere/master/docs/description.png
71
- :alt: Project description
72
-
73
-
74
- Install on Android
75
- ------------------
76
-
77
- App is available on `Google Play <https://play.google.com/store/apps/details?id=me.herethere.pythonhere>`_.
78
-
79
- Ready-to-use *PythonHere* APKs are available in the `Releases <https://github.com/b3b/pythonhere/releases>`_ section.
80
-
81
- For a list of installed Python packages, see: `buildozer.spec <./buildozer.spec>`_.
82
-
83
-
84
- Quick Start with Docker
85
- -----------------------
86
-
87
- Docker image is based on `Jupyter Docker Stacks <https://jupyter-docker-stacks.readthedocs.io/en/latest/>`_, and includes installed *PythonHere* with usage examples.
88
-
89
- Example command to start the Docker container::
90
-
91
- docker run \
92
- --rm \
93
- -p 8888:8888 \
94
- -v "$(pwd)/work":/home/jovyan/work \
95
- herethere/pythonhere:latest
96
-
97
-
98
- Command will expose the Jupyter Notebook server on host port 8888. Jupyter logs appear in the terminal and include an URL to the notebook server: http://127.0.0.1:8888/?token=... . Visiting this URL in a browser loads the Jupyter Notebook dashboard page.
99
-
100
- Files from the directory **work** inside container, will be available in the host directory with the same name: **work**.
101
-
102
-
103
- Run with Docker Compose
104
- -----------------------
105
-
106
- Commands to run with Docker Compose, in the source directory:::
107
-
108
- cp docker-compose.yml.tmpl docker-compose.yml
109
- docker-compose up
110
-
111
-
112
- Run locally
113
- -----------
114
-
115
- Commands to run locally::
116
-
117
- pip install pythonhere jupyter
118
- jupyter notebook
119
-
120
-
121
- Build Android app
122
- -----------------
123
-
124
- To build with `Buildozer <https://github.com/kivy/buildozer>`_, run in the source directory::
125
-
126
-
127
- buildozer android debug
128
-
129
-
130
-
131
- Related resources
132
- -----------------
133
-
134
- * `Kivy Remote Shell <https://github.com/kivy/kivy-remote-shell>`_ : Remote SSH+Python interactive shell application
135
- * `herethere <https://github.com/b3b/herethere>`_ : Library for interactive code execution, based on AsyncSSH
136
- * `AsyncSSH <https://github.com/ronf/asyncssh>`_ : Asynchronous SSH for Python
137
- * `Buildozer action <https://github.com/ArtemSBulgakov/buildozer-action>`_ : GitHub action that is used to build Android APK with Buildozer
138
-
139
-
@@ -1,23 +0,0 @@
1
- pythonhere/__init__.py,sha256=Z8wJ5iv1FshCKvQpoRKY28rulC7HTWfy8rj9TdYJIJ8,212
2
- pythonhere/android_here.py,sha256=niI9UJOMwBocblMg2bseZLtTRqDMz4-k-pUcY3PsmTg,3355
3
- pythonhere/enum_here.py,sha256=RBXnu8ppF4idfVvBYbMQ1JO3cfEXfRDrm-uYlPXzhrw,448
4
- pythonhere/exception_manager_here.py,sha256=rm8gHFU37cEJk10anyrIX_CyTqATBsnQVIJJLcgINhs,2399
5
- pythonhere/launcher_here.py,sha256=sBgFv9-GJ5LbxfEiikKlejcA8mDi7zPBxEbKWStVAGc,1274
6
- pythonhere/main.py,sha256=Crc76Py2rz7ERg9mEBD4KD1mOdCqvuy8N-hgMDPxMUY,5571
7
- pythonhere/network_here.py,sha256=vbVjAnnEtnysj2SfblVG2Fr0HNqnAPVHZQCU22K9BnQ,1555
8
- pythonhere/patches_here.py,sha256=bjUvdved7ZaL-9HTKJrtocCOEWEueSkGZs4dxr1L7rU,1728
9
- pythonhere/server_here.py,sha256=IDH_ZYCg0z4F9LjkfY9Wzs21Z4z2L7slfc6xqfMYxH4,1855
10
- pythonhere/version_here.py,sha256=tfw61Ig2RHTD_q6DG85fYMOUic2ygXuYT-eBNy9J8MY,24
11
- pythonhere/window_here.py,sha256=2VU1AC_78wvKtwObezCRYiBdrEYpNCr9lJ1c3nnPgMs,2022
12
- pythonhere/magic_here/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- pythonhere/magic_here/shortcuts.py,sha256=kk9JycYcBrmo3lbVdiXLAJz82Zle4Z6_SQLGuiW0SE4,2365
14
- pythonhere/ui_here/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- pythonhere/ui_here/connection_address_here.py,sha256=VrMMFG9wUJVwmegysi8cl-GZeyNRdKWSjCFcmQtm9ps,1160
16
- pythonhere/ui_here/layout_here.py,sha256=ofEpRTWhwvEmYF9MK8f_UKC6FuCTP9KEPBhOR_S7Z-Y,484
17
- pythonhere/ui_here/server_screen_here.py,sha256=3sBLldgRWvo48_NBG16YjHvAzom-GWg6ngVpUUCu0Uw,925
18
- pythonhere/ui_here/settings_here.py,sha256=P3KeR6TTX0ErGxXjNQ3LN8ujrJ9SQNEr4ZV-3UBw1M4,4099
19
- pythonhere-0.1.5.2.dist-info/LICENSE,sha256=nW9_eVi3dSMOkr6xghsobAovBEJiffjl_5WjujJTa74,1071
20
- pythonhere-0.1.5.2.dist-info/METADATA,sha256=n-NOX_4hz1SQEfC-h8cHYkew9M8bygBjj8CDCw491_M,5002
21
- pythonhere-0.1.5.2.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
22
- pythonhere-0.1.5.2.dist-info/top_level.txt,sha256=dvkfRGF1tFbkjXzD9vwqXTge1Znkv7ga5fRJy5yhJsE,11
23
- pythonhere-0.1.5.2.dist-info/RECORD,,