Wauplin HF staff commited on
Commit
ffefd3d
1 Parent(s): e91df8e

Update user_history.py

Browse files
Files changed (1) hide show
  1. user_history.py +18 -129
user_history.py CHANGED
@@ -1,6 +1,5 @@
1
  """
2
  User History is a plugin that you can add to your Spaces to cache generated images for your users.
3
-
4
  Key features:
5
  - 🤗 Sign in with Hugging Face
6
  - Save generated images with their metadata: prompts, timestamp, hyper-parameters, etc.
@@ -8,7 +7,6 @@ Key features:
8
  - Delete your history to respect privacy.
9
  - Compatible with Persistent Storage for long-term storage.
10
  - Admin panel to check configuration and disk usage .
11
-
12
  Useful links:
13
  - Demo: https://huggingface.co/spaces/Wauplin/gradio-user-history
14
  - README: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
@@ -37,18 +35,13 @@ def setup(folder_path: str | Path | None = None) -> None:
37
  user_history.folder_path = _resolve_folder_path(folder_path)
38
  user_history.initialized = True
39
 
40
- # TODO: remove this section once all Spaces have migrated
41
- _migrate_history()
42
-
43
 
44
  def render() -> None:
45
  user_history = _UserHistory()
46
 
47
  # initialize with default config
48
  if not user_history.initialized:
49
- print(
50
- "Initializing user history with default config. Use `user_history.setup(...)` to customize folder_path."
51
- )
52
  setup()
53
 
54
  # Render user history tab
@@ -83,18 +76,11 @@ def render() -> None:
83
 
84
  # "Export zip" row (hidden by default)
85
  with gr.Row():
86
- export_file = gr.File(
87
- file_count="single",
88
- file_types=[".zip"],
89
- label="Exported history",
90
- visible=False,
91
- )
92
 
93
  # "Config deletion" row (hidden by default)
94
  with gr.Row():
95
- confirm_button = gr.Button(
96
- "Confirm delete all history", variant="stop", visible=False
97
- )
98
  cancel_button = gr.Button("Cancel", visible=False)
99
 
100
  # Gallery
@@ -117,12 +103,8 @@ def render() -> None:
117
  gallery.attach_load_event(_fetch_user_history, every=None)
118
 
119
  # Interactions
120
- refresh_button.click(
121
- fn=_fetch_user_history, inputs=[], outputs=[gallery], queue=False
122
- )
123
- export_button.click(
124
- fn=_export_user_history, inputs=[], outputs=[export_file], queue=False
125
- )
126
 
127
  # Taken from https://github.com/gradio-app/gradio/issues/3324#issuecomment-1446382045
128
  delete_button.click(
@@ -203,9 +185,7 @@ class _UserHistory(object):
203
 
204
  def _user_lock(self, username: str) -> FileLock:
205
  """Ensure history is not corrupted if concurrent calls."""
206
- return FileLock(
207
- self.folder_path / f"{username}.lock"
208
- ) # lock outside of folder => better when exporting ZIP
209
 
210
  def _user_jsonl_path(self, username: str) -> Path:
211
  return self._user_path(username) / "history.jsonl"
@@ -225,9 +205,7 @@ def _fetch_user_history(profile: gr.OAuthProfile | None) -> List[Tuple[str, str]
225
 
226
  user_history = _UserHistory()
227
  if not user_history.initialized:
228
- warnings.warn(
229
- "User history is not set in Gradio demo. You must use `user_history.render(...)` first."
230
- )
231
  return []
232
 
233
  with user_history._user_lock(username):
@@ -253,17 +231,13 @@ def _export_user_history(profile: gr.OAuthProfile | None) -> Dict | None:
253
 
254
  user_history = _UserHistory()
255
  if not user_history.initialized:
256
- warnings.warn(
257
- "User history is not set in Gradio demo. You must use `user_history.render(...)` first."
258
- )
259
  return None
260
 
261
  # Zip history
262
  with user_history._user_lock(username):
263
  path = shutil.make_archive(
264
- str(_archives_path() / f"history_{username}"),
265
- "zip",
266
- user_history._user_path(username),
267
  )
268
 
269
  return gr.update(visible=True, value=path)
@@ -278,9 +252,7 @@ def _delete_user_history(profile: gr.OAuthProfile | None) -> None:
278
 
279
  user_history = _UserHistory()
280
  if not user_history.initialized:
281
- warnings.warn(
282
- "User history is not set in Gradio demo. You must use `user_history.render(...)` first."
283
- )
284
  return
285
 
286
  with user_history._user_lock(username):
@@ -317,9 +289,7 @@ def _resolve_folder_path(folder_path: str | Path | None) -> Path:
317
  if folder_path is not None:
318
  return Path(folder_path).expanduser().resolve()
319
 
320
- if os.getenv("SYSTEM") == "spaces" and os.path.exists(
321
- "/data"
322
- ): # Persistent storage is enabled!
323
  return Path("/data") / "_user_history"
324
 
325
  # Not in a Space or Persistent storage not enabled => local folder
@@ -357,21 +327,13 @@ def _display_if_admin() -> Callable:
357
  def _admin_content() -> str:
358
  return f"""
