tactus 0.34.0__py3-none-any.whl → 0.35.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.
Files changed (81) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/broker_log.py +17 -14
  3. tactus/adapters/channels/__init__.py +17 -15
  4. tactus/adapters/channels/base.py +16 -7
  5. tactus/adapters/channels/broker.py +43 -13
  6. tactus/adapters/channels/cli.py +19 -15
  7. tactus/adapters/channels/host.py +15 -6
  8. tactus/adapters/channels/ipc.py +82 -31
  9. tactus/adapters/channels/sse.py +41 -23
  10. tactus/adapters/cli_hitl.py +19 -19
  11. tactus/adapters/cli_log.py +4 -4
  12. tactus/adapters/control_loop.py +138 -99
  13. tactus/adapters/cost_collector_log.py +9 -9
  14. tactus/adapters/file_storage.py +56 -52
  15. tactus/adapters/http_callback_log.py +23 -13
  16. tactus/adapters/ide_log.py +17 -9
  17. tactus/adapters/lua_tools.py +4 -5
  18. tactus/adapters/mcp.py +16 -19
  19. tactus/adapters/mcp_manager.py +46 -30
  20. tactus/adapters/memory.py +9 -9
  21. tactus/adapters/plugins.py +42 -42
  22. tactus/broker/client.py +75 -78
  23. tactus/broker/protocol.py +57 -57
  24. tactus/broker/server.py +252 -197
  25. tactus/cli/app.py +3 -1
  26. tactus/cli/control.py +2 -2
  27. tactus/core/config_manager.py +181 -135
  28. tactus/core/dependencies/registry.py +66 -48
  29. tactus/core/dsl_stubs.py +222 -163
  30. tactus/core/exceptions.py +10 -1
  31. tactus/core/execution_context.py +152 -112
  32. tactus/core/lua_sandbox.py +72 -64
  33. tactus/core/message_history_manager.py +138 -43
  34. tactus/core/mocking.py +41 -27
  35. tactus/core/output_validator.py +49 -44
  36. tactus/core/registry.py +94 -80
  37. tactus/core/runtime.py +211 -176
  38. tactus/core/template_resolver.py +16 -16
  39. tactus/core/yaml_parser.py +55 -45
  40. tactus/docs/extractor.py +7 -6
  41. tactus/ide/server.py +119 -78
  42. tactus/primitives/control.py +10 -6
  43. tactus/primitives/file.py +48 -46
  44. tactus/primitives/handles.py +47 -35
  45. tactus/primitives/host.py +29 -27
  46. tactus/primitives/human.py +154 -137
  47. tactus/primitives/json.py +22 -23
  48. tactus/primitives/log.py +26 -26
  49. tactus/primitives/message_history.py +285 -31
  50. tactus/primitives/model.py +15 -9
  51. tactus/primitives/procedure.py +86 -64
  52. tactus/primitives/procedure_callable.py +58 -51
  53. tactus/primitives/retry.py +31 -29
  54. tactus/primitives/session.py +42 -29
  55. tactus/primitives/state.py +54 -43
  56. tactus/primitives/step.py +9 -13
  57. tactus/primitives/system.py +34 -21
  58. tactus/primitives/tool.py +44 -31
  59. tactus/primitives/tool_handle.py +76 -54
  60. tactus/primitives/toolset.py +25 -22
  61. tactus/sandbox/config.py +4 -4
  62. tactus/sandbox/container_runner.py +161 -107
  63. tactus/sandbox/docker_manager.py +20 -20
  64. tactus/sandbox/entrypoint.py +16 -14
  65. tactus/sandbox/protocol.py +15 -15
  66. tactus/stdlib/classify/llm.py +1 -3
  67. tactus/stdlib/core/validation.py +0 -3
  68. tactus/testing/pydantic_eval_runner.py +1 -1
  69. tactus/utils/asyncio_helpers.py +27 -0
  70. tactus/utils/cost_calculator.py +7 -7
  71. tactus/utils/model_pricing.py +11 -12
  72. tactus/utils/safe_file_library.py +156 -132
  73. tactus/utils/safe_libraries.py +27 -27
  74. tactus/validation/error_listener.py +18 -5
  75. tactus/validation/semantic_visitor.py +392 -333
  76. tactus/validation/validator.py +89 -49
  77. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/METADATA +12 -3
  78. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/RECORD +81 -80
  79. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/WHEEL +0 -0
  80. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/entry_points.txt +0 -0
  81. {tactus-0.34.0.dist-info → tactus-0.35.0.dist-info}/licenses/LICENSE +0 -0
