Compare commits
3 Commits
fix/restor
...
feature/fr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30774e429b | ||
|
|
a4b4d700c2 | ||
|
|
54e33e67ce |
114
README.md
114
README.md
@@ -62,13 +62,10 @@ Orchard is a centralized binary artifact storage system that provides content-ad
|
|||||||
| `POST` | `/api/v1/project/:project/:package/upload` | Upload an artifact |
|
| `POST` | `/api/v1/project/:project/:package/upload` | Upload an artifact |
|
||||||
| `GET` | `/api/v1/project/:project/:package/+/:ref` | Download an artifact (supports Range header) |
|
| `GET` | `/api/v1/project/:project/:package/+/:ref` | Download an artifact (supports Range header) |
|
||||||
| `HEAD` | `/api/v1/project/:project/:package/+/:ref` | Get artifact metadata without downloading |
|
| `HEAD` | `/api/v1/project/:project/:package/+/:ref` | Get artifact metadata without downloading |
|
||||||
| `GET` | `/api/v1/project/:project/:package/tags` | List tags (with pagination, search, sorting, artifact metadata) |
|
| `GET` | `/api/v1/project/:project/:package/tags` | List all tags |
|
||||||
| `POST` | `/api/v1/project/:project/:package/tags` | Create a tag |
|
| `POST` | `/api/v1/project/:project/:package/tags` | Create a tag |
|
||||||
| `GET` | `/api/v1/project/:project/:package/tags/:tag_name` | Get single tag with artifact metadata |
|
|
||||||
| `GET` | `/api/v1/project/:project/:package/tags/:tag_name/history` | Get tag change history |
|
|
||||||
| `GET` | `/api/v1/project/:project/:package/artifacts` | List artifacts in package (with filtering) |
|
|
||||||
| `GET` | `/api/v1/project/:project/:package/consumers` | List consumers of a package |
|
| `GET` | `/api/v1/project/:project/:package/consumers` | List consumers of a package |
|
||||||
| `GET` | `/api/v1/artifact/:id` | Get artifact metadata with referencing tags |
|
| `GET` | `/api/v1/artifact/:id` | Get artifact metadata by hash |
|
||||||
|
|
||||||
#### Resumable Upload Endpoints
|
#### Resumable Upload Endpoints
|
||||||
|
|
||||||
@@ -299,119 +296,12 @@ curl -X POST http://localhost:8080/api/v1/project/my-project/releases/tags \
|
|||||||
-d '{"name": "stable", "artifact_id": "a3f5d8e12b4c6789..."}'
|
-d '{"name": "stable", "artifact_id": "a3f5d8e12b4c6789..."}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### List Tags
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic listing with artifact metadata
|
|
||||||
curl http://localhost:8080/api/v1/project/my-project/releases/tags
|
|
||||||
|
|
||||||
# With pagination
|
|
||||||
curl "http://localhost:8080/api/v1/project/my-project/releases/tags?page=1&limit=10"
|
|
||||||
|
|
||||||
# Search by tag name
|
|
||||||
curl "http://localhost:8080/api/v1/project/my-project/releases/tags?search=v1"
|
|
||||||
|
|
||||||
# Sort by created_at descending
|
|
||||||
curl "http://localhost:8080/api/v1/project/my-project/releases/tags?sort=created_at&order=desc"
|
|
||||||
```
|
|
||||||
|
|
||||||
Response includes artifact metadata:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "uuid",
|
|
||||||
"package_id": "uuid",
|
|
||||||
"name": "v1.0.0",
|
|
||||||
"artifact_id": "a3f5d8e...",
|
|
||||||
"created_at": "2025-01-01T00:00:00Z",
|
|
||||||
"created_by": "user",
|
|
||||||
"artifact_size": 1048576,
|
|
||||||
"artifact_content_type": "application/gzip",
|
|
||||||
"artifact_original_name": "app-v1.0.0.tar.gz",
|
|
||||||
"artifact_created_at": "2025-01-01T00:00:00Z",
|
|
||||||
"artifact_format_metadata": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {"page": 1, "limit": 20, "total": 1, "total_pages": 1}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Single Tag
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/api/v1/project/my-project/releases/tags/v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Tag History
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8080/api/v1/project/my-project/releases/tags/latest/history
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns list of artifact changes for the tag (most recent first).
|
|
||||||
|
|
||||||
### List Artifacts in Package
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic listing
|
|
||||||
curl http://localhost:8080/api/v1/project/my-project/releases/artifacts
|
|
||||||
|
|
||||||
# Filter by content type
|
|
||||||
curl "http://localhost:8080/api/v1/project/my-project/releases/artifacts?content_type=application/gzip"
|
|
||||||
|
|
||||||
# Filter by date range
|
|
||||||
curl "http://localhost:8080/api/v1/project/my-project/releases/artifacts?created_after=2025-01-01T00:00:00Z"
|
|
||||||
```
|
|
||||||
|
|
||||||
Response includes tags pointing to each artifact:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "a3f5d8e...",
|
|
||||||
"size": 1048576,
|
|
||||||
"content_type": "application/gzip",
|
|
||||||
"original_name": "app-v1.0.0.tar.gz",
|
|
||||||
"created_at": "2025-01-01T00:00:00Z",
|
|
||||||
"created_by": "user",
|
|
||||||
"format_metadata": {},
|
|
||||||
"tags": ["v1.0.0", "latest", "stable"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {"page": 1, "limit": 20, "total": 1, "total_pages": 1}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get Artifact by ID
|
### Get Artifact by ID
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8080/api/v1/artifact/a3f5d8e12b4c67890abcdef1234567890abcdef1234567890abcdef12345678
|
curl http://localhost:8080/api/v1/artifact/a3f5d8e12b4c67890abcdef1234567890abcdef1234567890abcdef12345678
|
||||||
```
|
```
|
||||||
|
|
||||||
Response includes all tags/packages referencing the artifact:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "a3f5d8e...",
|
|
||||||
"size": 1048576,
|
|
||||||
"content_type": "application/gzip",
|
|
||||||
"original_name": "app-v1.0.0.tar.gz",
|
|
||||||
"created_at": "2025-01-01T00:00:00Z",
|
|
||||||
"created_by": "user",
|
|
||||||
"ref_count": 2,
|
|
||||||
"format_metadata": {},
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"id": "uuid",
|
|
||||||
"name": "v1.0.0",
|
|
||||||
"package_id": "uuid",
|
|
||||||
"package_name": "releases",
|
|
||||||
"project_name": "my-project"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query, Header, Response
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query, Header, Response
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -11,13 +10,13 @@ import hashlib
|
|||||||
|
|
||||||
from .database import get_db
|
from .database import get_db
|
||||||
from .storage import get_storage, S3Storage, MULTIPART_CHUNK_SIZE
|
from .storage import get_storage, S3Storage, MULTIPART_CHUNK_SIZE
|
||||||
from .models import Project, Package, Artifact, Tag, TagHistory, Upload, Consumer
|
from .models import Project, Package, Artifact, Tag, Upload, Consumer
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
ProjectCreate, ProjectResponse,
|
ProjectCreate, ProjectResponse,
|
||||||
PackageCreate, PackageResponse, PackageDetailResponse, TagSummary,
|
PackageCreate, PackageResponse, PackageDetailResponse, TagSummary,
|
||||||
PACKAGE_FORMATS, PACKAGE_PLATFORMS,
|
PACKAGE_FORMATS, PACKAGE_PLATFORMS,
|
||||||
ArtifactResponse, ArtifactDetailResponse, ArtifactTagInfo, PackageArtifactResponse,
|
ArtifactResponse,
|
||||||
TagCreate, TagResponse, TagDetailResponse, TagHistoryResponse,
|
TagCreate, TagResponse,
|
||||||
UploadResponse,
|
UploadResponse,
|
||||||
ConsumerResponse,
|
ConsumerResponse,
|
||||||
HealthResponse,
|
HealthResponse,
|
||||||
@@ -851,17 +850,8 @@ def download_artifact_compat(
|
|||||||
|
|
||||||
|
|
||||||
# Tag routes
|
# Tag routes
|
||||||
@router.get("/api/v1/project/{project_name}/{package_name}/tags", response_model=PaginatedResponse[TagDetailResponse])
|
@router.get("/api/v1/project/{project_name}/{package_name}/tags", response_model=List[TagResponse])
|
||||||
def list_tags(
|
def list_tags(project_name: str, package_name: str, db: Session = Depends(get_db)):
|
||||||
project_name: str,
|
|
||||||
package_name: str,
|
|
||||||
page: int = Query(default=1, ge=1, description="Page number"),
|
|
||||||
limit: int = Query(default=20, ge=1, le=100, description="Items per page"),
|
|
||||||
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)"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
project = db.query(Project).filter(Project.name == project_name).first()
|
project = db.query(Project).filter(Project.name == project_name).first()
|
||||||
if not project:
|
if not project:
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
@@ -870,65 +860,8 @@ def list_tags(
|
|||||||
if not package:
|
if not package:
|
||||||
raise HTTPException(status_code=404, detail="Package not found")
|
raise HTTPException(status_code=404, detail="Package not found")
|
||||||
|
|
||||||
# Validate sort field
|
tags = db.query(Tag).filter(Tag.package_id == package.id).order_by(Tag.name).all()
|
||||||
valid_sort_fields = {"name": Tag.name, "created_at": Tag.created_at}
|
return tags
|
||||||
if sort not in valid_sort_fields:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid sort field. Must be one of: {', '.join(valid_sort_fields.keys())}")
|
|
||||||
|
|
||||||
# Validate order
|
|
||||||
if order not in ("asc", "desc"):
|
|
||||||
raise HTTPException(status_code=400, detail="Invalid order. Must be 'asc' or 'desc'")
|
|
||||||
|
|
||||||
# Base query with JOIN to artifact for metadata
|
|
||||||
query = db.query(Tag, Artifact).join(Artifact, Tag.artifact_id == Artifact.id).filter(Tag.package_id == package.id)
|
|
||||||
|
|
||||||
# Apply search filter (case-insensitive on tag name)
|
|
||||||
if search:
|
|
||||||
query = query.filter(func.lower(Tag.name).contains(search.lower()))
|
|
||||||
|
|
||||||
# Get total count before pagination
|
|
||||||
total = query.count()
|
|
||||||
|
|
||||||
# Apply sorting
|
|
||||||
sort_column = valid_sort_fields[sort]
|
|
||||||
if order == "desc":
|
|
||||||
query = query.order_by(sort_column.desc())
|
|
||||||
else:
|
|
||||||
query = query.order_by(sort_column.asc())
|
|
||||||
|
|
||||||
# Apply pagination
|
|
||||||
offset = (page - 1) * limit
|
|
||||||
results = query.offset(offset).limit(limit).all()
|
|
||||||
|
|
||||||
# Calculate total pages
|
|
||||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
|
||||||
|
|
||||||
# Build detailed responses with artifact metadata
|
|
||||||
detailed_tags = []
|
|
||||||
for tag, artifact in results:
|
|
||||||
detailed_tags.append(TagDetailResponse(
|
|
||||||
id=tag.id,
|
|
||||||
package_id=tag.package_id,
|
|
||||||
name=tag.name,
|
|
||||||
artifact_id=tag.artifact_id,
|
|
||||||
created_at=tag.created_at,
|
|
||||||
created_by=tag.created_by,
|
|
||||||
artifact_size=artifact.size,
|
|
||||||
artifact_content_type=artifact.content_type,
|
|
||||||
artifact_original_name=artifact.original_name,
|
|
||||||
artifact_created_at=artifact.created_at,
|
|
||||||
artifact_format_metadata=artifact.format_metadata,
|
|
||||||
))
|
|
||||||
|
|
||||||
return PaginatedResponse(
|
|
||||||
items=detailed_tags,
|
|
||||||
pagination=PaginationMeta(
|
|
||||||
page=page,
|
|
||||||
limit=limit,
|
|
||||||
total=total,
|
|
||||||
total_pages=total_pages,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/v1/project/{project_name}/{package_name}/tags", response_model=TagResponse)
|
@router.post("/api/v1/project/{project_name}/{package_name}/tags", response_model=TagResponse)
|
||||||
@@ -975,70 +908,6 @@ def create_tag(
|
|||||||
return db_tag
|
return db_tag
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v1/project/{project_name}/{package_name}/tags/{tag_name}", response_model=TagDetailResponse)
|
|
||||||
def get_tag(
|
|
||||||
project_name: str,
|
|
||||||
package_name: str,
|
|
||||||
tag_name: str,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""Get a single tag with full artifact metadata"""
|
|
||||||
project = db.query(Project).filter(Project.name == project_name).first()
|
|
||||||
if not project:
|
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
|
||||||
|
|
||||||
package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first()
|
|
||||||
if not package:
|
|
||||||
raise HTTPException(status_code=404, detail="Package not found")
|
|
||||||
|
|
||||||
result = db.query(Tag, Artifact).join(Artifact, Tag.artifact_id == Artifact.id).filter(
|
|
||||||
Tag.package_id == package.id,
|
|
||||||
Tag.name == tag_name
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
tag, artifact = result
|
|
||||||
return TagDetailResponse(
|
|
||||||
id=tag.id,
|
|
||||||
package_id=tag.package_id,
|
|
||||||
name=tag.name,
|
|
||||||
artifact_id=tag.artifact_id,
|
|
||||||
created_at=tag.created_at,
|
|
||||||
created_by=tag.created_by,
|
|
||||||
artifact_size=artifact.size,
|
|
||||||
artifact_content_type=artifact.content_type,
|
|
||||||
artifact_original_name=artifact.original_name,
|
|
||||||
artifact_created_at=artifact.created_at,
|
|
||||||
artifact_format_metadata=artifact.format_metadata,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/v1/project/{project_name}/{package_name}/tags/{tag_name}/history", response_model=List[TagHistoryResponse])
|
|
||||||
def get_tag_history(
|
|
||||||
project_name: str,
|
|
||||||
package_name: str,
|
|
||||||
tag_name: str,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""Get the history of artifact assignments for a tag"""
|
|
||||||
project = db.query(Project).filter(Project.name == project_name).first()
|
|
||||||
if not project:
|
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
|
||||||
|
|
||||||
package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first()
|
|
||||||
if not package:
|
|
||||||
raise HTTPException(status_code=404, detail="Package not found")
|
|
||||||
|
|
||||||
tag = db.query(Tag).filter(Tag.package_id == package.id, Tag.name == tag_name).first()
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
history = db.query(TagHistory).filter(TagHistory.tag_id == tag.id).order_by(TagHistory.changed_at.desc()).all()
|
|
||||||
return history
|
|
||||||
|
|
||||||
|
|
||||||
# Consumer routes
|
# Consumer routes
|
||||||
@router.get("/api/v1/project/{project_name}/{package_name}/consumers", response_model=List[ConsumerResponse])
|
@router.get("/api/v1/project/{project_name}/{package_name}/consumers", response_model=List[ConsumerResponse])
|
||||||
def get_consumers(project_name: str, package_name: str, db: Session = Depends(get_db)):
|
def get_consumers(project_name: str, package_name: str, db: Session = Depends(get_db)):
|
||||||
@@ -1054,122 +923,10 @@ def get_consumers(project_name: str, package_name: str, db: Session = Depends(ge
|
|||||||
return consumers
|
return consumers
|
||||||
|
|
||||||
|
|
||||||
# Package artifacts
|
|
||||||
@router.get("/api/v1/project/{project_name}/{package_name}/artifacts", response_model=PaginatedResponse[PackageArtifactResponse])
|
|
||||||
def list_package_artifacts(
|
|
||||||
project_name: str,
|
|
||||||
package_name: str,
|
|
||||||
page: int = Query(default=1, ge=1, description="Page number"),
|
|
||||||
limit: int = Query(default=20, ge=1, le=100, description="Items per page"),
|
|
||||||
content_type: Optional[str] = Query(default=None, description="Filter by content type"),
|
|
||||||
created_after: Optional[datetime] = Query(default=None, description="Filter artifacts created after this date"),
|
|
||||||
created_before: Optional[datetime] = Query(default=None, description="Filter artifacts created before this date"),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
):
|
|
||||||
"""List all unique artifacts uploaded to a package"""
|
|
||||||
project = db.query(Project).filter(Project.name == project_name).first()
|
|
||||||
if not project:
|
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
|
||||||
|
|
||||||
package = db.query(Package).filter(Package.project_id == project.id, Package.name == package_name).first()
|
|
||||||
if not package:
|
|
||||||
raise HTTPException(status_code=404, detail="Package not found")
|
|
||||||
|
|
||||||
# Get distinct artifacts uploaded to this package via uploads table
|
|
||||||
artifact_ids_subquery = db.query(func.distinct(Upload.artifact_id)).filter(
|
|
||||||
Upload.package_id == package.id
|
|
||||||
).subquery()
|
|
||||||
|
|
||||||
query = db.query(Artifact).filter(Artifact.id.in_(artifact_ids_subquery))
|
|
||||||
|
|
||||||
# Apply content_type filter
|
|
||||||
if content_type:
|
|
||||||
query = query.filter(Artifact.content_type == content_type)
|
|
||||||
|
|
||||||
# Apply date range filters
|
|
||||||
if created_after:
|
|
||||||
query = query.filter(Artifact.created_at >= created_after)
|
|
||||||
if created_before:
|
|
||||||
query = query.filter(Artifact.created_at <= created_before)
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Calculate total pages
|
|
||||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
|
||||||
|
|
||||||
# Build responses with tag info
|
|
||||||
artifact_responses = []
|
|
||||||
for artifact in artifacts:
|
|
||||||
# Get tags pointing to this artifact in this package
|
|
||||||
tags = db.query(Tag.name).filter(
|
|
||||||
Tag.package_id == package.id,
|
|
||||||
Tag.artifact_id == artifact.id
|
|
||||||
).all()
|
|
||||||
tag_names = [t.name for t in tags]
|
|
||||||
|
|
||||||
artifact_responses.append(PackageArtifactResponse(
|
|
||||||
id=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.format_metadata,
|
|
||||||
tags=tag_names,
|
|
||||||
))
|
|
||||||
|
|
||||||
return PaginatedResponse(
|
|
||||||
items=artifact_responses,
|
|
||||||
pagination=PaginationMeta(
|
|
||||||
page=page,
|
|
||||||
limit=limit,
|
|
||||||
total=total,
|
|
||||||
total_pages=total_pages,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Artifact by ID
|
# Artifact by ID
|
||||||
@router.get("/api/v1/artifact/{artifact_id}", response_model=ArtifactDetailResponse)
|
@router.get("/api/v1/artifact/{artifact_id}", response_model=ArtifactResponse)
|
||||||
def get_artifact(artifact_id: str, db: Session = Depends(get_db)):
|
def get_artifact(artifact_id: str, db: Session = Depends(get_db)):
|
||||||
"""Get artifact metadata including list of packages/tags referencing it"""
|
|
||||||
artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first()
|
artifact = db.query(Artifact).filter(Artifact.id == artifact_id).first()
|
||||||
if not artifact:
|
if not artifact:
|
||||||
raise HTTPException(status_code=404, detail="Artifact not found")
|
raise HTTPException(status_code=404, detail="Artifact not found")
|
||||||
|
return artifact
|
||||||
# Get all tags referencing this artifact with package and project info
|
|
||||||
tags_with_context = 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()
|
|
||||||
|
|
||||||
tag_infos = [
|
|
||||||
ArtifactTagInfo(
|
|
||||||
id=tag.id,
|
|
||||||
name=tag.name,
|
|
||||||
package_id=package.id,
|
|
||||||
package_name=package.name,
|
|
||||||
project_name=project.name,
|
|
||||||
)
|
|
||||||
for tag, package, project in tags_with_context
|
|
||||||
]
|
|
||||||
|
|
||||||
return ArtifactDetailResponse(
|
|
||||||
id=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,
|
|
||||||
ref_count=artifact.ref_count,
|
|
||||||
format_metadata=artifact.format_metadata,
|
|
||||||
tags=tag_infos,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -129,78 +129,6 @@ class TagResponse(BaseModel):
|
|||||||
from_attributes = True
|
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
|
# Upload response
|
||||||
class UploadResponse(BaseModel):
|
class UploadResponse(BaseModel):
|
||||||
artifact_id: str
|
artifact_id: str
|
||||||
|
|||||||
Reference in New Issue
Block a user