359
  ## Admin section
360
-
361
  Running on **{os.getenv("SYSTEM", "local")}** (id: {os.getenv("SPACE_ID")}). {_get_msg_is_persistent_storage_enabled()}
362
-
363
  Admins: {', '.join(_fetch_admins())}
364
-
365
  {_get_nb_users()} user(s), {_get_nb_images()} image(s)
366
-
367
  ### Configuration
368
-
369
  History folder: *{_UserHistory().folder_path}*
370
-
371
  Exports folder: *{_archives_path()}*
372
-
373
  ### Disk usage
374
-
375
  {_disk_space_warning_message()}
376
  """
377
 
@@ -380,10 +342,8 @@ def _get_nb_users() -> int:
380
  user_history = _UserHistory()
381
  if not user_history.initialized:
382
  return 0
383
- if user_history.folder_path is not None:
384
- return len(
385
- [path for path in user_history.folder_path.iterdir() if path.is_dir()]
386
- )
387
  return 0
388
 
389
 
@@ -391,7 +351,7 @@ def _get_nb_images() -> int:
391
  user_history = _UserHistory()
392
  if not user_history.initialized:
393
  return 0
394
- if user_history.folder_path is not None:
395
  return len([path for path in user_history.folder_path.glob("*/images/*")])
396
  return 0
397
 
@@ -425,14 +385,10 @@ def _disk_space_warning_message() -> str:
425
 
426
 
427
  def _get_disk_usage(path: Path) -> Tuple[int, int, int]:
428
- for path in [path] + list(
429
- path.parents
430
- ): # first check target_dir, then each parents one by one
431
  try:
432
  return shutil.disk_usage(path)
433
- except (
434
- OSError
435
- ): # if doesn't exist or can't read => fail silently and try parent one
436
  pass
437
  return 0, 0, 0
438
 
@@ -451,74 +407,7 @@ def _fetch_admins() -> List[str]:
451
  # Running in Space => try to fetch organization members
452
  # Otherwise, it's not an organization => namespace is the user
453
  namespace = space_id.split("/")[0]
454
- response = requests.get(
455
- f"https://huggingface.co/api/organizations/{namespace}/members"
456
- )
457
  if response.status_code == 200:
458
- return sorted(
459
- (member["user"] for member in response.json()), key=lambda x: x.lower()
460
- )
461
  return [namespace]
462
-
463
-
464
- ################################################################
465
- # Legacy helpers to migrate image structure to new data format #
466
- ################################################################
467
- # TODO: remove this section once all Spaces have migrated
468
-
469
-
470
- def _migrate_history():
471
- """Script to migrate user history from v0 to v1."""
472
- legacy_history_path = _legacy_get_history_folder_path()
473
- if not legacy_history_path.exists():
474
- return
475
-
476
- error_count = 0
477
- for json_path in legacy_history_path.glob("*.json"):
478
- username = json_path.stem
479
- print(f"Migrating history for user {username}...")
480
- error_count += _legacy_move_user_history(username)
481
- print("Done.")
482
- print(f"Migration complete. {error_count} error(s) happened.")
483
-
484
- if error_count == 0:
485
- shutil.rmtree(legacy_history_path, ignore_errors=True)
486
-
487
-
488
- def _legacy_move_user_history(username: str) -> int:
489
- history = _legacy_read_user_history(username)
490
- error_count = 0
491
- for image, prompt in reversed(history):
492
- try:
493
- save_image(
494
- label=prompt, image=image, profile={"preferred_username": username}
495
- )
496
- except Exception as e:
497
- print("Issue while migrating image:", e)
498
- error_count += 1
499
- return error_count
500
-
501
-
502
- def _legacy_get_history_folder_path() -> Path:
503
- _folder = os.environ.get("HISTORY_FOLDER")
504
- if _folder is None:
505
- _folder = Path(__file__).parent / "history"
506
- return Path(_folder)
507
-
508
-
509
- def _legacy_read_user_history(username: str) -> List[Tuple[str, str]]:
510
- """Return saved history for that user."""
511
- with _legacy_user_lock(username):
512
- path = _legacy_user_history_path(username)
513
- if path.exists():
514
- return json.loads(path.read_text())
515
- return [] # No history yet
516
-
517
-
518
- def _legacy_user_history_path(username: str) -> Path:
519
- return _legacy_get_history_folder_path() / f"{username}.json"
520
-
521
-
522
- def _legacy_user_lock(username: str) -> FileLock:
523
- """Ensure history is not corrupted if concurrent calls."""
524
- return FileLock(f"{_legacy_user_history_path(username)}.lock")
 
1
  """