@@ -34,12 +34,12 @@ class PathValidator:
34
34
  """
35
35
  self.base_path = os.path.realpath(base_path)
36
36
 
37
- def validate(self, filepath: str) -> str:
37
+ def validate(self, file_path: str) -> str:
38
38
  """
39
39
  Validate and resolve a file path.
40
40
 
41
41
  Args:
42
- filepath: Relative or absolute file path to validate.
42
+ file_path: Relative or absolute file path to validate.
43
43
 
44
44
  Returns:
45
45
  Resolved absolute path if valid.
@@ -48,14 +48,16 @@ class PathValidator:
48
48
  PermissionError: If path is outside the base directory.
49
49
  """
50
50
  # Join with base path and resolve to absolute
51
- resolved = os.path.realpath(os.path.join(self.base_path, filepath))
51
+ resolved_path = os.path.realpath(os.path.join(self.base_path, file_path))
52
52
 
53
53
  # Check if resolved path is within base directory
54
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}")
55
+ if resolved_path != self.base_path and not resolved_path.startswith(
56
+ self.base_path + os.sep
57
+ ):
58
+ raise PermissionError(f"Access denied: path outside working directory: {file_path}")
57
59
 
58
- return resolved
60
+ return resolved_path
59
61
 
60
62
 
61
63
  def create_safe_file_library(base_path: str) -> Dict[str, Any]:
@@ -70,25 +72,27 @@ def create_safe_file_library(base_path: str) -> Dict[str, Any]:
70
72
  """
71
73
  validator = PathValidator(base_path)
72
74
 
73
- def read(filepath: str) -> str:
75
+ def read(file_path: str) -> str:
74
76
  """Read entire file as text."""
75
- path = validator.validate(filepath)
76
- with open(path, "r", encoding="utf-8") as f:
77
- return f.read()
77
+ validated_path = validator.validate(file_path)
78
+ with open(validated_path, "r", encoding="utf-8") as file_handle:
79
+ return file_handle.read()
78
80
 
79
- def write(filepath: str, content: str) -> None:
81
+ def write(file_path: str, content: str) -> None:
80
82
  """Write text to file."""
81
- path = validator.validate(filepath)
83
+ validated_path = validator.validate(file_path)
82
84
  # 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)
85
+ parent_directory = os.path.dirname(validated_path)
86
+ if parent_directory:
87
+ os.makedirs(parent_directory, exist_ok=True)
88
+ with open(validated_path, "w", encoding="utf-8") as file_handle:
89
+ file_handle.write(content)
86
90
 
87
- def exists(filepath: str) -> bool:
91
+ def exists(file_path: str) -> bool:
88
92
  """Check if file exists."""
89
93
  try:
90
- path = validator.validate(filepath)
91
- return os.path.exists(path)
94
+ validated_path = validator.validate(file_path)
95
+ return os.path.exists(validated_path)
92
96
  except PermissionError:
93
97
  return False
94
98
 
@@ -106,20 +110,20 @@ class LuaList:
106
110
  def __init__(self, data: List):
107
111
  self._data = data
108
112
 
109
- def __getitem__(self, index):
113
+ def __getitem__(self, key_or_index):
110
114
  # Lua method access comes through __getitem__ with string keys
111
- if isinstance(index, str):
115
+ if isinstance(key_or_index, str):
112
116
  # Handle method access
113
- if index == "len":
117
+ if key_or_index == "len":
114
118
  return self.len
115
- elif index == "get":
119
+ elif key_or_index == "get":
116
120
  return self.get
117
121
  else:
118
- raise KeyError(f"Unknown method: {index}")
122
+ raise KeyError(f"Unknown method: {key_or_index}")
119
123
  # Lua numbers are floats, convert to int for indexing
120
- if isinstance(index, float):
121
- index = int(index)
122
- return self._data[index]
124
+ if isinstance(key_or_index, float):
125
+ key_or_index = int(key_or_index)
126
+ return self._data[key_or_index]
123
127
 
124
128
  def __len__(self):
125
129
  return len(self._data)
@@ -131,11 +135,11 @@ class LuaList:
131
135
  """Return length - callable from Lua as data:len()."""
132
136
  return len(self._data)
133
137
 
