voxkit.storage.utils

This module provides utility functions for common VoxKit storage operations, including path management and unique identifier generation.

API

  • get_storage_root: Get the root directory for storing VoxKit data
  • generate_unique_id: Generate a unique identifier with timestamp
  • readable_from_unique_id: Convert a unique ID to human-readable format
  • is_first_launch: Check if this is the first launch of the application
  • mark_first_launch_complete: Mark the first launch as complete
  • save_json: Save JSON data to a file within storage

Notes

  • The storage root uses tilde (~) notation to ensure it references the user's home directory
  • Unique IDs are based on timestamps with microsecond precision (YYYYMMDD_HHMMSS_ffffff)
  • The storage root path is cached for performance using lru_cache
  • First launch tracking uses a flag file (.first_launch_complete) in the storage root
  1"""This module provides utility functions for common VoxKit storage operations,
  2including path management and unique identifier generation.
  3
  4API
  5---
  6- **get_storage_root**: Get the root directory for storing VoxKit data
  7- **generate_unique_id**: Generate a unique identifier with timestamp
  8- **readable_from_unique_id**: Convert a unique ID to human-readable format
  9- **is_first_launch**: Check if this is the first launch of the application
 10- **mark_first_launch_complete**: Mark the first launch as complete
 11- **save_json**: Save JSON data to a file within storage
 12
 13Notes
 14-----
 15- The storage root uses tilde (~) notation to ensure it references the user's home directory
 16- Unique IDs are based on timestamps with microsecond precision (YYYYMMDD_HHMMSS_ffffff)
 17- The storage root path is cached for performance using lru_cache
 18- First launch tracking uses a flag file (.first_launch_complete) in the storage root
 19"""
 20
 21import json
 22import threading
 23from datetime import datetime, timedelta
 24from functools import lru_cache
 25from pathlib import Path
 26from typing import Any
 27
 28from .constants import STORAGE_ROOT
 29
 30_id_lock = threading.Lock()
 31_last_id_dt: datetime | None = None
 32
 33
 34@lru_cache(maxsize=1)
 35def get_storage_root() -> Path:
 36    """Get the root directory for storing VoxKit data.
 37
 38    Returns:
 39        Path to the storage root directory (expanded from tilde notation)
 40
 41    Raises:
 42        ValueError: If STORAGE_ROOT does not start with tilde (~)
 43    """
 44    if STORAGE_ROOT.startswith("~"):
 45        return Path(STORAGE_ROOT).expanduser()
 46    else:
 47        raise ValueError("STORAGE_ROOT must be a valid path starting with '~'")
 48
 49
 50def generate_unique_id(prefix: str | None = None) -> str:
 51    """Generate a unique identifier with the given prefix and current timestamp.
 52
 53    Args:
 54        prefix: Optional prefix to prepend to the timestamp
 55
 56    Returns:
 57        Unique identifier string in format: [prefix_]YYYYMMDD_HHMMSS_ffffff
 58    """
 59    global _last_id_dt
 60    # Windows datetime.now() has ~15ms resolution; enforce monotonic microsecond precision.
 61    with _id_lock:
 62        now = datetime.now()
 63        if _last_id_dt is not None and now <= _last_id_dt:
 64            now = _last_id_dt + timedelta(microseconds=1)
 65        _last_id_dt = now
 66    now_str = now.strftime("%Y%m%d_%H%M%S_%f")
 67    if prefix:
 68        return f"{prefix}_{now_str}"
 69    return now_str
 70
 71
 72def readable_from_unique_id(date_str: str) -> str:
 73    """Convert a unique ID timestamp to a human-readable format.
 74
 75    Accepts both plain timestamps (YYYYMMDD_HHMMSS_ffffff) and prefixed IDs
 76    produced by generate_unique_id (e.g. prefix_YYYYMMDD_HHMMSS_ffffff).
 77
 78    Args:
 79        date_str: Unique ID string, optionally prefixed as [prefix_]YYYYMMDD_HHMMSS_ffffff
 80
 81    Returns:
 82        Human-readable date string (e.g., "January 01, 2024 at 12:00:00 PM")
 83    """
 84    parts = date_str.split("_")
 85    for i, part in enumerate(parts):
 86        if len(part) == 8 and part.isdigit():
 87            date_str = "_".join(parts[i:])
 88            break
 89    else:
 90        raise ValueError(f"No valid timestamp found in unique ID: {date_str!r}")
 91    dt = datetime.strptime(date_str, "%Y%m%d_%H%M%S_%f")
 92    return dt.strftime("%B %d, %Y at %I:%M:%S %p")
 93
 94
 95def is_first_launch() -> bool:
 96    """Check if this is the first launch of the application.
 97
 98    Returns:
 99        True if this is the first launch, False otherwise
100    """
101    storage_root = get_storage_root()
102    flag_file = storage_root / ".first_launch_complete"
103    return not flag_file.exists()
104
105
106def mark_first_launch_complete() -> None:
107    """Mark the first launch as complete by creating a flag file."""
108    storage_root = get_storage_root()
109    storage_root.mkdir(parents=True, exist_ok=True)
110    flag_file = storage_root / ".first_launch_complete"
111    flag_file.touch()
112
113
114def save_json(file_path: Path, data: dict[str, Any]) -> None:
115    """Save JSON data to a file within storage.
116
117    Creates parent directories if they don't exist.
118
119    Args:
120        file_path: Path to the JSON file (should be within storage root)
121        data: Dictionary to serialize as JSON
122    """
123    file_path.parent.mkdir(parents=True, exist_ok=True)
124    with open(file_path, "w", encoding="utf-8") as f:
125        json.dump(data, f, indent=4)
@lru_cache(maxsize=1)
def get_storage_root() -> pathlib.Path:
35@lru_cache(maxsize=1)
36def get_storage_root() -> Path:
37    """Get the root directory for storing VoxKit data.
38
39    Returns:
40        Path to the storage root directory (expanded from tilde notation)
41
42    Raises:
43        ValueError: If STORAGE_ROOT does not start with tilde (~)
44    """
45    if STORAGE_ROOT.startswith("~"):
46        return Path(STORAGE_ROOT).expanduser()
47    else:
48        raise ValueError("STORAGE_ROOT must be a valid path starting with '~'")

