Spaces:
Running
Running
File size: 4,157 Bytes
6303ae6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | """Single source of truth for every path the app resolves.
This is the ONE helper used by ``app.config``, ``desktop_app.py``, and the
launcher to decide:
* where read-only bundled resources live (``app/``, ``.streamlit/``,
``.env.example``) β :func:`resource_base` / :func:`resource_path`, and
* where user-writable state lives (the API-key ``.env`` and rotating logs) β
:func:`state_dir`.
It is deliberately dependency-free (stdlib only) so it can be imported from
anywhere β including the launcher, before anything else is set up β with no
risk of an import cycle.
Three run contexts must all work, and this module is what makes them
indistinguishable to the rest of the app:
1. **Dev checkout** β ``streamlit run app/main.py`` (or pytest). State stays in
the repo root; behaviour is unchanged.
2. **Zero-touch desktop app** β the embeddable-Python launcher sets
``UPS_PACKAGED=1`` and runs ``desktop_app.py`` from the project folder.
State moves to ``%APPDATA%`` so the (possibly relocated, possibly
read-only) folder is never written to.
3. **PyInstaller bundle** β ``sys.frozen`` is set and resources live under
``sys._MEIPASS``; state again moves to ``%APPDATA%``.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
# Brand folder name used under %APPDATA% (and mirrored by the Windows
# installer + the launcher's ensure_runtime step). Keep these in sync.
APP_DIRNAME = "UpworkProposalStrategist"
# Environment flag the desktop launcher sets to declare "this is the packaged
# desktop app, use per-user state" β even though an embeddable-Python run is
# not technically ``sys.frozen``. Dev ``streamlit run`` leaves it unset.
PACKAGED_ENV_FLAG = "UPS_PACKAGED"
def is_frozen() -> bool:
"""True when running from a PyInstaller one-file/one-dir bundle."""
return bool(getattr(sys, "frozen", False))
def is_packaged() -> bool:
"""True when running as the end-user desktop app (any packaged form).
Covers both packaged forms β a PyInstaller bundle (``sys.frozen``) and the
embeddable-Python launcher (which exports ``UPS_PACKAGED=1``) β so writable
state is redirected off the install folder in both. A plain dev / pytest /
``streamlit run`` invocation returns False and keeps state in the repo root.
"""
return is_frozen() or os.environ.get(PACKAGED_ENV_FLAG, "") == "1"
def resource_base() -> Path:
"""Directory that holds read-only bundled resources.
* PyInstaller bundle β ``sys._MEIPASS`` (the unpacked bundle dir).
* Everything else β the repo root (the parent of the ``app`` package).
Resolved from this file's location, so it is correct even after the whole
project folder is moved or renamed (the embeddable-runtime relocation test).
"""
if is_frozen():
return Path(getattr(sys, "_MEIPASS", os.path.dirname(sys.executable)))
# this file is <root>/app/paths.py β parent.parent is <root>.
return Path(__file__).resolve().parent.parent
def resource_path(*parts: str) -> Path:
"""Absolute path to a bundled resource, e.g. ``resource_path("app", "main.py")``."""
return resource_base().joinpath(*parts)
def user_state_dir() -> Path:
"""Per-user writable directory for app state, OUTSIDE the install folder.
On Windows this is ``%APPDATA%\\UpworkProposalStrategist`` β stable across
app restarts AND across moving/reinstalling the program folder. The
non-Windows branch keeps a frozen build sane if one is ever produced
elsewhere (the supported packaged target is Windows).
"""
if os.name == "nt":
base = os.environ.get("APPDATA") or os.path.expanduser("~")
return Path(base) / APP_DIRNAME
return Path(os.path.expanduser("~")) / ".upwork_proposal_strategist"
def state_dir() -> Path:
"""Where the app reads/writes ``.env`` and logs.
Per-user location when packaged; the repo root in a dev checkout (so tests
and ``streamlit run`` are unaffected). The directory is created lazily by
the caller (see :mod:`app.config`).
"""
return user_state_dir() if is_packaged() else resource_base()
|