hugpy-platform 0.2.2__tar.gz

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 (52) hide show
  1. hugpy_platform-0.2.2/LICENSE +41 -0
  2. hugpy_platform-0.2.2/PKG-INFO +41 -0
  3. hugpy_platform-0.2.2/README.md +7 -0
  4. hugpy_platform-0.2.2/pyproject.toml +64 -0
  5. hugpy_platform-0.2.2/setup.cfg +4 -0
  6. hugpy_platform-0.2.2/src/hugpy_platform/__init__.py +105 -0
  7. hugpy_platform-0.2.2/src/hugpy_platform/app_dirs.py +305 -0
  8. hugpy_platform-0.2.2/src/hugpy_platform/async_runtime.py +226 -0
  9. hugpy_platform-0.2.2/src/hugpy_platform/atomic_json.py +28 -0
  10. hugpy_platform-0.2.2/src/hugpy_platform/binaries.py +49 -0
  11. hugpy_platform-0.2.2/src/hugpy_platform/buildinfo.py +356 -0
  12. hugpy_platform-0.2.2/src/hugpy_platform/central.py +34 -0
  13. hugpy_platform-0.2.2/src/hugpy_platform/choices.py +9 -0
  14. hugpy_platform-0.2.2/src/hugpy_platform/client_liveness.py +207 -0
  15. hugpy_platform-0.2.2/src/hugpy_platform/compat_pydantic.py +177 -0
  16. hugpy_platform-0.2.2/src/hugpy_platform/constants.py +331 -0
  17. hugpy_platform-0.2.2/src/hugpy_platform/env.py +18 -0
  18. hugpy_platform-0.2.2/src/hugpy_platform/environment_status.py +19 -0
  19. hugpy_platform-0.2.2/src/hugpy_platform/except_utils.py +62 -0
  20. hugpy_platform-0.2.2/src/hugpy_platform/filesystem.py +54 -0
  21. hugpy_platform-0.2.2/src/hugpy_platform/formatting.py +23 -0
  22. hugpy_platform-0.2.2/src/hugpy_platform/hardware.py +169 -0
  23. hugpy_platform-0.2.2/src/hugpy_platform/model_keys.py +22 -0
  24. hugpy_platform-0.2.2/src/hugpy_platform/module_imports.py +305 -0
  25. hugpy_platform-0.2.2/src/hugpy_platform/no_think.py +25 -0
  26. hugpy_platform-0.2.2/src/hugpy_platform/paths.py +12 -0
  27. hugpy_platform-0.2.2/src/hugpy_platform/platform_facade.py +101 -0
  28. hugpy_platform-0.2.2/src/hugpy_platform/procutil.py +146 -0
  29. hugpy_platform-0.2.2/src/hugpy_platform/py.typed +0 -0
  30. hugpy_platform-0.2.2/src/hugpy_platform/results.py +48 -0
  31. hugpy_platform-0.2.2/src/hugpy_platform/runner_config.py +10 -0
  32. hugpy_platform-0.2.2/src/hugpy_platform/test_isolation.py +265 -0
  33. hugpy_platform-0.2.2/src/hugpy_platform/trust.py +56 -0
  34. hugpy_platform-0.2.2/src/hugpy_platform/utils.py +448 -0
  35. hugpy_platform-0.2.2/src/hugpy_platform/verdict.py +32 -0
  36. hugpy_platform-0.2.2/src/hugpy_platform/versioning.py +13 -0
  37. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/PKG-INFO +41 -0
  38. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/SOURCES.txt +50 -0
  39. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/dependency_links.txt +1 -0
  40. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/requires.txt +12 -0
  41. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/scm_file_list.json +47 -0
  42. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/scm_version.json +8 -0
  43. hugpy_platform-0.2.2/src/hugpy_platform.egg-info/top_level.txt +1 -0
  44. hugpy_platform-0.2.2/tests/conftest.py +35 -0
  45. hugpy_platform-0.2.2/tests/test_buildinfo.py +338 -0
  46. hugpy_platform-0.2.2/tests/test_constants.py +88 -0
  47. hugpy_platform-0.2.2/tests/test_import_policy.py +107 -0
  48. hugpy_platform-0.2.2/tests/test_platform_env_value.py +94 -0
  49. hugpy_platform-0.2.2/tests/test_public_api.py +25 -0
  50. hugpy_platform-0.2.2/tests/test_small_modules.py +113 -0
  51. hugpy_platform-0.2.2/tests/test_storage_isolation.py +117 -0
  52. hugpy_platform-0.2.2/tests/test_utils.py +80 -0
