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.
Files changed (109) hide show
  1. coding_client/__init__.py +1 -0
  2. coding_client/api/__init__.py +19 -0
  3. coding_client/api/exception_handlers.py +158 -0
  4. coding_client/api/logging_setup.py +44 -0
  5. coding_client/api/metrics.py +40 -0
  6. coding_client/api/openapi_config.py +124 -0
  7. coding_client/api/request_logging.py +60 -0
  8. coding_client/api/validation.py +150 -0
  9. coding_client/auth/__init__.py +78 -0
  10. coding_client/auth/_default_tenant.py +132 -0
  11. coding_client/auth/_header_trust.py +98 -0
  12. coding_client/auth/_onbehalf.py +315 -0
  13. coding_client/auth/_trusted_networks.py +94 -0
  14. coding_client/auth/dependencies.py +61 -0
  15. coding_client/auth/middleware.py +445 -0
  16. coding_client/auth/models.py +83 -0
  17. coding_client/auth/outbound.py +137 -0
  18. coding_client/auth/rbac.py +21 -0
  19. coding_client/auth/rbac_coding.py +5 -0
  20. coding_client/auth/tenant.py +650 -0
  21. coding_client/auth/websocket.py +148 -0
  22. coding_client/clients/__init__.py +24 -0
  23. coding_client/clients/base.py +131 -0
  24. coding_client/clients/coding_ai.py +33 -0
  25. coding_client/clients/coding_analysis.py +36 -0
  26. coding_client/clients/coding_containers.py +45 -0
  27. coding_client/clients/coding_github.py +127 -0
  28. coding_client/clients/coding_realtime.py +127 -0
  29. coding_client/clients/coding_security.py +51 -0
  30. coding_client/clients/coding_testing.py +31 -0
  31. coding_client/clients/coding_workspaces.py +265 -0
  32. coding_client/config/__init__.py +12 -0
  33. coding_client/config/base_settings.py +61 -0
  34. coding_client/config/yaml_config_loader.py +172 -0
  35. coding_client/db/__init__.py +42 -0
  36. coding_client/db/db_types.py +122 -0
  37. coding_client/db/exceptions.py +59 -0
  38. coding_client/db/postgres_connection.py +572 -0
  39. coding_client/db/tenant_query_helper.py +417 -0
  40. coding_client/db_init.py +49 -0
  41. coding_client/exceptions/__init__.py +67 -0
  42. coding_client/exceptions/api_exceptions.py +395 -0
  43. coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
  44. coding_client/interfaces/__init__.py +22 -0
  45. coding_client/interfaces/cache_interface.py +179 -0
  46. coding_client/interfaces/container_interface.py +49 -0
  47. coding_client/interfaces/docker_interface.py +283 -0
  48. coding_client/interfaces/graph_interface.py +28 -0
  49. coding_client/interfaces/security_interface.py +72 -0
  50. coding_client/interfaces/webhook_interface.py +67 -0
  51. coding_client/llm/__init__.py +54 -0
  52. coding_client/llm/base_agent.py +195 -0
  53. coding_client/llm/context.py +121 -0
  54. coding_client/llm/responses.py +102 -0
  55. coding_client/llm/routing.py +82 -0
  56. coding_client/llm/settings.py +102 -0
  57. coding_client/models/__init__.py +62 -0
  58. coding_client/models/agent.py +38 -0
  59. coding_client/models/ai_test_failure.py +745 -0
  60. coding_client/models/analysis.py +330 -0
  61. coding_client/models/architecture.py +685 -0
  62. coding_client/models/bug.py +128 -0
  63. coding_client/models/code_graph.py +317 -0
  64. coding_client/models/container.py +241 -0
  65. coding_client/models/core.py +878 -0
  66. coding_client/models/docker_container.py +104 -0
  67. coding_client/models/github.py +293 -0
  68. coding_client/models/logging.py +29 -0
  69. coding_client/models/package_manager.py +257 -0
  70. coding_client/models/pr_validation.py +440 -0
  71. coding_client/models/pydantic_ai.py +395 -0
  72. coding_client/models/repo_scanner.py +249 -0
  73. coding_client/models/responses.py +431 -0
  74. coding_client/models/security.py +185 -0
  75. coding_client/models/static_analysis.py +508 -0
  76. coding_client/models/system.py +48 -0
  77. coding_client/models/test_executor.py +204 -0
  78. coding_client/models/testing.py +673 -0
  79. coding_client/models/testing_config.py +200 -0
  80. coding_client/models/validation.py +127 -0
  81. coding_client/models_init.py +272 -0
  82. coding_client/telemetry.py +195 -0
  83. coding_client/testing.py +180 -0
  84. coding_client/types/__init__.py +130 -0
  85. coding_client/types/constraint_types.py +276 -0
  86. coding_client/types/core_types.py +112 -0
  87. coding_client/types/state_machine.py +443 -0
  88. coding_client/utils/__init__.py +86 -0
  89. coding_client/utils/archive_utils.py +387 -0
  90. coding_client/utils/ast_debug.py +45 -0
  91. coding_client/utils/async_timeout.py +364 -0
  92. coding_client/utils/constants.py +518 -0
  93. coding_client/utils/content_processing.py +326 -0
  94. coding_client/utils/database.py +916 -0
  95. coding_client/utils/encoding_helper.py +222 -0
  96. coding_client/utils/enum_helpers.py +166 -0
  97. coding_client/utils/file_operations.py +177 -0
  98. coding_client/utils/filesystem_ops.py +528 -0
  99. coding_client/utils/http_client.py +562 -0
  100. coding_client/utils/path_utils.py +471 -0
  101. coding_client/utils/profiling.py +421 -0
  102. coding_client/utils/response_builder.py +240 -0
  103. coding_client/utils/retry_helper.py +712 -0
  104. coding_client/utils/security_validation.py +423 -0
  105. coding_client-1.21.2.dist-info/METADATA +234 -0
  106. coding_client-1.21.2.dist-info/RECORD +109 -0
  107. coding_client-1.21.2.dist-info/WHEEL +5 -0
  108. coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
  109. 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))