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,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)
|