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,533 @@
1
+ """Custom exceptions for PydanticAI operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class PydanticAIException(Exception): # noqa: N818
9
+ """Base exception for PydanticAI-related errors."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ error_code: str | None = None,
15
+ details: dict[str, Any] | None = None,
16
+ cause: Exception | None = None,
17
+ ):
18
+ """Initialize PydanticAI exception.
19
+
20
+ Args:
21
+ message: Error message
22
+ error_code: Optional error code for categorization
23
+ details: Optional additional details
24
+ cause: Optional underlying cause exception
25
+ """
26
+ super().__init__(message)
27
+ self.message = message
28
+ self.error_code = error_code
29
+ self.details = details or {}
30
+ self.cause = cause
31
+
32
+ def __str__(self) -> str:
33
+ """String representation of the exception."""
34
+ parts = [self.message]
35
+
36
+ if self.error_code:
37
+ parts.append(f"Error code: {self.error_code}")
38
+
39
+ if self.details:
40
+ parts.append(f"Details: {self.details}")
41
+
42
+ if self.cause:
43
+ parts.append(f"Caused by: {self.cause}")
44
+
45
+ return " | ".join(parts)
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ """Convert exception to dictionary."""
49
+ return {
50
+ "type": self.__class__.__name__,
51
+ "message": self.message,
52
+ "error_code": self.error_code,
53
+ "details": self.details,
54
+ "cause": str(self.cause) if self.cause else None,
55
+ }
56
+
57
+
58
+ class LLMEmptyResponseError(PydanticAIException):
59
+ """Raised when the LLM plane returns an empty or unusable response.
60
+
61
+ In inference-router mode this typically means the provider chain was
62
+ exhausted (e.g. RTX fell through on schema, deepseek timed out, claude
63
+ returned a billing error). In direct mode it means the single configured
64
+ provider returned nothing usable after pydantic-ai's own retry budget.
65
+ Callers should catch this alongside ``httpx.HTTPError`` and degrade
66
+ gracefully.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ message: str,
72
+ agent_name: str | None = None,
73
+ **kwargs: Any,
74
+ ):
75
+ """Initialize empty-response error.
76
+
77
+ Args:
78
+ message: Error message
79
+ agent_name: Name of the agent whose call returned empty
80
+ **kwargs: Additional arguments for base exception
81
+ """
82
+ super().__init__(message, error_code="LLM_EMPTY_RESPONSE", **kwargs)
83
+ self.agent_name = agent_name
84
+ if agent_name:
85
+ self.details["agent_name"] = agent_name
86
+
87
+
88
+ class AgentConfigurationError(PydanticAIException):
89
+ """Exception raised when agent configuration is invalid."""
90
+
91
+ def __init__(
92
+ self,
93
+ message: str,
94
+ agent_id: str | None = None,
95
+ config_field: str | None = None,
96
+ **kwargs: Any,
97
+ ):
98
+ """Initialize agent configuration error.
99
+
100
+ Args:
101
+ message: Error message
102
+ agent_id: Agent ID that has configuration issues
103
+ config_field: Specific configuration field that's invalid
104
+ **kwargs: Additional arguments for base exception
105
+ """
106
+ super().__init__(message, **kwargs)
107
+ self.agent_id = agent_id
108
+ self.config_field = config_field
109
+
110
+ if agent_id:
111
+ self.details["agent_id"] = agent_id
112
+ if config_field:
113
+ self.details["config_field"] = config_field
114
+
115
+
116
+ class ModelProviderError(PydanticAIException):
117
+ """Exception raised when model provider encounters an error."""
118
+
119
+ def __init__(
120
+ self,
121
+ message: str,
122
+ provider: str | None = None,
123
+ model: str | None = None,
124
+ status_code: int | None = None,
125
+ **kwargs: Any,
126
+ ):
127
+ """Initialize model provider error.
128
+
129
+ Args:
130
+ message: Error message
131
+ provider: AI provider name
132
+ model: Model name
133
+ status_code: HTTP status code if applicable
134
+ **kwargs: Additional arguments for base exception
135
+ """
136
+ super().__init__(message, **kwargs)
137
+ self.provider = provider
138
+ self.model = model
139
+ self.status_code = status_code
140
+
141
+ if provider:
142
+ self.details["provider"] = provider
143
+ if model:
144
+ self.details["model"] = model
145
+ if status_code:
146
+ self.details["status_code"] = status_code
147
+
148
+
149
+ class APIKeyError(ModelProviderError):
150
+ """Exception raised when API key is missing or invalid."""
151
+
152
+ def __init__(
153
+ self,
154
+ message: str,
155
+ provider: str | None = None,
156
+ **kwargs: Any,
157
+ ):
158
+ """Initialize API key error.
159
+
160
+ Args:
161
+ message: Error message
162
+ provider: AI provider name
163
+ **kwargs: Additional arguments for base exception
164
+ """
165
+ super().__init__(message, provider=provider, error_code="API_KEY_ERROR", **kwargs)
166
+
167
+
168
+ class RateLimitError(ModelProviderError):
169
+ """Exception raised when rate limit is exceeded."""
170
+
171
+ def __init__(
172
+ self,
173
+ message: str,
174
+ provider: str | None = None,
175
+ retry_after: int | None = None,
176
+ **kwargs: Any,
177
+ ):
178
+ """Initialize rate limit error.
179
+
180
+ Args:
181
+ message: Error message
182
+ provider: AI provider name
183
+ retry_after: Seconds to wait before retrying
184
+ **kwargs: Additional arguments for base exception
185
+ """
186
+ super().__init__(message, provider=provider, error_code="RATE_LIMIT", **kwargs)
187
+ self.retry_after = retry_after
188
+
189
+ if retry_after:
190
+ self.details["retry_after"] = retry_after
191
+
192
+
193
+ class ModelNotFoundError(ModelProviderError):
194
+ """Exception raised when requested model is not found."""
195
+
196
+ def __init__(
197
+ self,
198
+ message: str,
199
+ provider: str | None = None,
200
+ model: str | None = None,
201
+ **kwargs: Any,
202
+ ):
203
+ """Initialize model not found error.
204
+
205
+ Args:
206
+ message: Error message
207
+ provider: AI provider name
208
+ model: Model name that was not found
209
+ **kwargs: Additional arguments for base exception
210
+ """
211
+ super().__init__(
212
+ message,
213
+ provider=provider,
214
+ model=model,
215
+ error_code="MODEL_NOT_FOUND",
216
+ **kwargs,
217
+ )
218
+
219
+
220
+ class TokenLimitError(ModelProviderError):
221
+ """Exception raised when token limit is exceeded."""
222
+
223
+ def __init__(
224
+ self,
225
+ message: str,
226
+ provider: str | None = None,
227
+ model: str | None = None,
228
+ token_count: int | None = None,
229
+ token_limit: int | None = None,
230
+ **kwargs: Any,
231
+ ):
232
+ """Initialize token limit error.
233
+
234
+ Args:
235
+ message: Error message
236
+ provider: AI provider name
237
+ model: Model name
238
+ token_count: Number of tokens in request
239
+ token_limit: Maximum token limit
240
+ **kwargs: Additional arguments for base exception
241
+ """
242
+ super().__init__(
243
+ message, provider=provider, model=model, error_code="TOKEN_LIMIT", **kwargs
244
+ )
245
+ self.token_count = token_count
246
+ self.token_limit = token_limit
247
+
248
+ if token_count:
249
+ self.details["token_count"] = token_count
250
+ if token_limit:
251
+ self.details["token_limit"] = token_limit
252
+
253
+
254
+ class AnalysisError(PydanticAIException):
255
+ """Exception raised during test failure analysis."""
256
+
257
+ def __init__(
258
+ self,
259
+ message: str,
260
+ analysis_type: str | None = None,
261
+ test_id: str | None = None,
262
+ **kwargs: Any,
263
+ ):
264
+ """Initialize analysis error.
265
+
266
+ Args:
267
+ message: Error message
268
+ analysis_type: Type of analysis that failed
269
+ test_id: Test ID being analyzed
270
+ **kwargs: Additional arguments for base exception
271
+ """
272
+ super().__init__(message, **kwargs)
273
+ self.analysis_type = analysis_type
274
+ self.test_id = test_id
275
+
276
+ if analysis_type:
277
+ self.details["analysis_type"] = analysis_type
278
+ if test_id:
279
+ self.details["test_id"] = test_id
280
+
281
+
282
+ class AIValidationError(PydanticAIException):
283
+ """Exception raised when AI input validation fails."""
284
+
285
+ def __init__(
286
+ self,
287
+ message: str,
288
+ field_name: str | None = None,
289
+ field_value: Any | None = None,
290
+ **kwargs: Any,
291
+ ):
292
+ """Initialize validation error.
293
+
294
+ Args:
295
+ message: Error message
296
+ field_name: Name of field that failed validation
297
+ field_value: Value that failed validation
298
+ **kwargs: Additional arguments for base exception
299
+ """
300
+ super().__init__(message, error_code="VALIDATION_ERROR", **kwargs)
301
+ self.field_name = field_name
302
+ self.field_value = field_value
303
+
304
+ if field_name:
305
+ self.details["field_name"] = field_name
306
+ if field_value is not None:
307
+ self.details["field_value"] = str(field_value)
308
+
309
+
310
+ class AITimeoutError(PydanticAIException):
311
+ """Exception raised when operation times out."""
312
+
313
+ def __init__(
314
+ self,
315
+ message: str,
316
+ timeout_seconds: float | None = None,
317
+ operation: str | None = None,
318
+ **kwargs: Any,
319
+ ):
320
+ """Initialize timeout error.
321
+
322
+ Args:
323
+ message: Error message
324
+ timeout_seconds: Timeout duration in seconds
325
+ operation: Operation that timed out
326
+ **kwargs: Additional arguments for base exception
327
+ """
328
+ super().__init__(message, error_code="TIMEOUT", **kwargs)
329
+ self.timeout_seconds = timeout_seconds
330
+ self.operation = operation
331
+
332
+ if timeout_seconds:
333
+ self.details["timeout_seconds"] = timeout_seconds
334
+ if operation:
335
+ self.details["operation"] = operation
336
+
337
+
338
+ class ResourceExhaustedError(PydanticAIException):
339
+ """Exception raised when resources are exhausted."""
340
+
341
+ def __init__(
342
+ self,
343
+ message: str,
344
+ resource_type: str | None = None,
345
+ **kwargs: Any,
346
+ ):
347
+ """Initialize resource exhausted error.
348
+
349
+ Args:
350
+ message: Error message
351
+ resource_type: Type of resource that's exhausted
352
+ **kwargs: Additional arguments for base exception
353
+ """
354
+ super().__init__(message, error_code="RESOURCE_EXHAUSTED", **kwargs)
355
+ self.resource_type = resource_type
356
+
357
+ if resource_type:
358
+ self.details["resource_type"] = resource_type
359
+
360
+
361
+ class RetryableError(PydanticAIException):
362
+ """Exception that can be retried."""
363
+
364
+ def __init__(
365
+ self,
366
+ message: str,
367
+ retry_count: int = 0,
368
+ max_retries: int = 3,
369
+ **kwargs: Any,
370
+ ):
371
+ """Initialize retryable error.
372
+
373
+ Args:
374
+ message: Error message
375
+ retry_count: Current retry count
376
+ max_retries: Maximum number of retries
377
+ **kwargs: Additional arguments for base exception
378
+ """
379
+ super().__init__(message, error_code="RETRYABLE", **kwargs)
380
+ self.retry_count = retry_count
381
+ self.max_retries = max_retries
382
+
383
+ self.details["retry_count"] = retry_count
384
+ self.details["max_retries"] = max_retries
385
+
386
+ def can_retry(self) -> bool:
387
+ """Check if error can be retried."""
388
+ return self.retry_count < self.max_retries
389
+
390
+ def increment_retry(self) -> None:
391
+ """Increment retry count."""
392
+ self.retry_count += 1
393
+ self.details["retry_count"] = self.retry_count
394
+
395
+
396
+ class NonRetryableError(PydanticAIException):
397
+ """Exception that should not be retried."""
398
+
399
+ def __init__(
400
+ self,
401
+ message: str,
402
+ **kwargs: Any,
403
+ ):
404
+ """Initialize non-retryable error.
405
+
406
+ Args:
407
+ message: Error message
408
+ **kwargs: Additional arguments for base exception
409
+ """
410
+ super().__init__(message, error_code="NON_RETRYABLE", **kwargs)
411
+
412
+
413
+ def is_retryable_error(error: Exception) -> bool:
414
+ """Check if an error is retryable.
415
+
416
+ Args:
417
+ error: Exception to check
418
+
419
+ Returns:
420
+ True if error is retryable, False otherwise
421
+ """
422
+ # PydanticAI specific retryable errors
423
+ if isinstance(error, RetryableError):
424
+ return error.can_retry()
425
+
426
+ # Rate limit errors are retryable
427
+ if isinstance(error, RateLimitError):
428
+ return True
429
+
430
+ # Non-retryable errors
431
+ if isinstance(error, (NonRetryableError, APIKeyError, ModelNotFoundError, AIValidationError)):
432
+ return False
433
+
434
+ # Network/connection errors are usually retryable
435
+ if isinstance(error, (ConnectionError, AITimeoutError)):
436
+ return True
437
+
438
+ # Server errors (5xx) are retryable, client errors (4xx) are not
439
+ if isinstance(error, ModelProviderError) and error.status_code:
440
+ return error.status_code >= 500
441
+
442
+ # Default to non-retryable
443
+ return False
444
+
445
+
446
+ def get_retry_delay(error: Exception, retry_count: int) -> float:
447
+ """Get retry delay for an error.
448
+
449
+ Args:
450
+ error: Exception that occurred
451
+ retry_count: Current retry count
452
+
453
+ Returns:
454
+ Delay in seconds before retrying
455
+ """
456
+ # Rate limit errors may specify retry delay
457
+ if isinstance(error, RateLimitError) and error.retry_after:
458
+ return float(error.retry_after)
459
+
460
+ # Exponential backoff: 2^retry_count seconds, max 60 seconds
461
+ delay = min(2**retry_count, 60)
462
+ return delay
463
+
464
+
465
+ def categorize_error(error: Exception) -> str:
466
+ """Categorize an error for logging and metrics.
467
+
468
+ Args:
469
+ error: Exception to categorize
470
+
471
+ Returns:
472
+ Error category string
473
+ """
474
+ if isinstance(error, APIKeyError):
475
+ return "authentication"
476
+ elif isinstance(error, RateLimitError):
477
+ return "rate_limit"
478
+ elif isinstance(error, ModelNotFoundError):
479
+ return "model_not_found"
480
+ elif isinstance(error, TokenLimitError):
481
+ return "token_limit"
482
+ elif isinstance(error, AITimeoutError):
483
+ return "timeout"
484
+ elif isinstance(error, AIValidationError):
485
+ return "validation"
486
+ elif isinstance(error, AgentConfigurationError):
487
+ return "configuration"
488
+ elif isinstance(error, ModelProviderError):
489
+ return "provider"
490
+ elif isinstance(error, AnalysisError):
491
+ return "analysis"
492
+ elif isinstance(error, ResourceExhaustedError):
493
+ return "resource_exhausted"
494
+ else:
495
+ return "unknown"
496
+
497
+
498
+ def create_error_response(
499
+ error: Exception,
500
+ request_id: str,
501
+ analysis_time_ms: float = 0.0,
502
+ ) -> dict[str, Any]:
503
+ """Create standardized error response.
504
+
505
+ Args:
506
+ error: Exception that occurred
507
+ request_id: Request ID for tracking
508
+ analysis_time_ms: Time spent on analysis
509
+
510
+ Returns:
511
+ Error response dictionary
512
+ """
513
+ error_category = categorize_error(error)
514
+
515
+ response: dict[str, Any] = {
516
+ "request_id": request_id,
517
+ "success": False,
518
+ "error_message": str(error),
519
+ "error_category": error_category,
520
+ "total_analysis_time_ms": analysis_time_ms,
521
+ "retryable": is_retryable_error(error),
522
+ "metadata": {
523
+ "error_type": type(error).__name__,
524
+ },
525
+ }
526
+
527
+ # Add additional details for PydanticAI exceptions
528
+ if isinstance(error, PydanticAIException):
529
+ response["error_code"] = error.error_code
530
+ response["error_details"] = error.details
531
+ response["metadata"].update(error.to_dict())
532
+
533
+ return response
@@ -0,0 +1,22 @@
1
+ """Abstract interfaces for dependency injection and circular import prevention.
2
+
3
+ These interfaces define contracts for service interactions without
4
+ importing concrete implementations.
5
+ """
6
+
7
+ from .cache_interface import IAnalysisCache, ICacheManager
8
+ from .container_interface import ContainerInfo as IContainerInfo
9
+ from .docker_interface import IDockerService
10
+ from .graph_interface import IGraphService
11
+ from .security_interface import ISecurityService
12
+ from .webhook_interface import IWebhookEventHandler
13
+
14
+ __all__ = [
15
+ "IAnalysisCache",
16
+ "ICacheManager",
17
+ "IContainerInfo",
18
+ "IDockerService",
19
+ "IGraphService",
20
+ "ISecurityService",
21
+ "IWebhookEventHandler",
22
+ ]