ostruct-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ostruct/__init__.py ADDED
File without changes
@@ -0,0 +1,19 @@
1
+ """Command-line interface for making structured OpenAI API calls."""
2
+
3
+ from .cli import (
4
+ ExitCode,
5
+ _main,
6
+ validate_schema_file,
7
+ validate_task_template,
8
+ validate_variable_mapping,
9
+ )
10
+ from .path_utils import validate_path_mapping
11
+
12
+ __all__ = [
13
+ "ExitCode",
14
+ "_main",
15
+ "validate_path_mapping",
16
+ "validate_schema_file",
17
+ "validate_task_template",
18
+ "validate_variable_mapping",
19
+ ]
@@ -0,0 +1,175 @@
1
+ """Cache management for file content.
2
+
3
+ This module provides a thread-safe cache manager for file content
4
+ with LRU eviction and automatic invalidation on file changes.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from typing import Any, Optional, Tuple
10
+
11
+ from cachetools import LRUCache
12
+ from cachetools.keys import hashkey
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Type alias for cache keys
17
+ CacheKey = Tuple[Any, ...]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CacheEntry:
22
+ """Represents a cached file entry.
23
+
24
+ Note: This class is immutable (frozen) to ensure thread safety
25
+ when used as a cache value.
26
+ """
27
+
28
+ content: str
29
+ encoding: Optional[str]
30
+ hash_value: Optional[str]
31
+ mtime_ns: int # Nanosecond precision mtime
32
+ size: int # Actual file size from stat
33
+
34
+
35
+ class FileCache:
36
+ """Thread-safe LRU cache for file content with size limit."""
37
+
38
+ def __init__(self, max_size_bytes: int = 50 * 1024 * 1024): # 50MB default
39
+ """Initialize cache with maximum size in bytes.
40
+
41
+ Args:
42
+ max_size_bytes: Maximum cache size in bytes
43
+ """
44
+ self._max_size = max_size_bytes
45
+ self._current_size = 0
46
+ self._cache: LRUCache[CacheKey, CacheEntry] = LRUCache(maxsize=1024)
47
+ logger.debug(
48
+ "Initialized FileCache with max_size=%d bytes, maxsize=%d entries",
49
+ max_size_bytes,
50
+ 1024,
51
+ )
52
+
53
+ def _remove_entry(self, key: CacheKey) -> None:
54
+ """Remove entry from cache and update size.
55
+
56
+ Args:
57
+ key: Cache key to remove
58
+ """
59
+ entry = self._cache.get(key)
60
+ if entry is not None:
61
+ self._current_size -= entry.size
62
+ logger.debug(
63
+ "Removed cache entry: key=%s, size=%d, new_total_size=%d",
64
+ key,
65
+ entry.size,
66
+ self._current_size,
67
+ )
68
+ self._cache.pop(key, None)
69
+
70
+ def get(
71
+ self, path: str, current_mtime_ns: int, current_size: int
72
+ ) -> Optional[CacheEntry]:
73
+ """Get cache entry if it exists and is valid.
74
+
75
+ Args:
76
+ path: Absolute path to the file
77
+ current_mtime_ns: Current modification time in nanoseconds
78
+ current_size: Current file size in bytes
79
+
80
+ Returns:
81
+ CacheEntry if valid cache exists, None otherwise
82
+ """
83
+ key = hashkey(path)
84
+ entry = self._cache.get(key)
85
+
86
+ if entry is None:
87
+ logger.debug("Cache miss for %s: no entry found", path)
88
+ return None
89
+
90
+ # Check if file has been modified using both mtime and size
91
+ if entry.mtime_ns != current_mtime_ns or entry.size != current_size:
92
+ logger.info(
93
+ "Cache invalidated for %s: mtime_ns=%d->%d (%s), size=%d->%d (%s)",
94
+ path,
95
+ entry.mtime_ns,
96
+ current_mtime_ns,
97
+ "changed" if entry.mtime_ns != current_mtime_ns else "same",
98
+ entry.size,
99
+ current_size,
100
+ "changed" if entry.size != current_size else "same",
101
+ )
102
+ self._remove_entry(key)
103
+ return None
104
+
105
+ logger.debug(
106
+ "Cache hit for %s: mtime_ns=%d, size=%d",
107
+ path,
108
+ entry.mtime_ns,
109
+ entry.size,
110
+ )
111
+ return entry
112
+
113
+ def put(
114
+ self,
115
+ path: str,
116
+ content: str,
117
+ encoding: Optional[str],
118
+ hash_value: Optional[str],
119
+ mtime_ns: int,
120
+ size: int,
121
+ ) -> None:
122
+ """Add or update cache entry.
123
+
124
+ Args:
125
+ path: Absolute path to the file
126
+ content: File content
127
+ encoding: File encoding
128
+ hash_value: Content hash
129
+ mtime_ns: File modification time in nanoseconds
130
+ size: File size in bytes from stat
131
+ """
132
+ if size > self._max_size:
133
+ logger.warning(
134
+ "File %s size (%d bytes) exceeds cache max size (%d bytes)",
135
+ path,
136
+ size,
137
+ self._max_size,
138
+ )
139
+ return
140
+
141
+ key = hashkey(path)
142
+ self._remove_entry(key)
143
+
144
+ entry = CacheEntry(content, encoding, hash_value, mtime_ns, size)
145
+
146
+ # Evict entries if needed
147
+ evicted_count = 0
148
+ while self._current_size + size > self._max_size and self._cache:
149
+ evicted_key, evicted = self._cache.popitem()
150
+ self._current_size -= evicted.size
151
+ evicted_count += 1
152
+ logger.debug(
153
+ "Evicted cache entry: key=%s, size=%d, new_total_size=%d",
154
+ evicted_key,
155
+ evicted.size,
156
+ self._current_size,
157
+ )
158
+
159
+ if evicted_count > 0:
160
+ logger.info(
161
+ "Evicted %d entries to make room for %s (size=%d)",
162
+ evicted_count,
163
+ path,
164
+ size,
165
+ )
166
+
167
+ self._cache[key] = entry
168
+ self._current_size += size
169
+ logger.debug(
170
+ "Added cache entry: path=%s, size=%d, total_size=%d/%d",
171
+ path,
172
+ size,
173
+ self._current_size,
174
+ self._max_size,
175
+ )