Spaces:
Sleeping
Sleeping
File size: 5,861 Bytes
d49ad25 e0f0851 d49ad25 e0f0851 123689b e0f0851 d49ad25 e0f0851 d49ad25 e0f0851 d49ad25 e0f0851 d49ad25 e0f0851 d49ad25 a9a62dc d49ad25 a9a62dc d49ad25 a9a62dc d49ad25 a9a62dc d49ad25 a9a62dc d49ad25 | 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 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """HF Dataset queue: async Mac β HF prediction pipeline.
Architecture (pull-based, Mac never opens a port):
HF Space β writes "pending" to HF Dataset queue
Mac worker β polls queue every 5 min β runs prediction β writes "done"
HF Space β reads fresh result from queue on next request
Queue file (queue.json in HF Dataset repo):
{ "<stock_no>": { "status": "pending|done|error",
"requested_at": "ISO",
"completed_at": "ISO|null",
"source": "hf_space|mac_worker",
"result": {...}|null } }
"""
import json
import logging
import os
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
logger = logging.getLogger(__name__)
DATASET_REPO = os.getenv("HF_QUEUE_DATASET", "DennisChan0909/stock-predictor-queue")
QUEUE_FILE = "queue.json"
RESULT_FRESH_HOURS = 24 # "done" results older than this are re-queued
STALE_REFRESH_HOURS = 2 # Mac refreshes results older than this
_QUEUE_READ_TTL = 90 # seconds β cache queue.json in-memory to avoid repeated HF Hub downloads
_queue_mem_cache: dict = {} # {"data": {...}, "ts": float}
def _get_token() -> str | None:
token = os.getenv("HF_TOKEN")
if token:
return token
token_file = Path(__file__).resolve().parent.parent / ".hf_token"
if token_file.exists():
t = token_file.read_text().strip()
if t and t != "PASTE_YOUR_HF_TOKEN_HERE":
return t
return None
def _hf_available() -> bool:
try:
import huggingface_hub # noqa: F401
return True
except ImportError:
return False
def is_on_hf_space() -> bool:
return bool(os.getenv("SPACE_ID") or os.getenv("HF_SPACE_ID") or os.getenv("SPACE_HOST"))
def read_queue(force: bool = False) -> dict:
if not _hf_available():
return {}
token = _get_token()
if not token:
return {}
# In-memory cache β avoids repeated HF Hub downloads on every predict request
if not force:
cached = _queue_mem_cache.get("data")
if cached is not None and time.time() - _queue_mem_cache.get("ts", 0) < _QUEUE_READ_TTL:
return cached
try:
from huggingface_hub import hf_hub_download
path = hf_hub_download(
repo_id=DATASET_REPO, filename=QUEUE_FILE,
repo_type="dataset", token=token, force_download=True,
)
with open(path) as f:
data = json.load(f)
_queue_mem_cache["data"] = data
_queue_mem_cache["ts"] = time.time()
return data
except Exception as e:
logger.debug("HF queue read: %s", e)
return {}
def write_queue(queue: dict) -> bool:
if not _hf_available():
return False
token = _get_token()
if not token:
return False
try:
from huggingface_hub import HfApi
api = HfApi()
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(queue, f, indent=2, default=str)
tmp = f.name
api.upload_file(
path_or_fileobj=tmp,
path_in_repo=QUEUE_FILE,
repo_id=DATASET_REPO,
repo_type="dataset",
token=token,
commit_message=f"worker {datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M')}",
)
os.unlink(tmp)
# Invalidate in-memory cache so next read gets fresh data
_queue_mem_cache.clear()
return True
except Exception as e:
logger.warning("HF queue write: %s", e)
return False
def is_publishable_result(result: object) -> bool:
"""Return True when a queue result is a completed ML-style prediction."""
if not isinstance(result, dict):
return False
if result.get("source") == "quick_rules":
return False
if str(result.get("disclaimer", "")).startswith("Quick estimate"):
return False
return True
def get_cached_result(stock_no: str, max_age_hours: float | None = RESULT_FRESH_HOURS) -> dict | None:
"""Return a fresh 'done' prediction from the queue, or None."""
queue = read_queue()
item = queue.get(stock_no)
if not item or item.get("status") != "done":
return None
result = item.get("result")
if not is_publishable_result(result):
return None
completed = item.get("completed_at")
if completed and max_age_hours is not None:
try:
ts = datetime.fromisoformat(completed.replace("Z", "+00:00"))
if datetime.now(timezone.utc) - ts > timedelta(hours=max_age_hours):
return None
except Exception:
pass
return result
def enqueue(stock_no: str) -> bool:
"""Add stock_no to queue as pending. Idempotent β skips if already pending or freshly done."""
queue = read_queue()
existing = queue.get(stock_no, {})
status = existing.get("status")
if status == "pending":
return True
if status == "done":
completed = existing.get("completed_at")
if completed:
try:
ts = datetime.fromisoformat(completed.replace("Z", "+00:00"))
if datetime.now(timezone.utc) - ts < timedelta(hours=STALE_REFRESH_HOURS):
return True # still fresh
except Exception:
pass
queue[stock_no] = {
"status": "pending",
"requested_at": datetime.now(timezone.utc).isoformat(),
"completed_at": None,
"source": "hf_space",
"result": None,
}
return write_queue(queue)
def queue_status(stock_no: str) -> dict:
"""Return the current queue entry for a stock, or {"status": "not_queued"}."""
queue = read_queue()
return queue.get(stock_no, {"status": "not_queued"})
|