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,423 @@
|
|
|
1
|
+
"""File security and validation utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for file security validation, size checks,
|
|
4
|
+
hash calculations, and permission validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .constants import CHUNK_SIZE, MAX_FILE_SIZE
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SecurityValidationError(Exception):
|
|
23
|
+
"""Base exception for security validation operations."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileSizeError(SecurityValidationError):
|
|
29
|
+
"""Raised when file size exceeds limits."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FilePermissionError(SecurityValidationError):
|
|
35
|
+
"""Raised when file permissions prevent operation."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FileNotFoundError(SecurityValidationError):
|
|
41
|
+
"""Raised when file is not found."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InvalidPathError(SecurityValidationError):
|
|
47
|
+
"""Raised when path is invalid or unsafe."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_file_access(
|
|
53
|
+
file_path: str | Path,
|
|
54
|
+
max_size: int | None = MAX_FILE_SIZE,
|
|
55
|
+
check_readable: bool = True,
|
|
56
|
+
check_writable: bool = False,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Validate file access and basic security checks."""
|
|
59
|
+
path = Path(file_path)
|
|
60
|
+
|
|
61
|
+
# Check if file exists
|
|
62
|
+
if not path.exists():
|
|
63
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
64
|
+
|
|
65
|
+
# Check if it's actually a file
|
|
66
|
+
if not path.is_file():
|
|
67
|
+
raise InvalidPathError(f"Not a file: {path}")
|
|
68
|
+
|
|
69
|
+
# Check file size
|
|
70
|
+
if max_size is not None:
|
|
71
|
+
file_size = path.stat().st_size
|
|
72
|
+
if file_size > max_size:
|
|
73
|
+
raise FileSizeError(f"File too large: {file_size} bytes (max: {max_size})")
|
|
74
|
+
|
|
75
|
+
# Check permissions
|
|
76
|
+
if check_readable and not os.access(path, os.R_OK):
|
|
77
|
+
raise FilePermissionError(f"File not readable: {path}")
|
|
78
|
+
|
|
79
|
+
if check_writable and not os.access(path, os.W_OK):
|
|
80
|
+
raise FilePermissionError(f"File not writable: {path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_directory_access(
|
|
84
|
+
dir_path: str | Path,
|
|
85
|
+
check_readable: bool = True,
|
|
86
|
+
check_writable: bool = False,
|
|
87
|
+
check_executable: bool = True,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Validate directory access and permissions."""
|
|
90
|
+
path = Path(dir_path)
|
|
91
|
+
|
|
92
|
+
# Check if directory exists
|
|
93
|
+
if not path.exists():
|
|
94
|
+
raise FileNotFoundError(f"Directory not found: {path}")
|
|
95
|
+
|
|
96
|
+
# Check if it's actually a directory
|
|
97
|
+
if not path.is_dir():
|
|
98
|
+
raise InvalidPathError(f"Not a directory: {path}")
|
|
99
|
+
|
|
100
|
+
# Check permissions
|
|
101
|
+
if check_readable and not os.access(path, os.R_OK):
|
|
102
|
+
raise FilePermissionError(f"Directory not readable: {path}")
|
|
103
|
+
|
|
104
|
+
if check_writable and not os.access(path, os.W_OK):
|
|
105
|
+
raise FilePermissionError(f"Directory not writable: {path}")
|
|
106
|
+
|
|
107
|
+
if check_executable and not os.access(path, os.X_OK):
|
|
108
|
+
raise FilePermissionError(f"Directory not executable: {path}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_file_size(file_path: str | Path) -> int:
|
|
112
|
+
"""Get file size in bytes."""
|
|
113
|
+
path = Path(file_path)
|
|
114
|
+
if not path.exists():
|
|
115
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return path.stat().st_size
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise SecurityValidationError(f"Failed to get file size: {e}") from e
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_file_hash(
|
|
124
|
+
file_path: str | Path,
|
|
125
|
+
algorithm: str = "sha256",
|
|
126
|
+
chunk_size: int = CHUNK_SIZE,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Calculate file hash."""
|
|
129
|
+
path = Path(file_path)
|
|
130
|
+
if not path.exists():
|
|
131
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
hash_obj = hashlib.new(algorithm)
|
|
135
|
+
|
|
136
|
+
with open(path, "rb") as f:
|
|
137
|
+
while chunk := f.read(chunk_size):
|
|
138
|
+
hash_obj.update(chunk)
|
|
139
|
+
|
|
140
|
+
return hash_obj.hexdigest()
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise SecurityValidationError(f"Failed to calculate hash: {e}") from e
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def verify_file_hash(
|
|
146
|
+
file_path: str | Path,
|
|
147
|
+
expected_hash: str,
|
|
148
|
+
algorithm: str = "sha256",
|
|
149
|
+
) -> bool:
|
|
150
|
+
"""Verify file hash matches expected value."""
|
|
151
|
+
try:
|
|
152
|
+
actual_hash = get_file_hash(file_path, algorithm)
|
|
153
|
+
return actual_hash.lower() == expected_hash.lower()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise SecurityValidationError(f"Hash verification failed: {e}") from e
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_file_permissions(file_path: str | Path) -> dict[str, bool]:
|
|
159
|
+
"""Get file permission details."""
|
|
160
|
+
path = Path(file_path)
|
|
161
|
+
if not path.exists():
|
|
162
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
file_stat = path.stat()
|
|
166
|
+
mode = file_stat.st_mode
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"owner_read": bool(mode & stat.S_IRUSR),
|
|
170
|
+
"owner_write": bool(mode & stat.S_IWUSR),
|
|
171
|
+
"owner_execute": bool(mode & stat.S_IXUSR),
|
|
172
|
+
"group_read": bool(mode & stat.S_IRGRP),
|
|
173
|
+
"group_write": bool(mode & stat.S_IWGRP),
|
|
174
|
+
"group_execute": bool(mode & stat.S_IXGRP),
|
|
175
|
+
"other_read": bool(mode & stat.S_IROTH),
|
|
176
|
+
"other_write": bool(mode & stat.S_IWOTH),
|
|
177
|
+
"other_execute": bool(mode & stat.S_IXOTH),
|
|
178
|
+
"is_suid": bool(mode & stat.S_ISUID),
|
|
179
|
+
"is_sgid": bool(mode & stat.S_ISGID),
|
|
180
|
+
"is_sticky": bool(mode & stat.S_ISVTX),
|
|
181
|
+
}
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise SecurityValidationError(f"Failed to get permissions: {e}") from e
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def is_safe_filename(filename: str) -> bool:
|
|
187
|
+
"""Check if filename is safe (no path traversal, etc.)."""
|
|
188
|
+
# Check for empty or None
|
|
189
|
+
if not filename:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
# Check for path separators
|
|
193
|
+
if os.sep in filename or os.altsep and os.altsep in filename:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# Check for current/parent directory references
|
|
197
|
+
if filename in (".", ".."):
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Check for null bytes
|
|
201
|
+
if "\x00" in filename:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# Check for control characters
|
|
205
|
+
if any(ord(char) < 32 for char in filename):
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
# Check for reserved names on Windows
|
|
209
|
+
reserved_names = {
|
|
210
|
+
"CON",
|
|
211
|
+
"PRN",
|
|
212
|
+
"AUX",
|
|
213
|
+
"NUL",
|
|
214
|
+
"COM1",
|
|
215
|
+
"COM2",
|
|
216
|
+
"COM3",
|
|
217
|
+
"COM4",
|
|
218
|
+
"COM5",
|
|
219
|
+
"COM6",
|
|
220
|
+
"COM7",
|
|
221
|
+
"COM8",
|
|
222
|
+
"COM9",
|
|
223
|
+
"LPT1",
|
|
224
|
+
"LPT2",
|
|
225
|
+
"LPT3",
|
|
226
|
+
"LPT4",
|
|
227
|
+
"LPT5",
|
|
228
|
+
"LPT6",
|
|
229
|
+
"LPT7",
|
|
230
|
+
"LPT8",
|
|
231
|
+
"LPT9",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
name_upper = filename.upper()
|
|
235
|
+
if name_upper in reserved_names or name_upper.split(".")[0] in reserved_names:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def sanitize_filename(filename: str, replacement: str = "_") -> str:
|
|
242
|
+
"""Sanitize filename by replacing unsafe characters."""
|
|
243
|
+
if not filename:
|
|
244
|
+
return "unnamed"
|
|
245
|
+
|
|
246
|
+
# Replace path separators
|
|
247
|
+
safe_name = filename.replace(os.sep, replacement)
|
|
248
|
+
if os.altsep:
|
|
249
|
+
safe_name = safe_name.replace(os.altsep, replacement)
|
|
250
|
+
|
|
251
|
+
# Replace control characters and null bytes
|
|
252
|
+
safe_chars = []
|
|
253
|
+
for char in safe_name:
|
|
254
|
+
if ord(char) < 32 or char == "\x00":
|
|
255
|
+
safe_chars.append(replacement)
|
|
256
|
+
else:
|
|
257
|
+
safe_chars.append(char)
|
|
258
|
+
|
|
259
|
+
safe_name = "".join(safe_chars)
|
|
260
|
+
|
|
261
|
+
# Handle reserved names
|
|
262
|
+
reserved_names = {
|
|
263
|
+
"CON",
|
|
264
|
+
"PRN",
|
|
265
|
+
"AUX",
|
|
266
|
+
"NUL",
|
|
267
|
+
"COM1",
|
|
268
|
+
"COM2",
|
|
269
|
+
"COM3",
|
|
270
|
+
"COM4",
|
|
271
|
+
"COM5",
|
|
272
|
+
"COM6",
|
|
273
|
+
"COM7",
|
|
274
|
+
"COM8",
|
|
275
|
+
"COM9",
|
|
276
|
+
"LPT1",
|
|
277
|
+
"LPT2",
|
|
278
|
+
"LPT3",
|
|
279
|
+
"LPT4",
|
|
280
|
+
"LPT5",
|
|
281
|
+
"LPT6",
|
|
282
|
+
"LPT7",
|
|
283
|
+
"LPT8",
|
|
284
|
+
"LPT9",
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
name_upper = safe_name.upper()
|
|
288
|
+
if name_upper in reserved_names or name_upper.split(".")[0] in reserved_names:
|
|
289
|
+
safe_name = f"file_{safe_name}"
|
|
290
|
+
|
|
291
|
+
# Ensure not empty after sanitization
|
|
292
|
+
if not safe_name or safe_name == replacement:
|
|
293
|
+
safe_name = "unnamed"
|
|
294
|
+
|
|
295
|
+
return safe_name
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_file_info(file_path: str | Path) -> dict[str, Any]:
|
|
299
|
+
"""Get comprehensive file information including security details."""
|
|
300
|
+
path = Path(file_path)
|
|
301
|
+
if not path.exists():
|
|
302
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
file_stat = path.stat()
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"path": str(path.absolute()),
|
|
309
|
+
"name": path.name,
|
|
310
|
+
"size": file_stat.st_size,
|
|
311
|
+
"created": datetime.fromtimestamp(file_stat.st_ctime),
|
|
312
|
+
"modified": datetime.fromtimestamp(file_stat.st_mtime),
|
|
313
|
+
"accessed": datetime.fromtimestamp(file_stat.st_atime),
|
|
314
|
+
"is_file": path.is_file(),
|
|
315
|
+
"is_dir": path.is_dir(),
|
|
316
|
+
"is_symlink": path.is_symlink(),
|
|
317
|
+
"suffix": path.suffix,
|
|
318
|
+
"suffixes": path.suffixes,
|
|
319
|
+
"stem": path.stem,
|
|
320
|
+
"parent": str(path.parent),
|
|
321
|
+
"permissions": get_file_permissions(path),
|
|
322
|
+
"owner_uid": file_stat.st_uid,
|
|
323
|
+
"group_gid": file_stat.st_gid,
|
|
324
|
+
"mode_octal": oct(file_stat.st_mode),
|
|
325
|
+
"inode": file_stat.st_ino,
|
|
326
|
+
"device": file_stat.st_dev,
|
|
327
|
+
"nlinks": file_stat.st_nlink,
|
|
328
|
+
}
|
|
329
|
+
except Exception as e:
|
|
330
|
+
raise SecurityValidationError(f"Failed to get file info: {e}") from e
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def check_file_integrity(
|
|
334
|
+
file_path: str | Path,
|
|
335
|
+
expected_size: int | None = None,
|
|
336
|
+
expected_hash: str | None = None,
|
|
337
|
+
hash_algorithm: str = "sha256",
|
|
338
|
+
) -> dict[str, bool | None]:
|
|
339
|
+
"""Check file integrity against expected values."""
|
|
340
|
+
results = {
|
|
341
|
+
"exists": False,
|
|
342
|
+
"size_match": None,
|
|
343
|
+
"hash_match": None,
|
|
344
|
+
"overall_valid": False,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
path = Path(file_path)
|
|
348
|
+
|
|
349
|
+
# Check if file exists
|
|
350
|
+
if not path.exists():
|
|
351
|
+
return results
|
|
352
|
+
|
|
353
|
+
results["exists"] = True
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Check size if provided
|
|
357
|
+
if expected_size is not None:
|
|
358
|
+
actual_size = get_file_size(path)
|
|
359
|
+
results["size_match"] = actual_size == expected_size
|
|
360
|
+
|
|
361
|
+
# Check hash if provided
|
|
362
|
+
if expected_hash is not None:
|
|
363
|
+
results["hash_match"] = verify_file_hash(path, expected_hash, hash_algorithm)
|
|
364
|
+
|
|
365
|
+
# Overall validity
|
|
366
|
+
checks = [v for v in results.values() if v is not None and isinstance(v, bool)]
|
|
367
|
+
results["overall_valid"] = all(checks) if checks else True
|
|
368
|
+
|
|
369
|
+
return results
|
|
370
|
+
except Exception as e:
|
|
371
|
+
raise SecurityValidationError(f"Integrity check failed: {e}") from e
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def is_executable_file(file_path: str | Path) -> bool:
|
|
375
|
+
"""Check if file is executable."""
|
|
376
|
+
path = Path(file_path)
|
|
377
|
+
return path.exists() and os.access(path, os.X_OK)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def get_safe_temp_dir() -> Path:
|
|
381
|
+
"""Get a safe temporary directory with appropriate permissions."""
|
|
382
|
+
import tempfile
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
temp_dir = Path(tempfile.gettempdir())
|
|
386
|
+
|
|
387
|
+
# Verify temp directory is safe
|
|
388
|
+
if not temp_dir.exists():
|
|
389
|
+
raise SecurityValidationError("System temp directory doesn't exist")
|
|
390
|
+
|
|
391
|
+
if not os.access(temp_dir, os.R_OK | os.W_OK | os.X_OK):
|
|
392
|
+
raise SecurityValidationError("Insufficient permissions on temp directory")
|
|
393
|
+
|
|
394
|
+
return temp_dir
|
|
395
|
+
except Exception as e:
|
|
396
|
+
raise SecurityValidationError(f"Failed to get safe temp directory: {e}") from e
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# Export all public functions
|
|
400
|
+
__all__ = [
|
|
401
|
+
# Exceptions
|
|
402
|
+
"SecurityValidationError",
|
|
403
|
+
"FileSizeError",
|
|
404
|
+
"FilePermissionError",
|
|
405
|
+
"FileNotFoundError",
|
|
406
|
+
"InvalidPathError",
|
|
407
|
+
# Validation
|
|
408
|
+
"validate_file_access",
|
|
409
|
+
"validate_directory_access",
|
|
410
|
+
# File info
|
|
411
|
+
"get_file_size",
|
|
412
|
+
"get_file_info",
|
|
413
|
+
"get_file_permissions",
|
|
414
|
+
# Hashing
|
|
415
|
+
"get_file_hash",
|
|
416
|
+
"verify_file_hash",
|
|
417
|
+
# Security checks
|
|
418
|
+
"is_safe_filename",
|
|
419
|
+
"sanitize_filename",
|
|
420
|
+
"check_file_integrity",
|
|
421
|
+
"is_executable_file",
|
|
422
|
+
"get_safe_temp_dir",
|
|
423
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coding-client
|
|
3
|
+
Version: 1.21.2
|
|
4
|
+
Summary: Shared library for coding-* microservices: models, auth, DB helpers, HTTP client
|
|
5
|
+
Author-email: Nonagentic <dev@nonagentic.ai>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://nonagentic.ai
|
|
8
|
+
Project-URL: Source, https://github.com/nonagenticai/coding-client
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: fastapi>=0.115.0
|
|
13
|
+
Requires-Dist: starlette>=1.0.1
|
|
14
|
+
Requires-Dist: pydantic>=2.12.5
|
|
15
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
16
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
17
|
+
Requires-Dist: psycopg[binary,pool]>=3.2.0
|
|
18
|
+
Requires-Dist: httpx>=0.28.0
|
|
19
|
+
Requires-Dist: redis>=7.1.0
|
|
20
|
+
Requires-Dist: structlog>=24.4.0
|
|
21
|
+
Requires-Dist: PyJWT[crypto]>=2.15.1
|
|
22
|
+
Requires-Dist: cryptography>=46.0.6
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.45.1
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.24.0
|
|
25
|
+
Requires-Dist: tenacity>=8.2.0
|
|
26
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: cachetools>=5.3.0
|
|
29
|
+
Requires-Dist: pydantic-ai<2.0.0,>=1.107.7
|
|
30
|
+
Requires-Dist: openai<3,>=1.0.0
|
|
31
|
+
Requires-Dist: pyasn1>=0.6.5
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff==0.17.4; extra == "dev"
|
|
37
|
+
Requires-Dist: ty==0.0.39; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# coding-client
|
|
41
|
+
|
|
42
|
+
[](https://github.com/nonagenticai/coding-client/actions/workflows/ci.yml)
|
|
43
|
+
[](https://github.com/nonagenticai/coding-client/releases)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
|
|
46
|
+
Shared Python library for the coding-* microservices ecosystem — models, auth, database, service clients, and utilities.
|
|
47
|
+
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Stack** | Python 3.14 · Pydantic 2.x · SQLAlchemy 2.x · psycopg 3.x · httpx · structlog |
|
|
51
|
+
| **Package** | `coding-client` (Apache-2.0) |
|
|
52
|
+
| **Used by** | coding-gateway, coding-analysis, coding-testing, coding-ai, coding-containers, coding-github, coding-security, coding-realtime |
|
|
53
|
+
|
|
54
|
+
## What It Provides
|
|
55
|
+
|
|
56
|
+
**Models** (`coding_client.models`) — 23 domain-specific Pydantic models covering code graphs, architecture, analysis, testing, containers, GitHub, security, AI agents, bug tracking, package management, PR validation, and more.
|
|
57
|
+
|
|
58
|
+
**Auth** (`coding_client.auth`) — Istio header-based authentication middleware, RBAC helpers (`require_role`, `require_access_coding`), multi-tenant isolation (`TenantMiddleware`, `require_tenant`, `get_tenant_id`), and FastAPI dependencies (`get_current_user`).
|
|
59
|
+
|
|
60
|
+
**Database** (`coding_client.db`) — PostgreSQL connection management via PgBouncer, SQLite code graph database, transaction management (`TransactionManager`, `TransactionContext`), tenant-aware queries (`TenantQueryBuilder`), migrations, and 9 custom exceptions.
|
|
61
|
+
|
|
62
|
+
**Clients** (`coding_client.clients`) — service-to-service HTTP clients with retry and timeout, including the canonical `CodingWorkspacesClient` and the compatibility-only `CodingContainersClient`. All extend a common `ServiceClient` base.
|
|
63
|
+
|
|
64
|
+
**Utilities** (`coding_client.utils`) — 16 modules: file operations, filesystem ops, path utils, archive utils, async timeout, encoding helper, HTTP client, retry helper, profiling, response builder, content processing, database utils, constants, enum helpers, security validation, AST debug.
|
|
65
|
+
|
|
66
|
+
**Interfaces** (`coding_client.interfaces`) — 6 abstract contracts for dependency injection: `IGraphService`, `IDockerService`, `ISecurityService`, `IAnalysisCache`, `ICacheManager`, `IWebhookEventHandler`.
|
|
67
|
+
|
|
68
|
+
**Types** (`coding_client.types`) — Shared type definitions, enums, constraint types, and workflow state machine definitions.
|
|
69
|
+
|
|
70
|
+
**API** (`coding_client.api`) — FastAPI middleware (`RequestLoggingMiddleware`, `HealthCheckFilter`), exception handlers, and OpenAPI configuration helpers.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Consumed as a **git dependency**, pinned to a commit:
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
[project]
|
|
78
|
+
dependencies = ["coding-client"]
|
|
79
|
+
|
|
80
|
+
[tool.uv.sources]
|
|
81
|
+
coding-client = { git = "https://github.com/nonagenticai/coding-client", rev = "<commit sha>" }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv lock # records the pinned commit in uv.lock
|
|
86
|
+
uv sync
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
No package index is involved, so this resolves identically on a laptop, in CI, and inside a
|
|
90
|
+
Docker build.
|
|
91
|
+
|
|
92
|
+
## Quick Start
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
uv sync
|
|
96
|
+
uv run pytest tests/ -v --cov=src
|
|
97
|
+
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/
|
|
98
|
+
uv run ty check .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Architecture
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
src/
|
|
105
|
+
├── __init__.py
|
|
106
|
+
├── api/
|
|
107
|
+
│ ├── exception_handlers.py # FastAPI exception handlers
|
|
108
|
+
│ ├── openapi_config.py # OpenAPI customization
|
|
109
|
+
│ └── request_logging.py # Request logging middleware
|
|
110
|
+
├── auth/
|
|
111
|
+
│ ├── dependencies.py # FastAPI auth dependencies (get_current_user)
|
|
112
|
+
│ ├── middleware.py # Istio header auth middleware
|
|
113
|
+
│ ├── models.py # User model
|
|
114
|
+
│ ├── rbac.py # Role-based access control
|
|
115
|
+
│ ├── rbac_coding.py # Coding-specific RBAC helpers
|
|
116
|
+
│ └── tenant.py # Multi-tenant isolation
|
|
117
|
+
├── clients/
|
|
118
|
+
│ ├── base.py # ServiceClient base with retry/timeout
|
|
119
|
+
│ ├── coding_ai.py # coding-ai client
|
|
120
|
+
│ ├── coding_analysis.py # coding-analysis client
|
|
121
|
+
│ ├── coding_containers.py # coding-containers client
|
|
122
|
+
│ ├── coding_workspaces.py # canonical repository-workspace client
|
|
123
|
+
│ ├── coding_github.py # coding-github client
|
|
124
|
+
│ ├── coding_realtime.py # coding-realtime client
|
|
125
|
+
│ ├── coding_security.py # coding-security client
|
|
126
|
+
│ └── coding_testing.py # coding-testing client
|
|
127
|
+
├── config/
|
|
128
|
+
│ └── yaml_config_loader.py # YAML config loading
|
|
129
|
+
├── db/
|
|
130
|
+
│ ├── code_graph_database.py # SQLite graph DB
|
|
131
|
+
│ ├── database_path_manager.py # DB file path management
|
|
132
|
+
│ ├── db_types.py # Database type definitions
|
|
133
|
+
│ ├── exceptions.py # 9 custom DB exceptions
|
|
134
|
+
│ ├── migrations.py # Schema migration helpers
|
|
135
|
+
│ ├── models/
|
|
136
|
+
│ │ └── architecture.py # ORM models
|
|
137
|
+
│ ├── performance_optimizer.py # Query performance tuning
|
|
138
|
+
│ ├── postgres_connection.py # PostgreSQL via PgBouncer
|
|
139
|
+
│ ├── tenant_query_helper.py # Tenant-aware query builder
|
|
140
|
+
│ ├── transaction_manager.py # Transaction management
|
|
141
|
+
│ ├── transaction_mixin.py # Transaction mixin
|
|
142
|
+
│ └── transaction_patterns.py # Transaction patterns
|
|
143
|
+
├── exceptions/
|
|
144
|
+
│ ├── api_exceptions.py # API exception hierarchy
|
|
145
|
+
│ └── pydantic_ai_exceptions.py # PydanticAI-specific exceptions
|
|
146
|
+
├── interfaces/ # 6 abstract contracts
|
|
147
|
+
├── models/ # 23 Pydantic model files
|
|
148
|
+
├── types/
|
|
149
|
+
│ ├── constraint_types.py # Validation constraints
|
|
150
|
+
│ ├── core_types.py # Enums and type hints
|
|
151
|
+
│ └── state_machine.py # Workflow state definitions
|
|
152
|
+
└── utils/ # 16 utility modules
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Tests
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
uv run pytest tests/ -v --cov=src --cov-report=term-missing # 29 test files
|
|
159
|
+
uv run ruff check src/ tests/ # Lint
|
|
160
|
+
uv run ruff format --check src/ tests/ # Format check
|
|
161
|
+
uv run ty check . # Type check
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Test structure mirrors source: `test_api/`, `test_auth/`, `test_clients/`, `test_config/`, `test_db/`, `test_exceptions/`, `test_models/`, `test_types/`, `test_utils/`.
|
|
165
|
+
|
|
166
|
+
The suite needs no database. ⛔ `coding_client.db_init` has **no default DSN** (it used to
|
|
167
|
+
fall back to `localhost:5432/code_graph_helper`, a host Postgres deleted 2026-10-21):
|
|
168
|
+
importing it without `DATABASE_URL` / `TEST_DATABASE_URL` raises `RuntimeError` naming the
|
|
169
|
+
variable. A consumer that wants a test DB starts an ephemeral `postgres:18` container on a
|
|
170
|
+
random port — never a host `:5432`.
|
|
171
|
+
|
|
172
|
+
### Bypassing auth in a consumer's tests
|
|
173
|
+
|
|
174
|
+
`SKIP_AUTH=true` was **removed in 1.16.0**. It made `require_tenant`,
|
|
175
|
+
`optional_tenant` and `get_current_user_with_tenant` return the hardcoded tenant
|
|
176
|
+
`"test-tenant"` before examining any credential — an ambient, global switch
|
|
177
|
+
compiled into the production wheel, where one environment variable disabled
|
|
178
|
+
authentication for a whole service.
|
|
179
|
+
|
|
180
|
+
Use `coding_client.testing` instead. Same job for a test suite; per-app,
|
|
181
|
+
explicit, and impossible to switch on from the environment:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from coding_client.testing import override_tenant_auth
|
|
185
|
+
|
|
186
|
+
@pytest.fixture
|
|
187
|
+
def client(app):
|
|
188
|
+
undo = override_tenant_auth(app) # or: with tenant_auth_override(app):
|
|
189
|
+
yield TestClient(app)
|
|
190
|
+
undo() # restores the previous overrides exactly
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
It neutralises **every** identity dependency this SDK exposes, from one list.
|
|
194
|
+
That matters: hand-rolled override lists drift, and one in this fleet already
|
|
195
|
+
had — a consumer's conftest overrode three dependencies and not `require_tenant`,
|
|
196
|
+
and only worked because it also set `SKIP_AUTH`.
|
|
197
|
+
|
|
198
|
+
Pass `extra={dep: replacement}` for a service's own forked dependency object or a
|
|
199
|
+
service-specific return shape.
|
|
200
|
+
|
|
201
|
+
## CI
|
|
202
|
+
|
|
203
|
+
`.github/workflows/ci.yml` calls the public `nonagenticai/ci-workflows` python-library pipeline:
|
|
204
|
+
`ruff + ty + unit tests → uv build → bandit + pip-audit`, on GitHub-hosted runners with no
|
|
205
|
+
repository secrets. The wheel is a build artifact only — nothing is published to a package index;
|
|
206
|
+
consumers pin the git commit.
|
|
207
|
+
|
|
208
|
+
## Dependencies
|
|
209
|
+
|
|
210
|
+
| Category | Packages |
|
|
211
|
+
|----------|----------|
|
|
212
|
+
| **Web/API** | fastapi, httpx, PyJWT[crypto] |
|
|
213
|
+
| **Data** | pydantic, pydantic-settings, sqlalchemy[asyncio] |
|
|
214
|
+
| **Database** | psycopg[binary,pool] (PostgreSQL v3.x), aiosqlite |
|
|
215
|
+
| **Async** | redis, tenacity, aiofiles |
|
|
216
|
+
| **Observability** | structlog, opentelemetry-api, opentelemetry-sdk |
|
|
217
|
+
| **Security** | cryptography |
|
|
218
|
+
| **Utilities** | uuid-v7, cachetools, pyyaml |
|
|
219
|
+
|
|
220
|
+
## Security
|
|
221
|
+
|
|
222
|
+
Report vulnerabilities privately through the repository's Security tab — see
|
|
223
|
+
[`SECURITY.md`](SECURITY.md). Do not open a public issue for a security problem.
|
|
224
|
+
|
|
225
|
+
## Contributing
|
|
226
|
+
|
|
227
|
+
Issues and pull requests are welcome. See
|
|
228
|
+
[`CONTRIBUTING.md`](https://github.com/nonagenticai/.github/blob/main/CONTRIBUTING.md) for the
|
|
229
|
+
organisation-wide rules; setup, test and lint commands are in this README. Commits carry DCO
|
|
230
|
+
sign-off.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
Apache-2.0 — see [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
|