coding-client 1.21.2__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.
- coding_client/__init__.py +1 -0
- coding_client/api/__init__.py +19 -0
- coding_client/api/exception_handlers.py +158 -0
- coding_client/api/logging_setup.py +44 -0
- coding_client/api/metrics.py +40 -0
- coding_client/api/openapi_config.py +124 -0
- coding_client/api/request_logging.py +60 -0
- coding_client/api/validation.py +150 -0
- coding_client/auth/__init__.py +78 -0
- coding_client/auth/_default_tenant.py +132 -0
- coding_client/auth/_header_trust.py +98 -0
- coding_client/auth/_onbehalf.py +315 -0
- coding_client/auth/_trusted_networks.py +94 -0
- coding_client/auth/dependencies.py +61 -0
- coding_client/auth/middleware.py +445 -0
- coding_client/auth/models.py +83 -0
- coding_client/auth/outbound.py +137 -0
- coding_client/auth/rbac.py +21 -0
- coding_client/auth/rbac_coding.py +5 -0
- coding_client/auth/tenant.py +650 -0
- coding_client/auth/websocket.py +148 -0
- coding_client/clients/__init__.py +24 -0
- coding_client/clients/base.py +131 -0
- coding_client/clients/coding_ai.py +33 -0
- coding_client/clients/coding_analysis.py +36 -0
- coding_client/clients/coding_containers.py +45 -0
- coding_client/clients/coding_github.py +127 -0
- coding_client/clients/coding_realtime.py +127 -0
- coding_client/clients/coding_security.py +51 -0
- coding_client/clients/coding_testing.py +31 -0
- coding_client/clients/coding_workspaces.py +265 -0
- coding_client/config/__init__.py +12 -0
- coding_client/config/base_settings.py +61 -0
- coding_client/config/yaml_config_loader.py +172 -0
- coding_client/db/__init__.py +42 -0
- coding_client/db/db_types.py +122 -0
- coding_client/db/exceptions.py +59 -0
- coding_client/db/postgres_connection.py +572 -0
- coding_client/db/tenant_query_helper.py +417 -0
- coding_client/db_init.py +49 -0
- coding_client/exceptions/__init__.py +67 -0
- coding_client/exceptions/api_exceptions.py +395 -0
- coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
- coding_client/interfaces/__init__.py +22 -0
- coding_client/interfaces/cache_interface.py +179 -0
- coding_client/interfaces/container_interface.py +49 -0
- coding_client/interfaces/docker_interface.py +283 -0
- coding_client/interfaces/graph_interface.py +28 -0
- coding_client/interfaces/security_interface.py +72 -0
- coding_client/interfaces/webhook_interface.py +67 -0
- coding_client/llm/__init__.py +54 -0
- coding_client/llm/base_agent.py +195 -0
- coding_client/llm/context.py +121 -0
- coding_client/llm/responses.py +102 -0
- coding_client/llm/routing.py +82 -0
- coding_client/llm/settings.py +102 -0
- coding_client/models/__init__.py +62 -0
- coding_client/models/agent.py +38 -0
- coding_client/models/ai_test_failure.py +745 -0
- coding_client/models/analysis.py +330 -0
- coding_client/models/architecture.py +685 -0
- coding_client/models/bug.py +128 -0
- coding_client/models/code_graph.py +317 -0
- coding_client/models/container.py +241 -0
- coding_client/models/core.py +878 -0
- coding_client/models/docker_container.py +104 -0
- coding_client/models/github.py +293 -0
- coding_client/models/logging.py +29 -0
- coding_client/models/package_manager.py +257 -0
- coding_client/models/pr_validation.py +440 -0
- coding_client/models/pydantic_ai.py +395 -0
- coding_client/models/repo_scanner.py +249 -0
- coding_client/models/responses.py +431 -0
- coding_client/models/security.py +185 -0
- coding_client/models/static_analysis.py +508 -0
- coding_client/models/system.py +48 -0
- coding_client/models/test_executor.py +204 -0
- coding_client/models/testing.py +673 -0
- coding_client/models/testing_config.py +200 -0
- coding_client/models/validation.py +127 -0
- coding_client/models_init.py +272 -0
- coding_client/telemetry.py +195 -0
- coding_client/testing.py +180 -0
- coding_client/types/__init__.py +130 -0
- coding_client/types/constraint_types.py +276 -0
- coding_client/types/core_types.py +112 -0
- coding_client/types/state_machine.py +443 -0
- coding_client/utils/__init__.py +86 -0
- coding_client/utils/archive_utils.py +387 -0
- coding_client/utils/ast_debug.py +45 -0
- coding_client/utils/async_timeout.py +364 -0
- coding_client/utils/constants.py +518 -0
- coding_client/utils/content_processing.py +326 -0
- coding_client/utils/database.py +916 -0
- coding_client/utils/encoding_helper.py +222 -0
- coding_client/utils/enum_helpers.py +166 -0
- coding_client/utils/file_operations.py +177 -0
- coding_client/utils/filesystem_ops.py +528 -0
- coding_client/utils/http_client.py +562 -0
- coding_client/utils/path_utils.py +471 -0
- coding_client/utils/profiling.py +421 -0
- coding_client/utils/response_builder.py +240 -0
- coding_client/utils/retry_helper.py +712 -0
- coding_client/utils/security_validation.py +423 -0
- coding_client-1.21.2.dist-info/METADATA +234 -0
- coding_client-1.21.2.dist-info/RECORD +109 -0
- coding_client-1.21.2.dist-info/WHEEL +5 -0
- coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
- coding_client-1.21.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""Archive and compression utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for backup creation, temporary file/directory
|
|
4
|
+
management, and atomic file operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
from collections.abc import AsyncIterator, Iterator
|
|
14
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import aiofiles
|
|
20
|
+
|
|
21
|
+
from .constants import DEFAULT_ENCODING, TEMP_DIR_PREFIX
|
|
22
|
+
|
|
23
|
+
HAS_AIOFILES = True
|
|
24
|
+
|
|
25
|
+
# Remove circular imports - they will be imported locally where needed
|
|
26
|
+
|
|
27
|
+
logger = __import__("logging").getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArchiveError(Exception):
|
|
31
|
+
"""Base exception for archive operations."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BackupError(ArchiveError):
|
|
37
|
+
"""Raised when backup operations fail."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TempFileError(ArchiveError):
|
|
43
|
+
"""Raised when temporary file operations fail."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_backup(
|
|
49
|
+
file_path: str | Path,
|
|
50
|
+
backup_dir: str | Path | None = None,
|
|
51
|
+
timestamp: bool = True,
|
|
52
|
+
) -> Path:
|
|
53
|
+
"""Create backup of file."""
|
|
54
|
+
from .filesystem_ops import FileNotFoundError
|
|
55
|
+
|
|
56
|
+
path = Path(file_path)
|
|
57
|
+
if not path.exists():
|
|
58
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
59
|
+
|
|
60
|
+
# Determine backup directory
|
|
61
|
+
if backup_dir:
|
|
62
|
+
backup_dir = Path(backup_dir)
|
|
63
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
else:
|
|
65
|
+
backup_dir = path.parent
|
|
66
|
+
|
|
67
|
+
# Create backup filename
|
|
68
|
+
if timestamp:
|
|
69
|
+
timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
70
|
+
backup_name = f"{path.stem}_backup_{timestamp_str}{path.suffix}"
|
|
71
|
+
else:
|
|
72
|
+
backup_name = f"{path.stem}_backup{path.suffix}"
|
|
73
|
+
|
|
74
|
+
backup_path = backup_dir / backup_name
|
|
75
|
+
|
|
76
|
+
# Ensure unique filename
|
|
77
|
+
counter = 1
|
|
78
|
+
while backup_path.exists():
|
|
79
|
+
if timestamp:
|
|
80
|
+
backup_name = f"{path.stem}_backup_{timestamp_str}_{counter}{path.suffix}"
|
|
81
|
+
else:
|
|
82
|
+
backup_name = f"{path.stem}_backup_{counter}{path.suffix}"
|
|
83
|
+
backup_path = backup_dir / backup_name
|
|
84
|
+
counter += 1
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
shutil.copy2(path, backup_path)
|
|
88
|
+
return backup_path
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise BackupError(f"Failed to create backup: {e}") from e
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def async_create_backup(
|
|
94
|
+
file_path: str | Path,
|
|
95
|
+
backup_dir: str | Path | None = None,
|
|
96
|
+
timestamp: bool = True,
|
|
97
|
+
) -> Path:
|
|
98
|
+
"""Asynchronously create backup of file."""
|
|
99
|
+
return await asyncio.to_thread(create_backup, file_path, backup_dir, timestamp)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def create_backup_directory(
|
|
103
|
+
source_dir: str | Path,
|
|
104
|
+
backup_base: str | Path | None = None,
|
|
105
|
+
timestamp: bool = True,
|
|
106
|
+
) -> Path:
|
|
107
|
+
"""Create backup of entire directory."""
|
|
108
|
+
from .filesystem_ops import FileOperationError
|
|
109
|
+
|
|
110
|
+
source_path = Path(source_dir)
|
|
111
|
+
if not source_path.is_dir():
|
|
112
|
+
raise FileOperationError(f"Not a directory: {source_path}")
|
|
113
|
+
|
|
114
|
+
# Determine backup base directory
|
|
115
|
+
if backup_base:
|
|
116
|
+
backup_base = Path(backup_base)
|
|
117
|
+
backup_base.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
else:
|
|
119
|
+
backup_base = source_path.parent
|
|
120
|
+
|
|
121
|
+
# Create backup directory name
|
|
122
|
+
if timestamp:
|
|
123
|
+
timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
124
|
+
backup_name = f"{source_path.name}_backup_{timestamp_str}"
|
|
125
|
+
else:
|
|
126
|
+
backup_name = f"{source_path.name}_backup"
|
|
127
|
+
|
|
128
|
+
backup_path = backup_base / backup_name
|
|
129
|
+
|
|
130
|
+
# Ensure unique directory name
|
|
131
|
+
counter = 1
|
|
132
|
+
while backup_path.exists():
|
|
133
|
+
if timestamp:
|
|
134
|
+
backup_name = f"{source_path.name}_backup_{timestamp_str}_{counter}"
|
|
135
|
+
else:
|
|
136
|
+
backup_name = f"{source_path.name}_backup_{counter}"
|
|
137
|
+
backup_path = backup_base / backup_name
|
|
138
|
+
counter += 1
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
shutil.copytree(source_path, backup_path)
|
|
142
|
+
return backup_path
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise BackupError(f"Failed to create directory backup: {e}") from e
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@contextmanager
|
|
148
|
+
def temp_file(
|
|
149
|
+
suffix: str = "",
|
|
150
|
+
prefix: str = TEMP_DIR_PREFIX,
|
|
151
|
+
dir: str | Path | None = None,
|
|
152
|
+
delete: bool = True,
|
|
153
|
+
) -> Iterator[Path]:
|
|
154
|
+
"""Context manager for temporary file."""
|
|
155
|
+
try:
|
|
156
|
+
fd, temp_path = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
|
|
157
|
+
os.close(fd)
|
|
158
|
+
|
|
159
|
+
path = Path(temp_path)
|
|
160
|
+
try:
|
|
161
|
+
yield path
|
|
162
|
+
finally:
|
|
163
|
+
if delete and path.exists():
|
|
164
|
+
try:
|
|
165
|
+
path.unlink()
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"Failed to delete temp file {path}: {e}")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise TempFileError(f"Failed to create temp file: {e}") from e
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@contextmanager
|
|
173
|
+
def temp_directory(
|
|
174
|
+
suffix: str = "",
|
|
175
|
+
prefix: str = TEMP_DIR_PREFIX,
|
|
176
|
+
dir: str | Path | None = None,
|
|
177
|
+
delete: bool = True,
|
|
178
|
+
) -> Iterator[Path]:
|
|
179
|
+
"""Context manager for temporary directory."""
|
|
180
|
+
try:
|
|
181
|
+
temp_dir = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dir)
|
|
182
|
+
path = Path(temp_dir)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
yield path
|
|
186
|
+
finally:
|
|
187
|
+
if delete and path.exists():
|
|
188
|
+
try:
|
|
189
|
+
shutil.rmtree(path)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.warning(f"Failed to delete temp directory {path}: {e}")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
raise TempFileError(f"Failed to create temp directory: {e}") from e
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@contextmanager
|
|
197
|
+
def atomic_write(
|
|
198
|
+
file_path: str | Path,
|
|
199
|
+
encoding: str = DEFAULT_ENCODING,
|
|
200
|
+
mode: str = "w",
|
|
201
|
+
) -> Iterator[Any]:
|
|
202
|
+
"""Context manager for atomic file writing."""
|
|
203
|
+
path = Path(file_path)
|
|
204
|
+
temp_path = path.with_suffix(path.suffix + ".tmp")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
with open(temp_path, mode, encoding=encoding) as f:
|
|
208
|
+
yield f
|
|
209
|
+
|
|
210
|
+
# Atomic move on success
|
|
211
|
+
temp_path.replace(path)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
# Clean up temp file on error
|
|
214
|
+
if temp_path.exists():
|
|
215
|
+
try:
|
|
216
|
+
temp_path.unlink()
|
|
217
|
+
except Exception:
|
|
218
|
+
pass # Ignore cleanup errors
|
|
219
|
+
raise TempFileError(f"Atomic write failed: {e}") from e
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@asynccontextmanager
|
|
223
|
+
async def async_atomic_write(
|
|
224
|
+
file_path: str | Path,
|
|
225
|
+
encoding: str = DEFAULT_ENCODING,
|
|
226
|
+
mode: str = "w",
|
|
227
|
+
) -> AsyncIterator[Any]:
|
|
228
|
+
"""Async context manager for atomic file writing."""
|
|
229
|
+
if not HAS_AIOFILES:
|
|
230
|
+
raise ImportError("aiofiles module not available - install with 'pip install aiofiles'")
|
|
231
|
+
|
|
232
|
+
path = Path(file_path)
|
|
233
|
+
temp_path = path.with_suffix(path.suffix + ".tmp")
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
async with aiofiles.open(temp_path, mode, encoding=encoding) as f:
|
|
237
|
+
yield f
|
|
238
|
+
|
|
239
|
+
# Atomic move on success
|
|
240
|
+
await asyncio.to_thread(temp_path.replace, path)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
# Clean up temp file on error
|
|
243
|
+
if temp_path.exists():
|
|
244
|
+
try:
|
|
245
|
+
await asyncio.to_thread(temp_path.unlink)
|
|
246
|
+
except Exception:
|
|
247
|
+
pass # Ignore cleanup errors
|
|
248
|
+
raise TempFileError(f"Async atomic write failed: {e}") from e
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@contextmanager
|
|
252
|
+
def preserve_permissions(file_path: str | Path) -> Iterator[None]:
|
|
253
|
+
"""Context manager to preserve file permissions during operations."""
|
|
254
|
+
path = Path(file_path)
|
|
255
|
+
original_stat = None
|
|
256
|
+
|
|
257
|
+
if path.exists():
|
|
258
|
+
try:
|
|
259
|
+
original_stat = path.stat()
|
|
260
|
+
except Exception:
|
|
261
|
+
pass # Ignore if we can't get stats
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
yield
|
|
265
|
+
finally:
|
|
266
|
+
if original_stat and path.exists():
|
|
267
|
+
try:
|
|
268
|
+
path.chmod(original_stat.st_mode)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.warning(f"Failed to restore permissions for {path}: {e}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def create_temp_copy(
|
|
274
|
+
source_path: str | Path,
|
|
275
|
+
suffix: str = "",
|
|
276
|
+
prefix: str = TEMP_DIR_PREFIX,
|
|
277
|
+
) -> Path:
|
|
278
|
+
"""Create a temporary copy of a file."""
|
|
279
|
+
source = Path(source_path)
|
|
280
|
+
if not source.exists():
|
|
281
|
+
raise FileNotFoundError(f"Source file not found: {source}")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
fd, temp_path = tempfile.mkstemp(suffix=suffix, prefix=prefix)
|
|
285
|
+
os.close(fd)
|
|
286
|
+
|
|
287
|
+
temp_file_path = Path(temp_path)
|
|
288
|
+
shutil.copy2(source, temp_file_path)
|
|
289
|
+
return temp_file_path
|
|
290
|
+
except Exception as e:
|
|
291
|
+
raise TempFileError(f"Failed to create temp copy: {e}") from e
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def safe_replace(
|
|
295
|
+
source: str | Path,
|
|
296
|
+
destination: str | Path,
|
|
297
|
+
backup: bool = True,
|
|
298
|
+
) -> Path | None:
|
|
299
|
+
"""Safely replace a file with another, optionally creating a backup."""
|
|
300
|
+
from .filesystem_ops import FileNotFoundError
|
|
301
|
+
|
|
302
|
+
source_path = Path(source)
|
|
303
|
+
dest_path = Path(destination)
|
|
304
|
+
|
|
305
|
+
if not source_path.exists():
|
|
306
|
+
raise FileNotFoundError(f"Source file not found: {source_path}")
|
|
307
|
+
|
|
308
|
+
backup_path = None
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
# Create backup if requested and destination exists
|
|
312
|
+
if backup and dest_path.exists():
|
|
313
|
+
backup_path = create_backup(dest_path)
|
|
314
|
+
|
|
315
|
+
# Replace the file atomically
|
|
316
|
+
source_path.replace(dest_path)
|
|
317
|
+
return backup_path
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
# If we created a backup and replacement failed, try to restore
|
|
321
|
+
if backup_path and backup_path.exists():
|
|
322
|
+
try:
|
|
323
|
+
backup_path.replace(dest_path)
|
|
324
|
+
logger.info(f"Restored backup after failed replacement: {backup_path}")
|
|
325
|
+
except Exception as restore_error:
|
|
326
|
+
logger.error(f"Failed to restore backup: {restore_error}")
|
|
327
|
+
|
|
328
|
+
raise ArchiveError(f"Failed to replace file: {e}") from e
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cleanup_temp_files(
|
|
332
|
+
directory: str | Path,
|
|
333
|
+
prefix: str = TEMP_DIR_PREFIX,
|
|
334
|
+
max_age_hours: int = 24,
|
|
335
|
+
) -> int:
|
|
336
|
+
"""Clean up old temporary files in a directory."""
|
|
337
|
+
import time
|
|
338
|
+
|
|
339
|
+
dir_path = Path(directory)
|
|
340
|
+
if not dir_path.is_dir():
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
current_time = time.time()
|
|
344
|
+
max_age_seconds = max_age_hours * 3600
|
|
345
|
+
cleaned_count = 0
|
|
346
|
+
|
|
347
|
+
for file_path in dir_path.iterdir():
|
|
348
|
+
if not file_path.name.startswith(prefix):
|
|
349
|
+
continue
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
file_age = current_time - file_path.stat().st_mtime
|
|
353
|
+
if file_age > max_age_seconds:
|
|
354
|
+
if file_path.is_file():
|
|
355
|
+
file_path.unlink()
|
|
356
|
+
elif file_path.is_dir():
|
|
357
|
+
shutil.rmtree(file_path)
|
|
358
|
+
cleaned_count += 1
|
|
359
|
+
logger.debug(f"Cleaned up temp file: {file_path}")
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.warning(f"Failed to clean up {file_path}: {e}")
|
|
362
|
+
|
|
363
|
+
return cleaned_count
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Export all public functions
|
|
367
|
+
__all__ = [
|
|
368
|
+
# Exceptions
|
|
369
|
+
"ArchiveError",
|
|
370
|
+
"BackupError",
|
|
371
|
+
"TempFileError",
|
|
372
|
+
# Backup operations
|
|
373
|
+
"create_backup",
|
|
374
|
+
"async_create_backup",
|
|
375
|
+
"create_backup_directory",
|
|
376
|
+
# Temporary file/directory operations
|
|
377
|
+
"temp_file",
|
|
378
|
+
"temp_directory",
|
|
379
|
+
"create_temp_copy",
|
|
380
|
+
# Atomic operations
|
|
381
|
+
"atomic_write",
|
|
382
|
+
"async_atomic_write",
|
|
383
|
+
"safe_replace",
|
|
384
|
+
# Utilities
|
|
385
|
+
"preserve_permissions",
|
|
386
|
+
"cleanup_temp_files",
|
|
387
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""AST debugging utilities.
|
|
2
|
+
|
|
3
|
+
TODO: Implement AST debugging helpers for development.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ASTDebugMixin:
|
|
15
|
+
"""Mixin class providing AST debugging utilities.
|
|
16
|
+
|
|
17
|
+
TODO: Implement AST visualization and debugging methods.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def debug_ast_node(self, node: Any, depth: int = 0) -> str:
|
|
21
|
+
"""Generate debug representation of an AST node (stub)."""
|
|
22
|
+
return repr(node)
|
|
23
|
+
|
|
24
|
+
def dump_ast(self, node: Any, indent: int = 2) -> str:
|
|
25
|
+
"""Dump AST tree as formatted string (stub)."""
|
|
26
|
+
import ast
|
|
27
|
+
|
|
28
|
+
if isinstance(node, ast.AST):
|
|
29
|
+
return ast.dump(node, indent=indent)
|
|
30
|
+
return repr(node)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log_ast_for_development(
|
|
34
|
+
node: Any,
|
|
35
|
+
message: str = "AST node",
|
|
36
|
+
level: int = logging.DEBUG,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Log an AST node for development/debugging purposes (stub).
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
node: The AST node to log.
|
|
42
|
+
message: Description to include in the log.
|
|
43
|
+
level: Logging level.
|
|
44
|
+
"""
|
|
45
|
+
logger.log(level, "%s: %s", message, repr(node))
|