2
  User History is a plugin that you can add to your Spaces to cache generated images for your users.
 
3
  Key features:
4
  - 🤗 Sign in with Hugging Face
5
  - Save generated images with their metadata: prompts, timestamp, hyper-parameters, etc.
 
7
  - Delete your history to respect privacy.
8
  - Compatible with Persistent Storage for long-term storage.
9
  - Admin panel to check configuration and disk usage .
 
10
  Useful links:
11
  - Demo: https://huggingface.co/spaces/Wauplin/gradio-user-history
12
  - README: https://huggingface.co/spaces/Wauplin/gradio-user-history/blob/main/README.md
 
35
  user_history.folder_path = _resolve_folder_path(folder_path)
36
  user_history.initialized = True
37
 
 
 
 
38
 
39
  def render() -> None:
40
  user_history = _UserHistory()
41
 
42
  # initialize with default config
43
  if not user_history.initialized:
44
+ print("Initializing user history with default config. Use `user_history.setup(...)` to customize folder_path.")
 
 
45
  setup()
46
 
47
  # Render user history tab
 
76
 
77
  # "Export zip" row (hidden by default)
78
  with gr.Row():
79
+ export_file = gr.File(file_count="single", file_types=[".zip"], label="Exported history", visible=False)
 
 
 
 
 
80
 
81
  # "Config deletion" row (hidden by default)
82
  with gr.Row():
83
+ confirm_button = gr.Button("Confirm delete all history", variant="stop", visible=False)
 
 
84
  cancel_button = gr.Button("Cancel", visible=False)
85
 
86
  # Gallery
 
103
  gallery.attach_load_event(_fetch_user_history, every=None)
104
 
105
  # Interactions
106
+ refresh_button.click(fn=_fetch_user_history, inputs=[], outputs=[gallery], queue=False)
107
+ export_button.click(fn=_export_user_history, inputs=[], outputs=[export_file], queue=False)
 
 
 
 
108
 
109
  # Taken from https://github.com/gradio-app/gradio/issues/3324#issuecomment-1446382045
