flet-desktop 0.82.3.dev7857__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,460 @@
1
+ import asyncio
2
+ import ctypes
3
+ import ctypes.util
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import signal
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ import tarfile
12
+ import tempfile
13
+ import urllib.request
14
+ import zipfile
15
+ from pathlib import Path
16
+
17
+ import flet_desktop
18
+ import flet_desktop.version
19
+ from flet.utils import (
20
+ get_arch,
21
+ is_linux,
22
+ is_macos,
23
+ is_windows,
24
+ random_string,
25
+ safe_tar_extractall,
26
+ safe_zip_extractall,
27
+ )
28
+
29
+ logger = logging.getLogger(flet_desktop.__name__)
30
+
31
+ # Supported Linux build targets ordered by glibc version.
32
+ # Each entry maps a minimum glibc (major, minor) to a distro_id used in
33
+ # release artifact filenames.
34
+ _GLIBC_DISTRO_TABLE = [
35
+ ((2, 28), "debian10"),
36
+ ((2, 31), "ubuntu20.04"),
37
+ ((2, 35), "ubuntu22.04"),
38
+ ((2, 36), "debian12"),
39
+ ((2, 39), "ubuntu24.04"),
40
+ ]
41
+
42
+
43
+ def get_package_bin_dir():
44
+ """
45
+ Return the directory that contains bundled desktop runtime artifacts.
46
+
47
+ The directory may contain platform-specific executables or compressed
48
+ archives used to provision the desktop client at runtime. When the
49
+ package is installed without bundled binaries the directory will be
50
+ empty and the download path is used instead.
51
+ """
52
+
53
+ return str(Path(__file__).parent.joinpath("app"))
54
+
55
+
56
+ def __get_desktop_flavor():
57
+ """
58
+ Return the desktop client flavor to use: ``"full"`` or ``"light"``.
59
+
60
+ Resolution order:
61
+
62
+ 1. ``FLET_DESKTOP_FLAVOR`` environment variable.
63
+ 2. ``[tool.flet].desktop_flavor`` in the project's ``pyproject.toml``.
64
+ 3. Default: ``"light"`` on Linux, ``"full"`` elsewhere.
65
+ """
66
+
67
+ env_flavor = os.environ.get("FLET_DESKTOP_FLAVOR", "").strip().lower()
68
+ if env_flavor in ("full", "light"):
69
+ return env_flavor
70
+
71
+ # Try reading from pyproject.toml in the current working directory.
72
+ try:
73
+ if sys.version_info >= (3, 11):
74
+ import tomllib
75
+ else:
76
+ import tomli as tomllib # type: ignore[no-redef]
77
+
78
+ pyproject_path = Path(os.getcwd()) / "pyproject.toml"
79
+ if pyproject_path.is_file():
80
+ with pyproject_path.open("rb") as f:
81
+ data = tomllib.load(f)
82
+ flavor = data.get("tool", {}).get("flet", {}).get("desktop_flavor", "")
83
+ if isinstance(flavor, str) and flavor.strip().lower() in (
84
+ "full",
85
+ "light",
86
+ ):
87
+ return flavor.strip().lower()
88
+ except Exception:
89
+ pass
90
+
91
+ return "light" if is_linux() else "full"
92
+
93
+
94
+ def __get_system_glibc_version():
95
+ """
96
+ Return the system glibc version as a ``(major, minor)`` tuple.
97
+
98
+ Falls back to ``(0, 0)`` when detection fails.
99
+ """
100
+
101
+ try:
102
+ libc_name = ctypes.util.find_library("c")
103
+ if not libc_name:
104
+ return (0, 0)
105
+ libc = ctypes.CDLL(libc_name)
106
+ gnu_get_libc_version = libc.gnu_get_libc_version
107
+ gnu_get_libc_version.restype = ctypes.c_char_p
108
+ ver_str = gnu_get_libc_version().decode("ascii")
109
+ parts = ver_str.split(".")
110
+ return (int(parts[0]), int(parts[1]))
111
+ except Exception:
112
+ return (0, 0)
113
+
114
+
115
+ def __get_linux_distro_id():
116
+ """
117
+ Return the distro id to use for downloading the Linux binary archive.
118
+
119
+ Uses glibc version detection to pick the best matching build target
120
+ (highest glibc requirement that is <= system glibc). Can be
121
+ overridden via the ``FLET_LINUX_DISTRO`` environment variable.
122
+ """
123
+
124
+ override = os.environ.get("FLET_LINUX_DISTRO", "").strip()
125
+ if override:
126
+ return override
127
+
128
+ sys_glibc = __get_system_glibc_version()
129
+ best = None
130
+ for required_glibc, distro_id in _GLIBC_DISTRO_TABLE:
131
+ if sys_glibc >= required_glibc:
132
+ best = distro_id
133
+ if best is None:
134
+ best = _GLIBC_DISTRO_TABLE[0][1] # oldest as last resort
135
+ logger.warning(
136
+ f"Could not detect glibc version (got {sys_glibc}), "
137
+ f"falling back to {best}. Set FLET_LINUX_DISTRO to override."
138
+ )
139
+ return best
140
+
141
+
142
+ def __get_artifact_filename():
143
+ """
144
+ Return the release artifact filename for the current platform.
145
+
146
+ Windows: ``flet-windows.zip``
147
+ macOS: ``flet-macos.tar.gz``
148
+ Linux: ``flet-linux-{distro}[-light]-{arch}.tar.gz``
149
+ """
150
+
151
+ if is_windows():
152
+ return "flet-windows.zip"
153
+ if is_macos():
154
+ return "flet-macos.tar.gz"
155
+ # Linux
156
+ distro = __get_linux_distro_id()
157
+ arch = get_arch()
158
+ flavor = __get_desktop_flavor()
159
+ if flavor == "light":
160
+ return f"flet-linux-{distro}-light-{arch}.tar.gz"
161
+ return f"flet-linux-{distro}-{arch}.tar.gz"
162
+
163
+
164
+ def __get_client_storage_dir():
165
+ """
166
+ Return a versioned local directory used to store unpacked desktop client files.
167
+
168
+ The path format is:
169
+ ``~/.flet/client/flet-desktop-{flavor}-{version}``.
170
+ """
171
+
172
+ flavor = __get_desktop_flavor()
173
+ return Path.home().joinpath(
174
+ ".flet", "client", f"flet-desktop-{flavor}-{flet_desktop.version.version}"
175
+ )
176
+
177
+
178
+ def __download_flet_client(file_name):
179
+ """
180
+ Download a Flet client archive from GitHub Releases.
181
+
182
+ The download URL is constructed from the version embedded in
183
+ ``flet_desktop.version.version``. It can be overridden entirely
184
+ via the ``FLET_CLIENT_URL`` environment variable.
185
+
186
+ Args:
187
+ file_name: Archive filename to download (e.g. ``flet-macos.tar.gz``).
188
+
189
+ Returns:
190
+ Local path to the downloaded archive.
191
+ """
192
+
193
+ ver = flet_desktop.version.version
194
+ flet_url = f"https://github.com/flet-dev/flet/releases/download/v{ver}/{file_name}"
195
+ flet_url = os.environ.get("FLET_CLIENT_URL", flet_url)
196
+ logger.info(f"Downloading Flet v{ver} from {flet_url}")
197
+ print(f"Preparing Flet v{ver} for the first use. This is a one-time operation...")
198
+ temp_arch = Path(tempfile.gettempdir()).joinpath(f"{file_name}.{random_string(10)}")
199
+ urllib.request.urlretrieve(flet_url, str(temp_arch))
200
+ return str(temp_arch)
201
+
202
+
203
+ def ensure_client_cached():
204
+ """
205
+ Ensure the desktop client is extracted in the local cache directory.
206
+
207
+ If the cache directory does not exist, looks for a bundled archive in
208
+ the package (for PyInstaller bundles) and falls back to downloading
209
+ from GitHub Releases.
210
+
211
+ Returns:
212
+ :class:`Path` to the cache directory containing the unpacked client.
213
+ """
214
+
215
+ cache_dir = __get_client_storage_dir()
216
+ if cache_dir.exists():
217
+ logger.info(f"Flet client found in cache: {cache_dir}")
218
+ return cache_dir
219
+
220
+ artifact = __get_artifact_filename()
221
+
222
+ # Check for a bundled archive (PyInstaller or legacy wheel).
223
+ bundled = os.path.join(get_package_bin_dir(), artifact)
224
+ if os.path.exists(bundled):
225
+ archive_path = bundled
226
+ else:
227
+ archive_path = __download_flet_client(artifact)
228
+
229
+ # Extract to a temp directory first, then atomically rename to the cache
230
+ # directory. This prevents a partially extracted cache from being treated
231
+ # as valid on a subsequent run.
232
+ temp_extract = cache_dir.parent / f"{cache_dir.name}.{random_string(8)}"
233
+ temp_extract.mkdir(parents=True, exist_ok=True)
234
+
235
+ logger.info(f"Extracting Flet client from {archive_path} to {temp_extract}")
236
+ try:
237
+ if archive_path.endswith(".zip"):
238
+ with zipfile.ZipFile(archive_path, "r") as zf:
239
+ safe_zip_extractall(zf, str(temp_extract))
240
+ else:
241
+ with tarfile.open(archive_path, "r:gz") as tar_arch:
242
+ safe_tar_extractall(tar_arch, str(temp_extract))
243
+
244
+ temp_extract.rename(cache_dir)
245
+ except Exception:
246
+ shutil.rmtree(temp_extract, ignore_errors=True)
247
+ raise
248
+
249
+ return cache_dir
250
+
251
+
252
+ def open_flet_view(page_url, assets_dir, hidden):
253
+ """
254
+ Start a desktop view process and return the process object and PID file path.
255
+
256
+ Args:
257
+ page_url: Page endpoint the desktop client should open.
258
+ assets_dir: Optional assets directory passed to the client process.
259
+ hidden: Whether the window should start hidden.
260
+
261
+ Returns:
262
+ A tuple containing:
263
+ - `subprocess.Popen`: started desktop process.
264
+ - `str`: path to a temporary PID file used by [`close_flet_view()`][(m).].
265
+ """
266
+
267
+ args, flet_env, pid_file = __locate_and_unpack_flet_view(
268
+ page_url, assets_dir, hidden
269
+ )
270
+ return subprocess.Popen(args, env=flet_env), pid_file
271
+
272
+
273
+ async def open_flet_view_async(page_url, assets_dir, hidden):
274
+ """
275
+ Asynchronously start a desktop view process.
276
+
277
+ Args:
278
+ page_url: Page endpoint the desktop client should open.
279
+ assets_dir: Optional assets directory passed to the client process.
280
+ hidden: Whether the window should start hidden.
281
+
282
+ Returns:
283
+ A tuple containing:
284
+ - `asyncio.subprocess.Process`: started desktop process.
285
+ - `str`: path to a temporary PID file used by [`close_flet_view()`][(m).].
286
+ """
287
+
288
+ args, flet_env, pid_file = __locate_and_unpack_flet_view(
289
+ page_url, assets_dir, hidden
290
+ )
291
+ return (
292
+ await asyncio.create_subprocess_exec(args[0], *args[1:], env=flet_env),
293
+ pid_file,
294
+ )
295
+
296
+
297
+ def close_flet_view(pid_file):
298
+ """
299
+ Terminate a running desktop view process using its PID file.
300
+
301
+ The function attempts to read the process ID from `pid_file`, send a
302
+ termination signal, and remove the PID file. Failures while terminating are
303
+ intentionally ignored, but the PID file is removed when possible.
304
+
305
+ Args:
306
+ pid_file: Path to the PID file returned by [`open_flet_view()`][(m).]
307
+ or [`open_flet_view_async()`][(m).].
308
+ """
309
+
310
+ if pid_file is not None and os.path.exists(pid_file):
311
+ try:
312
+ with open(pid_file, encoding="utf-8") as f:
313
+ fvp_pid = int(f.read())
314
+ logger.debug(f"Flet View process {fvp_pid}")
315
+ os.kill(fvp_pid, signal.SIGKILL)
316
+ except Exception:
317
+ pass
318
+ finally:
319
+ os.remove(pid_file)
320
+
321
+
322
+ def __locate_and_unpack_flet_view(page_url, assets_dir, hidden):
323
+ """
324
+ Resolve desktop client executable, prepare launch arguments, and environment.
325
+
326
+ Resolution strategy (per platform):
327
+
328
+ 1. Prefer app binaries produced by ``flet build`` in the current workspace.
329
+ 2. Use ``FLET_VIEW_PATH`` when provided.
330
+ 3. Use cached / downloaded client from ``~/.flet/client/``.
331
+
332
+ Platform-specific launch commands are prepared for Windows, macOS, and Linux.
333
+
334
+ Args:
335
+ page_url: Page endpoint the desktop client should open.
336
+ assets_dir: Optional assets directory passed to the client process.
337
+ hidden: Whether to set ``FLET_HIDE_WINDOW_ON_START=true`` in process env.
338
+
339
+ Returns:
340
+ A tuple containing:
341
+ - ``list[str]``: command arguments for the desktop client.
342
+ - ``dict[str, str]``: environment variables for the launched process.
343
+ - ``str``: path to the temporary PID file.
344
+
345
+ Raises:
346
+ FileNotFoundError: If a required desktop executable or archive
347
+ cannot be located or downloaded.
348
+ """
349
+
350
+ logger.info("Starting Flet View app...")
351
+
352
+ args = []
353
+
354
+ # pid file - Flet client writes its process ID to this file
355
+ pid_file = str(Path(tempfile.gettempdir()).joinpath(random_string(20)))
356
+
357
+ if is_windows():
358
+ flet_path = None
359
+ # 1. Try loading Flet client built with the latest run of `flet build`
360
+ build_windows = os.path.join(os.getcwd(), "build", "windows")
361
+ if os.path.exists(build_windows):
362
+ for f in os.listdir(build_windows):
363
+ if f.endswith(".exe"):
364
+ flet_path = os.path.join(build_windows, f)
365
+
366
+ # 2. Check FLET_VIEW_PATH (developer mode)
367
+ if not flet_path:
368
+ flet_view_path = os.environ.get("FLET_VIEW_PATH")
369
+ if flet_view_path and os.path.exists(flet_view_path):
370
+ exe_path = os.path.join(flet_view_path, "flet.exe")
371
+ if os.path.isfile(exe_path):
372
+ logger.info(f"Flet View found via FLET_VIEW_PATH: {flet_view_path}")
373
+ flet_path = exe_path
374
+ else:
375
+ logger.warning(
376
+ f"FLET_VIEW_PATH set to {flet_view_path} "
377
+ f"but flet.exe not found there"
378
+ )
379
+
380
+ # 3. Use cached or downloaded client
381
+ if not flet_path:
382
+ cache_dir = ensure_client_cached()
383
+ flet_path = str(cache_dir.joinpath("flet", "flet.exe"))
384
+
385
+ args = [flet_path, page_url, pid_file]
386
+
387
+ elif is_macos():
388
+ app_path = None
389
+ # 1. Try loading Flet client built with the latest run of `flet build`
390
+ build_macos = os.path.join(os.getcwd(), "build", "macos")
391
+ if os.path.exists(build_macos):
392
+ for f in os.listdir(build_macos):
393
+ if f.endswith(".app"):
394
+ app_path = os.path.join(build_macos, f)
395
+
396
+ # 2. Check FLET_VIEW_PATH (developer mode)
397
+ if not app_path:
398
+ flet_view_path = os.environ.get("FLET_VIEW_PATH")
399
+ if flet_view_path:
400
+ logger.info(f"Flet.app is set via FLET_VIEW_PATH: {flet_view_path}")
401
+ temp_flet_dir = Path(flet_view_path)
402
+ else:
403
+ # 3. Use cached or downloaded client
404
+ temp_flet_dir = ensure_client_cached()
405
+
406
+ app_name = None
407
+ for f in os.listdir(temp_flet_dir):
408
+ if f.endswith(".app"):
409
+ app_name = f
410
+ if app_name is None:
411
+ raise FileNotFoundError(
412
+ f"Application bundle not found in {temp_flet_dir}"
413
+ )
414
+ app_path = temp_flet_dir.joinpath(app_name)
415
+
416
+ logger.info(f"page_url: {page_url}")
417
+ logger.info(f"pid_file: {pid_file}")
418
+ args = ["open", str(app_path), "-n", "-W", "--args", page_url, pid_file]
419
+
420
+ elif is_linux():
421
+ app_path = None
422
+ # 1. Try loading Flet client built with the latest run of `flet build`
423
+ build_linux = os.path.join(os.getcwd(), "build", "linux")
424
+ if os.path.exists(build_linux):
425
+ for f in os.listdir(build_linux):
426
+ ef = os.path.join(build_linux, f)
427
+ if os.path.isfile(ef) and stat.S_IXUSR & os.stat(ef)[stat.ST_MODE]:
428
+ app_path = ef
429
+
430
+ # 2. Check FLET_VIEW_PATH (developer mode)
431
+ if not app_path:
432
+ flet_view_path = os.environ.get("FLET_VIEW_PATH")
433
+ if flet_view_path:
434
+ exe_path = str(Path(flet_view_path).joinpath("flet"))
435
+ if os.path.isfile(exe_path):
436
+ logger.info(
437
+ f"Flet View is set via FLET_VIEW_PATH: {flet_view_path}"
438
+ )
439
+ app_path = exe_path
440
+ else:
441
+ logger.warning(
442
+ f"FLET_VIEW_PATH set to {flet_view_path} "
443
+ f"but flet executable not found there"
444
+ )
445
+ if not app_path:
446
+ # 3. Use cached or downloaded client
447
+ cache_dir = ensure_client_cached()
448
+ app_path = str(cache_dir.joinpath("flet", "flet"))
449
+
450
+ args = [str(app_path), page_url, pid_file]
451
+
452
+ flet_env = {**os.environ}
453
+
454
+ if assets_dir:
455
+ args.append(assets_dir)
456
+
457
+ if hidden:
458
+ flet_env["FLET_HIDE_WINDOW_ON_START"] = "true"
459
+
460
+ return args, flet_env, pid_file
@@ -0,0 +1 @@
1
+ version = "0.82.3.dev7857"
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: flet-desktop
3
+ Version: 0.82.3.dev7857
4
+ Summary: Flet Desktop client in Flutter
5
+ Author-email: "Appveyor Systems Inc." <hello@flet.dev>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://flet.dev
8
+ Project-URL: Repository, https://github.com/flet-dev/flet
9
+ Project-URL: Documentation, https://flet.dev/docs
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: flet==0.82.3.dev7857
13
+
14
+ # Flet Desktop client in Flutter
15
+
16
+ [![python](https://img.shields.io/badge/python-%3E%3D3.10-%2334D058)](https://pypi.org/project/flet-desktop)
17
+ [![docstring coverage](https://docs.flet.dev/assets/badges/docs-coverage/flet-desktop.svg)](https://docs.flet.dev/assets/badges/docs-coverage/flet-desktop.svg)
18
+
19
+ This package contains a compiled Flutter Flet desktop client.
@@ -0,0 +1,6 @@
1
+ flet_desktop/__init__.py,sha256=IJxzvRYBHu3eb6ufGlfdE81IU7DktM4_lXhZMxgcdn0,15423
2
+ flet_desktop/version.py,sha256=Sdk7It9b6uveOKAtRc0iZjI8e6NPX6s2K5lqVybyv34,27
3
+ flet_desktop-0.82.3.dev7857.dist-info/METADATA,sha256=c9xWkQQiEd5yegj3qWjppTVS4NL6njmiopEis_PBSMA,806
4
+ flet_desktop-0.82.3.dev7857.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ flet_desktop-0.82.3.dev7857.dist-info/top_level.txt,sha256=ugIkH3TGoxP2-XUffs2tqRF1Qi9icFhrR1d1SSzgktc,13
6
+ flet_desktop-0.82.3.dev7857.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flet_desktop