134
- def get(self, index):
138
+ def get(self, requested_index):
135
139
  """Alternative access method - data:get(0) instead of data[0]."""
136
- if isinstance(index, float):
137
- index = int(index)
138
- return self._data[index]
140
+ if isinstance(requested_index, float):
141
+ requested_index = int(requested_index)
142
+ return self._data[requested_index]
139
143
 
140
144
 
141
145
  def create_safe_csv_library(base_path: str) -> Dict[str, Any]:
@@ -150,46 +154,48 @@ def create_safe_csv_library(base_path: str) -> Dict[str, Any]:
150
154
  """
151
155
  validator = PathValidator(base_path)
152
156
 
153
- def read(filepath: str) -> LuaList:
157
+ def read(file_path: str) -> LuaList:
154
158
  """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)
159
+ validated_path = validator.validate(file_path)
160
+ with open(validated_path, "r", encoding="utf-8", newline="") as file_handle:
161
+ reader = csv.DictReader(file_handle)
158
162
  return LuaList(list(reader))
159
163
 
160
- def write(filepath: str, data: List[Dict], options: Optional[Dict] = None) -> None:
164
+ def write(file_path: str, data: List[Dict], options: Optional[Dict] = None) -> None:
161
165
  """Write list of dictionaries to CSV file."""
162
- path = validator.validate(filepath)
166
+ validated_path = validator.validate(file_path)
163
167
 
164
168
  # Convert Lua table options to Python dict if needed
165
169
  if options and hasattr(options, "items"):
166
170
  options = dict(options.items())
167
171
  options = options or {}
168
- headers = options.get("headers")
172
+ header_row = options.get("headers")
169
173
 
170
174
  # Convert headers from Lua table to Python list if needed
171
- if headers and hasattr(headers, "values"):
172
- headers = list(headers.values())
175
+ if header_row and hasattr(header_row, "values"):
176
+ header_row = list(header_row.values())
173
177
 
174
178
  # Convert Lua table to Python list if needed
175
179
  if hasattr(data, "values"):
176
180
  data = list(data.values())
177
181
 
178
- if not headers and data:
182
+ if not header_row and data:
179
183
  # Get headers from first row
180
184
  first_row = data[0]
181
185
  if hasattr(first_row, "keys"):
182
- headers = list(first_row.keys())
186
+ header_row = list(first_row.keys())
183
187
  elif isinstance(first_row, dict):
184
- headers = list(first_row.keys())
188
+ header_row = list(first_row.keys())
185
189
  else:
186
190
  raise ValueError("Cannot determine headers from data")
187
191
 
188
192
  # Ensure parent directory exists
189
- os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
193
+ parent_directory = os.path.dirname(validated_path)
194
+ if parent_directory:
195
+ os.makedirs(parent_directory, exist_ok=True)
190
196
 
191
- with open(path, "w", encoding="utf-8", newline="") as f:
192
- writer = csv.DictWriter(f, fieldnames=headers)
197
+ with open(validated_path, "w", encoding="utf-8", newline="") as file_handle:
198
+ writer = csv.DictWriter(file_handle, fieldnames=header_row)
193
199
  writer.writeheader()
194
200
  for row in data:
195
201
  # Convert Lua table to dict if needed
@@ -212,45 +218,51 @@ def create_safe_tsv_library(base_path: str) -> Dict[str, Any]:
212
218
  """
213
219
  validator = PathValidator(base_path)
214
220
 
215
- def read(filepath: str) -> LuaList:
221
+ def read(file_path: str) -> LuaList:
216
222
  """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")
223
+ validated_path = validator.validate(file_path)
224
+ with open(validated_path, "r", encoding="utf-8", newline="") as file_handle:
225
+ reader = csv.DictReader(file_handle, delimiter="\t")
220
226
  return LuaList(list(reader))
221
227
 
222
- def write(filepath: str, data: List[Dict], options: Optional[Dict] = None) -> None:
228
+ def write(file_path: str, data: List[Dict], options: Optional[Dict] = None) -> None:
223
229
  """Write list of dictionaries to TSV file."""
224
- path = validator.validate(filepath)
230
+ validated_path = validator.validate(file_path)
225
231
 
226
232
  # Convert Lua table options to Python dict if needed
227
233
  if options and hasattr(options, "items"):
228
234
  options = dict(options.items())
229
235
  options = options or {}
230
- headers = options.get("headers")
236
+ header_row = options.get("headers")
231
237
 
