Enhanced query endpoints with filtering and global artifacts/tags APIs (#18)
- Add search and tag filters to /api/v1/uploads endpoint - Add sort/order parameters to /api/v1/uploads endpoint - Add min_size/max_size filters to package artifacts endpoint - Add sort/order parameters to package artifacts endpoint - Add from/to date filters to tags endpoint - Add global /api/v1/artifacts endpoint with project/package/tag/size/date filters - Add global /api/v1/tags endpoint with project/package/search/date filters - Add GlobalArtifactResponse and GlobalTagResponse schemas
This commit is contained in:
@@ -92,6 +92,8 @@ from .schemas import (
|
||||
CrossProjectDeduplicationResponse,
|
||||
TimeBasedStatsResponse,
|
||||
StatsReportResponse,
|
||||
GlobalArtifactResponse,
|
||||
GlobalTagResponse,
|
||||
)
|
||||
from .metadata import extract_metadata
|
||||
from .config import get_settings
|
||||
@@ -2034,6 +2036,12 @@ def list_tags(
|
||||
search: Optional[str] = Query(default=None, description="Search by tag name"),
|
||||
sort: str = Query(default="name", description="Sort field (name, created_at)"),
|
||||
order: str = Query(default="asc", description="Sort order (asc, desc)"),
|
||||
from_date: Optional[datetime] = Query(
|
||||
default=None, alias="from", description="Filter tags created after this date"
|
||||
),
|
||||
to_date: Optional[datetime] = Query(
|
||||
default=None, alias="to", description="Filter tags created before this date"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
project = db.query(Project).filter(Project.name == project_name).first()
|
||||
@@ -2079,6 +2087,12 @@ def list_tags(
|
||||
)
|
||||
)
|
||||
|
||||
# Apply date range filters
|
||||
if from_date:
|
||||
query = query.filter(Tag.created_at >= from_date)
|
||||
if to_date:
|
||||
query = query.filter(Tag.created_at <= to_date)
|
||||
|
||||
# Get total count before pagination
|
||||
total = query.count()
|
||||
|
||||
@@ -2455,9 +2469,19 @@ def list_package_artifacts(
|
||||
created_before: Optional[datetime] = Query(
|
||||
default=None, description="Filter artifacts created before this date"
|
||||
),
|
||||
min_size: Optional[int] = Query(
|
||||
default=None, ge=0, description="Minimum artifact size in bytes"
|
||||
),
|
||||
max_size: Optional[int] = Query(
|
||||
default=None, ge=0, description="Maximum artifact size in bytes"
|
||||
),
|
||||
sort: Optional[str] = Query(
|
||||
default=None, description="Sort field: created_at, size, original_name"
|
||||
),
|
||||
order: Optional[str] = Query(default="desc", description="Sort order: asc or desc"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List all unique artifacts uploaded to a package"""
|
||||
"""List all unique artifacts uploaded to a package with filtering and sorting."""
|
||||
project = db.query(Project).filter(Project.name == project_name).first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
@@ -2489,14 +2513,38 @@ def list_package_artifacts(
|
||||
if created_before:
|
||||
query = query.filter(Artifact.created_at <= created_before)
|
||||
|
||||
# Apply size range filters
|
||||
if min_size is not None:
|
||||
query = query.filter(Artifact.size >= min_size)
|
||||
if max_size is not None:
|
||||
query = query.filter(Artifact.size <= max_size)
|
||||
|
||||
# Validate and apply sorting
|
||||
valid_sort_fields = {
|
||||
"created_at": Artifact.created_at,
|
||||
"size": Artifact.size,
|
||||
"original_name": Artifact.original_name,
|
||||
}
|
||||
if sort and sort not in valid_sort_fields:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid sort field. Valid options: {', '.join(valid_sort_fields.keys())}",
|
||||
)
|
||||
sort_column = valid_sort_fields.get(sort, Artifact.created_at)
|
||||
if order and order.lower() not in ("asc", "desc"):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid order. Valid options: asc, desc"
|
||||
)
|
||||
sort_order = (
|
||||
sort_column.asc() if order and order.lower() == "asc" else sort_column.desc()
|
||||
)
|
||||
|
||||
# Get total count before pagination
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * limit
|
||||
artifacts = (
|
||||
query.order_by(Artifact.created_at.desc()).offset(offset).limit(limit).all()
|
||||
)
|
||||
artifacts = query.order_by(sort_order).offset(offset).limit(limit).all()
|
||||
|
||||
# Calculate total pages
|
||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
||||
@@ -2537,6 +2585,224 @@ def list_package_artifacts(
|
||||
)
|
||||
|
||||
|
||||
# Global artifacts listing
|
||||
@router.get(
|
||||
"/api/v1/artifacts",
|
||||
response_model=PaginatedResponse[GlobalArtifactResponse],
|
||||
)
|
||||
def list_all_artifacts(
|
||||
project: Optional[str] = Query(None, description="Filter by project name"),
|
||||
package: Optional[str] = Query(None, description="Filter by package name"),
|
||||
tag: Optional[str] = Query(None, description="Filter by tag name"),
|
||||
content_type: Optional[str] = Query(None, description="Filter by content type"),
|
||||
min_size: Optional[int] = Query(None, ge=0, description="Minimum size in bytes"),
|
||||
max_size: Optional[int] = Query(None, ge=0, description="Maximum size in bytes"),
|
||||
from_date: Optional[datetime] = Query(
|
||||
None, alias="from", description="Created after"
|
||||
),
|
||||
to_date: Optional[datetime] = Query(None, alias="to", description="Created before"),
|
||||
sort: Optional[str] = Query(None, description="Sort field: created_at, size"),
|
||||
order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all artifacts globally with filtering by project, package, tag, etc.
|
||||
|
||||
Returns artifacts with context about which projects/packages/tags reference them.
|
||||
"""
|
||||
# Start with base query
|
||||
query = db.query(Artifact)
|
||||
|
||||
# If filtering by project/package/tag, need to join through tags
|
||||
if project or package or tag:
|
||||
# Subquery to get artifact IDs that match the filters
|
||||
tag_query = (
|
||||
db.query(Tag.artifact_id)
|
||||
.join(Package, Tag.package_id == Package.id)
|
||||
.join(Project, Package.project_id == Project.id)
|
||||
)
|
||||
if project:
|
||||
tag_query = tag_query.filter(Project.name == project)
|
||||
if package:
|
||||
tag_query = tag_query.filter(Package.name == package)
|
||||
if tag:
|
||||
tag_query = tag_query.filter(Tag.name == tag)
|
||||
artifact_ids = tag_query.distinct().subquery()
|
||||
query = query.filter(Artifact.id.in_(artifact_ids))
|
||||
|
||||
# Apply content type filter
|
||||
if content_type:
|
||||
query = query.filter(Artifact.content_type == content_type)
|
||||
|
||||
# Apply size filters
|
||||
if min_size is not None:
|
||||
query = query.filter(Artifact.size >= min_size)
|
||||
if max_size is not None:
|
||||
query = query.filter(Artifact.size <= max_size)
|
||||
|
||||
# Apply date filters
|
||||
if from_date:
|
||||
query = query.filter(Artifact.created_at >= from_date)
|
||||
if to_date:
|
||||
query = query.filter(Artifact.created_at <= to_date)
|
||||
|
||||
# Validate and apply sorting
|
||||
valid_sort_fields = {"created_at": Artifact.created_at, "size": Artifact.size}
|
||||
if sort and sort not in valid_sort_fields:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid sort field. Valid options: {', '.join(valid_sort_fields.keys())}",
|
||||
)
|
||||
sort_column = valid_sort_fields.get(sort, Artifact.created_at)
|
||||
if order and order.lower() not in ("asc", "desc"):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid order. Valid options: asc, desc"
|
||||
)
|
||||
sort_order = (
|
||||
sort_column.asc() if order and order.lower() == "asc" else sort_column.desc()
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
||||
|
||||
artifacts = query.order_by(sort_order).offset((page - 1) * limit).limit(limit).all()
|
||||
|
||||
# Build responses with context
|
||||
items = []
|
||||
for artifact in artifacts:
|
||||
# Get all tags referencing this artifact with project/package info
|
||||
tags_info = (
|
||||
db.query(Tag, Package, Project)
|
||||
.join(Package, Tag.package_id == Package.id)
|
||||
.join(Project, Package.project_id == Project.id)
|
||||
.filter(Tag.artifact_id == artifact.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
projects = list(set(proj.name for _, _, proj in tags_info))
|
||||
packages = list(set(f"{proj.name}/{pkg.name}" for _, pkg, proj in tags_info))
|
||||
tags = [f"{proj.name}/{pkg.name}:{t.name}" for t, pkg, proj in tags_info]
|
||||
|
||||
items.append(
|
||||
GlobalArtifactResponse(
|
||||
id=artifact.id,
|
||||
sha256=artifact.id,
|
||||
size=artifact.size,
|
||||
content_type=artifact.content_type,
|
||||
original_name=artifact.original_name,
|
||||
created_at=artifact.created_at,
|
||||
created_by=artifact.created_by,
|
||||
format_metadata=artifact.artifact_metadata,
|
||||
ref_count=artifact.ref_count,
|
||||
projects=projects,
|
||||
packages=packages,
|
||||
tags=tags,
|
||||
)
|
||||
)
|
||||
|
||||
return PaginatedResponse(
|
||||
items=items,
|
||||
pagination=PaginationMeta(
|
||||
page=page,
|
||||
limit=limit,
|
||||
total=total,
|
||||
total_pages=total_pages,
|
||||
has_more=page < total_pages,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Global tags listing
|
||||
@router.get(
|
||||
"/api/v1/tags",
|
||||
response_model=PaginatedResponse[GlobalTagResponse],
|
||||
)
|
||||
def list_all_tags(
|
||||
project: Optional[str] = Query(None, description="Filter by project name"),
|
||||
package: Optional[str] = Query(None, description="Filter by package name"),
|
||||
search: Optional[str] = Query(None, description="Search by tag name"),
|
||||
from_date: Optional[datetime] = Query(
|
||||
None, alias="from", description="Created after"
|
||||
),
|
||||
to_date: Optional[datetime] = Query(None, alias="to", description="Created before"),
|
||||
sort: Optional[str] = Query(None, description="Sort field: name, created_at"),
|
||||
order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
List all tags globally with filtering by project, package, name, etc.
|
||||
"""
|
||||
query = (
|
||||
db.query(Tag, Package, Project, Artifact)
|
||||
.join(Package, Tag.package_id == Package.id)
|
||||
.join(Project, Package.project_id == Project.id)
|
||||
.join(Artifact, Tag.artifact_id == Artifact.id)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if project:
|
||||
query = query.filter(Project.name == project)
|
||||
if package:
|
||||
query = query.filter(Package.name == package)
|
||||
if search:
|
||||
query = query.filter(Tag.name.ilike(f"%{search}%"))
|
||||
if from_date:
|
||||
query = query.filter(Tag.created_at >= from_date)
|
||||
if to_date:
|
||||
query = query.filter(Tag.created_at <= to_date)
|
||||
|
||||
# Validate and apply sorting
|
||||
valid_sort_fields = {"name": Tag.name, "created_at": Tag.created_at}
|
||||
if sort and sort not in valid_sort_fields:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid sort field. Valid options: {', '.join(valid_sort_fields.keys())}",
|
||||
)
|
||||
sort_column = valid_sort_fields.get(sort, Tag.created_at)
|
||||
if order and order.lower() not in ("asc", "desc"):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid order. Valid options: asc, desc"
|
||||
)
|
||||
sort_order = (
|
||||
sort_column.asc() if order and order.lower() == "asc" else sort_column.desc()
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
||||
|
||||
results = query.order_by(sort_order).offset((page - 1) * limit).limit(limit).all()
|
||||
|
||||
items = [
|
||||
GlobalTagResponse(
|
||||
id=tag.id,
|
||||
name=tag.name,
|
||||
artifact_id=tag.artifact_id,
|
||||
created_at=tag.created_at,
|
||||
created_by=tag.created_by,
|
||||
project_name=proj.name,
|
||||
package_name=pkg.name,
|
||||
artifact_size=artifact.size,
|
||||
artifact_content_type=artifact.content_type,
|
||||
)
|
||||
for tag, pkg, proj, artifact in results
|
||||
]
|
||||
|
||||
return PaginatedResponse(
|
||||
items=items,
|
||||
pagination=PaginationMeta(
|
||||
page=page,
|
||||
limit=limit,
|
||||
total=total,
|
||||
total_pages=total_pages,
|
||||
has_more=page < total_pages,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Artifact by ID
|
||||
@router.get("/api/v1/artifact/{artifact_id}", response_model=ArtifactDetailResponse)
|
||||
def get_artifact(artifact_id: str, db: Session = Depends(get_db)):
|
||||
@@ -3744,6 +4010,12 @@ def list_all_uploads(
|
||||
deduplicated: Optional[bool] = Query(
|
||||
None, description="Filter by deduplication status"
|
||||
),
|
||||
search: Optional[str] = Query(None, description="Search by original filename"),
|
||||
tag: Optional[str] = Query(None, description="Filter by tag name"),
|
||||
sort: Optional[str] = Query(
|
||||
None, description="Sort field: uploaded_at, original_name, size"
|
||||
),
|
||||
order: Optional[str] = Query("desc", description="Sort order: asc or desc"),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -3757,6 +4029,8 @@ def list_all_uploads(
|
||||
- uploaded_by: Filter by user ID
|
||||
- from/to: Filter by timestamp range
|
||||
- deduplicated: Filter by deduplication status
|
||||
- search: Search by original filename (case-insensitive)
|
||||
- tag: Filter by tag name
|
||||
"""
|
||||
query = (
|
||||
db.query(Upload, Package, Project, Artifact)
|
||||
@@ -3778,16 +4052,35 @@ def list_all_uploads(
|
||||
query = query.filter(Upload.uploaded_at <= to_date)
|
||||
if deduplicated is not None:
|
||||
query = query.filter(Upload.deduplicated == deduplicated)
|
||||
if search:
|
||||
query = query.filter(Upload.original_name.ilike(f"%{search}%"))
|
||||
if tag:
|
||||
query = query.filter(Upload.tag_name == tag)
|
||||
|
||||
# Validate and apply sorting
|
||||
valid_sort_fields = {
|
||||
"uploaded_at": Upload.uploaded_at,
|
||||
"original_name": Upload.original_name,
|
||||
"size": Artifact.size,
|
||||
}
|
||||
if sort and sort not in valid_sort_fields:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid sort field. Valid options: {', '.join(valid_sort_fields.keys())}",
|
||||
)
|
||||
sort_column = valid_sort_fields.get(sort, Upload.uploaded_at)
|
||||
if order and order.lower() not in ("asc", "desc"):
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid order. Valid options: asc, desc"
|
||||
)
|
||||
sort_order = (
|
||||
sort_column.asc() if order and order.lower() == "asc" else sort_column.desc()
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
||||
|
||||
results = (
|
||||
query.order_by(Upload.uploaded_at.desc())
|
||||
.offset((page - 1) * limit)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
results = query.order_by(sort_order).offset((page - 1) * limit).limit(limit).all()
|
||||
|
||||
items = [
|
||||
UploadHistoryResponse(
|
||||
|
||||
Reference in New Issue
Block a user