fix(sprints): move velocity endpoint before {sprint_id} routes
FastAPI processes routes in order, so /velocity must be defined
before /{sprint_id} to prevent "velocity" from being parsed as a UUID.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -384,6 +384,68 @@ async def get_active_sprint(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/velocity",
|
||||||
|
response_model=list[SprintVelocity],
|
||||||
|
summary="Get Project Velocity",
|
||||||
|
description="""
|
||||||
|
Get velocity metrics for completed sprints in the project.
|
||||||
|
|
||||||
|
**Authentication**: Required (Bearer token)
|
||||||
|
**Authorization**: Project owner or superuser
|
||||||
|
|
||||||
|
Returns velocity data for the last N completed sprints (default 5).
|
||||||
|
Useful for capacity planning and sprint estimation.
|
||||||
|
|
||||||
|
**Rate Limit**: 60 requests/minute
|
||||||
|
""",
|
||||||
|
operation_id="get_project_velocity",
|
||||||
|
)
|
||||||
|
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
|
||||||
|
async def get_project_velocity(
|
||||||
|
request: Request,
|
||||||
|
project_id: UUID,
|
||||||
|
limit: int = Query(
|
||||||
|
default=5,
|
||||||
|
ge=1,
|
||||||
|
le=20,
|
||||||
|
description="Number of completed sprints to include",
|
||||||
|
),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Get velocity metrics for completed sprints.
|
||||||
|
|
||||||
|
Returns planned points, actual velocity, and velocity ratio
|
||||||
|
for the last N completed sprints, ordered chronologically.
|
||||||
|
"""
|
||||||
|
# Verify project access
|
||||||
|
await verify_project_ownership(db, project_id, current_user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
velocity_data = await sprint_crud.get_velocity(
|
||||||
|
db, project_id=project_id, limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
SprintVelocity(
|
||||||
|
sprint_number=item["sprint_number"],
|
||||||
|
sprint_name=item["sprint_name"],
|
||||||
|
planned_points=item["planned_points"],
|
||||||
|
velocity=item["velocity"],
|
||||||
|
velocity_ratio=item["velocity_ratio"],
|
||||||
|
)
|
||||||
|
for item in velocity_data
|
||||||
|
]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error getting velocity for project {project_id}: {e!s}", exc_info=True
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{sprint_id}",
|
"/{sprint_id}",
|
||||||
response_model=SprintResponse,
|
response_model=SprintResponse,
|
||||||
@@ -1116,70 +1178,3 @@ async def remove_issue_from_sprint(
|
|||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Sprint Metrics Endpoints
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/velocity",
|
|
||||||
response_model=list[SprintVelocity],
|
|
||||||
summary="Get Project Velocity",
|
|
||||||
description="""
|
|
||||||
Get velocity metrics for completed sprints in the project.
|
|
||||||
|
|
||||||
**Authentication**: Required (Bearer token)
|
|
||||||
**Authorization**: Project owner or superuser
|
|
||||||
|
|
||||||
Returns velocity data for the last N completed sprints (default 5).
|
|
||||||
Useful for capacity planning and sprint estimation.
|
|
||||||
|
|
||||||
**Rate Limit**: 60 requests/minute
|
|
||||||
""",
|
|
||||||
operation_id="get_project_velocity",
|
|
||||||
)
|
|
||||||
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
|
|
||||||
async def get_project_velocity(
|
|
||||||
request: Request,
|
|
||||||
project_id: UUID,
|
|
||||||
limit: int = Query(
|
|
||||||
default=5,
|
|
||||||
ge=1,
|
|
||||||
le=20,
|
|
||||||
description="Number of completed sprints to include",
|
|
||||||
),
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: AsyncSession = Depends(get_db),
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
Get velocity metrics for completed sprints.
|
|
||||||
|
|
||||||
Returns planned points, actual velocity, and velocity ratio
|
|
||||||
for the last N completed sprints, ordered chronologically.
|
|
||||||
"""
|
|
||||||
# Verify project access
|
|
||||||
await verify_project_ownership(db, project_id, current_user)
|
|
||||||
|
|
||||||
try:
|
|
||||||
velocity_data = await sprint_crud.get_velocity(
|
|
||||||
db, project_id=project_id, limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
return [
|
|
||||||
SprintVelocity(
|
|
||||||
sprint_number=item["sprint_number"],
|
|
||||||
sprint_name=item["sprint_name"],
|
|
||||||
planned_points=item["planned_points"],
|
|
||||||
velocity=item["velocity"],
|
|
||||||
velocity_ratio=item["velocity_ratio"],
|
|
||||||
)
|
|
||||||
for item in velocity_data
|
|
||||||
]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error getting velocity for project {project_id}: {e!s}", exc_info=True
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|||||||
Reference in New Issue
Block a user