232
238
  # Convert headers from Lua table to Python list if needed
233
- if headers and hasattr(headers, "values"):
234
- headers = list(headers.values())
239
+ if header_row and hasattr(header_row, "values"):
240
+ header_row = list(header_row.values())
235
241
 
236
242
  # Convert Lua table to Python list if needed
237
243
  if hasattr(data, "values"):
238
244
  data = list(data.values())
239
245
 
240
- if not headers and data:
246
+ if not header_row and data:
241
247
  first_row = data[0]
242
248
  if hasattr(first_row, "keys"):
243
- headers = list(first_row.keys())
249
+ header_row = list(first_row.keys())
244
250
  elif isinstance(first_row, dict):
245
- headers = list(first_row.keys())
251
+ header_row = list(first_row.keys())
246
252
  else:
247
253
  raise ValueError("Cannot determine headers from data")
248
254
 
249
255
  # 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")
256
+ parent_directory = os.path.dirname(validated_path)
257
+ if parent_directory:
258
+ os.makedirs(parent_directory, exist_ok=True)
259
+
260
+ with open(validated_path, "w", encoding="utf-8", newline="") as file_handle:
261
+ writer = csv.DictWriter(
262
+ file_handle,
263
+ fieldnames=header_row,
264
+ delimiter="\t",
265
+ )
254
266
  writer.writeheader()
255
267
  for row in data:
256
268
  if hasattr(row, "items"):
@@ -272,15 +284,15 @@ def create_safe_json_library(base_path: str) -> Dict[str, Any]:
272
284
  """
273
285
  validator = PathValidator(base_path)
274
286
 
275
- def read(filepath: str) -> Any:
287
+ def read(file_path: str) -> Any:
276
288
  """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)
289
+ validated_path = validator.validate(file_path)
290
+ with open(validated_path, "r", encoding="utf-8") as file_handle:
291
+ return json.load(file_handle)
280
292
 
281
- def write(filepath: str, data: Any, options: Optional[Dict] = None) -> None:
293
+ def write(file_path: str, data: Any, options: Optional[Dict] = None) -> None:
282
294
  """Write data to JSON file."""
283
- path = validator.validate(filepath)
295
+ validated_path = validator.validate(file_path)
284
296
 
285
297
  # Convert Lua table options to Python dict if needed
286
298
  if options and hasattr(options, "items"):
@@ -292,10 +304,12 @@ def create_safe_json_library(base_path: str) -> Dict[str, Any]:
292
304
  data = _lua_to_python(data)
293
305
 
294
306
  # Ensure parent directory exists
295
- os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
307
+ parent_directory = os.path.dirname(validated_path)
308
+ if parent_directory:
309
+ os.makedirs(parent_directory, exist_ok=True)
296
310
 
297
- with open(path, "w", encoding="utf-8") as f:
298
- json.dump(data, f, indent=indent, default=str)
311
+ with open(validated_path, "w", encoding="utf-8") as file_handle:
312
+ json.dump(data, file_handle, indent=indent, default=str)
299
313
 
300
314
  return {"read": read, "write": write}
301
315
 
@@ -315,24 +329,26 @@ def create_safe_parquet_library(base_path: str) -> Dict[str, Any]:
315
329
 
316
330
  validator = PathValidator(base_path)
317
331
 
318
- def read(filepath: str) -> LuaList:
332
+ def read(file_path: str) -> LuaList:
319
333
  """Read Parquet file, returning list of dictionaries."""
320
- path = validator.validate(filepath)
321
- table = pq.read_table(path)
334
+ validated_path = validator.validate(file_path)
335
+ table = pq.read_table(validated_path)
322
336
  return LuaList(table.to_pylist())
323
337
 
324
- def write(filepath: str, data: List[Dict]) -> None:
338
+ def write(file_path: str, data: List[Dict]) -> None:
325
339
  """Write list of dictionaries to Parquet file."""
326
- path = validator.validate(filepath)
340
+ validated_path = validator.validate(file_path)
327
341
 
328
342
  # Convert Lua tables to Python
329
343
  data = _lua_to_python(data)
330
344
 
331
345
  # Ensure parent directory exists
332
- os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
346
+ parent_directory = os.path.dirname(validated_path)
347
+ if parent_directory:
348
+ os.makedirs(parent_directory, exist_ok=True)
333
349
 
334
350
  table = pa.Table.from_pylist(data)
