pythonhere 0.3.1__py3-none-any.whl → 0.4.1__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.
@@ -0,0 +1,145 @@
1
+ """Find ML model files managed by PythonHere."""
2
+
3
+ import os
4
+ import sys
5
+
6
+
7
+ def _android_models_directory():
8
+ """Return the model directory belonging to the running Android app."""
9
+
10
+ from jnius import autoclass
11
+
12
+ PythonActivity = autoclass("org.kivy.android.PythonActivity")
13
+ activity = PythonActivity.mActivity
14
+ if activity is None:
15
+ raise RuntimeError("A running PythonHere activity is required")
16
+
17
+ external_files = activity.getExternalFilesDir(None)
18
+ if external_files is None:
19
+ raise RuntimeError("Android external app storage is unavailable")
20
+
21
+ return os.path.join(str(external_files.getAbsolutePath()), "models")
22
+
23
+
24
+ def _is_android():
25
+ return sys.platform == "android" or "ANDROID_ARGUMENT" in os.environ
26
+
27
+
28
+ def models_directory(create=False):
29
+ """Return PythonHere's platform-specific model directory.
30
+
31
+ Android uses the app-specific external files directory. Linux uses
32
+ PythonHere's per-user application data directory.
33
+ """
34
+
35
+ if _is_android():
36
+ path = _android_models_directory()
37
+ else:
38
+ path = os.path.expanduser("~/.local/share/pythonhere/models")
39
+
40
+ path = os.path.abspath(path)
41
+ if create:
42
+ os.makedirs(path, exist_ok=True)
43
+ return path
44
+
45
+
46
+ def _relative_parts(value, label):
47
+ """Return safe POSIX-style path components for a remote identifier."""
48
+
49
+ parts = value.split("/")
50
+ if not value or any(part in {"", ".", ".."} for part in parts):
51
+ raise ValueError(f"{label} must be a relative path without dot components")
52
+ return parts
53
+
54
+
55
+ def model_path(repo_id, filename):
56
+ """Return the local path for a model identified by repository and file."""
57
+
58
+ path = os.path.join(
59
+ models_directory(),
60
+ *_relative_parts(repo_id, "repo_id"),
61
+ *_relative_parts(filename, "filename"),
62
+ )
63
+ return os.path.abspath(path)
64
+
65
+
66
+ def require_model(repo_id, filename):
67
+ """Return an installed model path, or raise ``FileNotFoundError``."""
68
+
69
+ path = model_path(repo_id, filename)
70
+ if not os.path.isfile(path):
71
+ raise FileNotFoundError(
72
+ f"ML model is not installed: {repo_id}/{filename} ({path})"
73
+ )
74
+ return path
75
+
76
+
77
+ def _model_extensions(extensions):
78
+ """Normalize an optional model-extension filter."""
79
+
80
+ if extensions is None:
81
+ return None
82
+ if isinstance(extensions, str):
83
+ extensions = (extensions,)
84
+
85
+ normalized = set()
86
+ for extension in extensions:
87
+ if not isinstance(extension, str) or not extension:
88
+ raise ValueError("extensions must contain non-empty strings")
89
+ normalized_extension = extension.lower()
90
+ normalized.add(
91
+ normalized_extension
92
+ if normalized_extension.startswith(".")
93
+ else f".{normalized_extension}"
94
+ )
95
+ return tuple(sorted(normalized))
96
+
97
+
98
+ def _discover_directory(directory, extensions):
99
+ """Yield model files recursively beneath a directory."""
100
+
101
+ path = os.path.abspath(os.fspath(directory))
102
+ if not os.path.isdir(path):
103
+ return
104
+
105
+ def ignore_error(_error):
106
+ pass
107
+
108
+ for root, directories, files in os.walk(path, onerror=ignore_error):
109
+ directories.sort(key=str.casefold)
110
+ for filename in files:
111
+ lower_filename = filename.lower()
112
+ if lower_filename.endswith((".part", ".part.json")):
113
+ continue
114
+ if extensions is None or lower_filename.endswith(extensions):
115
+ yield os.path.abspath(os.path.join(root, filename))
116
+
117
+
118
+ def discover_models(extra_directories=(), *, extensions=".litertlm"):
119
+ """Find local model files, optionally filtered by filename extension.
120
+
121
+ ``extensions`` may be one extension or an iterable, with or without the
122
+ leading dot. The default finds LiteRT-LM models. Pass ``None`` to return
123
+ all completed files. Temporary files belonging to an in-progress download
124
+ are always ignored.
125
+ """
126
+
127
+ if isinstance(extra_directories, (str, bytes, os.PathLike)):
128
+ extra_directories = (extra_directories,)
129
+ extensions = _model_extensions(extensions)
130
+
131
+ models = []
132
+ seen = set()
133
+ for directory in (models_directory(), *extra_directories):
134
+ for discovered_path in _discover_directory(directory, extensions):
135
+ key = os.path.normcase(discovered_path)
136
+ if key not in seen:
137
+ seen.add(key)
138
+ models.append(discovered_path)
139
+ return sorted(
140
+ models,
141
+ key=lambda path: (os.path.basename(path).casefold(), path.casefold()),
142
+ )
143
+
144
+
145
+ __all__ = ("discover_models", "model_path", "models_directory", "require_model")
pythonhere/server_here.py CHANGED
@@ -39,6 +39,7 @@ async def run_ssh_server(app):
39
39
  namespace=app.ssh_server_namespace,
40
40
  server_factory=PythonHereServer,
41
41
  )
42
+ app.ssh_host_key_sha256 = server.host_key_sha256
42
43
  except Exception as exc:
43
44
  Logger.error("PythonHere: SSH server start error")
44
45
  Logger.exception(exc)