tactus 0.31.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.
- tactus/__init__.py +49 -0
- tactus/adapters/__init__.py +9 -0
- tactus/adapters/broker_log.py +76 -0
- tactus/adapters/cli_hitl.py +189 -0
- tactus/adapters/cli_log.py +223 -0
- tactus/adapters/cost_collector_log.py +56 -0
- tactus/adapters/file_storage.py +367 -0
- tactus/adapters/http_callback_log.py +109 -0
- tactus/adapters/ide_log.py +71 -0
- tactus/adapters/lua_tools.py +336 -0
- tactus/adapters/mcp.py +289 -0
- tactus/adapters/mcp_manager.py +196 -0
- tactus/adapters/memory.py +53 -0
- tactus/adapters/plugins.py +419 -0
- tactus/backends/http_backend.py +58 -0
- tactus/backends/model_backend.py +35 -0
- tactus/backends/pytorch_backend.py +110 -0
- tactus/broker/__init__.py +12 -0
- tactus/broker/client.py +247 -0
- tactus/broker/protocol.py +183 -0
- tactus/broker/server.py +1123 -0
- tactus/broker/stdio.py +12 -0
- tactus/cli/__init__.py +7 -0
- tactus/cli/app.py +2245 -0
- tactus/cli/commands/__init__.py +0 -0
- tactus/core/__init__.py +32 -0
- tactus/core/config_manager.py +790 -0
- tactus/core/dependencies/__init__.py +14 -0
- tactus/core/dependencies/registry.py +180 -0
- tactus/core/dsl_stubs.py +2117 -0
- tactus/core/exceptions.py +66 -0
- tactus/core/execution_context.py +480 -0
- tactus/core/lua_sandbox.py +508 -0
- tactus/core/message_history_manager.py +236 -0
- tactus/core/mocking.py +286 -0
- tactus/core/output_validator.py +291 -0
- tactus/core/registry.py +499 -0
- tactus/core/runtime.py +2907 -0
- tactus/core/template_resolver.py +142 -0
- tactus/core/yaml_parser.py +301 -0
- tactus/docker/Dockerfile +61 -0
- tactus/docker/entrypoint.sh +69 -0
- tactus/dspy/__init__.py +39 -0
- tactus/dspy/agent.py +1144 -0
- tactus/dspy/broker_lm.py +181 -0
- tactus/dspy/config.py +212 -0
- tactus/dspy/history.py +196 -0
- tactus/dspy/module.py +405 -0
- tactus/dspy/prediction.py +318 -0
- tactus/dspy/signature.py +185 -0
- tactus/formatting/__init__.py +7 -0
- tactus/formatting/formatter.py +437 -0
- tactus/ide/__init__.py +9 -0
- tactus/ide/coding_assistant.py +343 -0
- tactus/ide/server.py +2223 -0
- tactus/primitives/__init__.py +49 -0
- tactus/primitives/control.py +168 -0
- tactus/primitives/file.py +229 -0
- tactus/primitives/handles.py +378 -0
- tactus/primitives/host.py +94 -0
- tactus/primitives/human.py +342 -0
- tactus/primitives/json.py +189 -0
- tactus/primitives/log.py +187 -0
- tactus/primitives/message_history.py +157 -0
- tactus/primitives/model.py +163 -0
- tactus/primitives/procedure.py +564 -0
- tactus/primitives/procedure_callable.py +318 -0
- tactus/primitives/retry.py +155 -0
- tactus/primitives/session.py +152 -0
- tactus/primitives/state.py +182 -0
- tactus/primitives/step.py +209 -0
- tactus/primitives/system.py +93 -0
- tactus/primitives/tool.py +375 -0
- tactus/primitives/tool_handle.py +279 -0
- tactus/primitives/toolset.py +229 -0
- tactus/protocols/__init__.py +38 -0
- tactus/protocols/chat_recorder.py +81 -0
- tactus/protocols/config.py +97 -0
- tactus/protocols/cost.py +31 -0
- tactus/protocols/hitl.py +71 -0
- tactus/protocols/log_handler.py +27 -0
- tactus/protocols/models.py +355 -0
- tactus/protocols/result.py +33 -0
- tactus/protocols/storage.py +90 -0
- tactus/providers/__init__.py +13 -0
- tactus/providers/base.py +92 -0
- tactus/providers/bedrock.py +117 -0
- tactus/providers/google.py +105 -0
- tactus/providers/openai.py +98 -0
- tactus/sandbox/__init__.py +63 -0
- tactus/sandbox/config.py +171 -0
- tactus/sandbox/container_runner.py +1099 -0
- tactus/sandbox/docker_manager.py +433 -0
- tactus/sandbox/entrypoint.py +227 -0
- tactus/sandbox/protocol.py +213 -0
- tactus/stdlib/__init__.py +10 -0
- tactus/stdlib/io/__init__.py +13 -0
- tactus/stdlib/io/csv.py +88 -0
- tactus/stdlib/io/excel.py +136 -0
- tactus/stdlib/io/file.py +90 -0
- tactus/stdlib/io/fs.py +154 -0
- tactus/stdlib/io/hdf5.py +121 -0
- tactus/stdlib/io/json.py +109 -0
- tactus/stdlib/io/parquet.py +83 -0
- tactus/stdlib/io/tsv.py +88 -0
- tactus/stdlib/loader.py +274 -0
- tactus/stdlib/tac/tactus/tools/done.tac +33 -0
- tactus/stdlib/tac/tactus/tools/log.tac +50 -0
- tactus/testing/README.md +273 -0
- tactus/testing/__init__.py +61 -0
- tactus/testing/behave_integration.py +380 -0
- tactus/testing/context.py +486 -0
- tactus/testing/eval_models.py +114 -0
- tactus/testing/evaluation_runner.py +222 -0
- tactus/testing/evaluators.py +634 -0
- tactus/testing/events.py +94 -0
- tactus/testing/gherkin_parser.py +134 -0
- tactus/testing/mock_agent.py +315 -0
- tactus/testing/mock_dependencies.py +234 -0
- tactus/testing/mock_hitl.py +171 -0
- tactus/testing/mock_registry.py +168 -0
- tactus/testing/mock_tools.py +133 -0
- tactus/testing/models.py +115 -0
- tactus/testing/pydantic_eval_runner.py +508 -0
- tactus/testing/steps/__init__.py +13 -0
- tactus/testing/steps/builtin.py +902 -0
- tactus/testing/steps/custom.py +69 -0
- tactus/testing/steps/registry.py +68 -0
- tactus/testing/test_runner.py +489 -0
- tactus/tracing/__init__.py +5 -0
- tactus/tracing/trace_manager.py +417 -0
- tactus/utils/__init__.py +1 -0
- tactus/utils/cost_calculator.py +72 -0
- tactus/utils/model_pricing.py +132 -0
- tactus/utils/safe_file_library.py +502 -0
- tactus/utils/safe_libraries.py +234 -0
- tactus/validation/LuaLexerBase.py +66 -0
- tactus/validation/LuaParserBase.py +23 -0
- tactus/validation/README.md +224 -0
- tactus/validation/__init__.py +7 -0
- tactus/validation/error_listener.py +21 -0
- tactus/validation/generated/LuaLexer.interp +231 -0
- tactus/validation/generated/LuaLexer.py +5548 -0
- tactus/validation/generated/LuaLexer.tokens +124 -0
- tactus/validation/generated/LuaLexerBase.py +66 -0
- tactus/validation/generated/LuaParser.interp +173 -0
- tactus/validation/generated/LuaParser.py +6439 -0
- tactus/validation/generated/LuaParser.tokens +124 -0
- tactus/validation/generated/LuaParserBase.py +23 -0
- tactus/validation/generated/LuaParserVisitor.py +118 -0
- tactus/validation/generated/__init__.py +7 -0
- tactus/validation/grammar/LuaLexer.g4 +123 -0
- tactus/validation/grammar/LuaParser.g4 +178 -0
- tactus/validation/semantic_visitor.py +817 -0
- tactus/validation/validator.py +157 -0
- tactus-0.31.0.dist-info/METADATA +1809 -0
- tactus-0.31.0.dist-info/RECORD +160 -0
- tactus-0.31.0.dist-info/WHEEL +4 -0
- tactus-0.31.0.dist-info/entry_points.txt +2 -0
- tactus-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Safe file I/O libraries for Lua sandbox.
|
|
2
|
+
|
|
3
|
+
Provides sandboxed file operations restricted to the working directory.
|
|
4
|
+
All paths are validated to prevent directory traversal attacks.
|
|
5
|
+
|
|
6
|
+
Supported formats:
|
|
7
|
+
- File: Raw text read/write
|
|
8
|
+
- Csv: CSV with automatic header handling
|
|
9
|
+
- Tsv: Tab-separated values
|
|
10
|
+
- Json: JSON read/write
|
|
11
|
+
- Parquet: Apache Parquet (via pyarrow)
|
|
12
|
+
- Hdf5: HDF5 datasets (via h5py)
|
|
13
|
+
- Excel: Excel spreadsheets (via openpyxl)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import csv
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PathValidator:
|
|
26
|
+
"""Validates file paths are within allowed base directory."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, base_path: str):
|
|
29
|
+
"""
|
|
30
|
+
Initialize path validator.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
base_path: The base directory that all file operations are restricted to.
|
|
34
|
+
"""
|
|
35
|
+
self.base_path = os.path.realpath(base_path)
|
|
36
|
+
|
|
37
|
+
def validate(self, filepath: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Validate and resolve a file path.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
filepath: Relative or absolute file path to validate.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Resolved absolute path if valid.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
PermissionError: If path is outside the base directory.
|
|
49
|
+
"""
|
|
50
|
+
# Join with base path and resolve to absolute
|
|
51
|
+
resolved = os.path.realpath(os.path.join(self.base_path, filepath))
|
|
52
|
+
|
|
53
|
+
# Check if resolved path is within base directory
|
|
54
|
+
# Allow exact match (base_path itself) or paths that start with base_path + separator
|
|
55
|
+
if resolved != self.base_path and not resolved.startswith(self.base_path + os.sep):
|
|
56
|
+
raise PermissionError(f"Access denied: path outside working directory: {filepath}")
|
|
57
|
+
|
|
58
|
+
return resolved
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_safe_file_library(base_path: str) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
Create raw text file operations library.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
base_path: Base directory for file operations.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary of file operation functions.
|
|
70
|
+
"""
|
|
71
|
+
validator = PathValidator(base_path)
|
|
72
|
+
|
|
73
|
+
def read(filepath: str) -> str:
|
|
74
|
+
"""Read entire file as text."""
|
|
75
|
+
path = validator.validate(filepath)
|
|
76
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
77
|
+
return f.read()
|
|
78
|
+
|
|
79
|
+
def write(filepath: str, content: str) -> None:
|
|
80
|
+
"""Write text to file."""
|
|
81
|
+
path = validator.validate(filepath)
|
|
82
|
+
# Ensure parent directory exists
|
|
83
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
84
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
85
|
+
f.write(content)
|
|
86
|
+
|
|
87
|
+
def exists(filepath: str) -> bool:
|
|
88
|
+
"""Check if file exists."""
|
|
89
|
+
try:
|
|
90
|
+
path = validator.validate(filepath)
|
|
91
|
+
return os.path.exists(path)
|
|
92
|
+
except PermissionError:
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
return {"read": read, "write": write, "exists": exists}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LuaList:
|
|
99
|
+
"""
|
|
100
|
+
Wrapper for Python lists that works better with Lua via lupa.
|
|
101
|
+
|
|
102
|
+
Provides both 0-indexed access (Python style) and a len() method
|
|
103
|
+
that can be called from Lua.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, data: List):
|
|
107
|
+
self._data = data
|
|
108
|
+
|
|
109
|
+
def __getitem__(self, index):
|
|
110
|
+
# Lua method access comes through __getitem__ with string keys
|
|
111
|
+
if isinstance(index, str):
|
|
112
|
+
# Handle method access
|
|
113
|
+
if index == "len":
|
|
114
|
+
return self.len
|
|
115
|
+
elif index == "get":
|
|
116
|
+
return self.get
|
|
117
|
+
else:
|
|
118
|
+
raise KeyError(f"Unknown method: {index}")
|
|
119
|
+
# Lua numbers are floats, convert to int for indexing
|
|
120
|
+
if isinstance(index, float):
|
|
121
|
+
index = int(index)
|
|
122
|
+
return self._data[index]
|
|
123
|
+
|
|
124
|
+
def __len__(self):
|
|
125
|
+
return len(self._data)
|
|
126
|
+
|
|
127
|
+
def __iter__(self):
|
|
128
|
+
return iter(self._data)
|
|
129
|
+
|
|
130
|
+
def len(self):
|
|
131
|
+
"""Return length - callable from Lua as data:len()."""
|
|
132
|
+
return len(self._data)
|
|
133
|
+
|
|
134
|
+
def get(self, index):
|
|
135
|
+
"""Alternative access method - data:get(0) instead of data[0]."""
|
|
136
|
+
if isinstance(index, float):
|
|
137
|
+
index = int(index)
|
|
138
|
+
return self._data[index]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_safe_csv_library(base_path: str) -> Dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Create CSV file operations library.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
base_path: Base directory for file operations.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dictionary of CSV operation functions.
|
|
150
|
+
"""
|
|
151
|
+
validator = PathValidator(base_path)
|
|
152
|
+
|
|
153
|
+
def read(filepath: str) -> LuaList:
|
|
154
|
+
"""Read CSV file, returning list of dictionaries with headers as keys."""
|
|
155
|
+
path = validator.validate(filepath)
|
|
156
|
+
with open(path, "r", encoding="utf-8", newline="") as f:
|
|
157
|
+
reader = csv.DictReader(f)
|
|
158
|
+
return LuaList(list(reader))
|
|
159
|
+
|
|
160
|
+
def write(filepath: str, data: List[Dict], options: Optional[Dict] = None) -> None:
|
|
161
|
+
"""Write list of dictionaries to CSV file."""
|
|
162
|
+
path = validator.validate(filepath)
|
|
163
|
+
|
|
164
|
+
# Convert Lua table options to Python dict if needed
|
|
165
|
+
if options and hasattr(options, "items"):
|
|
166
|
+
options = dict(options.items())
|
|
167
|
+
options = options or {}
|
|
168
|
+
headers = options.get("headers")
|
|
169
|
+
|
|
170
|
+
# Convert headers from Lua table to Python list if needed
|
|
171
|
+
if headers and hasattr(headers, "values"):
|
|
172
|
+
headers = list(headers.values())
|
|
173
|
+
|
|
174
|
+
# Convert Lua table to Python list if needed
|
|
175
|
+
if hasattr(data, "values"):
|
|
176
|
+
data = list(data.values())
|
|
177
|
+
|
|
178
|
+
if not headers and data:
|
|
179
|
+
# Get headers from first row
|
|
180
|
+
first_row = data[0]
|
|
181
|
+
if hasattr(first_row, "keys"):
|
|
182
|
+
headers = list(first_row.keys())
|
|
183
|
+
elif isinstance(first_row, dict):
|
|
184
|
+
headers = list(first_row.keys())
|
|
185
|
+
else:
|
|
186
|
+
raise ValueError("Cannot determine headers from data")
|
|
187
|
+
|
|
188
|
+
# Ensure parent directory exists
|
|
189
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
190
|
+
|
|
191
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
192
|
+
writer = csv.DictWriter(f, fieldnames=headers)
|
|
193
|
+
writer.writeheader()
|
|
194
|
+
for row in data:
|
|
195
|
+
# Convert Lua table to dict if needed
|
|
196
|
+
if hasattr(row, "items"):
|
|
197
|
+
row = dict(row.items())
|
|
198
|
+
writer.writerow(row)
|
|
199
|
+
|
|
200
|
+
return {"read": read, "write": write}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_safe_tsv_library(base_path: str) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Create TSV (tab-separated values) file operations library.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
base_path: Base directory for file operations.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary of TSV operation functions.
|
|
212
|
+
"""
|
|
213
|
+
validator = PathValidator(base_path)
|
|
214
|
+
|
|
215
|
+
def read(filepath: str) -> LuaList:
|
|
216
|
+
"""Read TSV file, returning list of dictionaries with headers as keys."""
|
|
217
|
+
path = validator.validate(filepath)
|
|
218
|
+
with open(path, "r", encoding="utf-8", newline="") as f:
|
|
219
|
+
reader = csv.DictReader(f, delimiter="\t")
|
|
220
|
+
return LuaList(list(reader))
|
|
221
|
+
|
|
222
|
+
def write(filepath: str, data: List[Dict], options: Optional[Dict] = None) -> None:
|
|
223
|
+
"""Write list of dictionaries to TSV file."""
|
|
224
|
+
path = validator.validate(filepath)
|
|
225
|
+
|
|
226
|
+
# Convert Lua table options to Python dict if needed
|
|
227
|
+
if options and hasattr(options, "items"):
|
|
228
|
+
options = dict(options.items())
|
|
229
|
+
options = options or {}
|
|
230
|
+
headers = options.get("headers")
|
|
231
|
+
|
|
232
|
+
# Convert headers from Lua table to Python list if needed
|
|
233
|
+
if headers and hasattr(headers, "values"):
|
|
234
|
+
headers = list(headers.values())
|
|
235
|
+
|
|
236
|
+
# Convert Lua table to Python list if needed
|
|
237
|
+
if hasattr(data, "values"):
|
|
238
|
+
data = list(data.values())
|
|
239
|
+
|
|
240
|
+
if not headers and data:
|
|
241
|
+
first_row = data[0]
|
|
242
|
+
if hasattr(first_row, "keys"):
|
|
243
|
+
headers = list(first_row.keys())
|
|
244
|
+
elif isinstance(first_row, dict):
|
|
245
|
+
headers = list(first_row.keys())
|
|
246
|
+
else:
|
|
247
|
+
raise ValueError("Cannot determine headers from data")
|
|
248
|
+
|
|
249
|
+
# Ensure parent directory exists
|
|
250
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
251
|
+
|
|
252
|
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
|
253
|
+
writer = csv.DictWriter(f, fieldnames=headers, delimiter="\t")
|
|
254
|
+
writer.writeheader()
|
|
255
|
+
for row in data:
|
|
256
|
+
if hasattr(row, "items"):
|
|
257
|
+
row = dict(row.items())
|
|
258
|
+
writer.writerow(row)
|
|
259
|
+
|
|
260
|
+
return {"read": read, "write": write}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def create_safe_json_library(base_path: str) -> Dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
Create JSON file operations library.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
base_path: Base directory for file operations.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Dictionary of JSON operation functions.
|
|
272
|
+
"""
|
|
273
|
+
validator = PathValidator(base_path)
|
|
274
|
+
|
|
275
|
+
def read(filepath: str) -> Any:
|
|
276
|
+
"""Read JSON file and return parsed data."""
|
|
277
|
+
path = validator.validate(filepath)
|
|
278
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
279
|
+
return json.load(f)
|
|
280
|
+
|
|
281
|
+
def write(filepath: str, data: Any, options: Optional[Dict] = None) -> None:
|
|
282
|
+
"""Write data to JSON file."""
|
|
283
|
+
path = validator.validate(filepath)
|
|
284
|
+
|
|
285
|
+
# Convert Lua table options to Python dict if needed
|
|
286
|
+
if options and hasattr(options, "items"):
|
|
287
|
+
options = dict(options.items())
|
|
288
|
+
options = options or {}
|
|
289
|
+
indent = options.get("indent", 2)
|
|
290
|
+
|
|
291
|
+
# Convert Lua tables to Python dicts/lists
|
|
292
|
+
data = _lua_to_python(data)
|
|
293
|
+
|
|
294
|
+
# Ensure parent directory exists
|
|
295
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
296
|
+
|
|
297
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
298
|
+
json.dump(data, f, indent=indent, default=str)
|
|
299
|
+
|
|
300
|
+
return {"read": read, "write": write}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def create_safe_parquet_library(base_path: str) -> Dict[str, Any]:
|
|
304
|
+
"""
|
|
305
|
+
Create Parquet file operations library.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
base_path: Base directory for file operations.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dictionary of Parquet operation functions.
|
|
312
|
+
"""
|
|
313
|
+
import pyarrow as pa
|
|
314
|
+
import pyarrow.parquet as pq
|
|
315
|
+
|
|
316
|
+
validator = PathValidator(base_path)
|
|
317
|
+
|
|
318
|
+
def read(filepath: str) -> LuaList:
|
|
319
|
+
"""Read Parquet file, returning list of dictionaries."""
|
|
320
|
+
path = validator.validate(filepath)
|
|
321
|
+
table = pq.read_table(path)
|
|
322
|
+
return LuaList(table.to_pylist())
|
|
323
|
+
|
|
324
|
+
def write(filepath: str, data: List[Dict]) -> None:
|
|
325
|
+
"""Write list of dictionaries to Parquet file."""
|
|
326
|
+
path = validator.validate(filepath)
|
|
327
|
+
|
|
328
|
+
# Convert Lua tables to Python
|
|
329
|
+
data = _lua_to_python(data)
|
|
330
|
+
|
|
331
|
+
# Ensure parent directory exists
|
|
332
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
333
|
+
|
|
334
|
+
table = pa.Table.from_pylist(data)
|
|
335
|
+
pq.write_table(table, path)
|
|
336
|
+
|
|
337
|
+
return {"read": read, "write": write}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def create_safe_hdf5_library(base_path: str) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Create HDF5 file operations library.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
base_path: Base directory for file operations.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dictionary of HDF5 operation functions.
|
|
349
|
+
"""
|
|
350
|
+
import h5py
|
|
351
|
+
import numpy as np
|
|
352
|
+
|
|
353
|
+
validator = PathValidator(base_path)
|
|
354
|
+
|
|
355
|
+
def read(filepath: str, dataset: str) -> List[Any]:
|
|
356
|
+
"""Read dataset from HDF5 file."""
|
|
357
|
+
path = validator.validate(filepath)
|
|
358
|
+
with h5py.File(path, "r") as f:
|
|
359
|
+
return f[dataset][:].tolist()
|
|
360
|
+
|
|
361
|
+
def write(filepath: str, dataset: str, data: List) -> None:
|
|
362
|
+
"""Write data to HDF5 dataset."""
|
|
363
|
+
path = validator.validate(filepath)
|
|
364
|
+
|
|
365
|
+
# Convert Lua tables to Python
|
|
366
|
+
data = _lua_to_python(data)
|
|
367
|
+
|
|
368
|
+
# Ensure parent directory exists
|
|
369
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
370
|
+
|
|
371
|
+
with h5py.File(path, "a") as f:
|
|
372
|
+
if dataset in f:
|
|
373
|
+
del f[dataset]
|
|
374
|
+
f.create_dataset(dataset, data=np.array(data))
|
|
375
|
+
|
|
376
|
+
def list_datasets(filepath: str) -> List[str]:
|
|
377
|
+
"""List all datasets in HDF5 file."""
|
|
378
|
+
path = validator.validate(filepath)
|
|
379
|
+
datasets = []
|
|
380
|
+
with h5py.File(path, "r") as f:
|
|
381
|
+
|
|
382
|
+
def visitor(name, obj):
|
|
383
|
+
if isinstance(obj, h5py.Dataset):
|
|
384
|
+
datasets.append(name)
|
|
385
|
+
|
|
386
|
+
f.visititems(visitor)
|
|
387
|
+
return datasets
|
|
388
|
+
|
|
389
|
+
return {"read": read, "write": write, "list": list_datasets}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def create_safe_excel_library(base_path: str) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
Create Excel file operations library.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
base_path: Base directory for file operations.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Dictionary of Excel operation functions.
|
|
401
|
+
"""
|
|
402
|
+
from openpyxl import Workbook, load_workbook
|
|
403
|
+
|
|
404
|
+
validator = PathValidator(base_path)
|
|
405
|
+
|
|
406
|
+
def read(filepath: str, options: Optional[Dict] = None) -> LuaList:
|
|
407
|
+
"""Read Excel file, returning list of dictionaries."""
|
|
408
|
+
path = validator.validate(filepath)
|
|
409
|
+
|
|
410
|
+
# Convert Lua table options to Python dict if needed
|
|
411
|
+
if options and hasattr(options, "items"):
|
|
412
|
+
options = dict(options.items())
|
|
413
|
+
options = options or {}
|
|
414
|
+
sheet_name = options.get("sheet")
|
|
415
|
+
|
|
416
|
+
wb = load_workbook(path, read_only=True, data_only=True)
|
|
417
|
+
ws = wb[sheet_name] if sheet_name else wb.active
|
|
418
|
+
|
|
419
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
420
|
+
if not rows:
|
|
421
|
+
return LuaList([])
|
|
422
|
+
|
|
423
|
+
# First row is headers
|
|
424
|
+
headers = [str(h) if h is not None else f"col_{i}" for i, h in enumerate(rows[0])]
|
|
425
|
+
return LuaList([dict(zip(headers, row)) for row in rows[1:]])
|
|
426
|
+
|
|
427
|
+
def write(filepath: str, data: List[Dict], options: Optional[Dict] = None) -> None:
|
|
428
|
+
"""Write list of dictionaries to Excel file."""
|
|
429
|
+
path = validator.validate(filepath)
|
|
430
|
+
|
|
431
|
+
# Convert Lua table options to Python dict if needed
|
|
432
|
+
if options and hasattr(options, "items"):
|
|
433
|
+
options = dict(options.items())
|
|
434
|
+
options = options or {}
|
|
435
|
+
sheet_name = options.get("sheet", "Sheet1")
|
|
436
|
+
|
|
437
|
+
# Convert Lua tables to Python
|
|
438
|
+
data = _lua_to_python(data)
|
|
439
|
+
|
|
440
|
+
# Ensure parent directory exists
|
|
441
|
+
os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
|
|
442
|
+
|
|
443
|
+
wb = Workbook()
|
|
444
|
+
ws = wb.active
|
|
445
|
+
ws.title = sheet_name
|
|
446
|
+
|
|
447
|
+
if data:
|
|
448
|
+
headers = list(data[0].keys())
|
|
449
|
+
ws.append(headers)
|
|
450
|
+
for row in data:
|
|
451
|
+
ws.append([row.get(h) for h in headers])
|
|
452
|
+
|
|
453
|
+
wb.save(path)
|
|
454
|
+
|
|
455
|
+
def sheets(filepath: str) -> List[str]:
|
|
456
|
+
"""List sheet names in Excel file."""
|
|
457
|
+
path = validator.validate(filepath)
|
|
458
|
+
wb = load_workbook(path, read_only=True)
|
|
459
|
+
return wb.sheetnames
|
|
460
|
+
|
|
461
|
+
return {"read": read, "write": write, "sheets": sheets}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _lua_to_python(obj: Any) -> Any:
|
|
465
|
+
"""
|
|
466
|
+
Recursively convert Lua table-like objects to Python dicts/lists.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
obj: Object to convert (may be Lua table or Python object).
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Python dict, list, or original value.
|
|
473
|
+
"""
|
|
474
|
+
# Check if it's a Lua table (has values() or items() method)
|
|
475
|
+
if hasattr(obj, "items"):
|
|
476
|
+
# Could be a dict-like Lua table
|
|
477
|
+
try:
|
|
478
|
+
items = list(obj.items())
|
|
479
|
+
# Check if it's array-like (all integer keys starting from 1)
|
|
480
|
+
if items and all(isinstance(k, (int, float)) for k, v in items):
|
|
481
|
+
keys = [int(k) for k, v in items]
|
|
482
|
+
if keys == list(range(1, len(keys) + 1)):
|
|
483
|
+
# It's an array-like table, convert to list
|
|
484
|
+
return [_lua_to_python(obj[k]) for k in range(1, len(keys) + 1)]
|
|
485
|
+
# It's a dict-like table
|
|
486
|
+
return {k: _lua_to_python(v) for k, v in items}
|
|
487
|
+
except (TypeError, AttributeError):
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
if hasattr(obj, "values") and not isinstance(obj, (dict, str)):
|
|
491
|
+
try:
|
|
492
|
+
return [_lua_to_python(v) for v in obj.values()]
|
|
493
|
+
except (TypeError, AttributeError):
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
if isinstance(obj, dict):
|
|
497
|
+
return {k: _lua_to_python(v) for k, v in obj.items()}
|
|
498
|
+
|
|
499
|
+
if isinstance(obj, list):
|
|
500
|
+
return [_lua_to_python(item) for item in obj]
|
|
501
|
+
|
|
502
|
+
return obj
|