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()