110
  delete_button.click(
 
185
 
186
  def _user_lock(self, username: str) -> FileLock:
187
  """Ensure history is not corrupted if concurrent calls."""
188
+ return FileLock(self.folder_path / f"{username}.lock") # lock outside of folder => better when exporting ZIP
 
 
189
 
190
  def _user_jsonl_path(self, username: str) -> Path:
191
  return self._user_path(username) / "history.jsonl"
 
205
 
206
  user_history = _UserHistory()
207
  if not user_history.initialized:
208
+ warnings.warn("User history is not set in Gradio demo. You must use `user_history.render(...)` first.")
 
 
209
  return []
210
 
211
  with user_history._user_lock(username):
 
231
 
232
  user_history = _UserHistory()
233
  if not user_history.initialized:
234
+ warnings.warn("User history is not set in Gradio demo. You must use `user_history.render(...)` first.")
 
 
235
  return None
236
 
237
  # Zip history
238
  with user_history._user_lock(username):
239
  path = shutil.make_archive(
240
+ str(_archives_path() / f"history_{username}"), "zip", user_history._user_path(username)
 
 
241
  )
242
 
243
  return gr.update(visible=True, value=path)
 
252
 
253
  user_history = _UserHistory()
254
  if not user_history.initialized:
255
+ warnings.warn("User history is not set in Gradio demo. You must use `user_history.render(...)` first.")
 
 
256
  return
257
 
258
  with user_history._user_lock(username):
 
289
  if folder_path is not None:
290
  return Path(folder_path).expanduser().resolve()
291
 
292
+ if os.getenv("SYSTEM") == "spaces" and os.path.exists("/data"): # Persistent storage is enabled!
 
 
293
  return Path("/data") / "_user_history"
294
 
295
  # Not in a Space or Persistent storage not enabled => local folder
 
327
  def _admin_content() -> str:
328
  return f"""
329
  ## Admin section
 
330
  Running on **{os.getenv("SYSTEM", "local")}** (id: {os.getenv("SPACE_ID")}). {_get_msg_is_persistent_storage_enabled()}
 
331
  Admins: {', '.join(_fetch_admins())}
 
332
  {_get_nb_users()} user(s), {_get_nb_images()} image(s)
 
333
  ### Configuration
 
334
  History folder: *{_UserHistory().folder_path}*
 
335
  Exports folder: *{_archives_path()}*
 
336
  ### Disk usage
 
337
  {_disk_space_warning_message()}
338
  """
339
 
 
342
  user_history = _UserHistory()
343
  if not user_history.initialized:
344
  return 0
345
+ if user_history.folder_path is not None and user_history.folder_path.exists():
346
+ return len([path for path in user_history.folder_path.iterdir() if path.is_dir()])
 
 
347
  return 0
348
 
349
 
 
351
  user_history = _UserHistory()
352
  if not user_history.initialized:
353
  return 0
354
+ if user_history.folder_path is not None and user_history.folder_path.exists():
355
  return len([path for path in user_history.folder_path.glob("*/images/*")])
356
  return 0
357
 
 
385
 
386
 
387
  def _get_disk_usage(path: Path) -> Tuple[int, int, int]:
388
+ for path in [path] + list(path.parents): # first check target_dir, then each parents one by one
 
 
389
  try:
390
  return shutil.disk_usage(path)
391
+ except OSError: # if doesn't exist or can't read => fail silently and try parent one
 
 
392
  pass
393
  return 0, 0, 0
394
 
 
407
  # Running in Space => try to fetch organization members
408
  # Otherwise, it's not an organization => namespace is the user
409
  namespace = space_id.split("/")[0]
410
+ response = requests.get(f"https://huggingface.co/api/organizations/{namespace}/members")
 
 
411
  if response.status_code == 200:
412
+ return sorted((member["user"] for member in response.json()), key=lambda x: x.lower())
 
 
413
  return [namespace]