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.
Files changed (171) hide show
  1. kolega_code/__init__.py +151 -0
  2. kolega_code/agent/__init__.py +42 -0
  3. kolega_code/agent/baseagent.py +998 -0
  4. kolega_code/agent/browseragent.py +123 -0
  5. kolega_code/agent/coder.py +157 -0
  6. kolega_code/agent/common.py +41 -0
  7. kolega_code/agent/compression.py +81 -0
  8. kolega_code/agent/context.py +112 -0
  9. kolega_code/agent/conversation.py +408 -0
  10. kolega_code/agent/generalagent.py +146 -0
  11. kolega_code/agent/investigationagent.py +123 -0
  12. kolega_code/agent/planningagent.py +187 -0
  13. kolega_code/agent/prompt_provider.py +196 -0
  14. kolega_code/agent/prompt_templates/agents/browser.j2 +102 -0
  15. kolega_code/agent/prompt_templates/agents/coder_cli_mode.j2 +127 -0
  16. kolega_code/agent/prompt_templates/agents/general.j2 +68 -0
  17. kolega_code/agent/prompt_templates/agents/investigation.j2 +72 -0
  18. kolega_code/agent/prompt_templates/common/frontend_guidance.md +36 -0
  19. kolega_code/agent/prompt_templates/common/kolega_md_instructions.md +14 -0
  20. kolega_code/agent/prompt_templates/environment_variables/workspace_env_vars.md +11 -0
  21. kolega_code/agent/prompt_templates/template_guidance/expo-template.md +379 -0
  22. kolega_code/agent/prompt_templates/template_guidance/html-website-template.md +3 -0
  23. kolega_code/agent/prompt_templates/template_guidance/mern-stack-template.md +3 -0
  24. kolega_code/agent/prompt_templates/template_guidance/react-vite-shadcdn-template.md +182 -0
  25. kolega_code/agent/prompts.py +192 -0
  26. kolega_code/agent/tests/__init__.py +0 -0
  27. kolega_code/agent/tests/llm/__init__.py +0 -0
  28. kolega_code/agent/tests/llm/test_anthropic_token_counting.py +633 -0
  29. kolega_code/agent/tests/llm/test_billing_openai_cache.py +74 -0
  30. kolega_code/agent/tests/llm/test_client.py +773 -0
  31. kolega_code/agent/tests/llm/test_dashscope_mapping.py +32 -0
  32. kolega_code/agent/tests/llm/test_error_boundary.py +322 -0
  33. kolega_code/agent/tests/llm/test_exceptions.py +249 -0
  34. kolega_code/agent/tests/llm/test_instrumented_client.py +536 -0
  35. kolega_code/agent/tests/llm/test_instrumented_client_integration.py +547 -0
  36. kolega_code/agent/tests/llm/test_langfuse_normalization.py +39 -0
  37. kolega_code/agent/tests/llm/test_model_specs.py +17 -0
  38. kolega_code/agent/tests/llm/test_openai_cached_tokens.py +58 -0
  39. kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +74 -0
  40. kolega_code/agent/tests/llm/test_openai_message_conversion.py +30 -0
  41. kolega_code/agent/tests/llm/test_openai_token_counting.py +687 -0
  42. kolega_code/agent/tests/llm/test_tool_execution_ids.py +193 -0
  43. kolega_code/agent/tests/services/__init__.py +1 -0
  44. kolega_code/agent/tests/services/test_browser.py +447 -0
  45. kolega_code/agent/tests/services/test_browser_parity.py +353 -0
  46. kolega_code/agent/tests/services/test_file_system.py +699 -0
  47. kolega_code/agent/tests/services/test_sandbox_terminal_input.py +98 -0
  48. kolega_code/agent/tests/services/test_terminal.py +154 -0
  49. kolega_code/agent/tests/services/test_terminal_command_tracking.py +385 -0
  50. kolega_code/agent/tests/services/test_terminal_state_serializer.py +262 -0
  51. kolega_code/agent/tests/test_agent_tools_inventory.py +267 -0
  52. kolega_code/agent/tests/test_base_agent.py +1942 -0
  53. kolega_code/agent/tests/test_coder_attachments.py +330 -0
  54. kolega_code/agent/tests/test_coder_prompt_extensions.py +61 -0
  55. kolega_code/agent/tests/test_commands.py +179 -0
  56. kolega_code/agent/tests/test_duplicate_tool_results.py +556 -0
  57. kolega_code/agent/tests/test_empty_message_handling.py +48 -0
  58. kolega_code/agent/tests/test_general_agent.py +242 -0
  59. kolega_code/agent/tests/test_html.py +320 -0
  60. kolega_code/agent/tests/test_parallel_tool_calls.py +291 -0
  61. kolega_code/agent/tests/test_planning_agent.py +227 -0
  62. kolega_code/agent/tests/test_prompt_provider.py +271 -0
  63. kolega_code/agent/tests/test_tool_registry.py +102 -0
  64. kolega_code/agent/tests/test_tools.py +549 -0
  65. kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  66. kolega_code/agent/tests/tool_backend/test_agent_tool.py +356 -0
  67. kolega_code/agent/tests/tool_backend/test_base_tool.py +147 -0
  68. kolega_code/agent/tests/tool_backend/test_browser_tool.py +335 -0
  69. kolega_code/agent/tests/tool_backend/test_build_tool.py +93 -0
  70. kolega_code/agent/tests/tool_backend/test_create_file_tool.py +115 -0
  71. kolega_code/agent/tests/tool_backend/test_glob_tool.py +196 -0
  72. kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +230 -0
  73. kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +292 -0
  74. kolega_code/agent/tests/tool_backend/test_read_file_tool.py +173 -0
  75. kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +115 -0
  76. kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +141 -0
  77. kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +174 -0
  78. kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +228 -0
  79. kolega_code/agent/tests/tool_backend/test_terminal_tool.py +482 -0
  80. kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +189 -0
  81. kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +445 -0
  82. kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +194 -0
  83. kolega_code/agent/tool_backend/agent_tool.py +414 -0
  84. kolega_code/agent/tool_backend/apply_edit_tool.py +98 -0
  85. kolega_code/agent/tool_backend/apply_patch_tool.py +514 -0
  86. kolega_code/agent/tool_backend/base_tool.py +217 -0
  87. kolega_code/agent/tool_backend/browser_tool.py +271 -0
  88. kolega_code/agent/tool_backend/build_tool.py +93 -0
  89. kolega_code/agent/tool_backend/create_file_tool.py +52 -0
  90. kolega_code/agent/tool_backend/glob_tool.py +323 -0
  91. kolega_code/agent/tool_backend/list_directory_tool.py +300 -0
  92. kolega_code/agent/tool_backend/memory_tool.py +79 -0
  93. kolega_code/agent/tool_backend/read_file_tool.py +119 -0
  94. kolega_code/agent/tool_backend/replace_entire_file_tool.py +40 -0
  95. kolega_code/agent/tool_backend/replace_lines_tool.py +97 -0
  96. kolega_code/agent/tool_backend/search_and_replace_tool.py +146 -0
  97. kolega_code/agent/tool_backend/search_codebase_tool.py +377 -0
  98. kolega_code/agent/tool_backend/streaming_tool.py +47 -0
  99. kolega_code/agent/tool_backend/terminal_tool.py +643 -0
  100. kolega_code/agent/tool_backend/think_hard_tool.py +211 -0
  101. kolega_code/agent/tool_backend/web_fetch_tool.py +205 -0
  102. kolega_code/agent/tools.py +1704 -0
  103. kolega_code/agent/utils/commands.py +94 -0
  104. kolega_code/cli/__init__.py +1 -0
  105. kolega_code/cli/app.py +2756 -0
  106. kolega_code/cli/config.py +280 -0
  107. kolega_code/cli/connection.py +49 -0
  108. kolega_code/cli/file_index.py +147 -0
  109. kolega_code/cli/main.py +564 -0
  110. kolega_code/cli/mentions.py +155 -0
  111. kolega_code/cli/messages.py +89 -0
  112. kolega_code/cli/provider_registry.py +96 -0
  113. kolega_code/cli/session_store.py +207 -0
  114. kolega_code/cli/settings.py +87 -0
  115. kolega_code/cli/skills.py +409 -0
  116. kolega_code/cli/slash_commands.py +108 -0
  117. kolega_code/cli/tests/__init__.py +1 -0
  118. kolega_code/cli/tests/test_app.py +4251 -0
  119. kolega_code/cli/tests/test_cli_config.py +171 -0
  120. kolega_code/cli/tests/test_connection.py +26 -0
  121. kolega_code/cli/tests/test_file_index.py +103 -0
  122. kolega_code/cli/tests/test_main.py +455 -0
  123. kolega_code/cli/tests/test_mentions.py +108 -0
  124. kolega_code/cli/tests/test_session_store.py +67 -0
  125. kolega_code/cli/tests/test_settings.py +62 -0
  126. kolega_code/cli/tests/test_skills.py +157 -0
  127. kolega_code/cli/tests/test_slash_commands.py +88 -0
  128. kolega_code/cli/theme.py +180 -0
  129. kolega_code/config.py +154 -0
  130. kolega_code/events.py +202 -0
  131. kolega_code/llm/client.py +300 -0
  132. kolega_code/llm/exceptions.py +285 -0
  133. kolega_code/llm/instrumented_client.py +520 -0
  134. kolega_code/llm/models.py +1368 -0
  135. kolega_code/llm/providers/__init__.py +0 -0
  136. kolega_code/llm/providers/anthropic.py +387 -0
  137. kolega_code/llm/providers/base.py +71 -0
  138. kolega_code/llm/providers/google.py +157 -0
  139. kolega_code/llm/providers/models.py +37 -0
  140. kolega_code/llm/providers/openai.py +363 -0
  141. kolega_code/llm/ratelimit.py +40 -0
  142. kolega_code/llm/specs.py +67 -0
  143. kolega_code/llm/tool_execution_ids.py +18 -0
  144. kolega_code/models/__init__.py +9 -0
  145. kolega_code/models/sandbox_terminal_state.py +47 -0
  146. kolega_code/runtime.py +50 -0
  147. kolega_code/sandbox/README.md +200 -0
  148. kolega_code/sandbox/__init__.py +21 -0
  149. kolega_code/sandbox/async_filesystem.py +475 -0
  150. kolega_code/sandbox/base.py +297 -0
  151. kolega_code/sandbox/browser.py +25 -0
  152. kolega_code/sandbox/event_loop.py +43 -0
  153. kolega_code/sandbox/filesystem.py +341 -0
  154. kolega_code/sandbox/local.py +118 -0
  155. kolega_code/sandbox/serializer.py +175 -0
  156. kolega_code/sandbox/terminal.py +868 -0
  157. kolega_code/sandbox/utils.py +216 -0
  158. kolega_code/services/base.py +255 -0
  159. kolega_code/services/browser.py +444 -0
  160. kolega_code/services/file_system.py +749 -0
  161. kolega_code/services/html.py +221 -0
  162. kolega_code/services/terminal.py +903 -0
  163. kolega_code/tools/__init__.py +22 -0
  164. kolega_code/tools/core.py +33 -0
  165. kolega_code/tools/definitions.py +81 -0
  166. kolega_code/tools/registry.py +73 -0
  167. kolega_code-0.1.0.dist-info/METADATA +157 -0
  168. kolega_code-0.1.0.dist-info/RECORD +171 -0
  169. kolega_code-0.1.0.dist-info/WHEEL +4 -0
  170. kolega_code-0.1.0.dist-info/entry_points.txt +2 -0
  171. 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")