kinemotion 0.47.1__py3-none-any.whl → 0.47.3__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.
Potentially problematic release.
This version of kinemotion might be problematic. Click here for more details.
- kinemotion/api.py +35 -868
- kinemotion/cmj/__init__.py +1 -1
- kinemotion/cmj/analysis.py +2 -2
- kinemotion/cmj/api.py +433 -0
- kinemotion/cmj/cli.py +2 -1
- kinemotion/cmj/kinematics.py +165 -79
- kinemotion/cmj/metrics_validator.py +2 -2
- kinemotion/core/__init__.py +0 -4
- kinemotion/core/timing.py +3 -138
- kinemotion/dropjump/api.py +541 -0
- kinemotion/dropjump/cli.py +5 -5
- kinemotion/dropjump/validation_bounds.py +58 -39
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/METADATA +1 -1
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/RECORD +17 -15
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/WHEEL +0 -0
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/entry_points.txt +0 -0
- {kinemotion-0.47.1.dist-info → kinemotion-0.47.3.dist-info}/licenses/LICENSE +0 -0
kinemotion/cmj/kinematics.py
CHANGED
|
@@ -147,133 +147,219 @@ class CMJMetrics:
|
|
|
147
147
|
return result
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
def
|
|
150
|
+
def _calculate_scale_factor(
|
|
151
151
|
positions: NDArray[np.float64],
|
|
152
|
-
velocities: NDArray[np.float64],
|
|
153
|
-
standing_start_frame: float | None,
|
|
154
|
-
lowest_point_frame: float,
|
|
155
152
|
takeoff_frame: float,
|
|
156
153
|
landing_frame: float,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"""Calculate all CMJ metrics from detected phases.
|
|
154
|
+
jump_height: float,
|
|
155
|
+
) -> float:
|
|
156
|
+
"""Calculate meters per normalized unit scaling factor from flight phase.
|
|
161
157
|
|
|
162
158
|
Args:
|
|
163
|
-
positions: Array of vertical positions
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
takeoff_frame: Frame at takeoff (fractional)
|
|
168
|
-
landing_frame: Frame at landing (fractional)
|
|
169
|
-
fps: Video frames per second
|
|
170
|
-
tracking_method: Tracking method used ("foot" or "com")
|
|
159
|
+
positions: Array of vertical positions
|
|
160
|
+
takeoff_frame: Takeoff frame index
|
|
161
|
+
landing_frame: Landing frame index
|
|
162
|
+
jump_height: Calculated jump height in meters
|
|
171
163
|
|
|
172
164
|
Returns:
|
|
173
|
-
|
|
165
|
+
Scale factor (meters per normalized unit)
|
|
174
166
|
"""
|
|
175
|
-
# Calculate flight time from takeoff to landing
|
|
176
|
-
flight_time = (landing_frame - takeoff_frame) / fps
|
|
177
|
-
|
|
178
|
-
# Calculate jump height from flight time using kinematic formula
|
|
179
|
-
# h = g * t^2 / 8 (where t is total flight time)
|
|
180
|
-
g = 9.81 # gravity in m/s^2
|
|
181
|
-
jump_height = (g * flight_time**2) / 8
|
|
182
|
-
|
|
183
|
-
# Determine scaling factor (meters per normalized unit)
|
|
184
|
-
# We use the flight phase displacement in normalized units compared to
|
|
185
|
-
# kinematic jump height
|
|
186
167
|
flight_start_idx = int(takeoff_frame)
|
|
187
168
|
flight_end_idx = int(landing_frame)
|
|
188
169
|
flight_positions = positions[flight_start_idx:flight_end_idx]
|
|
189
170
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
# Peak height is minimum y value (highest point in frame)
|
|
193
|
-
peak_flight_pos = np.min(flight_positions)
|
|
194
|
-
takeoff_pos = positions[flight_start_idx]
|
|
195
|
-
# Displacement is upward (takeoff_pos - peak_pos) because y decreases upward
|
|
196
|
-
flight_displacement = takeoff_pos - peak_flight_pos
|
|
171
|
+
if len(flight_positions) == 0:
|
|
172
|
+
return 0.0
|
|
197
173
|
|
|
198
|
-
|
|
199
|
-
|
|
174
|
+
peak_flight_pos = np.min(flight_positions)
|
|
175
|
+
takeoff_pos = positions[flight_start_idx]
|
|
176
|
+
flight_displacement = takeoff_pos - peak_flight_pos
|
|
177
|
+
|
|
178
|
+
if flight_displacement > 0.001:
|
|
179
|
+
return jump_height / flight_displacement
|
|
180
|
+
return 0.0
|
|
200
181
|
|
|
201
|
-
# Calculate countermovement depth
|
|
202
|
-
if standing_start_frame is not None:
|
|
203
|
-
standing_position = positions[int(standing_start_frame)]
|
|
204
|
-
else:
|
|
205
|
-
# Use position at start of recording if standing not detected
|
|
206
|
-
standing_position = positions[0]
|
|
207
182
|
|
|
183
|
+
def _calculate_countermovement_depth(
|
|
184
|
+
positions: NDArray[np.float64],
|
|
185
|
+
standing_start_frame: float | None,
|
|
186
|
+
lowest_point_frame: float,
|
|
187
|
+
scale_factor: float,
|
|
188
|
+
) -> float:
|
|
189
|
+
"""Calculate countermovement depth in meters.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
positions: Array of vertical positions
|
|
193
|
+
standing_start_frame: Standing phase end frame (or None)
|
|
194
|
+
lowest_point_frame: Lowest point frame index
|
|
195
|
+
scale_factor: Meters per normalized unit
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Countermovement depth in meters
|
|
199
|
+
"""
|
|
200
|
+
standing_position = (
|
|
201
|
+
positions[int(standing_start_frame)]
|
|
202
|
+
if standing_start_frame is not None
|
|
203
|
+
else positions[0]
|
|
204
|
+
)
|
|
208
205
|
lowest_position = positions[int(lowest_point_frame)]
|
|
209
|
-
# Depth in normalized units
|
|
210
206
|
depth_normalized = abs(standing_position - lowest_position)
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
return depth_normalized * scale_factor
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _calculate_phase_durations(
|
|
211
|
+
standing_start_frame: float | None,
|
|
212
|
+
lowest_point_frame: float,
|
|
213
|
+
takeoff_frame: float,
|
|
214
|
+
fps: float,
|
|
215
|
+
) -> tuple[float, float, float]:
|
|
216
|
+
"""Calculate phase durations in seconds.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
standing_start_frame: Standing phase end frame (or None)
|
|
220
|
+
lowest_point_frame: Lowest point frame index
|
|
221
|
+
takeoff_frame: Takeoff frame index
|
|
222
|
+
fps: Frames per second
|
|
213
223
|
|
|
214
|
-
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (eccentric_duration, concentric_duration, total_movement_time)
|
|
226
|
+
"""
|
|
215
227
|
if standing_start_frame is not None:
|
|
216
228
|
eccentric_duration = (lowest_point_frame - standing_start_frame) / fps
|
|
217
229
|
total_movement_time = (takeoff_frame - standing_start_frame) / fps
|
|
218
230
|
else:
|
|
219
|
-
# If no standing phase detected, measure from start
|
|
220
231
|
eccentric_duration = lowest_point_frame / fps
|
|
221
232
|
total_movement_time = takeoff_frame / fps
|
|
222
233
|
|
|
223
234
|
concentric_duration = (takeoff_frame - lowest_point_frame) / fps
|
|
235
|
+
return eccentric_duration, concentric_duration, total_movement_time
|
|
224
236
|
|
|
225
|
-
# Velocity scaling factor: units/frame -> meters/second
|
|
226
|
-
# v_m_s = v_units_frame * fps * scale_factor
|
|
227
|
-
velocity_scale = scale_factor * fps
|
|
228
237
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
def _calculate_peak_velocities(
|
|
239
|
+
velocities: NDArray[np.float64],
|
|
240
|
+
standing_start_frame: float | None,
|
|
241
|
+
lowest_point_frame: float,
|
|
242
|
+
takeoff_frame: float,
|
|
243
|
+
velocity_scale: float,
|
|
244
|
+
) -> tuple[float, float]:
|
|
245
|
+
"""Calculate peak eccentric and concentric velocities.
|
|
235
246
|
|
|
247
|
+
Args:
|
|
248
|
+
velocities: Array of velocities
|
|
249
|
+
standing_start_frame: Standing phase end frame (or None)
|
|
250
|
+
lowest_point_frame: Lowest point frame index
|
|
251
|
+
takeoff_frame: Takeoff frame index
|
|
252
|
+
velocity_scale: Velocity scaling factor
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple of (peak_eccentric_velocity, peak_concentric_velocity)
|
|
256
|
+
"""
|
|
257
|
+
eccentric_start_idx = int(standing_start_frame) if standing_start_frame else 0
|
|
236
258
|
eccentric_end_idx = int(lowest_point_frame)
|
|
237
259
|
eccentric_velocities = velocities[eccentric_start_idx:eccentric_end_idx]
|
|
238
260
|
|
|
261
|
+
peak_eccentric_velocity = 0.0
|
|
239
262
|
if len(eccentric_velocities) > 0:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
peak_eccentric_velocity = float(np.max(eccentric_velocities)) * velocity_scale
|
|
243
|
-
# If max is negative (weird), it means no downward motion detected
|
|
244
|
-
if peak_eccentric_velocity < 0:
|
|
245
|
-
peak_eccentric_velocity = 0.0
|
|
246
|
-
else:
|
|
247
|
-
peak_eccentric_velocity = 0.0
|
|
263
|
+
peak = float(np.max(eccentric_velocities)) * velocity_scale
|
|
264
|
+
peak_eccentric_velocity = max(0.0, peak)
|
|
248
265
|
|
|
249
|
-
# Concentric phase: Upward motion = Negative velocity in image coords
|
|
250
266
|
concentric_start_idx = int(lowest_point_frame)
|
|
251
267
|
concentric_end_idx = int(takeoff_frame)
|
|
252
268
|
concentric_velocities = velocities[concentric_start_idx:concentric_end_idx]
|
|
253
269
|
|
|
270
|
+
peak_concentric_velocity = 0.0
|
|
254
271
|
if len(concentric_velocities) > 0:
|
|
255
|
-
# Peak concentric velocity is minimum value (most negative = fastest upward)
|
|
256
|
-
# We take abs to report magnitude
|
|
257
272
|
peak_concentric_velocity = (
|
|
258
273
|
abs(float(np.min(concentric_velocities))) * velocity_scale
|
|
259
274
|
)
|
|
260
|
-
else:
|
|
261
|
-
peak_concentric_velocity = 0.0
|
|
262
275
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
return peak_eccentric_velocity, peak_concentric_velocity
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _calculate_transition_time(
|
|
280
|
+
velocities: NDArray[np.float64],
|
|
281
|
+
lowest_point_frame: float,
|
|
282
|
+
fps: float,
|
|
283
|
+
) -> float | None:
|
|
284
|
+
"""Calculate transition/amortization time around lowest point.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
velocities: Array of velocities
|
|
288
|
+
lowest_point_frame: Lowest point frame index
|
|
289
|
+
fps: Frames per second
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Transition time in seconds, or None if no transition detected
|
|
293
|
+
"""
|
|
294
|
+
transition_threshold = 0.005
|
|
295
|
+
search_window = int(fps * 0.1)
|
|
267
296
|
|
|
268
297
|
transition_start_idx = max(0, int(lowest_point_frame) - search_window)
|
|
269
298
|
transition_end_idx = min(len(velocities), int(lowest_point_frame) + search_window)
|
|
270
299
|
|
|
271
|
-
transition_frames =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
transition_frames = sum(
|
|
301
|
+
1
|
|
302
|
+
for i in range(transition_start_idx, transition_end_idx)
|
|
303
|
+
if abs(velocities[i]) < transition_threshold
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return transition_frames / fps if transition_frames > 0 else None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def calculate_cmj_metrics(
|
|
310
|
+
positions: NDArray[np.float64],
|
|
311
|
+
velocities: NDArray[np.float64],
|
|
312
|
+
standing_start_frame: float | None,
|
|
313
|
+
lowest_point_frame: float,
|
|
314
|
+
takeoff_frame: float,
|
|
315
|
+
landing_frame: float,
|
|
316
|
+
fps: float,
|
|
317
|
+
tracking_method: str = "foot",
|
|
318
|
+
) -> CMJMetrics:
|
|
319
|
+
"""Calculate all CMJ metrics from detected phases.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
positions: Array of vertical positions (normalized coordinates)
|
|
323
|
+
velocities: Array of vertical velocities
|
|
324
|
+
standing_start_frame: Frame where countermovement begins (fractional)
|
|
325
|
+
lowest_point_frame: Frame at lowest point (fractional)
|
|
326
|
+
takeoff_frame: Frame at takeoff (fractional)
|
|
327
|
+
landing_frame: Frame at landing (fractional)
|
|
328
|
+
fps: Video frames per second
|
|
329
|
+
tracking_method: Tracking method used ("foot" or "com")
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
CMJMetrics object with all calculated metrics.
|
|
333
|
+
"""
|
|
334
|
+
# Calculate jump height from flight time using kinematic formula: h = g*t²/8
|
|
335
|
+
g = 9.81
|
|
336
|
+
flight_time = (landing_frame - takeoff_frame) / fps
|
|
337
|
+
jump_height = (g * flight_time**2) / 8
|
|
338
|
+
|
|
339
|
+
# Calculate scaling factor and derived metrics
|
|
340
|
+
scale_factor = _calculate_scale_factor(
|
|
341
|
+
positions, takeoff_frame, landing_frame, jump_height
|
|
342
|
+
)
|
|
343
|
+
countermovement_depth = _calculate_countermovement_depth(
|
|
344
|
+
positions, standing_start_frame, lowest_point_frame, scale_factor
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
eccentric_duration, concentric_duration, total_movement_time = (
|
|
348
|
+
_calculate_phase_durations(
|
|
349
|
+
standing_start_frame, lowest_point_frame, takeoff_frame, fps
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
velocity_scale = scale_factor * fps
|
|
354
|
+
peak_eccentric_velocity, peak_concentric_velocity = _calculate_peak_velocities(
|
|
355
|
+
velocities,
|
|
356
|
+
standing_start_frame,
|
|
357
|
+
lowest_point_frame,
|
|
358
|
+
takeoff_frame,
|
|
359
|
+
velocity_scale,
|
|
360
|
+
)
|
|
275
361
|
|
|
276
|
-
transition_time =
|
|
362
|
+
transition_time = _calculate_transition_time(velocities, lowest_point_frame, fps)
|
|
277
363
|
|
|
278
364
|
return CMJMetrics(
|
|
279
365
|
jump_height=jump_height,
|
|
@@ -449,7 +449,7 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
449
449
|
# Convert ms to seconds
|
|
450
450
|
flight_time = flight_time_ms / 1000.0
|
|
451
451
|
|
|
452
|
-
# h = g
|
|
452
|
+
# Calculate expected height using kinematic formula: h = g*t²/8
|
|
453
453
|
g = 9.81
|
|
454
454
|
expected_height = (g * flight_time**2) / 8
|
|
455
455
|
error_pct = abs(jump_height - expected_height) / expected_height
|
|
@@ -483,7 +483,7 @@ class CMJMetricsValidator(MetricsValidator):
|
|
|
483
483
|
if velocity is None or jump_height is None:
|
|
484
484
|
return
|
|
485
485
|
|
|
486
|
-
#
|
|
486
|
+
# Calculate expected velocity using kinematic formula: v² = 2*g*h
|
|
487
487
|
g = 9.81
|
|
488
488
|
expected_velocity = (2 * g * jump_height) ** 0.5
|
|
489
489
|
error_pct = abs(velocity - expected_velocity) / expected_velocity
|
kinemotion/core/__init__.py
CHANGED
|
@@ -24,9 +24,7 @@ from .smoothing import (
|
|
|
24
24
|
)
|
|
25
25
|
from .timing import (
|
|
26
26
|
NULL_TIMER,
|
|
27
|
-
CompositeTimer,
|
|
28
27
|
NullTimer,
|
|
29
|
-
OpenTelemetryTimer,
|
|
30
28
|
PerformanceTimer,
|
|
31
29
|
Timer,
|
|
32
30
|
)
|
|
@@ -59,8 +57,6 @@ __all__ = [
|
|
|
59
57
|
"Timer",
|
|
60
58
|
"NullTimer",
|
|
61
59
|
"NULL_TIMER",
|
|
62
|
-
"CompositeTimer",
|
|
63
|
-
"OpenTelemetryTimer",
|
|
64
60
|
# Video I/O
|
|
65
61
|
"VideoProcessor",
|
|
66
62
|
]
|
kinemotion/core/timing.py
CHANGED
|
@@ -26,43 +26,9 @@ Example:
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import time
|
|
29
|
-
from contextlib import AbstractContextManager
|
|
29
|
+
from contextlib import AbstractContextManager
|
|
30
30
|
from typing import Protocol, runtime_checkable
|
|
31
31
|
|
|
32
|
-
# OpenTelemetry related imports, guarded by try-except for optional dependency
|
|
33
|
-
_trace_module = None # This will hold the actual 'trace' module if imported
|
|
34
|
-
_otel_tracer_class = None # This will hold the actual 'Tracer' class if imported
|
|
35
|
-
|
|
36
|
-
try:
|
|
37
|
-
import opentelemetry.trace as _trace_module_import # Import the module directly
|
|
38
|
-
|
|
39
|
-
_otel_tracer_class = (
|
|
40
|
-
_trace_module_import.Tracer
|
|
41
|
-
) # Get the Tracer class from the module
|
|
42
|
-
_trace_module = (
|
|
43
|
-
_trace_module_import # Expose the trace module globally after successful import
|
|
44
|
-
)
|
|
45
|
-
except ImportError:
|
|
46
|
-
pass # No OTel, so these remain None
|
|
47
|
-
|
|
48
|
-
# Now define the global/module-level variables used elsewhere
|
|
49
|
-
# Conditionally expose 'trace' and 'Tracer' aliases
|
|
50
|
-
trace = _trace_module # This will be the actual module or None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class Tracer: # Dummy for type hints if actual Tracer is not available
|
|
54
|
-
pass
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if _otel_tracer_class:
|
|
58
|
-
Tracer = _otel_tracer_class # type: ignore # Override dummy if actual Tracer is available
|
|
59
|
-
|
|
60
|
-
# This _OPENTELEMETRY_AVAILABLE variable is assigned only once,
|
|
61
|
-
# after the try-except block
|
|
62
|
-
_OPENTELEMETRY_AVAILABLE = bool(
|
|
63
|
-
_otel_tracer_class
|
|
64
|
-
) # True if Tracer class was successfully loaded
|
|
65
|
-
|
|
66
32
|
|
|
67
33
|
@runtime_checkable
|
|
68
34
|
class Timer(Protocol):
|
|
@@ -152,11 +118,12 @@ class NullTimer:
|
|
|
152
118
|
The context manager protocol (__enter__/__exit__) has minimal overhead.
|
|
153
119
|
|
|
154
120
|
Args:
|
|
155
|
-
name:
|
|
121
|
+
name: Operation name (unused in no-op implementation)
|
|
156
122
|
|
|
157
123
|
Returns:
|
|
158
124
|
Singleton null context manager
|
|
159
125
|
"""
|
|
126
|
+
del name # Intentionally unused - satisfies Timer protocol
|
|
160
127
|
return _NULL_CONTEXT
|
|
161
128
|
|
|
162
129
|
def get_metrics(self) -> dict[str, float]:
|
|
@@ -278,105 +245,3 @@ class PerformanceTimer:
|
|
|
278
245
|
A copy of the metrics dictionary to prevent external modification.
|
|
279
246
|
"""
|
|
280
247
|
return self.metrics.copy()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
@contextmanager
|
|
284
|
-
def _composite_context_manager(contexts: list[AbstractContextManager[None]]):
|
|
285
|
-
"""Helper to combine multiple context managers into one.
|
|
286
|
-
|
|
287
|
-
Uses ExitStack to manage entering and exiting multiple contexts transparently.
|
|
288
|
-
"""
|
|
289
|
-
with ExitStack() as stack:
|
|
290
|
-
for ctx in contexts:
|
|
291
|
-
stack.enter_context(ctx)
|
|
292
|
-
yield
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
class CompositeTimer:
|
|
296
|
-
"""Timer that delegates measurements to multiple underlying timers.
|
|
297
|
-
|
|
298
|
-
Useful for enabling both local performance timing (for JSON output)
|
|
299
|
-
and distributed tracing (OpenTelemetry) simultaneously.
|
|
300
|
-
"""
|
|
301
|
-
|
|
302
|
-
__slots__ = ("timers",)
|
|
303
|
-
|
|
304
|
-
def __init__(self, timers: list[Timer]) -> None:
|
|
305
|
-
"""Initialize composite timer.
|
|
306
|
-
|
|
307
|
-
Args:
|
|
308
|
-
timers: List of timer instances to delegate to
|
|
309
|
-
"""
|
|
310
|
-
self.timers = timers
|
|
311
|
-
|
|
312
|
-
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
313
|
-
"""Measure using all underlying timers.
|
|
314
|
-
|
|
315
|
-
Args:
|
|
316
|
-
name: Name of the operation
|
|
317
|
-
|
|
318
|
-
Returns:
|
|
319
|
-
Context manager that manages all underlying timers
|
|
320
|
-
"""
|
|
321
|
-
contexts = [timer.measure(name) for timer in self.timers]
|
|
322
|
-
return _composite_context_manager(contexts)
|
|
323
|
-
|
|
324
|
-
def get_metrics(self) -> dict[str, float]:
|
|
325
|
-
"""Get combined metrics from all timers.
|
|
326
|
-
|
|
327
|
-
Returns:
|
|
328
|
-
Merged dictionary of metrics
|
|
329
|
-
"""
|
|
330
|
-
metrics = {}
|
|
331
|
-
for timer in self.timers:
|
|
332
|
-
metrics.update(timer.get_metrics())
|
|
333
|
-
return metrics
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
class OpenTelemetryTimer:
|
|
337
|
-
"""Timer implementation that creates OpenTelemetry spans.
|
|
338
|
-
|
|
339
|
-
Maps 'measure' calls to OTel spans. Requires opentelemetry-api installed.
|
|
340
|
-
"""
|
|
341
|
-
|
|
342
|
-
__slots__ = ("tracer",)
|
|
343
|
-
|
|
344
|
-
def __init__(self, tracer: Tracer | None = None) -> None:
|
|
345
|
-
"""Initialize OTel timer.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
tracer: Optional OTel tracer. If None, gets tracer for module name.
|
|
349
|
-
"""
|
|
350
|
-
if not _OPENTELEMETRY_AVAILABLE:
|
|
351
|
-
self.tracer = None # Always initialize self.tracer for __slots__
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
if trace is not None:
|
|
355
|
-
self.tracer = tracer or trace.get_tracer(__name__)
|
|
356
|
-
else:
|
|
357
|
-
# This branch should ideally not be reached if _OPENTELEMETRY_AVAILABLE
|
|
358
|
-
# is True but trace is None (meaning import succeeded but trace was not what
|
|
359
|
-
# expected). Defensive programming: ensure self.tracer is set.
|
|
360
|
-
self.tracer = None
|
|
361
|
-
|
|
362
|
-
def measure(self, name: str) -> AbstractContextManager[None]:
|
|
363
|
-
"""Start an OpenTelemetry span.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
name: Name of the span
|
|
367
|
-
|
|
368
|
-
Returns:
|
|
369
|
-
Span context manager (compatible with AbstractContextManager)
|
|
370
|
-
"""
|
|
371
|
-
if not _OPENTELEMETRY_AVAILABLE or self.tracer is None:
|
|
372
|
-
return _NULL_CONTEXT # Return the no-op context
|
|
373
|
-
|
|
374
|
-
return self.tracer.start_as_current_span(name)
|
|
375
|
-
|
|
376
|
-
def get_metrics(self) -> dict[str, float]:
|
|
377
|
-
"""Return empty metrics (OTel handles export asynchronously).
|
|
378
|
-
|
|
379
|
-
Returns:
|
|
380
|
-
Empty dictionary
|
|
381
|
-
"""
|
|
382
|
-
return {}
|