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,421 @@
1
+ """Performance profiling utilities for measuring memory usage and execution time.
2
+
3
+ This module provides decorators and context managers for detailed performance profiling
4
+ of memory usage, CPU time, and wall clock time, with optional callback reporting
5
+ for incremental progress monitoring.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import gc
12
+ import logging
13
+ import time
14
+ from collections.abc import Callable
15
+ from typing import (
16
+ Any,
17
+ ParamSpec,
18
+ TypeVar,
19
+ cast,
20
+ overload,
21
+ )
22
+
23
+ try:
24
+ import resource
25
+
26
+ import psutil
27
+
28
+ HAS_PSUTIL = True
29
+ except ImportError:
30
+ HAS_PSUTIL = False
31
+
32
+ # Type variables for decorators
33
+ F = TypeVar("F", bound=Callable[..., Any])
34
+ T = TypeVar("T")
35
+ P = ParamSpec("P") # For preserving parameter types in decorators
36
+ R = TypeVar("R") # For preserving return types
37
+
38
+ # Configure logger
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class ProfilingResult:
43
+ """Container for profiling measurements and metrics."""
44
+
45
+ def __init__(self) -> None:
46
+ """Initialize with empty metrics."""
47
+ self.metrics: dict[str, Any] = {
48
+ "start_time_ns": 0,
49
+ "end_time_ns": 0,
50
+ "elapsed_time_s": 0.0,
51
+ "cpu_percent": 0.0,
52
+ "memory_start_mb": 0.0,
53
+ "memory_peak_mb": 0.0,
54
+ "memory_end_mb": 0.0,
55
+ "memory_diff_mb": 0.0,
56
+ }
57
+
58
+ # Stats collection enabled flag
59
+ self._enabled = True
60
+
61
+ def start(self) -> None:
62
+ """
63
+ Record start metrics including timestamp and memory usage.
64
+
65
+ Forces garbage collection to establish a clean baseline for memory measurements.
66
+ """
67
+ if not self._enabled:
68
+ return
69
+
70
+ # Force garbage collection to get a clean baseline
71
+ gc.collect()
72
+
73
+ # Record starting metrics
74
+ self.metrics["start_time_ns"] = time.time_ns()
75
+ self.metrics["memory_start_mb"] = self._get_memory_usage()
76
+
77
+ def end(self) -> None:
78
+ """
79
+ Record end metrics and compute differentials.
80
+
81
+ Calculates elapsed time, memory usage changes, and CPU percentage
82
+ if psutil is available.
83
+ """
84
+ if not self._enabled:
85
+ return
86
+
87
+ # Record end time and compute elapsed
88
+ self.metrics["end_time_ns"] = time.time_ns()
89
+ self.metrics["elapsed_time_s"] = (
90
+ self.metrics["end_time_ns"] - self.metrics["start_time_ns"]
91
+ ) / 1_000_000_000 # Convert ns to s
92
+
93
+ # Memory metrics
94
+ self.metrics["memory_end_mb"] = self._get_memory_usage()
95
+ self.metrics["memory_diff_mb"] = (
96
+ self.metrics["memory_end_mb"] - self.metrics["memory_start_mb"]
97
+ )
98
+
99
+ # Get CPU percentage if psutil is available
100
+ if HAS_PSUTIL:
101
+ try:
102
+ self.metrics["cpu_percent"] = psutil.Process().cpu_percent()
103
+ except (
104
+ psutil.NoSuchProcess,
105
+ psutil.AccessDenied,
106
+ psutil.ZombieProcess,
107
+ ):
108
+ pass
109
+
110
+ def _get_memory_usage(self) -> float:
111
+ """
112
+ Get current memory usage in MB.
113
+
114
+ Prefers psutil for cross-platform support, falls back to resource module
115
+ on Unix-like systems.
116
+
117
+ Returns:
118
+ Memory usage in megabytes, or 0.0 if measurement fails
119
+ """
120
+ try:
121
+ if HAS_PSUTIL:
122
+ # Prefer psutil for cross-platform support
123
+ process = psutil.Process()
124
+ mem_info = process.memory_info()
125
+ # Use RSS (Resident Set Size) as the memory metric
126
+ return mem_info.rss / (1024 * 1024)
127
+ else:
128
+ # Fallback to resource module on Unix-like systems
129
+ return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
130
+ except (
131
+ psutil.NoSuchProcess,
132
+ psutil.AccessDenied,
133
+ psutil.ZombieProcess,
134
+ OSError,
135
+ ) as e:
136
+ logger.warning("Failed to get memory usage: %s", e)
137
+ return 0.0
138
+
139
+ def update_peak_memory(self) -> None:
140
+ """
141
+ Update peak memory usage if current usage exceeds previous peak.
142
+
143
+ Should be called periodically during long-running operations to track
144
+ the maximum memory usage throughout the profiling session.
145
+ """
146
+ if not self._enabled:
147
+ return
148
+
149
+ current = self._get_memory_usage()
150
+ if current > self.metrics["memory_peak_mb"]:
151
+ self.metrics["memory_peak_mb"] = current
152
+
153
+ def disable(self) -> None:
154
+ """
155
+ Disable stats collection.
156
+
157
+ When disabled, profiling methods will return early without
158
+ collecting metrics, reducing overhead.
159
+ """
160
+ self._enabled = False
161
+
162
+ def enable(self) -> None:
163
+ """
164
+ Enable stats collection.
165
+
166
+ Re-enables profiling after it has been disabled, allowing
167
+ metrics to be collected again.
168
+ """
169
+ self._enabled = True
170
+
171
+ def __str__(self) -> str:
172
+ """
173
+ Return a formatted string with profiling results.
174
+
175
+ Returns:
176
+ Human-readable string showing execution time and memory usage
177
+ """
178
+ return (
179
+ f"Time: {self.metrics['elapsed_time_s']:.2f}s, "
180
+ f"Memory: {self.metrics['memory_diff_mb']:.2f}MB (peak: {self.metrics['memory_peak_mb']:.2f}MB)"
181
+ )
182
+
183
+
184
+ class ProgressReporter:
185
+ """Reports progress for long-running operations."""
186
+
187
+ def __init__(
188
+ self,
189
+ total_steps: int,
190
+ callback: Callable[[int, int, dict[str, Any]], None] | None = None,
191
+ report_interval: float = 1.0,
192
+ name: str = "operation",
193
+ ) -> None:
194
+ """Initialize progress reporter.
195
+
196
+ Args:
197
+ total_steps: Total number of steps in the operation
198
+ callback: Optional callback to report progress (current, total, metrics)
199
+ report_interval: Minimum seconds between progress reports
200
+ name: Name of the operation for logging
201
+ """
202
+ self.total_steps = total_steps
203
+ self.current_step = 0
204
+ self.callback = callback
205
+ self.report_interval = report_interval
206
+ self.name = name
207
+ self.last_report_time = 0.0
208
+ self.start_time = time.time()
209
+ self.last_metrics: dict[str, Any] = {}
210
+
211
+ # Initial report at 0%
212
+ self._maybe_report()
213
+
214
+ def update(self, steps: int = 1, **kwargs: Any) -> None:
215
+ """Update progress by incrementing the current step count.
216
+
217
+ Args:
218
+ steps: Number of steps completed
219
+ **kwargs: Additional metrics to include in the report
220
+ """
221
+ self.current_step += steps
222
+ self.last_metrics.update(kwargs)
223
+ self._maybe_report()
224
+
225
+ def complete(self, **kwargs: Any) -> None:
226
+ """Mark the operation as complete.
227
+
228
+ Args:
229
+ **kwargs: Additional metrics to include in the report
230
+ """
231
+ self.current_step = self.total_steps
232
+ self.last_metrics.update(kwargs)
233
+ self._report(force=True)
234
+
235
+ def _maybe_report(self) -> None:
236
+ """Report progress if sufficient time has passed since last report."""
237
+ now = time.time()
238
+ if now - self.last_report_time >= self.report_interval:
239
+ self._report()
240
+
241
+ def _report(self, force: bool = False) -> None:
242
+ """Report current progress via callback and logging.
243
+
244
+ Args:
245
+ force: Force report regardless of time interval
246
+ """
247
+ # If force is True, always report regardless of time interval
248
+ # Otherwise we might want to throttle reports based on frequency
249
+ if not force and (time.time() - self.last_report_time) < self.report_interval:
250
+ return
251
+
252
+ # Update the last report time
253
+ self.last_report_time = time.time()
254
+
255
+ # Calculate percentage
256
+ percent = (
257
+ min(100, int((self.current_step / self.total_steps) * 100))
258
+ if self.total_steps > 0
259
+ else 0
260
+ )
261
+
262
+ # Calculate ETA if we have progress
263
+ metrics = self.last_metrics.copy()
264
+ if self.current_step > 0 and self.current_step < self.total_steps:
265
+ elapsed = time.time() - self.start_time
266
+ eta = (elapsed / self.current_step) * (self.total_steps - self.current_step)
267
+ metrics["eta_seconds"] = eta
268
+
269
+ # Add timestamp
270
+ metrics["timestamp"] = time.time()
271
+
272
+ # Log progress
273
+ logger.info(
274
+ "%s: %d%% complete (%d/%d steps)",
275
+ self.name,
276
+ percent,
277
+ self.current_step,
278
+ self.total_steps,
279
+ )
280
+
281
+ # Call the callback if provided
282
+ if self.callback:
283
+ try:
284
+ self.callback(self.current_step, self.total_steps, metrics)
285
+ except (TypeError, ValueError, RuntimeError) as e:
286
+ logger.error("Progress callback error: %s", e)
287
+
288
+
289
+ @overload
290
+ def profile[**P, R](func: Callable[P, R]) -> Callable[P, R]: ...
291
+
292
+
293
+ @overload
294
+ def profile(
295
+ *,
296
+ memory: bool = True,
297
+ log_level: int = logging.INFO,
298
+ callback: Callable[[dict[str, Any]], None] | None = None,
299
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
300
+
301
+
302
+ def profile[**P, R](
303
+ func: Callable[P, R] | None = None,
304
+ *,
305
+ memory: bool = True,
306
+ log_level: int = logging.INFO,
307
+ callback: Callable[[dict[str, Any]], None] | None = None,
308
+ ) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]:
309
+ """Decorator for profiling function execution time and memory usage.
310
+
311
+ Can be used as @profile or with parameters @profile(memory=False)
312
+
313
+ Args:
314
+ func: The function to decorate
315
+ memory: Whether to track memory usage
316
+ log_level: Level to log profile results
317
+ callback: Optional callback to receive profiling results
318
+
319
+ Returns:
320
+ Decorated function
321
+ """
322
+
323
+ def decorator(fn: Callable[P, R]) -> Callable[P, R]:
324
+ @functools.wraps(fn)
325
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
326
+ # Create profiling context
327
+ profiler = ProfilingResult()
328
+
329
+ # Start profiling
330
+ if not memory:
331
+ profiler.disable()
332
+ profiler.start()
333
+
334
+ try:
335
+ # Execute the function
336
+ result = fn(*args, **kwargs)
337
+ return result
338
+ finally:
339
+ # End profiling
340
+ profiler.end()
341
+
342
+ # Log results
343
+ logger.log(
344
+ log_level,
345
+ "PROFILE %s: %s",
346
+ getattr(fn, "__qualname__", repr(fn)),
347
+ str(profiler),
348
+ )
349
+
350
+ # Call callback if provided
351
+ if callback:
352
+ callback(profiler.metrics)
353
+
354
+ return cast(Callable[P, R], wrapper)
355
+
356
+ if func is not None:
357
+ return decorator(func)
358
+ return decorator
359
+
360
+
361
+ def progressive_task(
362
+ total_steps: int | None = None,
363
+ name: str | None = None,
364
+ report_interval: float = 1.0,
365
+ callback: Callable[[int, int, dict[str, Any]], None] | None = None,
366
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
367
+ """Decorator for tracking progress of long-running tasks.
368
+
369
+ The decorated function receives a progress_reporter argument that
370
+ can be used to update progress during execution.
371
+
372
+ Args:
373
+ total_steps: Total number of steps or None to determine from first arg
374
+ name: Operation name for logging
375
+ report_interval: Minimum seconds between progress reports
376
+ callback: Optional callback for progress updates
377
+
378
+ Returns:
379
+ Decorator function
380
+ """
381
+
382
+ def decorator(func: Callable[P, R]) -> Callable[P, R]:
383
+ func_qualname = getattr(func, "__qualname__", getattr(func, "__name__", repr(func)))
384
+
385
+ @functools.wraps(func)
386
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
387
+ # Determine operation name
388
+ operation_name = name or func_qualname
389
+
390
+ # Determine total steps
391
+ steps = total_steps
392
+ if steps is None and args and isinstance(args[0], (int, float)):
393
+ steps = int(args[0])
394
+ if steps is None:
395
+ steps = 100 # Default if we can't determine
396
+
397
+ # Create progress reporter
398
+ reporter = ProgressReporter(
399
+ total_steps=steps,
400
+ callback=callback,
401
+ report_interval=report_interval,
402
+ name=operation_name,
403
+ )
404
+
405
+ # Add reporter to kwargs — cast needed because ParamSpec kwargs
406
+ # doesn't allow arbitrary key assignment
407
+ kw = cast(dict[str, Any], kwargs)
408
+ kw["progress_reporter"] = reporter
409
+
410
+ # Execute function with progress tracking
411
+ try:
412
+ result = func(*args, **kwargs)
413
+ finally:
414
+ # Ensure we mark as complete
415
+ reporter.complete()
416
+
417
+ return result
418
+
419
+ return cast(Callable[P, R], wrapper)
420
+
421
+ return decorator
@@ -0,0 +1,240 @@
1
+ """Standardized response utilities for the refactored API.
2
+
3
+ This module provides utilities for creating consistent response formats across
4
+ all API endpoints. It includes:
5
+
6
+ - StandardResponse: Pydantic model for standardized API responses
7
+ - ResponseBuilder: Builder class for creating success and error responses
8
+ - Convenience functions for quick response creation
9
+
10
+ The response format includes:
11
+ - success: Boolean indicating success/failure
12
+ - data: The actual response data
13
+ - meta: Metadata including execution time, source, and timestamp
14
+ - links: Optional related links for API navigation
15
+ - error: Error details when applicable
16
+
17
+ Usage:
18
+ builder = ResponseBuilder(request)
19
+ return builder.success(data, source="container")
20
+
21
+ # Or use convenience functions
22
+ return build_success_response(data, request=request)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import time
28
+ from datetime import UTC, datetime
29
+ from typing import Any
30
+
31
+ from fastapi import Request
32
+ from pydantic import BaseModel
33
+
34
+
35
+ class StandardResponse(BaseModel):
36
+ """Standard response format for all API endpoints.
37
+
38
+ Attributes:
39
+ success: Boolean indicating whether the request was successful
40
+ data: The actual response data (can be dict, list, or primitive types)
41
+ meta: Metadata including execution time, source, and timestamp
42
+ links: Optional related links for API navigation
43
+ error: Error details when request fails (mutually exclusive with data)
44
+ """
45
+
46
+ success: bool
47
+ data: dict[str, Any] | list[Any] | str | int | bool | None = None
48
+ meta: dict[str, Any]
49
+ links: dict[str, str] | None = None
50
+ error: dict[str, Any] | None = None
51
+
52
+
53
+ class ResponseBuilder:
54
+ """Builder for creating standardized responses.
55
+
56
+ Provides methods to build consistent success and error responses with
57
+ automatic metadata generation including execution time and timestamps.
58
+
59
+ Attributes:
60
+ request: Optional FastAPI request object for context
61
+ start_time: Timestamp when builder was created (for execution time calculation)
62
+ """
63
+
64
+ def __init__(self, request: Request | None = None):
65
+ """
66
+ Initialize the response builder.
67
+
68
+ Args:
69
+ request: Optional FastAPI request object for generating contextual links
70
+ """
71
+ self.request = request
72
+ self.start_time = time.time()
73
+
74
+ def success(
75
+ self,
76
+ data: Any,
77
+ source: str = "local",
78
+ container_name: str | None = None,
79
+ additional_meta: dict[str, Any] | None = None,
80
+ ) -> dict[str, Any]:
81
+ """
82
+ Build a success response with standardized metadata.
83
+
84
+ Args:
85
+ data: The response data to include
86
+ source: Source of the data (e.g., "local", "container", "cached")
87
+ container_name: Optional name of the container that processed the request
88
+ additional_meta: Optional additional metadata to include
89
+
90
+ Returns:
91
+ Dictionary containing standardized success response
92
+ """
93
+
94
+ execution_time = int((time.time() - self.start_time) * 1000)
95
+
96
+ meta = {
97
+ "source": source,
98
+ "execution_time_ms": execution_time,
99
+ "api_version": "v2",
100
+ "timestamp": datetime.now(UTC).isoformat(),
101
+ }
102
+
103
+ if container_name:
104
+ meta["container_name"] = container_name
105
+
106
+ if additional_meta:
107
+ meta.update(additional_meta)
108
+
109
+ response = {"success": True, "data": data, "meta": meta}
110
+
111
+ # Add links if request context is available
112
+ if self.request:
113
+ response["links"] = self._build_links()
114
+
115
+ return response
116
+
117
+ def error(
118
+ self,
119
+ error_message: str,
120
+ error_code: str | None = None,
121
+ suggestions: list[str] | None = None,
122
+ available_endpoints: list[str] | None = None,
123
+ ) -> dict[str, Any]:
124
+ """
125
+ Build an error response with actionable guidance.
126
+
127
+ Args:
128
+ error_message: Human-readable error message
129
+ error_code: Optional error code for programmatic handling
130
+ suggestions: Optional list of suggestions to resolve the error
131
+ available_endpoints: Optional list of available endpoints for guidance
132
+
133
+ Returns:
134
+ Dictionary containing standardized error response
135
+ """
136
+
137
+ execution_time = int((time.time() - self.start_time) * 1000)
138
+
139
+ error_data: dict[str, Any] = {
140
+ "message": error_message,
141
+ "timestamp": datetime.now(UTC).isoformat(),
142
+ }
143
+
144
+ if error_code:
145
+ error_data["code"] = error_code
146
+
147
+ if suggestions:
148
+ error_data["suggestions"] = suggestions
149
+
150
+ if available_endpoints:
151
+ error_data["available_endpoints"] = available_endpoints
152
+
153
+ return {
154
+ "success": False,
155
+ "data": None,
156
+ "meta": {
157
+ "execution_time_ms": execution_time,
158
+ "api_version": "v2",
159
+ "timestamp": datetime.now(UTC).isoformat(),
160
+ },
161
+ "error": error_data,
162
+ }
163
+
164
+ def _build_links(self) -> dict[str, str]:
165
+ """
166
+ Build related links based on the current request.
167
+
168
+ Generates contextual links for API navigation based on the request path.
169
+
170
+ Returns:
171
+ Dictionary of related links for the current endpoint
172
+ """
173
+ if not self.request:
174
+ return {}
175
+
176
+ base_url = f"{self.request.url.scheme}://{self.request.url.netloc}"
177
+ path = str(self.request.url.path)
178
+
179
+ links = {"sel": f"{base_url}{path}"}
180
+
181
+ # Add contextual links based on the path
182
+ if "/graphs/" in path:
183
+ graph_id = path.split("/graphs/")[1].split("/")[0]
184
+ links.update(
185
+ {
186
+ "graph_details": f"{base_url}/api/v2/graphs/{graph_id}",
187
+ "graph_nodes": f"{base_url}/api/v2/graphs/{graph_id}/nodes",
188
+ "all_graphs": f"{base_url}/api/v2/graphs/",
189
+ }
190
+ )
191
+ elif "/containers/" in path:
192
+ links["all_containers"] = f"{base_url}/api/v2/containers/"
193
+
194
+ return links
195
+
196
+
197
+ def build_success_response(
198
+ data: Any,
199
+ source: str = "local",
200
+ container_name: str | None = None,
201
+ request: Request | None = None,
202
+ ) -> dict[str, Any]:
203
+ """
204
+ Convenience function to build success responses.
205
+
206
+ Args:
207
+ data: The response data to include
208
+ source: Source of the data (e.g., "local", "container", "cached")
209
+ container_name: Optional name of the container that processed the request
210
+ request: Optional FastAPI request object for generating contextual links
211
+
212
+ Returns:
213
+ Dictionary containing standardized success response
214
+ """
215
+ builder = ResponseBuilder(request)
216
+ return builder.success(data, source, container_name)
217
+
218
+
219
+ def build_error_response(
220
+ error_message: str,
221
+ error_code: str | None = None,
222
+ suggestions: list[str] | None = None,
223
+ available_endpoints: list[str] | None = None,
224
+ request: Request | None = None,
225
+ ) -> dict[str, Any]:
226
+ """
227
+ Convenience function to build error responses.
228
+
229
+ Args:
230
+ error_message: Human-readable error message
231
+ error_code: Optional error code for programmatic handling
232
+ suggestions: Optional list of suggestions to resolve the error
233
+ available_endpoints: Optional list of available endpoints for guidance
234
+ request: Optional FastAPI request object for generating contextual links
235
+
236
+ Returns:
237
+ Dictionary containing standardized error response
238
+ """
239
+ builder = ResponseBuilder(request)
240
+ return builder.error(error_message, error_code, suggestions, available_endpoints)