@@ -0,0 +1,41 @@
1
+ hugpy — Source-Available License
2
+
3
+ Copyright (c) 2026 putkoff (hugpy.ai). All rights reserved.
4
+
5
+ Permission is granted, free of charge, to use this software ("hugpy") for
6
+ personal and non-commercial purposes, and for time-limited commercial
7
+ evaluation, subject to the following conditions:
8
+
9
+ 1. Non-commercial use means use by an individual for personal purposes, or
10
+ use by a non-profit or educational institution for its own internal
11
+ purposes. Any use by, for, or on behalf of a for-profit business or in
12
+ connection with revenue-generating activity is commercial use — including
13
+ internal business use, use in producing goods or services, and use on
14
+ paid engagements.
15
+
16
+ 2. Commercial use requires a commercial license from the copyright holder.
17
+ Exception: a business may evaluate the software internally for up to
18
+ thirty (30) days free of charge; continued use after that requires a
19
+ commercial license.
20
+
21
+ 3. Redistribution of this software, in source or binary form, modified or
22
+ unmodified, is not permitted without prior written permission from the
23
+ copyright holder. Downloading the software from an official distribution
24
+ channel (PyPI, npm, hugpy.ai) is not redistribution.
25
+
26
+ 4. Modification for personal use or internal evaluation is permitted;
27
+ distribution of modified versions is not.
28
+
29
+ 5. This notice must be retained in all copies or substantial portions of
30
+ the software.
31
+
32
+ 6. Any use outside these terms automatically terminates this license.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING
38
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
39
+ IN THE SOFTWARE.
40
+
41
+ For commercial licensing or redistribution permission: https://hugpy.ai
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: hugpy-platform
3
+ Version: 0.2.2
4
+ Summary: Hugpy platform: stdlib-first foundation shared by every Hugpy package (env-backed configuration, app dirs, hardware and process probes, central URL, pydantic compatibility shim)
5
+ Author-email: putkoff <support@hugpy.ai>
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://hugpy.ai
8
+ Project-URL: Documentation, https://github.com/hugpy/hugpy/blob/main/py/foundation/hugpy_platform/README.md
9
+ Project-URL: Repository, https://github.com/hugpy/hugpy
10
+ Project-URL: Source, https://github.com/hugpy/hugpy/tree/main/py/foundation/hugpy_platform
11
+ Project-URL: Issues, https://github.com/hugpy/hugpy/issues
12
+ Project-URL: Changelog, https://github.com/hugpy/hugpy/releases
13
+ Project-URL: Architecture, https://github.com/hugpy/hugpy/blob/main/PARTITION.md
14
+ Keywords: hugpy,llm,self-hosted,platform,configuration
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: abstract_essentials
25
+ Requires-Dist: platformdirs
26
+ Provides-Extra: hardware
27
+ Requires-Dist: psutil; extra == "hardware"
28
+ Provides-Extra: hf
29
+ Requires-Dist: huggingface_hub; extra == "hf"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Requires-Dist: pytest-timeout; extra == "test"
33
+ Dynamic: license-file
34
+
35
+ # hugpy-platform
36
+
37
+ `hugpy_platform` — extracted from `abstract_hugpy_dev` as part of the Hugpy
38
+ partition. Ownership and allowed dependencies are declared in
39
+ `py/partition.toml`; see `PARTITION.md` at the workspace root.
40
+
41
+ Allowed Python dependencies inside the ecosystem: none.
@@ -0,0 +1,7 @@
1
+ # hugpy-platform
2
+
3
+ `hugpy_platform` — extracted from `abstract_hugpy_dev` as part of the Hugpy
4
+ partition. Ownership and allowed dependencies are declared in
5
+ `py/partition.toml`; see `PARTITION.md` at the workspace root.
6
+
7
+ Allowed Python dependencies inside the ecosystem: none.
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hugpy-platform"
7
+ dynamic = ["version"]
8
+ description = "Hugpy platform: stdlib-first foundation shared by every Hugpy package (env-backed configuration, app dirs, hardware and process probes, central URL, pydantic compatibility shim)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-Proprietary"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "putkoff", email = "support@hugpy.ai" }]
14
+ keywords = [
15
+ "hugpy",
16
+ "llm",
17
+ "self-hosted",
18
+ "platform",
19
+ "configuration",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Developers",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
28
+ ]
29
+ dependencies = [
30
+ "abstract_essentials",
31
+ "platformdirs",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://hugpy.ai"
36
+ Documentation = "https://github.com/hugpy/hugpy/blob/main/py/foundation/hugpy_platform/README.md"
37
+ Repository = "https://github.com/hugpy/hugpy"
38
+ Source = "https://github.com/hugpy/hugpy/tree/main/py/foundation/hugpy_platform"
39
+ Issues = "https://github.com/hugpy/hugpy/issues"
40
+ Changelog = "https://github.com/hugpy/hugpy/releases"
41
+ Architecture = "https://github.com/hugpy/hugpy/blob/main/PARTITION.md"
42
+
43
+ [project.optional-dependencies]
44
+ # hardware.free_ram_bytes() prefers psutil off-Linux; /proc/meminfo otherwise.
45
+ hardware = ["psutil"]
46
+ # constants.hfApi (lazy) — only callers that talk to the Hub need it.
47
+ hf = ["huggingface_hub"]
48
+ test = ["pytest>=8", "pytest-timeout"]
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.setuptools.package-data]
54
+ hugpy_platform = ["py.typed", "**/*.json", "**/*.sh", "**/*.md", "**/*.txt"]
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Lockstep workspace version (2026-09-22): every in-tree hugpy-* distribution
58
+ # takes ONE version from the workspace git tag (vX.Y.Z at the repo root), so a
59
+ # release is a tag and a build always carries the commit it came from
60
+ # (X.Y.Z.devN+g<sha>[.dirty] between tags). A checkout without git metadata
61
+ # builds as 0.0.0+unknown, which central refuses to advertise to workers.
62
+ [tool.setuptools_scm]
63
+ root = "../../.."
64
+ fallback_version = "0.0.0+unknown"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,105 @@
1
+ """hugpy-platform: the stdlib-first foundation every Hugpy package stands on.
2
+
3
+ What lives here (and nothing above it — no engine, storage, fleet or server):
4
+
5
+ central canonical central base URL (``HUGPY_BASE_URL`` + aliases)
6
+ platform_facade OS facts (``IS_WINDOWS``/``IS_MACOS``/``IS_LINUX``) and the
7
+ sanitising ``env_value`` seam over ``abstract_essentials``
8
+ app_dirs per-OS data/config/cache/engine dirs and the ``~/.hugpy``
9
+ runtime-file accessors
10
+ constants storage roots, HF cache layout, fleet-wide defaults
11
+ (import has side effects: creates dirs — import it explicitly)
12
+ utils small filesystem / message / naming helpers
13
+ module_imports ``get_<package>()`` lazy accessors for heavy ML libraries
14
+ async_runtime one process-wide asyncio loop + sync bridges
15
+ client_liveness "is the HTTP client still there" probe (Flask optional)
16
+ binaries / procutil / hardware executables, process trees, RAM/GPU probes
17
+ compat_pydantic pure-Python pydantic stand-in for platforms without pydantic_core
18
+ except_utils ``caught`` / ``attempt`` / ``catching`` logging helpers
19
+ trust Hugging Face publisher trust tiers
20
+ buildinfo which build is this process: workspace version, sha/dirty,
21
+ editable source, lockstep check (``/health``, ``/build``, heartbeat)
22
+
23
+ This ``__init__`` re-exports only the light, side-effect-free names below; the
24
+ heavier modules are imported by dotted path. ``buildinfo`` is exported lazily
25
+ (``hugpy_platform.buildinfo`` resolves on first attribute access, PEP 562) so
26
+ importing the package never pays for it.
27
+ """
28
+ from __future__ import annotations
29
+
30
+ from hugpy_platform.app_dirs import (
31
+ cache_dir,
32
+ config_dir,
33
+ data_dir,
34
+ engine_dir,
35
+ ensure_hugpy_home,
36
+ hugpy_config_dir,
37
+ hugpy_home,
38
+ hugpy_logs_dir,
39
+ hugpy_run_dir,
40
+ hugpy_state_dir,
41
+ models_root,
42
+ )
43
+ from hugpy_platform.central import CENTRAL_ENV_VARS, DEFAULT_CENTRAL, central_base_url
44
+ from hugpy_platform.compat_pydantic import ensure_pydantic
45
+ from hugpy_platform.except_utils import FAILED, attempt, catching, caught, caught_block
46
+ from hugpy_platform.platform_facade import EXE_SUFFIX, IS_LINUX, IS_MACOS, IS_WINDOWS, env_value
47
+ from hugpy_platform.trust import trust_label, trust_tier
48
+
49
+ try: # the installed distribution's version: the workspace tag/commit, never a literal
50
+ from importlib.metadata import version as _dist_version
51
+ __version__ = _dist_version("hugpy-platform")
52
+ except Exception: # noqa: BLE001 — source tree without metadata
53
+ __version__ = "0.0.0+unknown"
54
+
55
+ _LAZY_SUBMODULES = ("buildinfo",)
56
+
57
+
58
+ def __getattr__(name: str):
59
+ """Lazy submodule export: ``hugpy_platform.buildinfo`` without an import-time cost."""
60
+ if name in _LAZY_SUBMODULES:
61
+ import importlib
62
+ module = importlib.import_module(f"{__name__}.{name}")
63
+ globals()[name] = module
64
+ return module
65
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
66
+
67
+
68
+ __all__ = [
69
+ "__version__",
70
+ # central
71
+ "CENTRAL_ENV_VARS",
72
+ "DEFAULT_CENTRAL",
73
+ "central_base_url",
74
+ # platform facts + env seam
75
+ "EXE_SUFFIX",
76
+ "IS_LINUX",
77
+ "IS_MACOS",
78
+ "IS_WINDOWS",
79
+ "env_value",
80
+ # app dirs
81
+ "cache_dir",
82
+ "config_dir",
83
+ "data_dir",
84
+ "engine_dir",
85
+ "ensure_hugpy_home",
86
+ "hugpy_config_dir",
87
+ "hugpy_home",
88
+ "hugpy_logs_dir",
89
+ "hugpy_run_dir",
90
+ "hugpy_state_dir",
91
+ "models_root",
92
+ # pydantic shim
93
+ "ensure_pydantic",
94
+ # exception helpers
95
+ "FAILED",
96
+ "attempt",
97
+ "catching",
98
+ "caught",
99
+ "caught_block",
100
+ # trust tiers
101
+ "trust_label",
102
+ "trust_tier",
103
+ # build identity (lazy submodule)
104
+ "buildinfo",
105
+ ]
@@ -0,0 +1,305 @@
1
+ """Per-OS application directories — one source of truth.
2
+
3
+ Replaces the scattered hardcoded ``/srv/abstractendeavors/...``,
4
+ ``~/.local/share/hugpy``, ``/etc/llama-swap``, and ``/mnt/llm_storage`` literals.
5
+ Every path is overridable by the same env vars the code already honoured, so
6
+ existing Linux deployments are unaffected; only the *defaults* become per-OS:
7
+
8
+ data_dir() Linux ~/.local/share/hugpy macOS ~/Library/Application Support/hugpy Windows %LOCALAPPDATA%\\hugpy
9
+ config_dir() Linux ~/.config/hugpy macOS ~/Library/Application Support/hugpy Windows %LOCALAPPDATA%\\hugpy
10
+ cache_dir() Linux ~/.cache/hugpy macOS ~/Library/Caches/hugpy Windows %LOCALAPPDATA%\\hugpy\\Cache
11
+ engine_dir() data_dir()/engine — where the fetched llama.cpp binary lands
12
+ models_root() DEFAULT_ROOT or data_dir()/llm_storage
13
+
14
+ We use ``platformdirs`` when available (added to base deps) and fall back to a
15
+ hand-rolled per-OS layout so this module never hard-fails on import.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+
21
+ from hugpy_platform.platform_facade import IS_MACOS, IS_WINDOWS, env_value
22
+
23
+ _APP = "hugpy"
24
+
25
+
26
+ def _home(*parts: str) -> str:
27
+ return os.path.join(os.path.expanduser("~"), *parts)
28
+
29
+
30
+ def _fallback_data() -> str:
31
+ if IS_WINDOWS:
32
+ base = os.environ.get("LOCALAPPDATA") or _home("AppData", "Local")
33
+ return os.path.join(base, _APP)
34
+ if IS_MACOS:
35
+ return _home("Library", "Application Support", _APP)
36
+ return os.path.join(os.environ.get("XDG_DATA_HOME") or _home(".local", "share"), _APP)
37
+
38
+
39
+ def _fallback_config() -> str:
40
+ if IS_WINDOWS:
41
+ base = os.environ.get("LOCALAPPDATA") or _home("AppData", "Local")
42
+ return os.path.join(base, _APP)
43
+ if IS_MACOS:
44
+ return _home("Library", "Application Support", _APP)
45
+ return os.path.join(os.environ.get("XDG_CONFIG_HOME") or _home(".config"), _APP)
46
+
47
+
48
+ def _fallback_cache() -> str:
49
+ if IS_WINDOWS:
50
+ base = os.environ.get("LOCALAPPDATA") or _home("AppData", "Local")
51
+ return os.path.join(base, _APP, "Cache")
52
+ if IS_MACOS:
53
+ return _home("Library", "Caches", _APP)
54
+ return os.path.join(os.environ.get("XDG_CACHE_HOME") or _home(".cache"), _APP)
55
+
56
+
57
+ def _dirs():
58
+ try:
59
+ import platformdirs
60
+
61
+ return platformdirs.PlatformDirs(_APP, appauthor=False)
62
+ except Exception:
63
+ return None
64
+
65
+
66
+ def data_dir() -> str:
67
+ override = env_value("HUGPY_DATA_DIR")
68
+ if override:
69
+ return _ensure(override)
70
+ d = _dirs()
71
+ return _ensure(d.user_data_dir if d else _fallback_data())
72
+
73
+
74
+ def config_dir() -> str:
75
+ override = env_value("HUGPY_CONFIG_DIR")
76
+ if override:
77
+ return _ensure(override)
78
+ d = _dirs()
79
+ return _ensure(d.user_config_dir if d else _fallback_config())
80
+
81
+
82
+ def cache_dir() -> str:
83
+ override = env_value("HUGPY_CACHE_DIR")
84
+ if override:
85
+ return _ensure(override)
86
+ d = _dirs()
87
+ return _ensure(d.user_cache_dir if d else _fallback_cache())
88
+
89
+
90
+ def engine_dir() -> str:
91
+ """Where ``hugpy install-engine`` unpacks the native llama.cpp binaries."""
92
+ override = env_value("HUGPY_ENGINE_DIR") or env_value("LLAMA_CPP_DIR")
93
+ if override:
94
+ return _ensure(override)
95
+ return _ensure(os.path.join(data_dir(), "engine"))
96
+
97
+
98
+ def models_root() -> str:
99
+ """Model/upload/dataset storage root.
100
+
101
+ Honours the legacy ``DEFAULT_ROOT``/``MODELS_HOME`` env vars first — but only
102
+ if that path can actually be created and written. A stale/un-writable override
103
+ (e.g. ``DEFAULT_ROOT=/mnt/llm_storage`` carried in a server ``.env`` onto a
104
+ worker or a phone where ``/mnt`` is read-only) is ignored in favour of a
105
+ per-user dir under ``data_dir()``, so storage never lands on a dead path.
106
+ """
107
+ override = env_value("DEFAULT_ROOT")
108
+ if override and _usable(override):
109
+ return override
110
+ # Preserve the historical Linux mount when it exists and is writable.
111
+ legacy = "/mnt/llm_storage"
112
+ try:
113
+ if os.path.isdir(legacy) and os.access(legacy, os.W_OK):
114
+ return legacy
115
+ except OSError:
116
+ pass
117
+ return _ensure(os.path.join(data_dir(), "llm_storage"))
118
+
119
+
120
+ def demo_media_base() -> str:
121
+ """Base URL the video arm's canned demo loads its sample media from."""
122
+ return env_value("HUGPY_DEMO_MEDIA_BASE") or "https://hugpy.ai/demo-media"
123
+
124
+
125
+ def demo_media_dir() -> str:
126
+ """Local demo-media tree to serve at ``/demo-media/`` (self-hosters).
127
+
128
+ Empty string means "not configured" — deliberately NO default and NO
129
+ directory creation; the ``/demo-media/`` route only exists when this is set.
130
+ """
131
+ return env_value("HUGPY_DEMO_MEDIA_DIR") or ""
132
+
133
+
134
+ def _usable(path: str) -> bool:
135
+ """True only if *path* exists (or can be created) AND is writable."""
136
+ try:
137
+ os.makedirs(path, exist_ok=True)
138
+ return os.path.isdir(path) and os.access(path, os.W_OK)
139
+ except OSError:
140
+ return False
141
+
142
+
143
+ def _ensure(path: str) -> str:
144
+ try:
145
+ os.makedirs(path, exist_ok=True)
146
+ except OSError:
147
+ pass
148
+ return path
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # hugpy runtime "mechanic" files (HUGPY_HOME, default ~/.hugpy)
153
+ # ---------------------------------------------------------------------------
154
+ # Historically these state/config/log files were written straight into $HOME
155
+ # (``~/todo.json``, ``~/steward-state.json``, ``~/.abstract_hugpy_worker.json``,
156
+ # ``~/model_metadata.db`` …), strewing the home directory. They now live under a
157
+ # single base dir — ``HUGPY_HOME`` (default ``~/.hugpy``) — split into::
158
+ #
159
+ # state/ todo.json, steward-state.json, model_metadata.db, flow.json, …
160
+ # config/ abstract_hugpy_worker.json (+ .settings.json/.update.json)
161
+ # logs/ bridge-mail.jsonl, bugreport.json
162
+ # run/ *.lock
163
+ #
164
+ # Each named accessor MIGRATES a legacy ``~/<name>`` file into its new home the
165
+ # first time it is resolved (atomic same-fs rename, cross-fs copy fallback), so
166
+ # upgrades are seamless and idempotent — no flag-day, no data loss. Every path
167
+ # stays overridable so existing deployments and tests can pin locations.
168
+ #
169
+ # NOTE: standalone station-stack scripts (keeper_relay.py, bugreport/scanner.py)
170
+ # run under the system interpreter and cannot import this package; they carry a
171
+ # byte-identical inline resolver. Keep the two in sync — this module is the spec.
172
+
173
+ def hugpy_home() -> str:
174
+ """Single base directory for hugpy's runtime files. Default ``~/.hugpy``."""
175
+ return _ensure(env_value("HUGPY_HOME") or _home(".hugpy"))
176
+
177
+
178
+ def hugpy_state_dir() -> str:
179
+ return _ensure(os.path.join(hugpy_home(), "state"))
180
+
181
+
182
+ def hugpy_config_dir() -> str:
183
+ return _ensure(os.path.join(hugpy_home(), "config"))
184
+
185
+
186
+ def hugpy_logs_dir() -> str:
187
+ return _ensure(os.path.join(hugpy_home(), "logs"))
188
+
189
+
190
+ def hugpy_run_dir() -> str:
191
+ return _ensure(os.path.join(hugpy_home(), "run"))
192
+
193
+
194
+ def _relocate(new_path: str, *legacy_names: str) -> str:
195
+ """Return *new_path*, first migrating a legacy ``~/<name>`` into it once.
196
+
197
+ Idempotent and safe: if *new_path* already exists nothing moves. Uses an
198
+ atomic rename on the same filesystem and falls back to a copy+unlink across
199
+ filesystems. Any failure leaves the legacy file untouched and returns the
200
+ new path anyway (a fresh file is then created there).
201
+ """
202
+ try:
203
+ if os.path.exists(new_path):
204
+ return new_path
205
+ for name in legacy_names:
206
+ old = _home(name)
207
+ if not os.path.exists(old):
208
+ continue
209
+ if os.path.abspath(old) == os.path.abspath(new_path):
210
+ continue
211
+ try:
212
+ os.replace(old, new_path) # atomic, same filesystem
213
+ except OSError:
214
+ import shutil
215
+ shutil.move(old, new_path) # cross-filesystem fallback
216
+ break
217
+ except OSError:
218
+ pass
219
+ return new_path
220
+
221
+
222
+ # --- named accessors --------------------------------------------------------
223
+
224
+ def worker_id_file() -> str:
225
+ """``config/abstract_hugpy_worker.json`` — the worker identity file.
226
+
227
+ Honors an explicit ``WORKER_ID_FILE`` override (used by tests / multi-worker
228
+ hosts); otherwise migrates the legacy ``~/.abstract_hugpy_worker.json`` plus
229
+ its ``.settings.json`` / ``.update.json`` sidecars into ``config/``.
230
+ """
231
+ override = env_value("WORKER_ID_FILE")
232
+ if override:
233
+ return override
234
+ new = os.path.join(hugpy_config_dir(), "abstract_hugpy_worker.json")
235
+ _relocate(new, ".abstract_hugpy_worker.json")
236
+ for suffix in (".settings.json", ".update.json"):
237
+ _relocate(new + suffix, ".abstract_hugpy_worker.json" + suffix)
238
+ return new
239
+
240
+
241
+ def gguf_worker_id_file() -> str:
242
+ """``config/gguf_worker.json`` — sibling worker identity (same pattern)."""
243
+ override = env_value("WORKER_ID_FILE")
244
+ if override:
245
+ return override
246
+ new = os.path.join(hugpy_config_dir(), "gguf_worker.json")
247
+ return _relocate(new, ".gguf_worker.json")
248
+
249
+
250
+ def todo_file() -> str:
251
+ return _relocate(os.path.join(hugpy_state_dir(), "todo.json"), "todo.json")
252
+
253
+
254
+ def todo_history_file() -> str:
255
+ return _relocate(
256
+ os.path.join(hugpy_state_dir(), "todo-history.jsonl"), "todo-history.jsonl"
257
+ )
258
+
259
+
260
+ def todo_lock() -> str:
261
+ return _relocate(os.path.join(hugpy_run_dir(), "todo.lock"), ".todo.lock")
262
+
263
+
264
+ def steward_config() -> str:
265
+ return _relocate(os.path.join(hugpy_config_dir(), "steward.json"), "steward.json")
266
+
267
+
268
+ def steward_state() -> str:
269
+ return _relocate(
270
+ os.path.join(hugpy_state_dir(), "steward-state.json"), "steward-state.json"
271
+ )
272
+
273
+
274
+ def model_metadata_db() -> str:
275
+ return _relocate(
276
+ os.path.join(hugpy_state_dir(), "model_metadata.db"), "model_metadata.db"
277
+ )
278
+
279
+
280
+ def model_physical_json() -> str:
281
+ # The ``.lock`` sidecar is derived by callers as ``path + ".lock"`` and rides
282
+ # along in state/ next to the file — intentionally not split into run/.
283
+ return _relocate(
284
+ os.path.join(hugpy_state_dir(), "model_physical.json"), "model_physical.json"
285
+ )
286
+
287
+
288
+ def flow_json() -> str:
289
+ return _relocate(os.path.join(hugpy_state_dir(), "flow.json"), "flow.json")
290
+
291
+
292
+ def bridge_mail() -> str:
293
+ return _relocate(
294
+ os.path.join(hugpy_logs_dir(), "bridge-mail.jsonl"), ".bridge-mail.jsonl"
295
+ )
296
+
297
+
298
+ def bugreport_json() -> str:
299
+ return _relocate(os.path.join(hugpy_logs_dir(), "bugreport.json"), "bugreport.json")
300
+
301
+
302
+ def ensure_hugpy_home() -> str:
303
+ """Create the full ``HUGPY_HOME`` skeleton. Called by installers/postinst."""
304
+ hugpy_state_dir(); hugpy_config_dir(); hugpy_logs_dir(); hugpy_run_dir()
305
+ return hugpy_home()