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,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tactus primitives - Lua-callable Python objects.
|
|
3
|
+
|
|
4
|
+
These primitives are injected into the Lua sandbox and provide
|
|
5
|
+
the core functionality for workflow execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tactus.primitives.state import StatePrimitive
|
|
9
|
+
from tactus.primitives.control import IterationsPrimitive, StopPrimitive
|
|
10
|
+
from tactus.primitives.tool import ToolPrimitive
|
|
11
|
+
from tactus.primitives.log import LogPrimitive
|
|
12
|
+
from tactus.primitives.step import StepPrimitive, CheckpointPrimitive
|
|
13
|
+
from tactus.primitives.json import JsonPrimitive
|
|
14
|
+
from tactus.primitives.retry import RetryPrimitive
|
|
15
|
+
from tactus.primitives.file import FilePrimitive
|
|
16
|
+
|
|
17
|
+
from tactus.primitives.human import HumanPrimitive
|
|
18
|
+
from tactus.primitives.system import SystemPrimitive
|
|
19
|
+
from tactus.primitives.host import HostPrimitive
|
|
20
|
+
|
|
21
|
+
# MessageHistory primitive is now available
|
|
22
|
+
from tactus.primitives.message_history import MessageHistoryPrimitive
|
|
23
|
+
|
|
24
|
+
# NOTE: AgentPrimitive and ResultPrimitive have been replaced by DSPy implementation
|
|
25
|
+
# Agent functionality is now provided by tactus.dspy.agent
|
|
26
|
+
|
|
27
|
+
# These will be imported when their dependencies are ready
|
|
28
|
+
# from tactus.primitives.system import SystemPrimitive
|
|
29
|
+
# from tactus.primitives.procedure import ProcedurePrimitive
|
|
30
|
+
# from tactus.primitives.graph import GraphNodePrimitive
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"StatePrimitive",
|
|
34
|
+
"IterationsPrimitive",
|
|
35
|
+
"StopPrimitive",
|
|
36
|
+
"ToolPrimitive",
|
|
37
|
+
"HumanPrimitive",
|
|
38
|
+
"LogPrimitive",
|
|
39
|
+
"StepPrimitive",
|
|
40
|
+
"CheckpointPrimitive",
|
|
41
|
+
"MessageHistoryPrimitive",
|
|
42
|
+
"JsonPrimitive",
|
|
43
|
+
"RetryPrimitive",
|
|
44
|
+
"FilePrimitive",
|
|
45
|
+
"SystemPrimitive",
|
|
46
|
+
"HostPrimitive",
|
|
47
|
+
# "AgentPrimitive", # Replaced by DSPy implementation
|
|
48
|
+
# "ResultPrimitive", # Replaced by DSPy implementation
|
|
49
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Control Primitives - Flow control and termination.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Iterations.current() - Get current iteration number
|
|
6
|
+
- Iterations.exceeded(max) - Check if exceeded max iterations
|
|
7
|
+
- Stop.requested() - Check if stop was requested
|
|
8
|
+
- Stop.reason() - Get stop reason
|
|
9
|
+
- Stop.success() - Check if stop was successful
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IterationsPrimitive:
|
|
19
|
+
"""
|
|
20
|
+
Tracks iteration count for procedure execution.
|
|
21
|
+
|
|
22
|
+
Provides safety limits and iteration-based control flow.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize iteration counter."""
|
|
27
|
+
self._current_iteration = 0
|
|
28
|
+
logger.debug("IterationsPrimitive initialized")
|
|
29
|
+
|
|
30
|
+
def current(self) -> int:
|
|
31
|
+
"""
|
|
32
|
+
Get the current iteration number.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Current iteration count (0-indexed)
|
|
36
|
+
|
|
37
|
+
Example (Lua):
|
|
38
|
+
local iter = Iterations.current()
|
|
39
|
+
Log.info("Iteration: " .. iter)
|
|
40
|
+
"""
|
|
41
|
+
return self._current_iteration
|
|
42
|
+
|
|
43
|
+
def exceeded(self, max_iterations: int) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if current iteration has exceeded the maximum.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
max_iterations: Maximum allowed iterations
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if current iteration >= max_iterations
|
|
52
|
+
|
|
53
|
+
Example (Lua):
|
|
54
|
+
if Iterations.exceeded(100) then
|
|
55
|
+
return {success = false, reason = "Max iterations exceeded"}
|
|
56
|
+
end
|
|
57
|
+
"""
|
|
58
|
+
exceeded = self._current_iteration >= max_iterations
|
|
59
|
+
if exceeded:
|
|
60
|
+
logger.warning(f"Iterations exceeded: {self._current_iteration} >= {max_iterations}")
|
|
61
|
+
return exceeded
|
|
62
|
+
|
|
63
|
+
def increment(self) -> int:
|
|
64
|
+
"""
|
|
65
|
+
Increment the iteration counter.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
New iteration count
|
|
69
|
+
|
|
70
|
+
Note: This is called internally by the runtime, not from Lua
|
|
71
|
+
"""
|
|
72
|
+
self._current_iteration += 1
|
|
73
|
+
logger.debug(f"Iteration incremented to {self._current_iteration}")
|
|
74
|
+
return self._current_iteration
|
|
75
|
+
|
|
76
|
+
def reset(self) -> None:
|
|
77
|
+
"""Reset iteration counter (mainly for testing)."""
|
|
78
|
+
self._current_iteration = 0
|
|
79
|
+
logger.debug("Iterations reset to 0")
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return f"IterationsPrimitive(current={self._current_iteration})"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class StopPrimitive:
|
|
86
|
+
"""
|
|
87
|
+
Manages procedure termination state.
|
|
88
|
+
|
|
89
|
+
Tracks when a stop was requested and the reason/success status.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
"""Initialize stop state."""
|
|
94
|
+
self._requested = False
|
|
95
|
+
self._reason: Optional[str] = None
|
|
96
|
+
self._success = True
|
|
97
|
+
logger.debug("StopPrimitive initialized")
|
|
98
|
+
|
|
99
|
+
def requested(self) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Check if a stop was requested.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True if stop was requested
|
|
105
|
+
|
|
106
|
+
Example (Lua):
|
|
107
|
+
if Stop.requested() then
|
|
108
|
+
return {success = true, message = "Procedure stopped"}
|
|
109
|
+
end
|
|
110
|
+
"""
|
|
111
|
+
return self._requested
|
|
112
|
+
|
|
113
|
+
def reason(self) -> Optional[str]:
|
|
114
|
+
"""
|
|
115
|
+
Get the reason for stopping.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Stop reason string or None if not stopped
|
|
119
|
+
|
|
120
|
+
Example (Lua):
|
|
121
|
+
if Stop.requested() then
|
|
122
|
+
Log.info("Stopped because: " .. Stop.reason())
|
|
123
|
+
end
|
|
124
|
+
"""
|
|
125
|
+
return self._reason
|
|
126
|
+
|
|
127
|
+
def success(self) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Check if the stop was due to successful completion.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if stopped successfully, False if stopped due to error
|
|
133
|
+
|
|
134
|
+
Example (Lua):
|
|
135
|
+
if Stop.requested() and Stop.success() then
|
|
136
|
+
return {success = true}
|
|
137
|
+
else
|
|
138
|
+
return {success = false}
|
|
139
|
+
end
|
|
140
|
+
"""
|
|
141
|
+
return self._success
|
|
142
|
+
|
|
143
|
+
def request(self, reason: str, success: bool = True) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Request a stop (called by tools or runtime).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
reason: Reason for stopping
|
|
149
|
+
success: Whether this is a successful completion
|
|
150
|
+
|
|
151
|
+
Note: This is called internally, not from Lua
|
|
152
|
+
"""
|
|
153
|
+
self._requested = True
|
|
154
|
+
self._reason = reason
|
|
155
|
+
self._success = success
|
|
156
|
+
|
|
157
|
+
log_level = logging.INFO if success else logging.WARNING
|
|
158
|
+
logger.log(log_level, f"Stop requested: {reason} (success={success})")
|
|
159
|
+
|
|
160
|
+
def reset(self) -> None:
|
|
161
|
+
"""Reset stop state (mainly for testing)."""
|
|
162
|
+
self._requested = False
|
|
163
|
+
self._reason = None
|
|
164
|
+
self._success = True
|
|
165
|
+
logger.debug("Stop state reset")
|
|
166
|
+
|
|
167
|
+
def __repr__(self) -> str:
|
|
168
|
+
return f"StopPrimitive(requested={self._requested}, success={self._success})"
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File Primitive - File I/O operations for workflows.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- File.read(path) - Read file contents
|
|
6
|
+
- File.write(path, content) - Write content to file
|
|
7
|
+
- File.exists(path) - Check if file exists
|
|
8
|
+
- File.size(path) - Get file size in bytes
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FilePrimitive:
|
|
19
|
+
"""
|
|
20
|
+
Handles file operations for procedures.
|
|
21
|
+
|
|
22
|
+
Enables workflows to:
|
|
23
|
+
- Read file contents
|
|
24
|
+
- Write data to files
|
|
25
|
+
- Check file existence
|
|
26
|
+
- Get file metadata
|
|
27
|
+
|
|
28
|
+
Note: File operations are non-deterministic (files can change between executions).
|
|
29
|
+
Wrap in Step.checkpoint() for durability.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, base_path: Optional[str] = None, execution_context=None):
|
|
33
|
+
"""
|
|
34
|
+
Initialize File primitive.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_path: Optional base directory for relative paths (defaults to cwd)
|
|
38
|
+
execution_context: Optional ExecutionContext for determinism checking
|
|
39
|
+
"""
|
|
40
|
+
self.base_path = Path(base_path) if base_path else Path.cwd()
|
|
41
|
+
self.execution_context = execution_context
|
|
42
|
+
logger.debug(f"FilePrimitive initialized with base_path: {self.base_path}")
|
|
43
|
+
|
|
44
|
+
def _check_determinism(self, operation: str):
|
|
45
|
+
"""Warn if file operation called outside checkpoint."""
|
|
46
|
+
if self.execution_context and not getattr(
|
|
47
|
+
self.execution_context, "_inside_checkpoint", False
|
|
48
|
+
):
|
|
49
|
+
import warnings
|
|
50
|
+
|
|
51
|
+
warnings.warn(
|
|
52
|
+
f"\n{'=' * 70}\n"
|
|
53
|
+
f"DETERMINISM WARNING: File.{operation}() called outside checkpoint\n"
|
|
54
|
+
f"{'=' * 70}\n\n"
|
|
55
|
+
f"File operations are non-deterministic - "
|
|
56
|
+
f"file contents can change between executions.\n\n"
|
|
57
|
+
f"To fix, wrap in Step.checkpoint():\n\n"
|
|
58
|
+
f" state.data = Step.checkpoint(function()\n"
|
|
59
|
+
f" return File.{operation}(...)\n"
|
|
60
|
+
f" end)\n\n"
|
|
61
|
+
f"Why: Files can be modified, deleted, or created "
|
|
62
|
+
f"between procedure executions,\n"
|
|
63
|
+
f"causing different behavior on replay.\n"
|
|
64
|
+
f"\n{'=' * 70}\n",
|
|
65
|
+
UserWarning,
|
|
66
|
+
stacklevel=3,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def read(self, path: str) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Read file contents as string.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
path: File path to read (absolute or relative to base_path)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
File contents as string
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
FileNotFoundError: If file doesn't exist
|
|
81
|
+
IOError: If file cannot be read
|
|
82
|
+
|
|
83
|
+
Example (Lua):
|
|
84
|
+
local config = File.read("config.json")
|
|
85
|
+
Log.info("Config loaded", {length = #config})
|
|
86
|
+
"""
|
|
87
|
+
self._check_determinism("read")
|
|
88
|
+
file_path = self._resolve_path(path)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
logger.debug(f"Reading file: {file_path}")
|
|
92
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
logger.info(f"Read {len(content)} bytes from {file_path}")
|
|
95
|
+
return content
|
|
96
|
+
|
|
97
|
+
except FileNotFoundError:
|
|
98
|
+
error_msg = f"File not found: {file_path}"
|
|
99
|
+
logger.error(error_msg)
|
|
100
|
+
raise FileNotFoundError(error_msg)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
error_msg = f"Failed to read file {file_path}: {e}"
|
|
104
|
+
logger.error(error_msg)
|
|
105
|
+
raise IOError(error_msg)
|
|
106
|
+
|
|
107
|
+
def write(self, path: str, content: str) -> bool:
|
|
108
|
+
"""
|
|
109
|
+
Write content to file.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
path: File path to write (absolute or relative to base_path)
|
|
113
|
+
content: Content to write
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if successful
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
IOError: If file cannot be written
|
|
120
|
+
|
|
121
|
+
Example (Lua):
|
|
122
|
+
local data = Json.encode({status = "complete"})
|
|
123
|
+
File.write("output.json", data)
|
|
124
|
+
Log.info("Data written")
|
|
125
|
+
"""
|
|
126
|
+
self._check_determinism("write")
|
|
127
|
+
file_path = self._resolve_path(path)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Create parent directories if needed
|
|
131
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
logger.debug(f"Writing to file: {file_path}")
|
|
134
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write(content)
|
|
136
|
+
|
|
137
|
+
logger.info(f"Wrote {len(content)} bytes to {file_path}")
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
error_msg = f"Failed to write file {file_path}: {e}"
|
|
142
|
+
logger.error(error_msg)
|
|
143
|
+
raise IOError(error_msg)
|
|
144
|
+
|
|
145
|
+
def exists(self, path: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Check if file exists.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
path: File path to check (absolute or relative to base_path)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if file exists, False otherwise
|
|
154
|
+
|
|
155
|
+
Example (Lua):
|
|
156
|
+
if File.exists("cache.json") then
|
|
157
|
+
local data = File.read("cache.json")
|
|
158
|
+
Log.info("Using cached data")
|
|
159
|
+
else
|
|
160
|
+
Log.info("No cache found")
|
|
161
|
+
end
|
|
162
|
+
"""
|
|
163
|
+
self._check_determinism("exists")
|
|
164
|
+
file_path = self._resolve_path(path)
|
|
165
|
+
exists = file_path.exists() and file_path.is_file()
|
|
166
|
+
logger.debug(f"File exists check for {file_path}: {exists}")
|
|
167
|
+
return exists
|
|
168
|
+
|
|
169
|
+
def size(self, path: str) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Get file size in bytes.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
path: File path to check (absolute or relative to base_path)
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
File size in bytes
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
FileNotFoundError: If file doesn't exist
|
|
181
|
+
|
|
182
|
+
Example (Lua):
|
|
183
|
+
local size = File.size("data.csv")
|
|
184
|
+
Log.info("File size", {bytes = size, kb = size / 1024})
|
|
185
|
+
"""
|
|
186
|
+
self._check_determinism("size")
|
|
187
|
+
file_path = self._resolve_path(path)
|
|
188
|
+
|
|
189
|
+
if not file_path.exists():
|
|
190
|
+
error_msg = f"File not found: {file_path}"
|
|
191
|
+
logger.error(error_msg)
|
|
192
|
+
raise FileNotFoundError(error_msg)
|
|
193
|
+
|
|
194
|
+
size = file_path.stat().st_size
|
|
195
|
+
logger.debug(f"File size for {file_path}: {size} bytes")
|
|
196
|
+
return size
|
|
197
|
+
|
|
198
|
+
def _resolve_path(self, path: str) -> Path:
|
|
199
|
+
"""
|
|
200
|
+
Resolve file path relative to base_path with security validation.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
path: File path to resolve (must be relative)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Resolved Path object
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ValueError: If absolute path or path traversal detected
|
|
210
|
+
"""
|
|
211
|
+
path_obj = Path(path)
|
|
212
|
+
|
|
213
|
+
# Security: Never allow absolute paths
|
|
214
|
+
if path_obj.is_absolute():
|
|
215
|
+
raise ValueError(f"Absolute paths not allowed: {path}")
|
|
216
|
+
|
|
217
|
+
# Resolve relative to base_path
|
|
218
|
+
resolved = (self.base_path / path_obj).resolve()
|
|
219
|
+
|
|
220
|
+
# Security: Verify resolved path is under base_path
|
|
221
|
+
try:
|
|
222
|
+
resolved.relative_to(self.base_path)
|
|
223
|
+
except ValueError:
|
|
224
|
+
raise ValueError(f"Path traversal detected: {path} resolves outside base directory")
|
|
225
|
+
|
|
226
|
+
return resolved
|
|
227
|
+
|
|
228
|
+
def __repr__(self) -> str:
|
|
229
|
+
return f"FilePrimitive(base_path={self.base_path})"
|