someone-in-the-world Claude Sonnet 4.6 commited on
Commit
ca0e7d6
·
1 Parent(s): 9e47f62

Auto-prune log files older than LOG_MAX_DAYS (default 7) after each upload

Browse files

Without 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>

Files changed (2) hide show
  1. logging_utils.py +113 -79
  2. 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
- input_jpegs = [_to_jpeg(img) for img in pil_inputs]
65
- output_jpeg = _to_jpeg(output_pil)
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
- print(f"[log] uploading {path_in_repo} ({os.path.getsize(tmp_path)//1024}KB)")
92
- api.upload_file(
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