pymobile-framework 0.1.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.
Files changed (72) hide show
  1. pymobile/__init__.py +109 -0
  2. pymobile/__main__.py +12 -0
  3. pymobile/cli.py +417 -0
  4. pymobile/compiler/__init__.py +26 -0
  5. pymobile/compiler/backends/__init__.py +11 -0
  6. pymobile/compiler/backends/native.py +541 -0
  7. pymobile/compiler/cache.py +89 -0
  8. pymobile/compiler/collector.py +106 -0
  9. pymobile/compiler/icon.py +119 -0
  10. pymobile/compiler/manifest.py +104 -0
  11. pymobile/compiler/packager.py +107 -0
  12. pymobile/compiler/pipeline.py +380 -0
  13. pymobile/compiler/runtime.py +90 -0
  14. pymobile/compiler/scaffold.py +87 -0
  15. pymobile/compiler/sdk_installer.py +189 -0
  16. pymobile/compiler/toolchain.py +251 -0
  17. pymobile/core/__init__.py +68 -0
  18. pymobile/core/api/__init__.py +9 -0
  19. pymobile/core/api/notifications.py +84 -0
  20. pymobile/core/api/permissions.py +137 -0
  21. pymobile/core/api/vibration.py +69 -0
  22. pymobile/core/app.py +197 -0
  23. pymobile/core/bridge/__init__.py +60 -0
  24. pymobile/core/bridge/android.py +116 -0
  25. pymobile/core/bridge/base.py +96 -0
  26. pymobile/core/bridge/jni.py +196 -0
  27. pymobile/core/bridge/stub.py +97 -0
  28. pymobile/core/config.py +226 -0
  29. pymobile/core/events.py +101 -0
  30. pymobile/core/net/__init__.py +7 -0
  31. pymobile/core/net/http.py +257 -0
  32. pymobile/core/platform.py +55 -0
  33. pymobile/core/ui/__init__.py +31 -0
  34. pymobile/core/ui/components.py +235 -0
  35. pymobile/core/ui/layout.py +72 -0
  36. pymobile/core/ui/screen.py +175 -0
  37. pymobile/core/ui/style.py +121 -0
  38. pymobile/core/ui/widget.py +148 -0
  39. pymobile/errors.py +59 -0
  40. pymobile/logging.py +80 -0
  41. pymobile/py.typed +0 -0
  42. pymobile/resources/__init__.py +52 -0
  43. pymobile/resources/android/java/DeviceServices.java +181 -0
  44. pymobile/resources/android/java/MainActivity.java +203 -0
  45. pymobile/resources/android/java/Native.java +73 -0
  46. pymobile/resources/android/java/PythonRuntime.java +103 -0
  47. pymobile/resources/android/java/ViewBuilder.java +485 -0
  48. pymobile/resources/android/jni/pymobile_jni.c +593 -0
  49. pymobile/resources/android/prebuilt/arm64-v8a/classes.dex +0 -0
  50. pymobile/resources/android/prebuilt/arm64-v8a/libpymobile.so +0 -0
  51. pymobile/resources/icons/default_icon.png +0 -0
  52. pymobile/resources/templates/README.md.template +24 -0
  53. pymobile/resources/templates/gitignore.template +10 -0
  54. pymobile/resources/templates/main.py.template +58 -0
  55. pymobile/resources/templates/pymobile.toml.template +42 -0
  56. pymobile/tests/__init__.py +1 -0
  57. pymobile/tests/conftest.py +32 -0
  58. pymobile/tests/test_android_api.py +141 -0
  59. pymobile/tests/test_cli.py +267 -0
  60. pymobile/tests/test_compiler.py +453 -0
  61. pymobile/tests/test_config.py +155 -0
  62. pymobile/tests/test_core.py +171 -0
  63. pymobile/tests/test_http.py +205 -0
  64. pymobile/tests/test_native.py +1006 -0
  65. pymobile/tests/test_ui.py +284 -0
  66. pymobile-demo/main.py +840 -0
  67. pymobile_framework-0.1.0.dist-info/METADATA +1019 -0
  68. pymobile_framework-0.1.0.dist-info/RECORD +72 -0
  69. pymobile_framework-0.1.0.dist-info/WHEEL +5 -0
  70. pymobile_framework-0.1.0.dist-info/entry_points.txt +2 -0
  71. pymobile_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
  72. pymobile_framework-0.1.0.dist-info/top_level.txt +2 -0