335
- pq.write_table(table, path)
351
+ pq.write_table(table, validated_path)
336
352
 
337
353
  return {"read": read, "write": write}
338
354
 
@@ -352,38 +368,40 @@ def create_safe_hdf5_library(base_path: str) -> Dict[str, Any]:
352
368
 
353
369
  validator = PathValidator(base_path)
354
370
 
355
- def read(filepath: str, dataset: str) -> List[Any]:
371
+ def read(file_path: str, dataset: str) -> List[Any]:
356
372
  """Read dataset from HDF5 file."""
357
- path = validator.validate(filepath)
358
- with h5py.File(path, "r") as f:
359
- return f[dataset][:].tolist()
373
+ validated_path = validator.validate(file_path)
374
+ with h5py.File(validated_path, "r") as hdf5_file:
375
+ return hdf5_file[dataset][:].tolist()
360
376
 
361
- def write(filepath: str, dataset: str, data: List) -> None:
377
+ def write(file_path: str, dataset: str, data: List) -> None:
362
378
  """Write data to HDF5 dataset."""
363
- path = validator.validate(filepath)
379
+ validated_path = validator.validate(file_path)
364
380
 
365
381
  # Convert Lua tables to Python
366
382
  data = _lua_to_python(data)
367
383
 
368
384
  # Ensure parent directory exists
369
- os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
385
+ parent_directory = os.path.dirname(validated_path)
386
+ if parent_directory:
387
+ os.makedirs(parent_directory, exist_ok=True)
370
388
 
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))
389
+ with h5py.File(validated_path, "a") as hdf5_file:
390
+ if dataset in hdf5_file:
391
+ del hdf5_file[dataset]
392
+ hdf5_file.create_dataset(dataset, data=np.array(data))
375
393
 
376
- def list_datasets(filepath: str) -> List[str]:
394
+ def list_datasets(file_path: str) -> List[str]:
377
395
  """List all datasets in HDF5 file."""
378
- path = validator.validate(filepath)
396
+ validated_path = validator.validate(file_path)
379
397
  datasets = []
380
- with h5py.File(path, "r") as f:
398
+ with h5py.File(validated_path, "r") as hdf5_file:
381
399
 
382
- def visitor(name, obj):
383
- if isinstance(obj, h5py.Dataset):
400
+ def visitor(name: str, hdf5_object: Any) -> None:
401
+ if isinstance(hdf5_object, h5py.Dataset):
384
402
  datasets.append(name)
385
403
 
386
- f.visititems(visitor)
404
+ hdf5_file.visititems(visitor)
387
405
  return datasets
388
406
 
389
407
  return {"read": read, "write": write, "list": list_datasets}
@@ -403,9 +421,9 @@ def create_safe_excel_library(base_path: str) -> Dict[str, Any]:
403
421
 
404
422
  validator = PathValidator(base_path)
405
423
 
406
- def read(filepath: str, options: Optional[Dict] = None) -> LuaList:
424
+ def read(file_path: str, options: Optional[Dict] = None) -> LuaList:
407
425
  """Read Excel file, returning list of dictionaries."""
408
- path = validator.validate(filepath)
426
+ validated_path = validator.validate(file_path)
409
427
 
410
428
  # Convert Lua table options to Python dict if needed
411
429
  if options and hasattr(options, "items"):
@@ -413,20 +431,24 @@ def create_safe_excel_library(base_path: str) -> Dict[str, Any]:
413
431
  options = options or {}
414
432
  sheet_name = options.get("sheet")
415
433
 
416
- wb = load_workbook(path, read_only=True, data_only=True)
417
- ws = wb[sheet_name] if sheet_name else wb.active
434
+ workbook = load_workbook(validated_path, read_only=True, data_only=True)
435
+ worksheet = workbook[sheet_name] if sheet_name else workbook.active
418
436
 
419
- rows = list(ws.iter_rows(values_only=True))
437
+ rows = list(worksheet.iter_rows(values_only=True))
420
438
  if not rows:
421
439
  return LuaList([])
422
440
 
423
441
  # 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:
442
+ header_row = [
443
+ str(header_value) if header_value is not None else f"col_{index}"
444
+ for index, header_value in enumerate(rows[0])
445
+ ]
446
+ data_rows = rows[1:]
447
+ return LuaList([dict(zip(header_row, row_values)) for row_values in data_rows])
448
+
449
+ def write(file_path: str, data: List[Dict], options: Optional[Dict] = None) -> None:
428
450
  """Write list of dictionaries to Excel file."""
