Spaces:
Running on Zero
Running on Zero
Commit ·
ca0e7d6
1
Parent(s): 9e47f62
Auto-prune log files older than LOG_MAX_DAYS (default 7) after each upload
Browse filesWithout pruning, parquet files accumulate indefinitely and risk exceeding
the HF private storage free tier. After each successful upload, old files
are deleted from the dataset repo using a date-prefix comparison on the
filename. Refactors log_inference into single-responsibility helpers
(_img_to_jpeg, _build_table, _upload_parquet, _prune_old_files) to make
the prune logic unit-testable without real network calls or waiting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- logging_utils.py +113 -79
- tests/test_logging_utils.py +68 -0
logging_utils.py
CHANGED
|
@@ -1,10 +1,118 @@
|
|
| 1 |
import os
|
| 2 |
import uuid
|
| 3 |
from io import BytesIO
|
| 4 |
-
from datetime import datetime, timezone
|
| 5 |
|
| 6 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 7 |
DATASET_REPO = os.environ.get("LOG_DATASET_REPO")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def log_inference(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
|
|
@@ -12,94 +120,20 @@ def log_inference(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
|
|
| 12 |
if not HF_TOKEN or not DATASET_REPO:
|
| 13 |
print(f"[log] skipped — HF_TOKEN={'set' if HF_TOKEN else 'missing'}, DATASET_REPO={'set' if DATASET_REPO else 'missing'}")
|
| 14 |
return
|
| 15 |
-
tmp_path = None
|
| 16 |
try:
|
| 17 |
-
import tempfile
|
| 18 |
-
import json as _json
|
| 19 |
-
import pyarrow as pa
|
| 20 |
-
import pyarrow.parquet as pq
|
| 21 |
from huggingface_hub import HfApi
|
| 22 |
|
| 23 |
-
img_struct = pa.struct([("bytes", pa.binary()), ("path", pa.string())])
|
| 24 |
-
hf_meta = _json.dumps({"info": {"features": {
|
| 25 |
-
"timestamp": {"dtype": "float64", "_type": "Value"},
|
| 26 |
-
"prompt": {"dtype": "string", "_type": "Value"},
|
| 27 |
-
"seed": {"dtype": "int32", "_type": "Value"},
|
| 28 |
-
"steps": {"dtype": "int32", "_type": "Value"},
|
| 29 |
-
"guidance_scale": {"dtype": "float32", "_type": "Value"},
|
| 30 |
-
"input_images": {"feature": {"_type": "Image"}, "_type": "Sequence"},
|
| 31 |
-
"output_image": {"_type": "Image"},
|
| 32 |
-
"duration_seconds": {"dtype": "float32", "_type": "Value"},
|
| 33 |
-
"input_width": {"dtype": "int32", "_type": "Value"},
|
| 34 |
-
"input_height": {"dtype": "int32", "_type": "Value"},
|
| 35 |
-
"success": {"dtype": "bool", "_type": "Value"},
|
| 36 |
-
"error_message": {"dtype": "string", "_type": "Value"},
|
| 37 |
-
}}}).encode()
|
| 38 |
-
schema = pa.schema([
|
| 39 |
-
("timestamp", pa.float64()),
|
| 40 |
-
("prompt", pa.string()),
|
| 41 |
-
("seed", pa.int32()),
|
| 42 |
-
("steps", pa.int32()),
|
| 43 |
-
("guidance_scale", pa.float32()),
|
| 44 |
-
("input_images", pa.list_(img_struct)),
|
| 45 |
-
("output_image", img_struct),
|
| 46 |
-
("duration_seconds", pa.float32()),
|
| 47 |
-
("input_width", pa.int32()),
|
| 48 |
-
("input_height", pa.int32()),
|
| 49 |
-
("success", pa.bool_()),
|
| 50 |
-
("error_message", pa.string()),
|
| 51 |
-
], metadata={b"huggingface": hf_meta})
|
| 52 |
-
|
| 53 |
-
def _to_jpeg(img, quality=85):
|
| 54 |
-
if img is None:
|
| 55 |
-
return None
|
| 56 |
-
buf = BytesIO()
|
| 57 |
-
img.convert("RGB").save(buf, format="JPEG", quality=quality)
|
| 58 |
-
return buf.getvalue()
|
| 59 |
-
|
| 60 |
-
def _img(b):
|
| 61 |
-
return {"bytes": b, "path": None}
|
| 62 |
-
|
| 63 |
now = datetime.now(timezone.utc)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
table = pa.table({
|
| 68 |
-
"timestamp": pa.array([now.timestamp()], type=pa.float64()),
|
| 69 |
-
"prompt": pa.array([prompt], type=pa.string()),
|
| 70 |
-
"seed": pa.array([int(seed)], type=pa.int32()),
|
| 71 |
-
"steps": pa.array([int(steps)], type=pa.int32()),
|
| 72 |
-
"guidance_scale": pa.array([float(guidance_scale)], type=pa.float32()),
|
| 73 |
-
"input_images": pa.array([[_img(b) for b in input_jpegs]], type=pa.list_(img_struct)),
|
| 74 |
-
"output_image": pa.array([_img(output_jpeg) if output_jpeg else None], type=img_struct),
|
| 75 |
-
"duration_seconds": pa.array([float(duration_seconds)], type=pa.float32()),
|
| 76 |
-
"input_width": pa.array([int(input_width)], type=pa.int32()),
|
| 77 |
-
"input_height": pa.array([int(input_height)], type=pa.int32()),
|
| 78 |
-
"success": pa.array([bool(success)], type=pa.bool_()),
|
| 79 |
-
"error_message": pa.array([str(error_message)], type=pa.string()),
|
| 80 |
-
}, schema=schema)
|
| 81 |
|
| 82 |
uid = uuid.uuid4().hex[:8]
|
| 83 |
path_in_repo = f"data/{now.strftime('%Y-%m-%d-%H%M%S')}-{uid}.parquet"
|
| 84 |
|
| 85 |
-
with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
|
| 86 |
-
tmp_path = tmp.name
|
| 87 |
-
pq.write_table(table, tmp_path)
|
| 88 |
-
|
| 89 |
api = HfApi(token=HF_TOKEN)
|
| 90 |
api.create_repo(repo_id=DATASET_REPO, repo_type="dataset", private=True, exist_ok=True)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
path_or_fileobj=tmp_path, path_in_repo=path_in_repo,
|
| 94 |
-
repo_id=DATASET_REPO, repo_type="dataset",
|
| 95 |
-
)
|
| 96 |
-
print(f"[log] upload done — {DATASET_REPO}/{path_in_repo}")
|
| 97 |
except Exception as log_err:
|
| 98 |
import traceback as _tb
|
| 99 |
print(f"[log] WARNING: {log_err}\n{_tb.format_exc()}")
|
| 100 |
-
finally:
|
| 101 |
-
if tmp_path:
|
| 102 |
-
try:
|
| 103 |
-
os.unlink(tmp_path)
|
| 104 |
-
except Exception:
|
| 105 |
-
pass
|
|
|
|
| 1 |
import os
|
| 2 |
import uuid
|
| 3 |
from io import BytesIO
|
| 4 |
+
from datetime import datetime, timezone, timedelta
|
| 5 |
|
| 6 |
HF_TOKEN = os.environ.get("HF_TOKEN")
|
| 7 |
DATASET_REPO = os.environ.get("LOG_DATASET_REPO")
|
| 8 |
+
MAX_LOG_DAYS = int(os.environ.get("LOG_MAX_DAYS", "7"))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _img_to_jpeg(img, quality=85):
|
| 12 |
+
if img is None:
|
| 13 |
+
return None
|
| 14 |
+
buf = BytesIO()
|
| 15 |
+
img.convert("RGB").save(buf, format="JPEG", quality=quality)
|
| 16 |
+
return buf.getvalue()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _build_table(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
|
| 20 |
+
input_width, input_height, duration_seconds, success, error_message, now):
|
| 21 |
+
import json as _json
|
| 22 |
+
import pyarrow as pa
|
| 23 |
+
|
| 24 |
+
img_struct = pa.struct([("bytes", pa.binary()), ("path", pa.string())])
|
| 25 |
+
hf_meta = _json.dumps({"info": {"features": {
|
| 26 |
+
"timestamp": {"dtype": "float64", "_type": "Value"},
|
| 27 |
+
"prompt": {"dtype": "string", "_type": "Value"},
|
| 28 |
+
"seed": {"dtype": "int32", "_type": "Value"},
|
| 29 |
+
"steps": {"dtype": "int32", "_type": "Value"},
|
| 30 |
+
"guidance_scale": {"dtype": "float32", "_type": "Value"},
|
| 31 |
+
"input_images": {"feature": {"_type": "Image"}, "_type": "Sequence"},
|
| 32 |
+
"output_image": {"_type": "Image"},
|
| 33 |
+
"duration_seconds": {"dtype": "float32", "_type": "Value"},
|
| 34 |
+
"input_width": {"dtype": "int32", "_type": "Value"},
|
| 35 |
+
"input_height": {"dtype": "int32", "_type": "Value"},
|
| 36 |
+
"success": {"dtype": "bool", "_type": "Value"},
|
| 37 |
+
"error_message": {"dtype": "string", "_type": "Value"},
|
| 38 |
+
}}}).encode()
|
| 39 |
+
schema = pa.schema([
|
| 40 |
+
("timestamp", pa.float64()),
|
| 41 |
+
("prompt", pa.string()),
|
| 42 |
+
("seed", pa.int32()),
|
| 43 |
+
("steps", pa.int32()),
|
| 44 |
+
("guidance_scale", pa.float32()),
|
| 45 |
+
("input_images", pa.list_(img_struct)),
|
| 46 |
+
("output_image", img_struct),
|
| 47 |
+
("duration_seconds", pa.float32()),
|
| 48 |
+
("input_width", pa.int32()),
|
| 49 |
+
("input_height", pa.int32()),
|
| 50 |
+
("success", pa.bool_()),
|
| 51 |
+
("error_message", pa.string()),
|
| 52 |
+
], metadata={b"huggingface": hf_meta})
|
| 53 |
+
|
| 54 |
+
def _img(b):
|
| 55 |
+
return {"bytes": b, "path": None}
|
| 56 |
+
|
| 57 |
+
input_jpegs = [_img_to_jpeg(img) for img in pil_inputs]
|
| 58 |
+
output_jpeg = _img_to_jpeg(output_pil)
|
| 59 |
+
|
| 60 |
+
return pa.table({
|
| 61 |
+
"timestamp": pa.array([now.timestamp()], type=pa.float64()),
|
| 62 |
+
"prompt": pa.array([prompt], type=pa.string()),
|
| 63 |
+
"seed": pa.array([int(seed)], type=pa.int32()),
|
| 64 |
+
"steps": pa.array([int(steps)], type=pa.int32()),
|
| 65 |
+
"guidance_scale": pa.array([float(guidance_scale)], type=pa.float32()),
|
| 66 |
+
"input_images": pa.array([[_img(b) for b in input_jpegs]], type=pa.list_(img_struct)),
|
| 67 |
+
"output_image": pa.array([_img(output_jpeg) if output_jpeg else None], type=img_struct),
|
| 68 |
+
"duration_seconds": pa.array([float(duration_seconds)], type=pa.float32()),
|
| 69 |
+
"input_width": pa.array([int(input_width)], type=pa.int32()),
|
| 70 |
+
"input_height": pa.array([int(input_height)], type=pa.int32()),
|
| 71 |
+
"success": pa.array([bool(success)], type=pa.bool_()),
|
| 72 |
+
"error_message": pa.array([str(error_message)], type=pa.string()),
|
| 73 |
+
}, schema=schema)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _upload_parquet(api, repo_id, table, path_in_repo):
|
| 77 |
+
import tempfile
|
| 78 |
+
import pyarrow.parquet as pq
|
| 79 |
+
|
| 80 |
+
tmp_path = None
|
| 81 |
+
try:
|
| 82 |
+
with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
|
| 83 |
+
tmp_path = tmp.name
|
| 84 |
+
pq.write_table(table, tmp_path)
|
| 85 |
+
print(f"[log] uploading {path_in_repo} ({os.path.getsize(tmp_path)//1024}KB)")
|
| 86 |
+
api.upload_file(
|
| 87 |
+
path_or_fileobj=tmp_path, path_in_repo=path_in_repo,
|
| 88 |
+
repo_id=repo_id, repo_type="dataset",
|
| 89 |
+
)
|
| 90 |
+
print(f"[log] upload done — {repo_id}/{path_in_repo}")
|
| 91 |
+
finally:
|
| 92 |
+
if tmp_path:
|
| 93 |
+
try:
|
| 94 |
+
os.unlink(tmp_path)
|
| 95 |
+
except Exception:
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def _prune_old_files(api, repo_id, keep_days):
|
| 100 |
+
if keep_days <= 0:
|
| 101 |
+
return
|
| 102 |
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=keep_days)).strftime("%Y-%m-%d")
|
| 103 |
+
try:
|
| 104 |
+
to_delete = [
|
| 105 |
+
f.path
|
| 106 |
+
for f in api.list_repo_tree(repo_id, repo_type="dataset", path_in_repo="data")
|
| 107 |
+
if f.path.endswith(".parquet") and os.path.basename(f.path)[:10] < cutoff
|
| 108 |
+
]
|
| 109 |
+
for path in to_delete:
|
| 110 |
+
api.delete_file(path_in_repo=path, repo_id=repo_id, repo_type="dataset")
|
| 111 |
+
print(f"[log] pruned: {path}")
|
| 112 |
+
if to_delete:
|
| 113 |
+
print(f"[log] pruned {len(to_delete)} old file(s)")
|
| 114 |
+
except Exception as e:
|
| 115 |
+
print(f"[log] prune warning: {e}")
|
| 116 |
|
| 117 |
|
| 118 |
def log_inference(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
|
|
|
|
| 120 |
if not HF_TOKEN or not DATASET_REPO:
|
| 121 |
print(f"[log] skipped — HF_TOKEN={'set' if HF_TOKEN else 'missing'}, DATASET_REPO={'set' if DATASET_REPO else 'missing'}")
|
| 122 |
return
|
|
|
|
| 123 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
from huggingface_hub import HfApi
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
now = datetime.now(timezone.utc)
|
| 127 |
+
table = _build_table(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
|
| 128 |
+
input_width, input_height, duration_seconds, success, error_message, now)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
uid = uuid.uuid4().hex[:8]
|
| 131 |
path_in_repo = f"data/{now.strftime('%Y-%m-%d-%H%M%S')}-{uid}.parquet"
|
| 132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
api = HfApi(token=HF_TOKEN)
|
| 134 |
api.create_repo(repo_id=DATASET_REPO, repo_type="dataset", private=True, exist_ok=True)
|
| 135 |
+
_upload_parquet(api, DATASET_REPO, table, path_in_repo)
|
| 136 |
+
_prune_old_files(api, DATASET_REPO, MAX_LOG_DAYS)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
except Exception as log_err:
|
| 138 |
import traceback as _tb
|
| 139 |
print(f"[log] WARNING: {log_err}\n{_tb.format_exc()}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_logging_utils.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timezone, timedelta
|
| 2 |
+
from types import SimpleNamespace
|
| 3 |
+
from unittest.mock import MagicMock, call
|
| 4 |
+
|
| 5 |
+
from logging_utils import _prune_old_files
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _make_api(paths):
|
| 9 |
+
"""Return a mock HfApi whose list_repo_tree yields RepoFile-like objects for `paths`."""
|
| 10 |
+
api = MagicMock()
|
| 11 |
+
api.list_repo_tree.return_value = [SimpleNamespace(path=p) for p in paths]
|
| 12 |
+
return api
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _old(name):
|
| 16 |
+
return f"data/2020-01-01-000000-{name}.parquet"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _recent(name):
|
| 20 |
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
| 21 |
+
return f"data/{today}-000000-{name}.parquet"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_old_files_are_deleted():
|
| 25 |
+
api = _make_api([_old("aaa"), _old("bbb")])
|
| 26 |
+
_prune_old_files(api, "org/repo", keep_days=7)
|
| 27 |
+
assert api.delete_file.call_count == 2
|
| 28 |
+
deleted = {c.kwargs["path_in_repo"] for c in api.delete_file.call_args_list}
|
| 29 |
+
assert deleted == {_old("aaa"), _old("bbb")}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_recent_files_are_kept():
|
| 33 |
+
api = _make_api([_recent("aaa"), _recent("bbb")])
|
| 34 |
+
_prune_old_files(api, "org/repo", keep_days=7)
|
| 35 |
+
api.delete_file.assert_not_called()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_only_old_files_deleted_in_mixed_set():
|
| 39 |
+
api = _make_api([_old("aaa"), _recent("bbb"), _old("ccc")])
|
| 40 |
+
_prune_old_files(api, "org/repo", keep_days=7)
|
| 41 |
+
deleted = {c.kwargs["path_in_repo"] for c in api.delete_file.call_args_list}
|
| 42 |
+
assert deleted == {_old("aaa"), _old("ccc")}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def test_empty_directory_does_nothing():
|
| 46 |
+
api = _make_api([])
|
| 47 |
+
_prune_old_files(api, "org/repo", keep_days=7)
|
| 48 |
+
api.delete_file.assert_not_called()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_keep_days_zero_skips_pruning():
|
| 52 |
+
api = _make_api([_old("aaa")])
|
| 53 |
+
_prune_old_files(api, "org/repo", keep_days=0)
|
| 54 |
+
api.list_repo_tree.assert_not_called()
|
| 55 |
+
api.delete_file.assert_not_called()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_non_parquet_files_are_ignored():
|
| 59 |
+
api = _make_api(["data/2020-01-01-README.md", _old("aaa")])
|
| 60 |
+
_prune_old_files(api, "org/repo", keep_days=7)
|
| 61 |
+
deleted = {c.kwargs["path_in_repo"] for c in api.delete_file.call_args_list}
|
| 62 |
+
assert deleted == {_old("aaa")}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_api_error_does_not_raise():
|
| 66 |
+
api = MagicMock()
|
| 67 |
+
api.list_repo_tree.side_effect = RuntimeError("network error")
|
| 68 |
+
_prune_old_files(api, "org/repo", keep_days=7) # must not raise
|