pymobile/__init__.py ADDED
@@ -0,0 +1,109 @@
1
+ """PyMobile — a Python framework for building Android applications.
2
+
3
+ Quick start::
4
+
5
+ from pymobile import App, Column, Label, Button, Screen
6
+
7
+ class Home(Screen):
8
+ def build(self):
9
+ return Column(
10
+ Label("Hello, Android!"),
11
+ Button("Tap", on_press=lambda: print("tapped")),
12
+ )
13
+
14
+ App("Demo").run(Home())
15
+
16
+ Build it with ``pymobile build``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ __version__ = "0.1.0"
22
+ __author__ = "PyMobile Contributors"
23
+ __license__ = "MIT"
24
+
25
+ from .core.api import Notifications, Permission, PermissionManager, Vibration
26
+ from .core.app import App
27
+ from .core.config import ProjectConfig, load_config
28
+ from .core.events import Event, EventBus
29
+ from .core.net import HttpClient, Response
30
+ from .core.platform import Platform, current_platform, is_android, is_desktop
31
+ from .core.ui import (
32
+ Align,
33
+ Button,
34
+ Color,
35
+ Column,
36
+ Container,
37
+ EdgeInsets,
38
+ Image,
39
+ Label,
40
+ Navigator,
41
+ ProgressBar,
42
+ Row,
43
+ Screen,
44
+ ScrollView,
45
+ Spacer,
46
+ Stack,
47
+ Style,
48
+ Switch,
49
+ TextInput,
50
+ Widget,
51
+ )
52
+ from .errors import (
53
+ BridgeError,
54
+ ConfigError,
55
+ NetworkError,
56
+ PlatformError,
57
+ PyMobileError,
58
+ ResourceError,
59
+ )
60
+
61
+ __all__ = [
62
+ "__version__",
63
+ # application
64
+ "App",
65
+ "ProjectConfig",
66
+ "load_config",
67
+ "Event",
68
+ "EventBus",
69
+ # platform
70
+ "Platform",
71
+ "current_platform",
72
+ "is_android",
73
+ "is_desktop",
74
+ # android apis
75
+ "Notifications",
76
+ "Vibration",
77
+ "Permission",
78
+ "PermissionManager",
79
+ # networking
80
+ "HttpClient",
81
+ "Response",
82
+ # ui
83
+ "Widget",
84
+ "Container",
85
+ "Label",
86
+ "Button",
87
+ "TextInput",
88
+ "Image",
89
+ "Switch",
90
+ "ProgressBar",
91
+ "Spacer",
92
+ "Column",
93
+ "Row",
94
+ "ScrollView",
95
+ "Stack",
96
+ "Screen",
97
+ "Navigator",
98
+ "Style",
99
+ "Color",
100
+ "Align",
101
+ "EdgeInsets",
102
+ # errors
103
+ "PyMobileError",
104
+ "ConfigError",
105
+ "BridgeError",
106
+ "PlatformError",
107
+ "NetworkError",
108
+ "ResourceError",
109
+ ]
pymobile/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Allow ``python -m pymobile`` as an alias for the ``pymobile`` command.
2
+
3
+ Useful when pip's scripts directory is not on ``PATH`` — a very common
4
+ situation on Windows — because the module form always works.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .cli import main
10
+
11
+ if __name__ == "__main__":
12
+ raise SystemExit(main())
pymobile/cli.py ADDED
@@ -0,0 +1,417 @@
1
+ """Command line interface.
2
+
3
+ Sub-commands: ``init``, ``build``, ``run``, ``info``, ``clean``, ``doctor``.
4
+ Every command returns an exit code; :func:`main` is the console-script entry
5
+ point declared in ``pyproject.toml``.
6
+
7
+ Errors are printed as a short message plus an actionable hint — tracebacks only
8
+ appear with ``--verbose``, because a build tool that dumps a stack trace at a
9
+ user who typed a wrong package name is not helpful.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import platform
18
+ import shutil
19
+ import sys
20
+ from collections.abc import Sequence
21
+ from pathlib import Path
22
+
23
+ from . import __version__
24
+ from .compiler.pipeline import BuildPipeline
25
+ from .compiler.scaffold import create_project
26
+ from .core.config import CONFIG_FILENAME, ProjectConfig, load_config
27
+ from .errors import PyMobileError
28
+ from .logging import configure, get_logger, supports_color
29
+
30
+ __all__ = ["main", "build_parser"]
31
+
32
+ _log = get_logger("cli")
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # output helpers
37
+ # ---------------------------------------------------------------------------
38
+ class _Out:
39
+ """Minimal styled console output."""
40
+
41
+ def __init__(self) -> None:
42
+ self.color = supports_color(sys.stdout)
43
+
44
+ def _paint(self, text: str, code: str) -> str:
45
+ return f"\033[{code}m{text}\033[0m" if self.color else text
46
+
47
+ def ok(self, message: str) -> None:
48
+ # stdout is block-buffered when piped while stderr is not, so progress
49
+ # lines are flushed to keep them in order with warnings and errors.
50
+ print(self._paint("✓", "32") + f" {message}", flush=True)
51
+
52
+ def info(self, message: str) -> None:
53
+ print(self._paint("•", "36") + f" {message}", flush=True)
54
+
55
+ def warn(self, message: str) -> None:
56
+ print(self._paint("!", "33") + f" {message}", file=sys.stderr)
57
+
58
+ def error(self, message: str) -> None:
59
+ print(self._paint("✗", "31") + f" {message}", file=sys.stderr)
60
+
61
+ def hint(self, message: str) -> None:
62
+ print(f" {self._paint('hint:', '2;37')} {message}", file=sys.stderr)
63
+
64
+ def field(self, label: str, value: object) -> None:
65
+ print(f" {label:<14} {value}", flush=True)
66
+
67
+
68
+ _out = _Out()
69
+
70
+
71
+ def _invocation() -> str:
72
+ """How the user launched us: ``pymobile`` or ``python -m pymobile``.
73
+
74
+ pip's Scripts directory is frequently missing from PATH on Windows, so
75
+ echoing back a bare ``pymobile ...`` would print a command that does not
76
+ work for that user.
77
+ """
78
+ launched_as_module = Path(sys.argv[0]).name in ("__main__.py", "cli.py")
79
+ if launched_as_module:
80
+ return f"{Path(sys.executable).name} -m pymobile"
81
+ return "pymobile"
82
+
83
+
84
+ def _export_command(name: str, value: object) -> str:
85
+ """Render an environment-variable assignment for the host shell."""
86
+ if platform.system() == "Windows":
87
+ return f'$env:{name} = "{value}"'
88
+ return f"export {name}={value}"
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # commands
93
+ # ---------------------------------------------------------------------------
94
+ def cmd_init(args: argparse.Namespace) -> int:
95
+ """Create a new project."""
96
+ directory = Path(args.directory).resolve()
97
+ name = args.name or directory.name.replace("-", " ").replace("_", " ").title()
98
+ result = create_project(directory, name, package=args.package, force=args.force)
99
+ _out.ok(f"created project {name!r} in {result.directory}")
100
+ for path in result.files:
101
+ _out.field("", path.relative_to(result.directory))
102
+ print()
103
+ _out.info(f"cd {result.directory.name} && {_invocation()} build --native")
104
+ return 0
105
+
106
+
107
+ def cmd_build(args: argparse.Namespace) -> int:
108
+ """Compile the project into an APK."""
109
+ config = _load(args)
110
+ if args.icon:
111
+ config.icon = args.icon
112
+ if args.output:
113
+ config.output_dir = args.output
114
+ if args.no_optimize:
115
+ config.optimize = False
116
+ config.validate()
117
+
118
+ if args.clean:
119
+ _clean(config)
120
+
121
+ native = getattr(args, "native", False)
122
+ if native:
123
+ _out.info("native build: this may take a few minutes on the first run")
124
+ pipeline = BuildPipeline(
125
+ config,
126
+ use_cache=not args.no_cache and not args.clean,
127
+ native=native,
128
+ on_stage=lambda stage: _out.info(f"{stage}…") if args.verbose else None,
129
+ )
130
+ result = pipeline.run()
131
+
132
+ for warning in result.warnings:
133
+ _out.warn(warning)
134
+
135
+ if result.cached:
136
+ size = (
137
+ f"{result.size / (1024 * 1024):.1f} MB"
138
+ if result.size >= 1024 * 1024
139
+ else f"{result.size_kb:.1f} KB"
140
+ )
141
+ _out.ok(f"up to date: {result.apk.name} ({size})")
142
+ _out.hint("use --clean to force a full rebuild")
143
+ return 0
144
+
145
+ _out.ok(result.summary())
146
+ _out.field("apk", result.apk)
147
+ _out.field("icon", "default" if result.icon_is_default else config.icon)
148
+ if result.native:
149
+ _out.field("install", f"adb install -r {result.apk.name}")
150
+ if args.verbose:
151
+ for timing in result.timings:
152
+ _out.field(timing.name, f"{timing.seconds * 1000:.0f} ms")
153
+ return 0
154
+
155
+
156
+ def cmd_run(args: argparse.Namespace) -> int:
157
+ """Import the entry point and render the first screen on the desktop."""
158
+ config = _load(args)
159
+ entry = config.entrypoint_path
160
+ if not entry.exists():
161
+ raise PyMobileError(
162
+ f"Entry point not found: {entry}",
163
+ hint="Check `entrypoint` in your configuration.",
164
+ )
165
+
166
+ _out.info(f"running {entry.name} in desktop preview mode")
167
+ sys.path.insert(0, str(config.source_path))
168
+ namespace: dict[str, object] = {"__name__": "__main__", "__file__": str(entry)}
169
+ exec(compile(entry.read_text(encoding="utf-8"), str(entry), "exec"), namespace)
170
+ return 0
171
+
172
+
173
+ def cmd_info(args: argparse.Namespace) -> int:
174
+ """Print the resolved configuration."""
175
+ config = _load(args)
176
+ if args.json:
177
+ print(json.dumps(config.to_dict(), indent=2, sort_keys=True))
178
+ return 0
179
+
180
+ _out.info(f"{config.name} {config.version} ({config.package})")
181
+ _out.field("entrypoint", config.entrypoint_path)
182
+ _out.field("source", config.source_path)
183
+ _out.field("output", config.output_path)
184
+ _out.field("apk", config.apk_name)
185
+ _out.field("sdk", f"min {config.min_sdk} / target {config.target_sdk}")
186
+ _out.field("abis", ", ".join(config.abis))
187
+ _out.field("icon", config.icon or "default")
188
+ _out.field("optimize", config.optimize)
189
+ print(" permissions")
190
+ for permission in sorted(config.permissions):
191
+ _out.field("", permission)
192
+ return 0
193
+
194
+
195
+ def cmd_clean(args: argparse.Namespace) -> int:
196
+ """Remove build artifacts."""
197
+ config = _load(args)
198
+ removed = _clean(config)
199
+ _out.ok(f"removed {removed}" if removed else "nothing to clean")
200
+ return 0
201
+
202
+
203
+ def cmd_setup_sdk(args: argparse.Namespace) -> int:
204
+ """Download and install the Android toolchain."""
205
+ from .compiler.sdk_installer import default_sdk_home, install_sdk
206
+
207
+ with_ndk = getattr(args, "with_ndk", False)
208
+ size = "~2.7 GB" if with_ndk else "~800 MB"
209
+ _out.info(f"installing the Android toolchain ({size}, one time)")
210
+ if not with_ndk:
211
+ _out.info("using the prebuilt native bridge; pass --with-ndk to build it from source")
212
+ sdk = install_sdk(Path(args.path) if args.path else None, with_ndk=with_ndk)
213
+ _out.ok(f"toolchain ready: {sdk}")
214
+ _out.info(f"now run: {_invocation()} build --native")
215
+ if not os.environ.get("ANDROID_HOME"):
216
+ _out.hint(f"optional: {_export_command('ANDROID_HOME', sdk)}")
217
+ _ = default_sdk_home
218
+ return 0
219
+
220
+
221
+ def cmd_doctor(args: argparse.Namespace) -> int:
222
+ """Check the environment and configuration."""
223
+ problems = 0
224
+ _out.info(f"pymobile {__version__} on Python {sys.version.split()[0]}")
225
+
226
+ try:
227
+ import PIL # noqa: F401
228
+
229
+ _out.ok("Pillow available — icons will be resized for every density")
230
+ except ImportError:
231
+ _out.warn("Pillow missing — icons are copied without resizing")
232
+ _out.hint("pip install Pillow")
233
+
234
+ try:
235
+ config = _load(args)
236
+ except PyMobileError as exc:
237
+ _out.warn(f"no usable project here: {exc}")
238
+ return 0
239
+
240
+ _out.ok(f"configuration valid: {config.name} ({config.package})")
241
+ if not config.entrypoint_path.exists():
242
+ _out.error(f"entry point missing: {config.entrypoint_path}")
243
+ problems += 1
244
+ else:
245
+ _out.ok(f"entry point found: {config.entrypoint}")
246
+
247
+ icon = config.icon_path
248
+ if icon is not None and not icon.exists():
249
+ _out.error(f"icon missing: {icon}")
250
+ problems += 1
251
+ elif icon is not None:
252
+ _out.ok(f"custom icon: {icon.name}")
253
+ else:
254
+ _out.ok("using the default icon")
255
+
256
+ from .compiler.toolchain import ToolchainError, find_toolchain
257
+
258
+ try:
259
+ toolchain = find_toolchain()
260
+ toolchain.verify()
261
+ _out.ok(f"Android toolchain ready (build-tools {toolchain.build_tools.name})")
262
+ if toolchain.has_ndk:
263
+ _out.ok("NDK present — the native bridge can be rebuilt from source")
264
+ else:
265
+ _out.ok("using the prebuilt native bridge (no NDK needed)")
266
+ _out.info(f"native APK builds available: {_invocation()} build --native")
267
+ except (ToolchainError, PyMobileError) as exc:
268
+ _out.warn(f"Android SDK not usable — only structural builds are available: {exc}")
269
+ _out.hint("run `pymobile setup-sdk` to install it automatically")
270
+
271
+ if problems:
272
+ _out.error(f"{problems} problem(s) found")
273
+ return 1
274
+ _out.ok("everything looks good")
275
+ return 0
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # helpers
280
+ # ---------------------------------------------------------------------------
281
+ def _load(args: argparse.Namespace) -> ProjectConfig:
282
+ """Load the project configuration honouring ``--config``."""
283
+ return load_config(getattr(args, "config", None) or Path.cwd())
284
+
285
+
286
+ def _clean(config: ProjectConfig) -> str:
287
+ """Delete the output directory; returns what was removed."""
288
+ output = config.output_path
289
+ if output.exists():
290
+ shutil.rmtree(output)
291
+ return str(output)
292
+ return ""
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # parser
297
+ # ---------------------------------------------------------------------------
298
+ def build_parser() -> argparse.ArgumentParser:
299
+ """Construct the argument parser.
300
+
301
+ ``--verbose`` and ``--config`` are attached both globally and to every
302
+ sub-command, so ``pymobile -v build`` and ``pymobile build -v`` both work —
303
+ users should not have to remember where a flag belongs.
304
+ """
305
+ # SUPPRESS keeps unset flags out of the namespace, so a sub-command does
306
+ # not silently reset a value given before the sub-command name.
307
+ common = argparse.ArgumentParser(add_help=False)
308
+ common.add_argument(
309
+ "-v",
310
+ "--verbose",
311
+ action="store_true",
312
+ default=argparse.SUPPRESS,
313
+ help="show detailed output",
314
+ )
315
+ common.add_argument(
316
+ "-c",
317
+ "--config",
318
+ metavar="PATH",
319
+ default=argparse.SUPPRESS,
320
+ help=f"path to {CONFIG_FILENAME} or a project directory",
321
+ )
322
+
323
+ parser = argparse.ArgumentParser(
324
+ prog="pymobile",
325
+ description="Build Android applications with Python.",
326
+ epilog="Docs: https://github.com/pymobile/pymobile",
327
+ parents=[common],
328
+ )
329
+ parser.add_argument("--version", action="version", version=f"pymobile {__version__}")
330
+
331
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
332
+
333
+ init = sub.add_parser("init", help="create a new project", parents=[common])
334
+ init.add_argument("directory", nargs="?", default=".", help="target directory")
335
+ init.add_argument("-n", "--name", help="application name")
336
+ init.add_argument("-p", "--package", help="package id, e.g. com.example.app")
337
+ init.add_argument("-f", "--force", action="store_true", help="write into a non-empty directory")
338
+ init.set_defaults(func=cmd_init)
339
+
340
+ build = sub.add_parser("build", help="compile the project into an APK", parents=[common])
341
+ build.add_argument("-o", "--output", help="output directory")
342
+ build.add_argument("-i", "--icon", help="path to a custom launcher icon")
343
+ build.add_argument(
344
+ "--native",
345
+ action="store_true",
346
+ help="build a real, signed, installable APK (needs the Android SDK/NDK)",
347
+ )
348
+ build.add_argument("--clean", action="store_true", help="rebuild from scratch")
349
+ build.add_argument("--no-cache", action="store_true", help="ignore the incremental cache")
350
+ build.add_argument(
351
+ "--no-optimize", action="store_true", help="ship sources instead of bytecode"
352
+ )
353
+ build.set_defaults(func=cmd_build)
354
+
355
+ run = sub.add_parser("run", help="preview the app on this machine", parents=[common])
356
+ run.set_defaults(func=cmd_run)
357
+
358
+ info = sub.add_parser("info", help="show the resolved configuration", parents=[common])
359
+ info.add_argument("--json", action="store_true", help="machine-readable output")
360
+ info.set_defaults(func=cmd_info)
361
+
362
+ clean = sub.add_parser("clean", help="remove build artifacts", parents=[common])
363
+ clean.set_defaults(func=cmd_clean)
364
+
365
+ setup = sub.add_parser(
366
+ "setup-sdk", help="download the Android SDK/NDK for native builds", parents=[common]
367
+ )
368
+ setup.add_argument("--path", help="install directory (default: ~/.andro)")
369
+ setup.add_argument(
370
+ "--with-ndk",
371
+ action="store_true",
372
+ help="also download the NDK (~2 GB), only needed to rebuild the native bridge",
373
+ )
374
+ setup.set_defaults(func=cmd_setup_sdk)
375
+
376
+ doctor = sub.add_parser("doctor", help="check the environment", parents=[common])
377
+ doctor.set_defaults(func=cmd_doctor)
378
+
379
+ return parser
380
+
381
+
382
+ def main(argv: Sequence[str] | None = None) -> int:
383
+ """CLI entry point; returns a process exit code."""
384
+ parser = build_parser()
385
+ args = parser.parse_args(argv)
386
+ # restore the defaults suppressed above
387
+ args.verbose = getattr(args, "verbose", False)
388
+ args.config = getattr(args, "config", None)
389
+
390
+ if not getattr(args, "command", None):
391
+ parser.print_help()
392
+ return 0
393
+
394
+ configure("debug" if args.verbose else "warning")
395
+
396
+ try:
397
+ return int(args.func(args))
398
+ except PyMobileError as exc:
399
+ _out.error(str(exc))
400
+ if exc.hint:
401
+ _out.hint(exc.hint)
402
+ if args.verbose:
403
+ raise
404
+ return 1
405
+ except KeyboardInterrupt: # pragma: no cover - interactive
406
+ _out.warn("interrupted")
407
+ return 130
408
+ except Exception as exc:
409
+ _out.error(f"unexpected error: {exc}")
410
+ if args.verbose:
411
+ raise
412
+ _out.hint("re-run with --verbose to see the full traceback")
413
+ return 1
414
+
415
+
416
+ if __name__ == "__main__": # pragma: no cover
417
+ raise SystemExit(main())
@@ -0,0 +1,26 @@
1
+ """Compiler: turns a Python project into an Android APK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cache import BuildCache
6
+ from .collector import SourceSet, collect_sources
7
+ from .icon import IconSet, prepare_icons
8
+ from .manifest import build_manifest
9
+ from .packager import ApkPackager, PackageResult
10
+ from .pipeline import BuildPipeline, BuildResult, build_apk
11
+ from .scaffold import create_project
12
+
13
+ __all__ = [
14
+ "BuildPipeline",
15
+ "BuildResult",
16
+ "build_apk",
17
+ "build_manifest",
18
+ "collect_sources",
19
+ "SourceSet",
20
+ "prepare_icons",
21
+ "IconSet",
22
+ "ApkPackager",
23
+ "PackageResult",
24
+ "BuildCache",
25
+ "create_project",
26
+ ]
@@ -0,0 +1,11 @@
1
+ """Build backends.
2
+
3
+ * :mod:`native` — full Android toolchain, produces an installable APK.
4
+ The default structural packager lives in :mod:`pymobile.compiler.packager`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .native import NativeBackend, NativeBuildResult
10
+
11
+ __all__ = ["NativeBackend", "NativeBuildResult"]