429
- path = validator.validate(filepath)
451
+ validated_path = validator.validate(file_path)
430
452
 
431
453
  # Convert Lua table options to Python dict if needed
432
454
  if options and hasattr(options, "items"):
@@ -438,65 +460,67 @@ def create_safe_excel_library(base_path: str) -> Dict[str, Any]:
438
460
  data = _lua_to_python(data)
439
461
 
440
462
  # Ensure parent directory exists
441
- os.makedirs(os.path.dirname(path), exist_ok=True) if os.path.dirname(path) else None
463
+ parent_directory = os.path.dirname(validated_path)
464
+ if parent_directory:
465
+ os.makedirs(parent_directory, exist_ok=True)
442
466
 
443
- wb = Workbook()
444
- ws = wb.active
445
- ws.title = sheet_name
467
+ workbook = Workbook()
468
+ worksheet = workbook.active
469
+ worksheet.title = sheet_name
446
470
 
447
471
  if data:
448
- headers = list(data[0].keys())
449
- ws.append(headers)
472
+ header_row = list(data[0].keys())
473
+ worksheet.append(header_row)
450
474
  for row in data:
451
- ws.append([row.get(h) for h in headers])
475
+ worksheet.append([row.get(header) for header in header_row])
452
476
 
453
- wb.save(path)
477
+ workbook.save(validated_path)
454
478
 
455
- def sheets(filepath: str) -> List[str]:
479
+ def sheets(file_path: str) -> List[str]:
456
480
  """List sheet names in Excel file."""
457
- path = validator.validate(filepath)
458
- wb = load_workbook(path, read_only=True)
459
- return wb.sheetnames
481
+ validated_path = validator.validate(file_path)
482
+ workbook = load_workbook(validated_path, read_only=True)
483
+ return workbook.sheetnames
460
484
 
461
485
  return {"read": read, "write": write, "sheets": sheets}
462
486
 
463
487
 
464
- def _lua_to_python(obj: Any) -> Any:
488
+ def _lua_to_python(value: Any) -> Any:
465
489
  """
466
490
  Recursively convert Lua table-like objects to Python dicts/lists.
467
491
 
468
492
  Args:
469
- obj: Object to convert (may be Lua table or Python object).
493
+ value: Object to convert (may be Lua table or Python object).
470
494
 
471
495
  Returns:
472
496
  Python dict, list, or original value.
473
497
  """
474
498
  # Check if it's a Lua table (has values() or items() method)
475
- if hasattr(obj, "items"):
499
+ if hasattr(value, "items"):
476
500
  # Could be a dict-like Lua table
477
501
  try:
478
- items = list(obj.items())
502
+ lua_items = list(value.items())
479
503
  # 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)):
504
+ if lua_items and all(isinstance(key, (int, float)) for key, _ in lua_items):
505
+ lua_keys = [int(key) for key, _ in lua_items]
506
+ if lua_keys == list(range(1, len(lua_keys) + 1)):
483
507
  # It's an array-like table, convert to list
484
- return [_lua_to_python(obj[k]) for k in range(1, len(keys) + 1)]
508
+ return [_lua_to_python(value[key]) for key in range(1, len(lua_keys) + 1)]
485
509
  # It's a dict-like table
486
- return {k: _lua_to_python(v) for k, v in items}
510
+ return {key: _lua_to_python(item) for key, item in lua_items}
487
511
  except (TypeError, AttributeError):
488
512
  pass
489
513
 
490
- if hasattr(obj, "values") and not isinstance(obj, (dict, str)):
514
+ if hasattr(value, "values") and not isinstance(value, (dict, str)):
491
515
  try:
492
- return [_lua_to_python(v) for v in obj.values()]
516
+ return [_lua_to_python(item) for item in value.values()]
493
517
  except (TypeError, AttributeError):
494
518
  pass
495
519
 
496
- if isinstance(obj, dict):
497
- return {k: _lua_to_python(v) for k, v in obj.items()}
520
+ if isinstance(value, dict):
521
+ return {key: _lua_to_python(item) for key, item in value.items()}
498
522
 
499
- if isinstance(obj, list):
500
- return [_lua_to_python(item) for item in obj]
523
+ if isinstance(value, list):
524
+ return [_lua_to_python(item) for item in value]
501
525
 
502
- return obj
526
+ return value