hugpy-platform 0.3.0__tar.gz → 0.3.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.
- hugpy_platform-0.3.2/PKG-INFO +83 -0
- hugpy_platform-0.3.2/README.md +63 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/pyproject.toml +1 -1
- hugpy_platform-0.3.2/src/hugpy_platform/imports/atomic_json.py +29 -0
- hugpy_platform-0.3.2/src/hugpy_platform/imports/choices.py +10 -0
- hugpy_platform-0.3.2/src/hugpy_platform/imports/constants.py +332 -0
- hugpy_platform-0.3.2/src/hugpy_platform/imports/filesystem.py +55 -0
- hugpy_platform-0.3.2/src/hugpy_platform/imports/formatting.py +24 -0
- hugpy_platform-0.3.2/src/hugpy_platform/imports/hardware.py +73 -0
- hugpy_platform-0.3.2/src/hugpy_platform/module_imports.py +306 -0
- hugpy_platform-0.3.2/src/hugpy_platform/platform_facade.py +102 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/results.py +20 -26
- hugpy_platform-0.3.2/src/hugpy_platform/runner_config.py +11 -0
- hugpy_platform-0.3.2/src/hugpy_platform.egg-info/PKG-INFO +83 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform.egg-info/SOURCES.txt +10 -1
- hugpy_platform-0.3.0/PKG-INFO +0 -23
- hugpy_platform-0.3.0/README.md +0 -3
- hugpy_platform-0.3.0/src/hugpy_platform.egg-info/PKG-INFO +0 -23
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/setup.cfg +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/__init__.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/app_dirs.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/async_runtime.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/atomic_json.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/binaries.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/choices.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/client_liveness.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/constants.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/filesystem.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/formatting.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/hardware.py +0 -0
- {hugpy_platform-0.3.0/src/hugpy_platform → hugpy_platform-0.3.2/src/hugpy_platform/imports}/module_imports.py +0 -0
- {hugpy_platform-0.3.0/src/hugpy_platform → hugpy_platform-0.3.2/src/hugpy_platform/imports}/platform_facade.py +0 -0
- {hugpy_platform-0.3.0/src/hugpy_platform → hugpy_platform-0.3.2/src/hugpy_platform/imports}/runner_config.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/procutil.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/py.typed +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/utils.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/verdict.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform/versioning.py +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform.egg-info/dependency_links.txt +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform.egg-info/requires.txt +0 -0
- {hugpy_platform-0.3.0 → hugpy_platform-0.3.2}/src/hugpy_platform.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hugpy-platform
|
|
3
|
+
Version: 0.3.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
|
+
Keywords: hugpy,llm,self-hosted,platform,configuration
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: abstract_essentials
|
|
17
|
+
Requires-Dist: platformdirs
|
|
18
|
+
Provides-Extra: hardware
|
|
19
|
+
Requires-Dist: psutil; extra == "hardware"
|
|
20
|
+
|
|
21
|
+
# hugpy-platform
|
|
22
|
+
|
|
23
|
+
The stdlib-first foundation that the hugpy arms build on: app directories,
|
|
24
|
+
storage-root constants, executable resolution, a process-wide async runtime,
|
|
25
|
+
hardware and process probes, and small formatting and result helpers.
|
|
26
|
+
|
|
27
|
+
Distribution `hugpy-platform` **0.3.0** ("lean"), import `hugpy_platform`.
|
|
28
|
+
Dependencies: `abstract_essentials`, `platformdirs`. Optional extra: `hardware`
|
|
29
|
+
(`psutil`).
|
|
30
|
+
|
|
31
|
+
**Status: LIVE.** It is a library only: no service, no console script, no
|
|
32
|
+
entry points. It is installed in `/opt/hugpy-media/venv` and
|
|
33
|
+
`/opt/hugpy-video/venv`, and imported by:
|
|
34
|
+
|
|
35
|
+
- `hugpy_media`
|
|
36
|
+
- `hugpy_video`
|
|
37
|
+
- `hugpy_engine`
|
|
38
|
+
- `hugpy_oracle`
|
|
39
|
+
- core's legacy `config/prompt.py`, which is not live
|
|
40
|
+
|
|
41
|
+
Core's live basic mode and the wrapper do not use it; the wrapper carries its
|
|
42
|
+
own lifted copies under `fitevict/lifted/`.
|
|
43
|
+
|
|
44
|
+
The lean 0.3.0 release ships only the modules the live arms import. The rest
|
|
45
|
+
of the pre-0.3 package is retired. The `pyproject.toml` description still mentions "central URL", but
|
|
46
|
+
`hugpy_platform.central` is not shipped. `hugpy_oracle/assessment/benchmark.py` imports
|
|
47
|
+
it inside a `try` and records no figure when it is missing.
|
|
48
|
+
`hugpy_engine/placement.py` falls back the same way for
|
|
49
|
+
`hugpy_platform.model_keys`.
|
|
50
|
+
|
|
51
|
+
## Modules
|
|
52
|
+
|
|
53
|
+
`import hugpy_platform` re-exports only the light, side-effect-free accessors:
|
|
54
|
+
|
|
55
|
+
- `cache_dir`, `config_dir`, `data_dir`, `engine_dir`
|
|
56
|
+
- `hugpy_home`, `ensure_hugpy_home`, `hugpy_config_dir`, `hugpy_logs_dir`,
|
|
57
|
+
`hugpy_run_dir`, `hugpy_state_dir`
|
|
58
|
+
- `models_root`
|
|
59
|
+
- `EXE_SUFFIX`, `IS_LINUX`, `IS_MACOS`, `IS_WINDOWS`, `env_value`
|
|
60
|
+
|
|
61
|
+
Import everything else by dotted path.
|
|
62
|
+
|
|
63
|
+
| module | what it provides |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `app_dirs` | per-OS application directories, the single source of truth for them |
|
|
66
|
+
| `platform_facade` | OS facts and the `env_value` environment seam |
|
|
67
|
+
| `constants` | storage roots resolved from the environment: `DEFAULT_ROOT`, `MODELS_HOME`, `UPLOADS_HOME`, `PROJECTS_HOME`, `IDENTITIES_HOME`, …, `HF_TOKEN`, `env_bool()` |
|
|
68
|
+
| `utils` | small filesystem, message and naming helpers (`get_messages`, `get_request_id`, `exists`, …) |
|
|
69
|
+
| `async_runtime` | one long-lived event loop in a daemon thread: `run`, `submit`, `await_sync`, `iter_sync` |
|
|
70
|
+
| `binaries` | portable executable resolution: `resolve_bin`, `with_exe` |
|
|
71
|
+
| `module_imports` | lazy heavy imports with actionable errors: `require`, `get_torch`, `get_transformers`, `get_whisper`, … |
|
|
72
|
+
| `runner_config` | `RunnerConfig`, the common constructor state for media runner adapters |
|
|
73
|
+
| `results` | result-object adapters: `result_text`, `vision_result_text`, `result_payload` |
|
|
74
|
+
| `filesystem` | `free_bytes`, `directory_bytes`, `is_within`, `write_text`, … |
|
|
75
|
+
| `hardware` | `total_vram_bytes` (torch or nvidia-smi) |
|
|
76
|
+
| `procutil` | `pid_alive`, `popen_detached`, `terminate_tree`, `reexec` |
|
|
77
|
+
| `atomic_json`, `formatting`, `choices`, `verdict`, `versioning`, `client_liveness` | atomic JSON writes, byte formatting, value selection, `VERDICT/SCORE/WHY` parsing, version lookup, client-disconnect detection |
|
|
78
|
+
|
|
79
|
+
This package has no `imports/` layer (see
|
|
80
|
+
[the convention](../../docs/packages.md#the-imports-layout-convention)).
|
|
81
|
+
|
|
82
|
+
See also [docs/packages.md](../../docs/packages.md) and
|
|
83
|
+
[docs/architecture.md](../../docs/architecture.md).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# hugpy-platform
|
|
2
|
+
|
|
3
|
+
The stdlib-first foundation that the hugpy arms build on: app directories,
|
|
4
|
+
storage-root constants, executable resolution, a process-wide async runtime,
|
|
5
|
+
hardware and process probes, and small formatting and result helpers.
|
|
6
|
+
|
|
7
|
+
Distribution `hugpy-platform` **0.3.0** ("lean"), import `hugpy_platform`.
|
|
8
|
+
Dependencies: `abstract_essentials`, `platformdirs`. Optional extra: `hardware`
|
|
9
|
+
(`psutil`).
|
|
10
|
+
|
|
11
|
+
**Status: LIVE.** It is a library only: no service, no console script, no
|
|
12
|
+
entry points. It is installed in `/opt/hugpy-media/venv` and
|
|
13
|
+
`/opt/hugpy-video/venv`, and imported by:
|
|
14
|
+
|
|
15
|
+
- `hugpy_media`
|
|
16
|
+
- `hugpy_video`
|
|
17
|
+
- `hugpy_engine`
|
|
18
|
+
- `hugpy_oracle`
|
|
19
|
+
- core's legacy `config/prompt.py`, which is not live
|
|
20
|
+
|
|
21
|
+
Core's live basic mode and the wrapper do not use it; the wrapper carries its
|
|
22
|
+
own lifted copies under `fitevict/lifted/`.
|
|
23
|
+
|
|
24
|
+
The lean 0.3.0 release ships only the modules the live arms import. The rest
|
|
25
|
+
of the pre-0.3 package is retired. The `pyproject.toml` description still mentions "central URL", but
|
|
26
|
+
`hugpy_platform.central` is not shipped. `hugpy_oracle/assessment/benchmark.py` imports
|
|
27
|
+
it inside a `try` and records no figure when it is missing.
|
|
28
|
+
`hugpy_engine/placement.py` falls back the same way for
|
|
29
|
+
`hugpy_platform.model_keys`.
|
|
30
|
+
|
|
31
|
+
## Modules
|
|
32
|
+
|
|
33
|
+
`import hugpy_platform` re-exports only the light, side-effect-free accessors:
|
|
34
|
+
|
|
35
|
+
- `cache_dir`, `config_dir`, `data_dir`, `engine_dir`
|
|
36
|
+
- `hugpy_home`, `ensure_hugpy_home`, `hugpy_config_dir`, `hugpy_logs_dir`,
|
|
37
|
+
`hugpy_run_dir`, `hugpy_state_dir`
|
|
38
|
+
- `models_root`
|
|
39
|
+
- `EXE_SUFFIX`, `IS_LINUX`, `IS_MACOS`, `IS_WINDOWS`, `env_value`
|
|
40
|
+
|
|
41
|
+
Import everything else by dotted path.
|
|
42
|
+
|
|
43
|
+
| module | what it provides |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `app_dirs` | per-OS application directories, the single source of truth for them |
|
|
46
|
+
| `platform_facade` | OS facts and the `env_value` environment seam |
|
|
47
|
+
| `constants` | storage roots resolved from the environment: `DEFAULT_ROOT`, `MODELS_HOME`, `UPLOADS_HOME`, `PROJECTS_HOME`, `IDENTITIES_HOME`, …, `HF_TOKEN`, `env_bool()` |
|
|
48
|
+
| `utils` | small filesystem, message and naming helpers (`get_messages`, `get_request_id`, `exists`, …) |
|
|
49
|
+
| `async_runtime` | one long-lived event loop in a daemon thread: `run`, `submit`, `await_sync`, `iter_sync` |
|
|
50
|
+
| `binaries` | portable executable resolution: `resolve_bin`, `with_exe` |
|
|
51
|
+
| `module_imports` | lazy heavy imports with actionable errors: `require`, `get_torch`, `get_transformers`, `get_whisper`, … |
|
|
52
|
+
| `runner_config` | `RunnerConfig`, the common constructor state for media runner adapters |
|
|
53
|
+
| `results` | result-object adapters: `result_text`, `vision_result_text`, `result_payload` |
|
|
54
|
+
| `filesystem` | `free_bytes`, `directory_bytes`, `is_within`, `write_text`, … |
|
|
55
|
+
| `hardware` | `total_vram_bytes` (torch or nvidia-smi) |
|
|
56
|
+
| `procutil` | `pid_alive`, `popen_detached`, `terminate_tree`, `reexec` |
|
|
57
|
+
| `atomic_json`, `formatting`, `choices`, `verdict`, `versioning`, `client_liveness` | atomic JSON writes, byte formatting, value selection, `VERDICT/SCORE/WHY` parsing, version lookup, client-disconnect detection |
|
|
58
|
+
|
|
59
|
+
This package has no `imports/` layer (see
|
|
60
|
+
[the convention](../../docs/packages.md#the-imports-layout-convention)).
|
|
61
|
+
|
|
62
|
+
See also [docs/packages.md](../../docs/packages.md) and
|
|
63
|
+
[docs/architecture.md](../../docs/architecture.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hugpy-platform"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
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
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# LEAN 0.3.0 — lifted verbatim from /srv/hugpy_next/py/foundation/hugpy_platform/src/hugpy_platform/atomic_json.py; unused symbols removed.
|
|
2
|
+
"""Atomic JSON writes for the small shared state files."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_json_dict(path: str) -> dict | None:
|
|
12
|
+
"""Read a JSON object, returning None for absent or malformed files."""
|
|
13
|
+
try:
|
|
14
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
15
|
+
data = json.load(fh)
|
|
16
|
+
except Exception: # noqa: BLE001 — unreadable and malformed are both absent
|
|
17
|
+
return None
|
|
18
|
+
return data if isinstance(data, dict) else None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def save_json(path: str, data: dict) -> None:
|
|
22
|
+
"""Write JSON with a unique temporary file and an atomic replacement."""
|
|
23
|
+
parent = os.path.dirname(path)
|
|
24
|
+
if parent:
|
|
25
|
+
os.makedirs(parent, exist_ok=True)
|
|
26
|
+
tmp = f"{path}.{os.getpid()}.{secrets.token_hex(4)}.tmp"
|
|
27
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(data, f, indent=2, sort_keys=True)
|
|
29
|
+
os.replace(tmp, path)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# LEAN 0.3.0 — lifted verbatim from /srv/hugpy_next/py/foundation/hugpy_platform/src/hugpy_platform/choices.py; unused symbols removed.
|
|
2
|
+
"""Small value selection helpers."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def first_not_none(explicit, from_preset, schema_default):
|
|
6
|
+
if explicit is not None:
|
|
7
|
+
return explicit
|
|
8
|
+
if from_preset is not None:
|
|
9
|
+
return from_preset
|
|
10
|
+
return schema_default
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# LEAN 0.3.0 — lifted verbatim from /srv/hugpy_next/py/foundation/hugpy_platform/src/hugpy_platform/constants.py; unused symbols removed.
|
|
2
|
+
"""Storage roots, Hugging Face cache layout and fleet-wide defaults.
|
|
3
|
+
|
|
4
|
+
Every value is env-overridable. Env reads go through
|
|
5
|
+
:func:`hugpy_platform.platform_facade.env_value` (process environment first,
|
|
6
|
+
then a ``.env`` file via ``abstract_essentials``), which also strips inline
|
|
7
|
+
``# comments`` and stray quotes so a path can never be poisoned by a note in a
|
|
8
|
+
``.env`` file.
|
|
9
|
+
|
|
10
|
+
Importing this module has deliberate side effects: the storage directories are
|
|
11
|
+
created (best-effort) and ``HF_HOME``/``HF_HUB_CACHE``/``TORCH_HOME``/
|
|
12
|
+
``PIP_CACHE_DIR`` are seeded into the process environment. ``hfApi`` is
|
|
13
|
+
resolved lazily (PEP 562) so ``huggingface_hub`` is only imported by callers
|
|
14
|
+
that actually use it.
|
|
15
|
+
"""
|
|
16
|
+
import os
|
|
17
|
+
import logging
|
|
18
|
+
import re
|
|
19
|
+
from typing import Literal, Optional
|
|
20
|
+
|
|
21
|
+
from abstract_essentials.json_utils import safe_dump_to_file
|
|
22
|
+
from abstract_essentials.types import make_list
|
|
23
|
+
|
|
24
|
+
from hugpy_platform.platform_facade import env_value as get_env_value
|
|
25
|
+
|
|
26
|
+
# Tokenizers set this as a sentinel for "no enforced limit". It's never a real window.
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------
|
|
29
|
+
# Model storage root
|
|
30
|
+
# ---------------------------------------------------------------------
|
|
31
|
+
HUGGINGFACE_DOMAIN = "https://huggingface.co"
|
|
32
|
+
|
|
33
|
+
# Don't phone home usage telemetry on the HF calls we do still make (the
|
|
34
|
+
# per-repo metadata cache in hugpy_storage.model_metadata minimizes those calls; this
|
|
35
|
+
# strips the tracking headers from the rest). setdefault ONLY — an
|
|
36
|
+
# operator-set value is never clobbered.
|
|
37
|
+
os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
|
|
38
|
+
|
|
39
|
+
HF_TOKEN = get_env_value("HF_TOKEN") or False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def env_bool(key: str, default: bool = False) -> bool:
|
|
43
|
+
"""Env flag -> bool. `get_env_value(...) or default` can never yield False
|
|
44
|
+
(the string \"false\" is truthy, unset -> default), so flags parsed that way
|
|
45
|
+
are stuck at their default forever. This coerces properly.
|
|
46
|
+
|
|
47
|
+
Process environment wins over the .env file: get_env_value only reads the
|
|
48
|
+
.env file, but operational flags must also respond to a systemd
|
|
49
|
+
`Environment=` line or a `FLAG=false cmd` shell prefix."""
|
|
50
|
+
value = os.environ.get(key)
|
|
51
|
+
if value is None:
|
|
52
|
+
value = get_env_value(key)
|
|
53
|
+
if value is None:
|
|
54
|
+
return default
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
return value
|
|
57
|
+
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
|
58
|
+
|
|
59
|
+
def _resolve_default_root():
|
|
60
|
+
# Single source of truth for the storage root, per-OS. Honours DEFAULT_ROOT,
|
|
61
|
+
# keeps the historical /mnt/llm_storage mount when present and writable, and
|
|
62
|
+
# otherwise lands in a per-user data dir (XDG / AppData / Library) so the API
|
|
63
|
+
# works out of the box on a fresh box, Windows, or macOS.
|
|
64
|
+
from hugpy_platform.app_dirs import models_root
|
|
65
|
+
|
|
66
|
+
return models_root()
|
|
67
|
+
|
|
68
|
+
DEFAULT_ROOT = _resolve_default_root()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _root_is_usable(path: str) -> bool:
|
|
72
|
+
"""True only if *path* can be created AND actually written to.
|
|
73
|
+
|
|
74
|
+
``os.makedirs`` catches the PermissionError case (an unwritable parent like
|
|
75
|
+
``/opt/flaskapps``); the create+unlink probe catches filesystems where
|
|
76
|
+
``os.access`` lies (root-squash NFS, euid=0). Cheap, local, no network — this
|
|
77
|
+
runs once at import. Mirrors ``app_dirs._usable`` but with a real
|
|
78
|
+
write probe because an env-supplied root that can't be written is the exact
|
|
79
|
+
failure we're guarding against."""
|
|
80
|
+
try:
|
|
81
|
+
os.makedirs(path, exist_ok=True)
|
|
82
|
+
except OSError:
|
|
83
|
+
return False
|
|
84
|
+
probe = os.path.join(path, ".hugpy_write_test")
|
|
85
|
+
try:
|
|
86
|
+
with open(probe, "w"):
|
|
87
|
+
pass
|
|
88
|
+
os.unlink(probe)
|
|
89
|
+
return True
|
|
90
|
+
except OSError:
|
|
91
|
+
try:
|
|
92
|
+
os.unlink(probe)
|
|
93
|
+
except OSError:
|
|
94
|
+
pass
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _env_root_or_default(env_key: str, default_path: str) -> str:
|
|
99
|
+
"""Resolve a storage root from *env_key*, falling back to *default_path* when
|
|
100
|
+
the env value points somewhere we cannot actually write.
|
|
101
|
+
|
|
102
|
+
Root cause (worker boxes, 2026-07-05): a stale ``~/.env`` carried
|
|
103
|
+
``MODELS_HOME`` / ``DEFAULT_ROOT`` etc. pointing at an unwritable legacy path
|
|
104
|
+
(``/opt/flaskapps``, a dead ``/mnt/llm_storage`` mount). Provisioning then
|
|
105
|
+
died with PermissionError forever and the models_local scan looked in the
|
|
106
|
+
wrong place. An env-supplied root that can't be created + written is a
|
|
107
|
+
misconfiguration, not a destination: warn LOUDLY naming the offending var and
|
|
108
|
+
value, and use the platform default (derived from the already-validated
|
|
109
|
+
DEFAULT_ROOT) instead of carrying the poisoned path forward.
|
|
110
|
+
|
|
111
|
+
Unset env -> default, no probe: the default is under DEFAULT_ROOT, which
|
|
112
|
+
``_platform.paths.models_root()`` already validated. Env set + usable -> the
|
|
113
|
+
env value UNCHANGED, so central and every correctly-configured box behave
|
|
114
|
+
exactly as before (the reported /mnt/llm_storage on central IS writable and
|
|
115
|
+
passes)."""
|
|
116
|
+
value = get_env_value(env_key)
|
|
117
|
+
if not value:
|
|
118
|
+
return default_path
|
|
119
|
+
if _root_is_usable(value):
|
|
120
|
+
return value
|
|
121
|
+
logging.getLogger("abstract_hugpy").warning(
|
|
122
|
+
"%s=%r is not writable (cannot create/write there); IGNORING it and "
|
|
123
|
+
"falling back to %s. Fix or remove this env var — it is usually a stale "
|
|
124
|
+
"~/.env carrying a legacy/dead storage root onto this box.",
|
|
125
|
+
env_key, value, default_path,
|
|
126
|
+
)
|
|
127
|
+
return default_path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
MODELS_HOME = MODELS_DIR = _env_root_or_default("MODELS_HOME", os.path.join(DEFAULT_ROOT,"models"))
|
|
131
|
+
|
|
132
|
+
UPLOADS_HOME = CHAT_UPLOAD_DIR = _env_root_or_default("UPLOADS_HOME", os.path.join(DEFAULT_ROOT,"uploads"))
|
|
133
|
+
|
|
134
|
+
PROJECTS_HOME = PROJECTS_DIR = _env_root_or_default("PROJECTS_HOME", os.path.join(DEFAULT_ROOT,"projects"))
|
|
135
|
+
|
|
136
|
+
# Identity profiles are PERSISTENT library items that OWN their reference images.
|
|
137
|
+
# IDENTITIES_HOME is a first-class storage root and a SIBLING of UPLOADS_HOME (both
|
|
138
|
+
# under DEFAULT_ROOT) — never a child of UPLOADS_HOME — so the session-scoped
|
|
139
|
+
# upload reaper (upload_routes._wipe_session, jailed to UPLOADS_HOME) structurally
|
|
140
|
+
# cannot reach an identity's copied reference images. Env-overridable exactly like
|
|
141
|
+
# its siblings so tests can repoint it before import.
|
|
142
|
+
IDENTITIES_HOME = IDENTITIES_DIR = _env_root_or_default("IDENTITIES_HOME", os.path.join(DEFAULT_ROOT,"identities"))
|
|
143
|
+
|
|
144
|
+
PROJECTS_PLACEMENT_PATH = get_env_value("PROJECTS_PLACEMENT_PATH") or os.path.join(PROJECTS_HOME,"placement.json")
|
|
145
|
+
|
|
146
|
+
DATASETS_HOME = DATASETS_DIR = _env_root_or_default("DATASETS_HOME", os.path.join(DEFAULT_ROOT,"datasets"))
|
|
147
|
+
|
|
148
|
+
MODELS_DISCOVERY_PATH = get_env_value("MODELS_DISCOVERY_PATH") or os.path.join(PROJECTS_HOME,"model_discovery.json")
|
|
149
|
+
|
|
150
|
+
MODELS_DICT_PATH = get_env_value("MODELS_DICT_PATH") or os.path.join(PROJECTS_HOME,"model_manifest.json")
|
|
151
|
+
|
|
152
|
+
# ── Console-managed Hugging Face token ──────────────────────────────────────
|
|
153
|
+
# The operator can save an HF token from the console so HF calls (search /
|
|
154
|
+
# metadata / downloads) are authenticated instead of anonymously rate-limited.
|
|
155
|
+
# It is persisted as a 0600 file OUTSIDE any git tree, next to the model
|
|
156
|
+
# manifest under PROJECTS_HOME — the same runtime state root api_keys.json uses.
|
|
157
|
+
# The storage-side store (hugpy_storage.hf_token) OWNS writing,
|
|
158
|
+
# validation, and the routes; it reads THIS path so there is one source of truth
|
|
159
|
+
# for where the file lives, with no flask->constants layering violation.
|
|
160
|
+
#
|
|
161
|
+
# Precedence: a STORED token wins; an env HF_TOKEN is the fallback (source:"env").
|
|
162
|
+
HF_TOKEN_PATH = get_env_value("HF_TOKEN_PATH") or os.path.join(PROJECTS_HOME, "hf_token")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def read_stored_hf_token():
|
|
166
|
+
"""The console-saved HF token from HF_TOKEN_PATH, or False if none/unreadable.
|
|
167
|
+
Never raises; never logs the token."""
|
|
168
|
+
try:
|
|
169
|
+
with open(HF_TOKEN_PATH, "r", encoding="utf-8") as fh:
|
|
170
|
+
tok = fh.read().strip()
|
|
171
|
+
return tok or False
|
|
172
|
+
except OSError:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# The GENUINE operator-supplied env token, captured ONCE before we pollute the
|
|
177
|
+
# process env with a stored token below. Source detection / fallback keys off
|
|
178
|
+
# THIS (never live os.environ, which apply_hf_token_to_env overwrites) so a
|
|
179
|
+
# stored token can never masquerade as an "env" token after it is cleared.
|
|
180
|
+
# Both the process env and the .env-backed get_env_value are consulted.
|
|
181
|
+
HF_TOKEN_ENV = (os.environ.get("HF_TOKEN") or get_env_value("HF_TOKEN") or "").strip() or False
|
|
182
|
+
|
|
183
|
+
# Seed the process token: a stored token overrides the env one. Setting
|
|
184
|
+
# os.environ["HF_TOKEN"] means every huggingface_hub call that does NOT force
|
|
185
|
+
# token=False (bare hf_hub_download / snapshot_download / HfApi()) picks it up
|
|
186
|
+
# automatically at call time; the lazy ``hfApi`` (module __getattr__ below) and
|
|
187
|
+
# the server's search routes honour this resolved value.
|
|
188
|
+
_stored_hf_token = read_stored_hf_token()
|
|
189
|
+
if _stored_hf_token:
|
|
190
|
+
HF_TOKEN = _stored_hf_token
|
|
191
|
+
if HF_TOKEN:
|
|
192
|
+
os.environ["HF_TOKEN"] = HF_TOKEN
|
|
193
|
+
os.environ["HUGGING_FACE_HUB_TOKEN"] = HF_TOKEN
|
|
194
|
+
|
|
195
|
+
HF_CACHE = _env_root_or_default("HF_CACHE", os.path.join(MODELS_HOME,"cache"))
|
|
196
|
+
|
|
197
|
+
HF_HOME = get_env_value("HF_HOME") or os.path.join(HF_CACHE,"huggingface")
|
|
198
|
+
|
|
199
|
+
HF_HUB_CACHE = get_env_value("HF_HUB_CACHE") or os.path.join(HF_HOME,"hub")
|
|
200
|
+
|
|
201
|
+
TORCH_HOME = get_env_value("TORCH_HOME") or os.path.join(HF_CACHE,"torch")
|
|
202
|
+
|
|
203
|
+
PIP_CACHE_DIR = get_env_value("PIP_CACHE_DIR") or os.path.join(HF_CACHE,"pip")
|
|
204
|
+
|
|
205
|
+
PATHS = [
|
|
206
|
+
MODELS_HOME,
|
|
207
|
+
UPLOADS_HOME,
|
|
208
|
+
PROJECTS_HOME,
|
|
209
|
+
IDENTITIES_HOME,
|
|
210
|
+
DATASETS_HOME,
|
|
211
|
+
HF_HUB_CACHE,
|
|
212
|
+
HF_CACHE,
|
|
213
|
+
HF_HUB_CACHE,
|
|
214
|
+
TORCH_HOME,
|
|
215
|
+
PIP_CACHE_DIR
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ensure_dirs(paths):
|
|
221
|
+
"""Best-effort create the storage dirs.
|
|
222
|
+
|
|
223
|
+
Importing abstract_hugpy must never hard-crash just because a storage path
|
|
224
|
+
can't be made — e.g. on a worker box where DEFAULT_ROOT (/mnt/llm_storage)
|
|
225
|
+
is a broken/stale mount (OSError errno 5) or simply not present. Each dir is
|
|
226
|
+
created independently; failures are warned about, not fatal. Set
|
|
227
|
+
DEFAULT_ROOT to a local, writable path on such boxes.
|
|
228
|
+
"""
|
|
229
|
+
import logging
|
|
230
|
+
failed = []
|
|
231
|
+
for path in paths:
|
|
232
|
+
try:
|
|
233
|
+
os.makedirs(path, exist_ok=True)
|
|
234
|
+
except OSError as exc:
|
|
235
|
+
failed.append((path, exc))
|
|
236
|
+
if failed:
|
|
237
|
+
logging.getLogger("abstract_hugpy").warning(
|
|
238
|
+
"could not create %d storage dir(s); continuing. "
|
|
239
|
+
"Set DEFAULT_ROOT to a writable path to silence this. Details: %s",
|
|
240
|
+
len(failed),
|
|
241
|
+
"; ".join(f"{p} ({e.__class__.__name__}: {e})" for p, e in failed),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
_ensure_dirs(PATHS)
|
|
246
|
+
if not os.path.isfile(PROJECTS_PLACEMENT_PATH):
|
|
247
|
+
safe_dump_to_file(file_path=PROJECTS_PLACEMENT_PATH,data={})
|
|
248
|
+
os.environ.setdefault("HF_HOME", HF_HOME)
|
|
249
|
+
os.environ.setdefault("HF_HUB_CACHE", HF_HUB_CACHE)
|
|
250
|
+
os.environ.setdefault("TORCH_HOME", TORCH_HOME)
|
|
251
|
+
os.environ.setdefault("PIP_CACHE_DIR", PIP_CACHE_DIR)
|
|
252
|
+
|
|
253
|
+
HUGPY_MARKER= get_env_value("HUGPY_MARKER") or "hugpy.json"
|
|
254
|
+
|
|
255
|
+
LLAMA_HOST= get_env_value("LLAMA_HOST") or "http://127.0.0.1"
|
|
256
|
+
VISION_HOST= get_env_value("VISION_HOST") or "http://127.0.0.1"
|
|
257
|
+
|
|
258
|
+
# `legacy` is the operator's data-hoarding store — kept on disk but never
|
|
259
|
+
# walked, indexed, or counted by any metric/appendage in these systems.
|
|
260
|
+
EXCLUDE_DIR_NAMES = make_list(get_env_value("EXCLUDE_DIR_NAMES") or ".cache,.git,.locks,snapshots,blobs,refs,1_Pooling,2_Normalize,onnx,legacy,_archive,incoming,dedupes") # _archive/incoming/dedupes (2026-09-10): cold storage + staging are not live catalog rows
|
|
261
|
+
EXCLUDE_DIR_NAMES = frozenset(EXCLUDE_DIR_NAMES)
|
|
262
|
+
|
|
263
|
+
EXCLUDE_DIR_PREFIXES = make_list(get_env_value("EXCLUDE_DIR_PREFIXES") or "models--")
|
|
264
|
+
EXCLUDE_DIR_PREFIXES = tuple(EXCLUDE_DIR_PREFIXES) # HF cache root naming
|
|
265
|
+
|
|
266
|
+
TOKENIZER_SENTINEL_THRESHOLD = float(get_env_value("TOKENIZER_SENTINEL_THRESHOLD") or 10**9)
|
|
267
|
+
DEFAULT_TIMEOUT= float(get_env_value("DEFAULT_TIMEOUT") or 3600.0)
|
|
268
|
+
DEFAULT_MAX_TOKENS= int(get_env_value("DEFAULT_MAX_TOKENS") or 32768)
|
|
269
|
+
MIN_INPUT_WORDS_DEFAULT = get_env_value("MIN_INPUT_WORDS_DEFAULT") or 10
|
|
270
|
+
SOURCEKIND = make_list(get_env_value("SOURCEKIND") or "text,url,file,image")
|
|
271
|
+
SOURCEKIND =Literal[*SOURCEKIND]
|
|
272
|
+
|
|
273
|
+
JOBSTATUS = make_list(get_env_value("JOBSTATUS") or "queued,running,completed,failed,cancelled")
|
|
274
|
+
JOBSTATUS =Literal[*JOBSTATUS]
|
|
275
|
+
|
|
276
|
+
DEFAULT_TEMPERATURE = float(get_env_value("DEFAULT_TEMPERATURE") or 0.1)
|
|
277
|
+
DEFAULT_TOP_P = float(get_env_value("DEFAULT_TOP_P") or 1)
|
|
278
|
+
|
|
279
|
+
FINISH_REASONS = make_list(get_env_value("FINISH_REASONS") or "stop,max_tokens,cancelled,error")
|
|
280
|
+
FINISH_REASONS =Literal[*FINISH_REASONS]
|
|
281
|
+
|
|
282
|
+
ROLES = make_list(get_env_value("ROLES") or "system,user,assistant,tool")
|
|
283
|
+
ROLES = Literal[*ROLES]
|
|
284
|
+
|
|
285
|
+
# Stock defaults: one model per task, every one a curated staple in
|
|
286
|
+
# models_config.MODELS — efficiency-first picks (whole fleet ~17GB).
|
|
287
|
+
DEFAULT_CHAT_MODEL = get_env_value("DEFAULT_CHAT_MODEL") or "Qwen2.5-7B-Instruct-GGUF"
|
|
288
|
+
# The AGENT BRAIN — the model agent nodes/loops default to. A dedicated
|
|
289
|
+
# variable (operator ask, 2026-07-17) so the agents' default is never
|
|
290
|
+
# conflated with the generic chat default or the Discord bot's
|
|
291
|
+
# DEFAULT_MODEL_KEY. Brain switch 2026-07-17 (P3.4 scorecard, reliability
|
|
292
|
+
# over speed: coder 4/4 vs flux2-klein 3/4): Qwen3-Coder-Next. The
|
|
293
|
+
# hugpy_agent CLIENT package carries the same default with its own
|
|
294
|
+
# HUGPY_AGENT_BRAIN env knob; this is central's copy of the truth.
|
|
295
|
+
DEFAULT_AGENT_BRAIN = get_env_value("DEFAULT_AGENT_BRAIN") or "Qwen~Qwen3-Coder-Next-GGUF"
|
|
296
|
+
DEFAULT_VISION_MODEL = get_env_value("DEFAULT_VISION_MODEL") or "Qwen2.5-VL-3B-Instruct-GGUF"
|
|
297
|
+
DEFAULT_WHISPER_MODEL = get_env_value("DEFAULT_WHISPER_MODEL") or "whisper-large-v3-turbo"
|
|
298
|
+
DEFAULT_SUMMARIZE_MODEL = get_env_value("DEFAULT_SUMMARIZE_MODEL") or "flan-t5-large"
|
|
299
|
+
DEFAULT_EMBED_MODEL = get_env_value("DEFAULT_EMBED_MODEL") or "all-minilm-l6-v2"
|
|
300
|
+
DEFAULT_IMAGEGEN_MODEL = get_env_value("DEFAULT_IMAGEGEN_MODEL") or "sd-turbo"
|
|
301
|
+
DEFAULT_KEYWORDS_MODEL = get_env_value("DEFAULT_KEYWORDS_MODEL") or "all-minilm-l6-v2"
|
|
302
|
+
# Vision-analysis family (generic transformers-pipeline runner).
|
|
303
|
+
DEFAULT_DEPTH_MODEL = get_env_value("DEFAULT_DEPTH_MODEL") or "depth-anything-v2-small"
|
|
304
|
+
DEFAULT_DETECT_MODEL = get_env_value("DEFAULT_DETECT_MODEL") or "detr-resnet-50"
|
|
305
|
+
DEFAULT_IMG_CLASSIFY_MODEL = get_env_value("DEFAULT_IMG_CLASSIFY_MODEL") or "vit-base-patch16-224"
|
|
306
|
+
DEFAULT_SEGMENT_MODEL = get_env_value("DEFAULT_SEGMENT_MODEL") or "segformer-b0-ade"
|
|
307
|
+
|
|
308
|
+
DISK_AUTHORITATIVE = make_list(get_env_value("DISK_AUTHORITATIVE") or "name,folder,framework,filename")
|
|
309
|
+
OVERLAY_ALLOWED = set(make_list(get_env_value("OVERLAY_ALLOWED") or "port, host, timeout_s, include"))
|
|
310
|
+
|
|
311
|
+
GGUF_QUANT = re.compile(r"(Q\d+_[A-Z0-9_]+|F16|BF16|F32)", re.I)
|
|
312
|
+
|
|
313
|
+
DEFAULT_LOCAL_FILES_ONLY = env_bool("DEFAULT_LOCAL_FILES_ONLY", True)
|
|
314
|
+
|
|
315
|
+
# Kill switch for resolve()-time staple downloads. Default on: a fresh install
|
|
316
|
+
# pulls the curated MODELS fleet on first use. Set HUGPY_AUTO_DOWNLOAD=false on
|
|
317
|
+
# air-gapped boxes / workers that must never touch the network.
|
|
318
|
+
HUGPY_AUTO_DOWNLOAD = env_bool("HUGPY_AUTO_DOWNLOAD", True)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def __getattr__(name: str):
|
|
322
|
+
"""Lazy ``hfApi`` (PEP 562): an authenticated ``huggingface_hub.HfApi`` bound
|
|
323
|
+
to the resolved ``HF_TOKEN``. Built on first access and cached in the module
|
|
324
|
+
namespace; ``hugpy_storage.hf_token`` rebinds it when the operator changes the
|
|
325
|
+
stored token. Keeps ``huggingface_hub`` out of the platform import path."""
|
|
326
|
+
if name == "hfApi":
|
|
327
|
+
from huggingface_hub import HfApi # optional extra: hugpy-platform[hf]
|
|
328
|
+
|
|
329
|
+
api = HfApi(token=HF_TOKEN)
|
|
330
|
+
globals()["hfApi"] = api
|
|
331
|
+
return api
|
|
332
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# LEAN 0.3.0 — lifted verbatim from /srv/hugpy_next/py/foundation/hugpy_platform/src/hugpy_platform/filesystem.py; unused symbols removed.
|
|
2
|
+
"""Small filesystem probes shared across packages."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def free_bytes(path: str) -> int | None:
|
|
11
|
+
"""Free bytes on the target's filesystem, falling back to root."""
|
|
12
|
+
try:
|
|
13
|
+
probe = path if os.path.exists(path) else "/"
|
|
14
|
+
return shutil.disk_usage(probe).free
|
|
15
|
+
except OSError:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_parent_best_effort(path: str) -> None:
|
|
20
|
+
parent = os.path.dirname(path)
|
|
21
|
+
if parent:
|
|
22
|
+
try:
|
|
23
|
+
os.makedirs(parent, exist_ok=True)
|
|
24
|
+
except OSError:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_text(path: str, content: str) -> str:
|
|
29
|
+
"""Write UTF-8 text, creating its parent, and return the path."""
|
|
30
|
+
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
31
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
32
|
+
fh.write(content)
|
|
33
|
+
return path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_within(path: str, root: str) -> bool:
|
|
37
|
+
"""Whether a real path stays under a real root."""
|
|
38
|
+
resolved_path = os.path.realpath(path)
|
|
39
|
+
resolved_root = os.path.realpath(root)
|
|
40
|
+
try:
|
|
41
|
+
return os.path.commonpath([resolved_path, resolved_root]) == resolved_root
|
|
42
|
+
except ValueError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def directory_bytes(path: str) -> int:
|
|
47
|
+
"""Count readable file bytes under a directory, tolerating vanished files."""
|
|
48
|
+
total = 0
|
|
49
|
+
for root, _, files in os.walk(path):
|
|
50
|
+
for name in files:
|
|
51
|
+
try:
|
|
52
|
+
total += os.path.getsize(os.path.join(root, name))
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
return total
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# LEAN 0.3.0 — lifted verbatim from /srv/hugpy_next/py/foundation/hugpy_platform/src/hugpy_platform/formatting.py; unused symbols removed.
|
|
2
|
+
"""Pure formatting of byte counts for logs and operator messages."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ffmpeg_num(value) -> str:
|
|
8
|
+
"""Use a compact numeric argument for ffmpeg."""
|
|
9
|
+
if isinstance(value, float) and value.is_integer():
|
|
10
|
+
return str(int(value))
|
|
11
|
+
return repr(value) if isinstance(value, float) else str(value)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def human_bytes(value, *, empty: str = "0 B") -> str:
|
|
15
|
+
"""Format bytes in binary units, preserving each caller's empty label."""
|
|
16
|
+
if not value:
|
|
17
|
+
return empty
|
|
18
|
+
units = ("B", "KB", "MB", "GB", "TB")
|
|
19
|
+
number = float(value)
|
|
20
|
+
index = 0
|
|
21
|
+
while number >= 1024 and index < len(units) - 1:
|
|
22
|
+
number /= 1024
|
|
23
|
+
index += 1
|
|
24
|
+
return f"{number:.1f} {units[index]}"
|