- Enhanced GET /api/v1/project/{project}/{package}/tags with pagination,
search, sort, order parameters and artifact metadata
- Added GET /api/v1/project/{project}/{package}/tags/{tag_name} for
single tag with full artifact details
- Added GET /api/v1/project/{project}/{package}/tags/{tag_name}/history
for tag change history
- Added GET /api/v1/project/{project}/{package}/artifacts for listing
all artifacts in a package with filtering by content_type and date range
- Enhanced GET /api/v1/artifact/{artifact_id} to include list of
packages/tags referencing the artifact
- Added new schemas: TagDetailResponse, TagHistoryResponse,
ArtifactDetailResponse, ArtifactTagInfo, PackageArtifactResponse
276 lines
6.1 KiB
Python
276 lines
6.1 KiB
Python
from datetime import datetime
|
|
from typing import Optional, List, Dict, Any, Generic, TypeVar
|
|
from pydantic import BaseModel
|
|
from uuid import UUID
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
# Pagination schemas
|
|
class PaginationMeta(BaseModel):
|
|
page: int
|
|
limit: int
|
|
total: int
|
|
total_pages: int
|
|
|
|
|
|
class PaginatedResponse(BaseModel, Generic[T]):
|
|
items: List[T]
|
|
pagination: PaginationMeta
|
|
|
|
|
|
# Project schemas
|
|
class ProjectCreate(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
is_public: bool = True
|
|
|
|
|
|
class ProjectResponse(BaseModel):
|
|
id: UUID
|
|
name: str
|
|
description: Optional[str]
|
|
is_public: bool
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
created_by: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Package format and platform enums
|
|
PACKAGE_FORMATS = ["generic", "npm", "pypi", "docker", "deb", "rpm", "maven", "nuget", "helm"]
|
|
PACKAGE_PLATFORMS = ["any", "linux", "darwin", "windows", "linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", "windows-amd64"]
|
|
|
|
|
|
# Package schemas
|
|
class PackageCreate(BaseModel):
|
|
name: str
|
|
description: Optional[str] = None
|
|
format: str = "generic"
|
|
platform: str = "any"
|
|
|
|
|
|
class PackageResponse(BaseModel):
|
|
id: UUID
|
|
project_id: UUID
|
|
name: str
|
|
description: Optional[str]
|
|
format: str
|
|
platform: str
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TagSummary(BaseModel):
|
|
"""Lightweight tag info for embedding in package responses"""
|
|
name: str
|
|
artifact_id: str
|
|
created_at: datetime
|
|
|
|
|
|
class PackageDetailResponse(BaseModel):
|
|
"""Package with aggregated metadata"""
|
|
id: UUID
|
|
project_id: UUID
|
|
name: str
|
|
description: Optional[str]
|
|
format: str
|
|
platform: str
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
# Aggregated fields
|
|
tag_count: int = 0
|
|
artifact_count: int = 0
|
|
total_size: int = 0
|
|
latest_tag: Optional[str] = None
|
|
latest_upload_at: Optional[datetime] = None
|
|
# Recent tags (limit 5)
|
|
recent_tags: List[TagSummary] = []
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Artifact schemas
|
|
class ArtifactResponse(BaseModel):
|
|
id: str
|
|
size: int
|
|
content_type: Optional[str]
|
|
original_name: Optional[str]
|
|
created_at: datetime
|
|
created_by: str
|
|
ref_count: int
|
|
format_metadata: Optional[Dict[str, Any]] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Tag schemas
|
|
class TagCreate(BaseModel):
|
|
name: str
|
|
artifact_id: str
|
|
|
|
|
|
class TagResponse(BaseModel):
|
|
id: UUID
|
|
package_id: UUID
|
|
name: str
|
|
artifact_id: str
|
|
created_at: datetime
|
|
created_by: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TagDetailResponse(BaseModel):
|
|
"""Tag with embedded artifact metadata"""
|
|
id: UUID
|
|
package_id: UUID
|
|
name: str
|
|
artifact_id: str
|
|
created_at: datetime
|
|
created_by: str
|
|
# Artifact metadata
|
|
artifact_size: int
|
|
artifact_content_type: Optional[str]
|
|
artifact_original_name: Optional[str]
|
|
artifact_created_at: datetime
|
|
artifact_format_metadata: Optional[Dict[str, Any]] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class TagHistoryResponse(BaseModel):
|
|
"""History entry for tag changes"""
|
|
id: UUID
|
|
tag_id: UUID
|
|
old_artifact_id: Optional[str]
|
|
new_artifact_id: str
|
|
changed_at: datetime
|
|
changed_by: str
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ArtifactTagInfo(BaseModel):
|
|
"""Tag info for embedding in artifact responses"""
|
|
id: UUID
|
|
name: str
|
|
package_id: UUID
|
|
package_name: str
|
|
project_name: str
|
|
|
|
|
|
class ArtifactDetailResponse(BaseModel):
|
|
"""Artifact with list of tags/packages referencing it"""
|
|
id: str
|
|
size: int
|
|
content_type: Optional[str]
|
|
original_name: Optional[str]
|
|
created_at: datetime
|
|
created_by: str
|
|
ref_count: int
|
|
format_metadata: Optional[Dict[str, Any]] = None
|
|
tags: List[ArtifactTagInfo] = []
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class PackageArtifactResponse(BaseModel):
|
|
"""Artifact with tags for package artifact listing"""
|
|
id: str
|
|
size: int
|
|
content_type: Optional[str]
|
|
original_name: Optional[str]
|
|
created_at: datetime
|
|
created_by: str
|
|
format_metadata: Optional[Dict[str, Any]] = None
|
|
tags: List[str] = [] # Tag names pointing to this artifact
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Upload response
|
|
class UploadResponse(BaseModel):
|
|
artifact_id: str
|
|
size: int
|
|
project: str
|
|
package: str
|
|
tag: Optional[str]
|
|
format_metadata: Optional[Dict[str, Any]] = None
|
|
deduplicated: bool = False
|
|
|
|
|
|
# Resumable upload schemas
|
|
class ResumableUploadInitRequest(BaseModel):
|
|
"""Request to initiate a resumable upload"""
|
|
expected_hash: str # SHA256 hash of the file (client must compute)
|
|
filename: str
|
|
content_type: Optional[str] = None
|
|
size: int
|
|
tag: Optional[str] = None
|
|
|
|
|
|
class ResumableUploadInitResponse(BaseModel):
|
|
"""Response from initiating a resumable upload"""
|
|
upload_id: Optional[str] # None if file already exists
|
|
already_exists: bool
|
|
artifact_id: Optional[str] = None # Set if already_exists is True
|
|
chunk_size: int # Recommended chunk size for parts
|
|
|
|
|
|
class ResumableUploadPartResponse(BaseModel):
|
|
"""Response from uploading a part"""
|
|
part_number: int
|
|
etag: str
|
|
|
|
|
|
class ResumableUploadCompleteRequest(BaseModel):
|
|
"""Request to complete a resumable upload"""
|
|
tag: Optional[str] = None
|
|
|
|
|
|
class ResumableUploadCompleteResponse(BaseModel):
|
|
"""Response from completing a resumable upload"""
|
|
artifact_id: str
|
|
size: int
|
|
project: str
|
|
package: str
|
|
tag: Optional[str]
|
|
|
|
|
|
class ResumableUploadStatusResponse(BaseModel):
|
|
"""Status of a resumable upload"""
|
|
upload_id: str
|
|
uploaded_parts: List[int]
|
|
total_uploaded_bytes: int
|
|
|
|
|
|
# Consumer schemas
|
|
class ConsumerResponse(BaseModel):
|
|
id: UUID
|
|
package_id: UUID
|
|
project_url: str
|
|
last_access: datetime
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Health check
|
|
class HealthResponse(BaseModel):
|
|
status: str
|
|
version: str = "1.0.0"
|