Get the root directory for storing VoxKit data.

Returns: Path to the storage root directory (expanded from tilde notation)

Raises: ValueError: If STORAGE_ROOT does not start with tilde (~)

def generate_unique_id(prefix: str | None = None) -> str:
51def generate_unique_id(prefix: str | None = None) -> str:
52    """Generate a unique identifier with the given prefix and current timestamp.
53
54    Args:
55        prefix: Optional prefix to prepend to the timestamp
56
57    Returns:
58        Unique identifier string in format: [prefix_]YYYYMMDD_HHMMSS_ffffff
59    """
60    global _last_id_dt
61    # Windows datetime.now() has ~15ms resolution; enforce monotonic microsecond precision.
62    with _id_lock:
63        now = datetime.now()
64        if _last_id_dt is not None and now <= _last_id_dt:
65            now = _last_id_dt + timedelta(microseconds=1)
66        _last_id_dt = now
67    now_str = now.strftime("%Y%m%d_%H%M%S_%f")
68    if prefix:
69        return f"{prefix}_{now_str}"
70    return now_str

Generate a unique identifier with the given prefix and current timestamp.

Args: prefix: Optional prefix to prepend to the timestamp

Returns: Unique identifier string in format: [prefix_]YYYYMMDD_HHMMSS_ffffff

def readable_from_unique_id(date_str: str) -> str:
73def readable_from_unique_id(date_str: str) -> str:
74    """Convert a unique ID timestamp to a human-readable format.
75
76    Accepts both plain timestamps (YYYYMMDD_HHMMSS_ffffff) and prefixed IDs
77    produced by generate_unique_id (e.g. prefix_YYYYMMDD_HHMMSS_ffffff).
78
79    Args:
80        date_str: Unique ID string, optionally prefixed as [prefix_]YYYYMMDD_HHMMSS_ffffff
81
82    Returns:
83        Human-readable date string (e.g., "January 01, 2024 at 12:00:00 PM")
84    """
85    parts = date_str.split("_")
86    for i, part in enumerate(parts):
87        if len(part) == 8 and part.isdigit():
88            date_str = "_".join(parts[i:])
89            break
90    else:
91        raise ValueError(f"No valid timestamp found in unique ID: {date_str!r}")
92    dt = datetime.strptime(date_str, "%Y%m%d_%H%M%S_%f")
93    return dt.strftime("%B %d, %Y at %I:%M:%S %p")

Convert a unique ID timestamp to a human-readable format.

Accepts both plain timestamps (YYYYMMDD_HHMMSS_ffffff) and prefixed IDs produced by generate_unique_id (e.g. prefix_YYYYMMDD_HHMMSS_ffffff).

Args: date_str: Unique ID string, optionally prefixed as [prefix_]YYYYMMDD_HHMMSS_ffffff

Returns: Human-readable date string (e.g., "January 01, 2024 at 12:00:00 PM")

def is_first_launch() -> bool:
 96def is_first_launch() -> bool:
 97    """Check if this is the first launch of the application.
 98
 99    Returns:
100        True if this is the first launch, False otherwise
101    """
102    storage_root = get_storage_root()
103    flag_file = storage_root / ".first_launch_complete"
104    return not flag_file.exists()

Check if this is the first launch of the application.

Returns: True if this is the first launch, False otherwise

def mark_first_launch_complete() -> None:
107def mark_first_launch_complete() -> None:
108    """Mark the first launch as complete by creating a flag file."""
109    storage_root = get_storage_root()
110    storage_root.mkdir(parents=True, exist_ok=True)
111    flag_file = storage_root / ".first_launch_complete"
112    flag_file.touch()

Mark the first launch as complete by creating a flag file.

def save_json(file_path: pathlib.Path, data: dict[str, typing.Any]) -> None:
115def save_json(file_path: Path, data: dict[str, Any]) -> None:
116    """Save JSON data to a file within storage.
117
118    Creates parent directories if they don't exist.
119
120    Args:
121        file_path: Path to the JSON file (should be within storage root)
122        data: Dictionary to serialize as JSON
123    """
124    file_path.parent.mkdir(parents=True, exist_ok=True)
125    with open(file_path, "w", encoding="utf-8") as f:
126        json.dump(data, f, indent=4)

Save JSON data to a file within storage.

Creates parent directories if they don't exist.

Args: file_path: Path to the JSON file (should be within storage root) data: Dictionary to serialize as JSON