kolega-code 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.
- kolega_code/__init__.py +151 -0
- kolega_code/agent/__init__.py +42 -0
- kolega_code/agent/baseagent.py +998 -0
- kolega_code/agent/browseragent.py +123 -0
- kolega_code/agent/coder.py +157 -0
- kolega_code/agent/common.py +41 -0
- kolega_code/agent/compression.py +81 -0
- kolega_code/agent/context.py +112 -0
- kolega_code/agent/conversation.py +408 -0
- kolega_code/agent/generalagent.py +146 -0
- kolega_code/agent/investigationagent.py +123 -0
- kolega_code/agent/planningagent.py +187 -0
- kolega_code/agent/prompt_provider.py +196 -0
- kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
- kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
- kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
- kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
- kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
- kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
- kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
- kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
- kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
- kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
- kolega_code/agent/prompts.py +192 -0
- kolega_code/agent/tests/__init__.py +0 -0
- kolega_code/agent/tests/llm/__init__.py +0 -0
- kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
- kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
- kolega_code/agent/tests/llm/test_client.py +773 -0
- kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
- kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
- kolega_code/agent/tests/llm/test_exceptions.py +249 -0
- kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
- kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
- kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
- kolega_code/agent/tests/llm/test_model_specs.py +17 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
- kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
- kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
- kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
- kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
- kolega_code/agent/tests/services/__init__.py +1 -0
- kolega_code/agent/tests/services/test_browser.py +447 -0
- kolega_code/agent/tests/services/test_browser_parity.py +353 -0
- kolega_code/agent/tests/services/test_file_system.py +699 -0
- kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
- kolega_code/agent/tests/services/test_terminal.py +154 -0
- kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
- kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
- kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
- kolega_code/agent/tests/test_base_agent.py +1942 -0
- kolega_code/agent/tests/test_coder_attachments.py +330 -0
- kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
- kolega_code/agent/tests/test_commands.py +179 -0
- kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
- kolega_code/agent/tests/test_empty_message_handling.py +48 -0
- kolega_code/agent/tests/test_general_agent.py +242 -0
- kolega_code/agent/tests/test_html.py +320 -0
- kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
- kolega_code/agent/tests/test_planning_agent.py +227 -0
- kolega_code/agent/tests/test_prompt_provider.py +271 -0
- kolega_code/agent/tests/test_tool_registry.py +102 -0
- kolega_code/agent/tests/test_tools.py +549 -0
- kolega_code/agent/tests/tool_backend/__init__.py +0 -0
- kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
- kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
- kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
- kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
- kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
- kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
- kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
- kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
- kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
- kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
- kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
- kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
- kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
- kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
- kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
- kolega_code/agent/tool_backend/agent_tool.py +414 -0
- kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
- kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
- kolega_code/agent/tool_backend/base_tool.py +217 -0
- kolega_code/agent/tool_backend/browser_tool.py +271 -0
- kolega_code/agent/tool_backend/build_tool.py +93 -0
- kolega_code/agent/tool_backend/create_file_tool.py +52 -0
- kolega_code/agent/tool_backend/glob_tool.py +323 -0
- kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
- kolega_code/agent/tool_backend/memory_tool.py +79 -0
- kolega_code/agent/tool_backend/read_file_tool.py +119 -0
- kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
- kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
- kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
- kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
- kolega_code/agent/tool_backend/streaming_tool.py +47 -0
- kolega_code/agent/tool_backend/terminal_tool.py +643 -0
- kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
- kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
- kolega_code/agent/tools.py +1704 -0
- kolega_code/agent/utils/commands.py +94 -0
- kolega_code/cli/__init__.py +1 -0
- kolega_code/cli/app.py +2756 -0
- kolega_code/cli/config.py +280 -0
- kolega_code/cli/connection.py +49 -0
- kolega_code/cli/file_index.py +147 -0
- kolega_code/cli/main.py +564 -0
- kolega_code/cli/mentions.py +155 -0
- kolega_code/cli/messages.py +89 -0
- kolega_code/cli/provider_registry.py +96 -0
- kolega_code/cli/session_store.py +207 -0
- kolega_code/cli/settings.py +87 -0
- kolega_code/cli/skills.py +409 -0
- kolega_code/cli/slash_commands.py +108 -0
- kolega_code/cli/tests/__init__.py +1 -0
- kolega_code/cli/tests/test_app.py +4251 -0
- kolega_code/cli/tests/test_cli_config.py +171 -0
- kolega_code/cli/tests/test_connection.py +26 -0
- kolega_code/cli/tests/test_file_index.py +103 -0
- kolega_code/cli/tests/test_main.py +455 -0
- kolega_code/cli/tests/test_mentions.py +108 -0
- kolega_code/cli/tests/test_session_store.py +67 -0
- kolega_code/cli/tests/test_settings.py +62 -0
- kolega_code/cli/tests/test_skills.py +157 -0
- kolega_code/cli/tests/test_slash_commands.py +88 -0
- kolega_code/cli/theme.py +180 -0
- kolega_code/config.py +154 -0
- kolega_code/events.py +202 -0
- kolega_code/llm/client.py +300 -0
- kolega_code/llm/exceptions.py +285 -0
- kolega_code/llm/instrumented_client.py +520 -0
- kolega_code/llm/models.py +1368 -0
- kolega_code/llm/providers/__init__.py +0 -0
- kolega_code/llm/providers/anthropic.py +387 -0
- kolega_code/llm/providers/base.py +71 -0
- kolega_code/llm/providers/google.py +157 -0
- kolega_code/llm/providers/models.py +37 -0
- kolega_code/llm/providers/openai.py +363 -0
- kolega_code/llm/ratelimit.py +40 -0
- kolega_code/llm/specs.py +67 -0
- kolega_code/llm/tool_execution_ids.py +18 -0
- kolega_code/models/__init__.py +9 -0
- kolega_code/models/sandbox_terminal_state.py +47 -0
- kolega_code/runtime.py +50 -0
- kolega_code/sandbox/README.md +200 -0
- kolega_code/sandbox/__init__.py +21 -0
- kolega_code/sandbox/async_filesystem.py +475 -0
- kolega_code/sandbox/base.py +297 -0
- kolega_code/sandbox/browser.py +25 -0
- kolega_code/sandbox/event_loop.py +43 -0
- kolega_code/sandbox/filesystem.py +341 -0
- kolega_code/sandbox/local.py +118 -0
- kolega_code/sandbox/serializer.py +175 -0
- kolega_code/sandbox/terminal.py +868 -0
- kolega_code/sandbox/utils.py +216 -0
- kolega_code/services/base.py +255 -0
- kolega_code/services/browser.py +444 -0
- kolega_code/services/file_system.py +749 -0
- kolega_code/services/html.py +221 -0
- kolega_code/services/terminal.py +903 -0
- kolega_code/tools/__init__.py +22 -0
- kolega_code/tools/core.py +33 -0
- kolega_code/tools/definitions.py +81 -0
- kolega_code/tools/registry.py +73 -0
- kolega_code-0.1.0.dist-info/METADATA +157 -0
- kolega_code-0.1.0.dist-info/RECORD +171 -0
- kolega_code-0.1.0.dist-info/WHEEL +4 -0
- kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
- kolega_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import os
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, BinaryIO, Dict, List, Optional, Union, Iterator, ContextManager
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileSystemPath:
|
|
10
|
+
"""
|
|
11
|
+
A path-like object that provides filesystem operations through the FileSystem interface.
|
|
12
|
+
This allows for more natural path operations while still going through the abstraction layer.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, filesystem: "FileSystem", path: str):
|
|
16
|
+
self.filesystem = filesystem
|
|
17
|
+
self.path = path
|
|
18
|
+
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return self.path
|
|
21
|
+
|
|
22
|
+
def __truediv__(self, other: str) -> "FileSystemPath":
|
|
23
|
+
"""Support path / 'subpath' syntax"""
|
|
24
|
+
if self.path == ".":
|
|
25
|
+
new_path = other
|
|
26
|
+
else:
|
|
27
|
+
new_path = f"{self.path}/{other}"
|
|
28
|
+
return FileSystemPath(self.filesystem, new_path)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
"""Get the final component of the path"""
|
|
33
|
+
return self.filesystem.get_name(self.path)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def suffix(self) -> str:
|
|
37
|
+
"""Get the file extension"""
|
|
38
|
+
return self.filesystem.get_suffix(self.path)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def parent(self) -> "FileSystemPath":
|
|
42
|
+
"""Get the parent directory"""
|
|
43
|
+
parent_path = self.filesystem.get_parent(self.path)
|
|
44
|
+
return FileSystemPath(self.filesystem, parent_path)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def parents(self) -> List["FileSystemPath"]:
|
|
48
|
+
"""Get all parent directories"""
|
|
49
|
+
parent_paths = self.filesystem.get_parents(self.path)
|
|
50
|
+
return [FileSystemPath(self.filesystem, p) for p in parent_paths]
|
|
51
|
+
|
|
52
|
+
def exists(self) -> bool:
|
|
53
|
+
return self.filesystem.exists(self.path)
|
|
54
|
+
|
|
55
|
+
def is_file(self) -> bool:
|
|
56
|
+
return self.filesystem.is_file(self.path)
|
|
57
|
+
|
|
58
|
+
def is_dir(self) -> bool:
|
|
59
|
+
return self.filesystem.is_dir(self.path)
|
|
60
|
+
|
|
61
|
+
def stat(self) -> Dict[str, Any]:
|
|
62
|
+
return self.filesystem.stat(self.path)
|
|
63
|
+
|
|
64
|
+
def mkdir(self, parents: bool = False, exist_ok: bool = False) -> None:
|
|
65
|
+
self.filesystem.mkdir(self.path, parents=parents, exist_ok=exist_ok)
|
|
66
|
+
|
|
67
|
+
def open(self, mode: str = "r", encoding: Optional[str] = None) -> ContextManager:
|
|
68
|
+
return self.filesystem.open(self.path, mode=mode, encoding=encoding)
|
|
69
|
+
|
|
70
|
+
def read_text(self, encoding: str = "utf-8") -> str:
|
|
71
|
+
return self.filesystem.read_text(self.path, encoding=encoding)
|
|
72
|
+
|
|
73
|
+
def write_text(self, content: str, encoding: str = "utf-8") -> None:
|
|
74
|
+
self.filesystem.write_text(self.path, content, encoding=encoding)
|
|
75
|
+
|
|
76
|
+
def read_bytes(self) -> bytes:
|
|
77
|
+
return self.filesystem.read_bytes(self.path)
|
|
78
|
+
|
|
79
|
+
def write_bytes(self, content: bytes) -> None:
|
|
80
|
+
self.filesystem.write_bytes(self.path, content)
|
|
81
|
+
|
|
82
|
+
def unlink(self, missing_ok: bool = False) -> None:
|
|
83
|
+
self.filesystem.remove(self.path, missing_ok=missing_ok)
|
|
84
|
+
|
|
85
|
+
def rmdir(self) -> None:
|
|
86
|
+
self.filesystem.rmdir(self.path)
|
|
87
|
+
|
|
88
|
+
def iterdir(self) -> Iterator["FileSystemPath"]:
|
|
89
|
+
"""Iterate over directory contents"""
|
|
90
|
+
for item in self.filesystem.iterdir(self.path):
|
|
91
|
+
yield FileSystemPath(self.filesystem, item)
|
|
92
|
+
|
|
93
|
+
def glob(self, pattern: str) -> Iterator["FileSystemPath"]:
|
|
94
|
+
"""Find paths matching a glob pattern relative to this path"""
|
|
95
|
+
full_pattern = f"{self.path}/{pattern}" if self.path != "." else pattern
|
|
96
|
+
for match in self.filesystem.glob(full_pattern):
|
|
97
|
+
yield FileSystemPath(self.filesystem, match)
|
|
98
|
+
|
|
99
|
+
def relative_to(self, other: Union[str, "FileSystemPath"]) -> str:
|
|
100
|
+
"""Get path relative to another path"""
|
|
101
|
+
other_path = str(other) if isinstance(other, FileSystemPath) else other
|
|
102
|
+
return self.filesystem.relative_to(self.path, other_path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class FileSystem(ABC):
|
|
106
|
+
"""
|
|
107
|
+
Abstract base class defining the interface for filesystem operations.
|
|
108
|
+
Implementations of this class provide access to different storage backends.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def validate_root(self) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Validate that the filesystem root is usable.
|
|
114
|
+
|
|
115
|
+
Default: no-op. Remote filesystems (e.g. cloud sandboxes) are
|
|
116
|
+
provisioned by their manager and may not exist at construction time;
|
|
117
|
+
LocalFileSystem overrides this to check the directory eagerly.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If the root is known to be invalid.
|
|
121
|
+
"""
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def open(self, path: str, mode: str = "r", encoding: Optional[str] = None) -> Union[BinaryIO, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Open a file and return a file-like object.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path: Path to the file
|
|
131
|
+
mode: Mode to open the file in ('r', 'w', 'rb', etc.)
|
|
132
|
+
encoding: Text encoding to use (for text modes)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A file-like object
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
FileNotFoundError: If the file doesn't exist in read mode
|
|
139
|
+
PermissionError: If the file cannot be accessed
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def read_text(self, path: str, encoding: str = "utf-8") -> str:
|
|
144
|
+
"""
|
|
145
|
+
Read the entire contents of a file as text.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
path: Path to the file
|
|
149
|
+
encoding: Text encoding to use
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The file contents as a string
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
FileNotFoundError: If the file doesn't exist
|
|
156
|
+
PermissionError: If the file cannot be accessed
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
def read_bytes(self, path: str) -> bytes:
|
|
161
|
+
"""
|
|
162
|
+
Read the entire contents of a file as bytes.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
path: Path to the file
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The file contents as bytes
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
FileNotFoundError: If the file doesn't exist
|
|
172
|
+
PermissionError: If the file cannot be accessed
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
@abstractmethod
|
|
176
|
+
def write_text(self, path: str, content: str, encoding: str = "utf-8") -> None:
|
|
177
|
+
"""
|
|
178
|
+
Write text content to a file, creating the file if it doesn't exist.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
path: Path to the file
|
|
182
|
+
content: Text content to write
|
|
183
|
+
encoding: Text encoding to use
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
PermissionError: If the file cannot be accessed or created
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def write_bytes(self, path: str, content: bytes) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Write binary content to a file, creating the file if it doesn't exist.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
path: Path to the file
|
|
196
|
+
content: Binary content to write
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
PermissionError: If the file cannot be accessed or created
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def exists(self, path: str) -> bool:
|
|
204
|
+
"""
|
|
205
|
+
Check if a path exists.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
path: Path to check
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
True if the path exists, False otherwise
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def is_file(self, path: str) -> bool:
|
|
216
|
+
"""
|
|
217
|
+
Check if a path is a file.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
path: Path to check
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if the path is a file, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
@abstractmethod
|
|
227
|
+
def is_dir(self, path: str) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Check if a path is a directory.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
path: Path to check
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if the path is a directory, False otherwise
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
@abstractmethod
|
|
239
|
+
def stat(self, path: str) -> Dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Get file or directory metadata.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
path: Path to get metadata for
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Dictionary with metadata (size, modified_time, etc.)
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
FileNotFoundError: If the path doesn't exist
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
@abstractmethod
|
|
254
|
+
def mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Create a directory.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
path: Path to create
|
|
260
|
+
parents: If True, create parent directories as needed
|
|
261
|
+
exist_ok: If True, don't raise an error if directory exists
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
FileExistsError: If the directory exists and exist_ok is False
|
|
265
|
+
PermissionError: If the directory cannot be created
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
@abstractmethod
|
|
269
|
+
def remove(self, path: str, missing_ok: bool = False) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Remove a file.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
path: Path to remove
|
|
275
|
+
missing_ok: If True, don't raise an error if file doesn't exist
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
FileNotFoundError: If the file doesn't exist and missing_ok is False
|
|
279
|
+
PermissionError: If the file cannot be removed
|
|
280
|
+
IsADirectoryError: If the path is a directory
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
@abstractmethod
|
|
284
|
+
def rmdir(self, path: str) -> None:
|
|
285
|
+
"""
|
|
286
|
+
Remove an empty directory.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
path: Path to remove
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
FileNotFoundError: If the directory doesn't exist
|
|
293
|
+
PermissionError: If the directory cannot be removed
|
|
294
|
+
OSError: If the directory is not empty
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def rmtree(self, path: str) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Remove a directory and all its contents.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
path: Path to remove
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
FileNotFoundError: If the directory doesn't exist
|
|
307
|
+
PermissionError: If the directory cannot be removed
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
@abstractmethod
|
|
311
|
+
def listdir(self, path: str) -> List[str]:
|
|
312
|
+
"""
|
|
313
|
+
List the contents of a directory.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
path: Path to list
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
List of filenames in the directory
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
FileNotFoundError: If the directory doesn't exist
|
|
323
|
+
NotADirectoryError: If the path is not a directory
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
@abstractmethod
|
|
327
|
+
def iterdir(self, path: str) -> Iterator[str]:
|
|
328
|
+
"""
|
|
329
|
+
Iterate over the contents of a directory.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
path: Path to iterate over
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Iterator of paths in the directory
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
FileNotFoundError: If the directory doesn't exist
|
|
339
|
+
NotADirectoryError: If the path is not a directory
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
@abstractmethod
|
|
343
|
+
def glob(self, pattern: str) -> List[str]:
|
|
344
|
+
"""
|
|
345
|
+
Find paths matching a glob pattern.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
pattern: Glob pattern to match
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
List of paths matching the pattern
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
@abstractmethod
|
|
355
|
+
def is_binary_file(self, path: str) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Determine if a file is binary.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
path: Path to check
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
True if the file is binary, False otherwise
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
FileNotFoundError: If the file doesn't exist
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
@abstractmethod
|
|
370
|
+
def get_name(self, path: str) -> str:
|
|
371
|
+
"""
|
|
372
|
+
Get the final component of the path.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
path: Path to get name from
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The final component of the path
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
@abstractmethod
|
|
382
|
+
def get_suffix(self, path: str) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Get the file extension.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
path: Path to get suffix from
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The file extension (including the dot)
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
@abstractmethod
|
|
394
|
+
def get_parent(self, path: str) -> str:
|
|
395
|
+
"""
|
|
396
|
+
Get the parent directory path.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
path: Path to get parent from
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
The parent directory path
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
@abstractmethod
|
|
406
|
+
def get_parents(self, path: str) -> List[str]:
|
|
407
|
+
"""
|
|
408
|
+
Get all parent directories.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
path: Path to get parents from
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
List of parent directory paths
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
@abstractmethod
|
|
418
|
+
def relative_to(self, path: str, other: str) -> str:
|
|
419
|
+
"""
|
|
420
|
+
Get path relative to another path.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
path: The path to make relative
|
|
424
|
+
other: The base path
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
The relative path
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
ValueError: If path is not relative to other
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
@abstractmethod
|
|
434
|
+
def join_path(self, *parts: str) -> str:
|
|
435
|
+
"""
|
|
436
|
+
Join path components.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
parts: Path components to join
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
The joined path
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
@abstractmethod
|
|
446
|
+
def is_absolute(self, path: str) -> bool:
|
|
447
|
+
"""
|
|
448
|
+
Check if a path is absolute.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
path: Path to check
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
True if the path is absolute, False otherwise
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
def path(self, path: str) -> FileSystemPath:
|
|
458
|
+
"""
|
|
459
|
+
Create a FileSystemPath object for more natural path operations.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
path: The path string
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
A FileSystemPath object
|
|
466
|
+
"""
|
|
467
|
+
return FileSystemPath(self, path)
|
|
468
|
+
|
|
469
|
+
# Convenience methods for backward compatibility
|
|
470
|
+
def get_extension(self, path: str) -> str:
|
|
471
|
+
"""Alias for get_suffix for backward compatibility."""
|
|
472
|
+
return self.get_suffix(path)
|
|
473
|
+
|
|
474
|
+
def is_directory(self, path: str) -> bool:
|
|
475
|
+
"""Alias for is_dir for backward compatibility."""
|
|
476
|
+
return self.is_dir(path)
|
|
477
|
+
|
|
478
|
+
def get_path(self, path: str) -> Path:
|
|
479
|
+
"""Get a Path object for the given path."""
|
|
480
|
+
return self._resolve_path(path) if hasattr(self, "_resolve_path") else Path(path)
|
|
481
|
+
|
|
482
|
+
def list_directory(self, path: str) -> List[str]:
|
|
483
|
+
"""Alias for iterdir that returns a list."""
|
|
484
|
+
return list(self.iterdir(path))
|
|
485
|
+
|
|
486
|
+
def create_directory(self, path: str, parents: bool = True, exist_ok: bool = True) -> None:
|
|
487
|
+
"""Create a directory with sensible defaults."""
|
|
488
|
+
self.mkdir(path, parents=parents, exist_ok=exist_ok)
|
|
489
|
+
|
|
490
|
+
def delete(self, path: str) -> None:
|
|
491
|
+
"""Delete a file or directory."""
|
|
492
|
+
if self.is_dir(path):
|
|
493
|
+
self.rmtree(path)
|
|
494
|
+
else:
|
|
495
|
+
self.remove(path, missing_ok=True)
|
|
496
|
+
|
|
497
|
+
def get_size(self, path: str) -> int:
|
|
498
|
+
"""Get the size of a file."""
|
|
499
|
+
stat_info = self.stat(path)
|
|
500
|
+
return stat_info.get("size", 0)
|
|
501
|
+
|
|
502
|
+
def get_modification_time(self, path: str) -> datetime:
|
|
503
|
+
"""Get the modification time of a file."""
|
|
504
|
+
stat_info = self.stat(path)
|
|
505
|
+
return datetime.fromtimestamp(stat_info.get("modified_time", 0))
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class LocalFileSystem(FileSystem):
|
|
509
|
+
"""
|
|
510
|
+
Implementation of FileSystem that uses the local filesystem.
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
def __init__(self, root_path: Optional[Union[str, Path]] = None):
|
|
514
|
+
"""
|
|
515
|
+
Initialize with an optional root path to use as a base for all operations.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
root_path: Optional root path to use as base for relative paths
|
|
519
|
+
"""
|
|
520
|
+
self.root_path = Path(root_path) if root_path else None
|
|
521
|
+
|
|
522
|
+
def validate_root(self) -> None:
|
|
523
|
+
if not self.exists("."):
|
|
524
|
+
raise ValueError(f"Project path does not exist: {self.root_path}")
|
|
525
|
+
if not self.is_dir("."):
|
|
526
|
+
raise ValueError(f"Project path is not a directory: {self.root_path}")
|
|
527
|
+
|
|
528
|
+
def _resolve_path(self, path: str) -> Path:
|
|
529
|
+
"""
|
|
530
|
+
Resolve a potentially relative path against the root path.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
path: Path to resolve
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Resolved path as a Path object
|
|
537
|
+
"""
|
|
538
|
+
if self.root_path:
|
|
539
|
+
return self.root_path / path
|
|
540
|
+
return Path(path)
|
|
541
|
+
|
|
542
|
+
def open(self, path: str, mode: str = "r", encoding: Optional[str] = None) -> Union[BinaryIO, Any]:
|
|
543
|
+
resolved_path = self._resolve_path(path)
|
|
544
|
+
return resolved_path.open(mode=mode, encoding=encoding)
|
|
545
|
+
|
|
546
|
+
def read_text(self, path: str, encoding: str = "utf-8") -> str:
|
|
547
|
+
resolved_path = self._resolve_path(path)
|
|
548
|
+
return resolved_path.read_text(encoding=encoding)
|
|
549
|
+
|
|
550
|
+
def read_bytes(self, path: str) -> bytes:
|
|
551
|
+
resolved_path = self._resolve_path(path)
|
|
552
|
+
return resolved_path.read_bytes()
|
|
553
|
+
|
|
554
|
+
def write_text(self, path: str, content: str, encoding: str = "utf-8") -> None:
|
|
555
|
+
resolved_path = self._resolve_path(path)
|
|
556
|
+
resolved_path.write_text(content, encoding=encoding)
|
|
557
|
+
|
|
558
|
+
def write_bytes(self, path: str, content: bytes) -> None:
|
|
559
|
+
resolved_path = self._resolve_path(path)
|
|
560
|
+
resolved_path.write_bytes(content)
|
|
561
|
+
|
|
562
|
+
def exists(self, path: str) -> bool:
|
|
563
|
+
resolved_path = self._resolve_path(path)
|
|
564
|
+
return resolved_path.exists()
|
|
565
|
+
|
|
566
|
+
def is_file(self, path: str) -> bool:
|
|
567
|
+
resolved_path = self._resolve_path(path)
|
|
568
|
+
return resolved_path.is_file()
|
|
569
|
+
|
|
570
|
+
def is_dir(self, path: str) -> bool:
|
|
571
|
+
resolved_path = self._resolve_path(path)
|
|
572
|
+
return resolved_path.is_dir()
|
|
573
|
+
|
|
574
|
+
def stat(self, path: str) -> Dict[str, Any]:
|
|
575
|
+
resolved_path = self._resolve_path(path)
|
|
576
|
+
stat_result = resolved_path.stat()
|
|
577
|
+
return {
|
|
578
|
+
"size": stat_result.st_size,
|
|
579
|
+
"modified_time": stat_result.st_mtime,
|
|
580
|
+
"created_time": stat_result.st_ctime,
|
|
581
|
+
"accessed_time": stat_result.st_atime,
|
|
582
|
+
"is_directory": resolved_path.is_dir(),
|
|
583
|
+
"is_file": resolved_path.is_file(),
|
|
584
|
+
"stat_result": stat_result, # Include the full stat result for advanced operations
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
def mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> None:
|
|
588
|
+
resolved_path = self._resolve_path(path)
|
|
589
|
+
resolved_path.mkdir(parents=parents, exist_ok=exist_ok)
|
|
590
|
+
|
|
591
|
+
def remove(self, path: str, missing_ok: bool = False) -> None:
|
|
592
|
+
resolved_path = self._resolve_path(path)
|
|
593
|
+
resolved_path.unlink(missing_ok=missing_ok)
|
|
594
|
+
|
|
595
|
+
def rmdir(self, path: str) -> None:
|
|
596
|
+
resolved_path = self._resolve_path(path)
|
|
597
|
+
resolved_path.rmdir()
|
|
598
|
+
|
|
599
|
+
def rmtree(self, path: str) -> None:
|
|
600
|
+
resolved_path = self._resolve_path(path)
|
|
601
|
+
shutil.rmtree(resolved_path)
|
|
602
|
+
|
|
603
|
+
def listdir(self, path: str) -> List[str]:
|
|
604
|
+
resolved_path = self._resolve_path(path)
|
|
605
|
+
return [str(p.name) for p in resolved_path.iterdir()]
|
|
606
|
+
|
|
607
|
+
def iterdir(self, path: str) -> Iterator[str]:
|
|
608
|
+
resolved_path = self._resolve_path(path)
|
|
609
|
+
for item in resolved_path.iterdir():
|
|
610
|
+
if self.root_path:
|
|
611
|
+
yield str(item.relative_to(self.root_path))
|
|
612
|
+
else:
|
|
613
|
+
yield str(item)
|
|
614
|
+
|
|
615
|
+
def glob(self, pattern: str) -> List[str]:
|
|
616
|
+
# If we have a root path, we need to make the pattern relative to it
|
|
617
|
+
if self.root_path:
|
|
618
|
+
paths = list(self.root_path.glob(pattern))
|
|
619
|
+
# Return paths relative to the root path
|
|
620
|
+
return [str(p.relative_to(self.root_path)) for p in paths]
|
|
621
|
+
else:
|
|
622
|
+
return [str(p) for p in Path().glob(pattern)]
|
|
623
|
+
|
|
624
|
+
def is_binary_file(self, path: str) -> bool:
|
|
625
|
+
resolved_path = self._resolve_path(path)
|
|
626
|
+
|
|
627
|
+
# Check extension for common binary formats
|
|
628
|
+
binary_extensions = {
|
|
629
|
+
".pyc",
|
|
630
|
+
".so",
|
|
631
|
+
".dll",
|
|
632
|
+
".exe",
|
|
633
|
+
".bin",
|
|
634
|
+
".jar",
|
|
635
|
+
".war",
|
|
636
|
+
".jpg",
|
|
637
|
+
".jpeg",
|
|
638
|
+
".png",
|
|
639
|
+
".gif",
|
|
640
|
+
".bmp",
|
|
641
|
+
".ico",
|
|
642
|
+
".svg",
|
|
643
|
+
".pdf",
|
|
644
|
+
".zip",
|
|
645
|
+
".tar",
|
|
646
|
+
".gz",
|
|
647
|
+
".tgz",
|
|
648
|
+
".rar",
|
|
649
|
+
".7z",
|
|
650
|
+
".mp3",
|
|
651
|
+
".mp4",
|
|
652
|
+
".avi",
|
|
653
|
+
".mov",
|
|
654
|
+
".mkv",
|
|
655
|
+
".wav",
|
|
656
|
+
".o",
|
|
657
|
+
".obj",
|
|
658
|
+
".class",
|
|
659
|
+
".binary",
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if resolved_path.suffix.lower() in binary_extensions:
|
|
663
|
+
return True
|
|
664
|
+
|
|
665
|
+
# Sample file content to check for null bytes
|
|
666
|
+
try:
|
|
667
|
+
with resolved_path.open("rb") as f:
|
|
668
|
+
sample = f.read(1024)
|
|
669
|
+
if b"\x00" in sample: # If null byte is present, likely binary
|
|
670
|
+
return True
|
|
671
|
+
except Exception:
|
|
672
|
+
# If there's an error reading the file, consider it binary to be safe
|
|
673
|
+
return True
|
|
674
|
+
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
def get_name(self, path: str) -> str:
|
|
678
|
+
resolved_path = self._resolve_path(path)
|
|
679
|
+
return resolved_path.name
|
|
680
|
+
|
|
681
|
+
def get_suffix(self, path: str) -> str:
|
|
682
|
+
resolved_path = self._resolve_path(path)
|
|
683
|
+
return resolved_path.suffix
|
|
684
|
+
|
|
685
|
+
def get_parent(self, path: str) -> str:
|
|
686
|
+
resolved_path = self._resolve_path(path)
|
|
687
|
+
parent = resolved_path.parent
|
|
688
|
+
if self.root_path:
|
|
689
|
+
try:
|
|
690
|
+
return str(parent.relative_to(self.root_path))
|
|
691
|
+
except ValueError:
|
|
692
|
+
# If parent is outside root_path, return the absolute parent
|
|
693
|
+
return str(parent)
|
|
694
|
+
return str(parent)
|
|
695
|
+
|
|
696
|
+
def get_parents(self, path: str) -> List[str]:
|
|
697
|
+
resolved_path = self._resolve_path(path)
|
|
698
|
+
parents = []
|
|
699
|
+
for parent in resolved_path.parents:
|
|
700
|
+
if self.root_path:
|
|
701
|
+
try:
|
|
702
|
+
parents.append(str(parent.relative_to(self.root_path)))
|
|
703
|
+
except ValueError:
|
|
704
|
+
# If parent is outside root_path, return the absolute parent
|
|
705
|
+
parents.append(str(parent))
|
|
706
|
+
else:
|
|
707
|
+
parents.append(str(parent))
|
|
708
|
+
return parents
|
|
709
|
+
|
|
710
|
+
def relative_to(self, path: str, other: str) -> str:
|
|
711
|
+
resolved_path = self._resolve_path(path)
|
|
712
|
+
other_resolved = self._resolve_path(other)
|
|
713
|
+
return str(resolved_path.relative_to(other_resolved))
|
|
714
|
+
|
|
715
|
+
def join_path(self, *parts: str) -> str:
|
|
716
|
+
if self.root_path:
|
|
717
|
+
result = self.root_path
|
|
718
|
+
for part in parts:
|
|
719
|
+
result = result / part
|
|
720
|
+
return str(result.relative_to(self.root_path))
|
|
721
|
+
else:
|
|
722
|
+
result = Path(parts[0]) if parts else Path()
|
|
723
|
+
for part in parts[1:]:
|
|
724
|
+
result = result / part
|
|
725
|
+
return str(result)
|
|
726
|
+
|
|
727
|
+
def is_absolute(self, path: str) -> bool:
|
|
728
|
+
return Path(path).is_absolute()
|
|
729
|
+
|
|
730
|
+
# Additional utility methods for compatibility with os.path operations
|
|
731
|
+
def path_join(self, *parts: str) -> str:
|
|
732
|
+
"""Equivalent to os.path.join"""
|
|
733
|
+
return os.path.join(*parts)
|
|
734
|
+
|
|
735
|
+
def path_exists(self, path: str) -> bool:
|
|
736
|
+
"""Equivalent to os.path.exists"""
|
|
737
|
+
return os.path.exists(path)
|
|
738
|
+
|
|
739
|
+
def path_isdir(self, path: str) -> bool:
|
|
740
|
+
"""Equivalent to os.path.isdir"""
|
|
741
|
+
return os.path.isdir(path)
|
|
742
|
+
|
|
743
|
+
def path_isfile(self, path: str) -> bool:
|
|
744
|
+
"""Equivalent to os.path.isfile"""
|
|
745
|
+
return os.path.isfile(path)
|
|
746
|
+
|
|
747
|
+
def format_datetime(self, timestamp: float) -> str:
|
|
748
|
+
"""Format a timestamp as a datetime string"""
|
|
749
|
+
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|