Switch to angular
This commit is contained in:
117
app/api/tags.py
Normal file
117
app/api/tags.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.tag import Tag
|
||||
from app.schemas.tag import TagCreate, TagUpdate, TagResponse
|
||||
|
||||
router = APIRouter(prefix="/api/v1/tags", tags=["tags"])
|
||||
|
||||
|
||||
@router.post("/", response_model=TagResponse, status_code=201)
|
||||
async def create_tag(tag: TagCreate, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Create a new tag
|
||||
|
||||
- **name**: Tag name (unique, required)
|
||||
- **description**: Tag description (optional)
|
||||
- **color**: Hex color code (optional, e.g., #FF5733)
|
||||
"""
|
||||
# Check if tag already exists
|
||||
existing_tag = db.query(Tag).filter(Tag.name == tag.name).first()
|
||||
if existing_tag:
|
||||
raise HTTPException(status_code=400, detail=f"Tag with name '{tag.name}' already exists")
|
||||
|
||||
db_tag = Tag(**tag.model_dump())
|
||||
db.add(db_tag)
|
||||
db.commit()
|
||||
db.refresh(db_tag)
|
||||
|
||||
return db_tag
|
||||
|
||||
|
||||
@router.get("/", response_model=List[TagResponse])
|
||||
async def list_tags(
|
||||
limit: int = Query(default=100, le=1000),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""List all tags with pagination"""
|
||||
tags = db.query(Tag).order_by(Tag.name).offset(offset).limit(limit).all()
|
||||
return tags
|
||||
|
||||
|
||||
@router.get("/{tag_id}", response_model=TagResponse)
|
||||
async def get_tag(tag_id: int, db: Session = Depends(get_db)):
|
||||
"""Get tag by ID"""
|
||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||
if not tag:
|
||||
raise HTTPException(status_code=404, detail="Tag not found")
|
||||
return tag
|
||||
|
||||
|
||||
@router.get("/name/{tag_name}", response_model=TagResponse)
|
||||
async def get_tag_by_name(tag_name: str, db: Session = Depends(get_db)):
|
||||
"""Get tag by name"""
|
||||
tag = db.query(Tag).filter(Tag.name == tag_name).first()
|
||||
if not tag:
|
||||
raise HTTPException(status_code=404, detail="Tag not found")
|
||||
return tag
|
||||
|
||||
|
||||
@router.put("/{tag_id}", response_model=TagResponse)
|
||||
async def update_tag(tag_id: int, tag_update: TagUpdate, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Update a tag
|
||||
|
||||
- **name**: Tag name (optional)
|
||||
- **description**: Tag description (optional)
|
||||
- **color**: Hex color code (optional)
|
||||
"""
|
||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||
if not tag:
|
||||
raise HTTPException(status_code=404, detail="Tag not found")
|
||||
|
||||
# Check if new name conflicts with existing tag
|
||||
if tag_update.name and tag_update.name != tag.name:
|
||||
existing_tag = db.query(Tag).filter(Tag.name == tag_update.name).first()
|
||||
if existing_tag:
|
||||
raise HTTPException(status_code=400, detail=f"Tag with name '{tag_update.name}' already exists")
|
||||
|
||||
# Update fields
|
||||
update_data = tag_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(tag, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(tag)
|
||||
|
||||
return tag
|
||||
|
||||
|
||||
@router.delete("/{tag_id}")
|
||||
async def delete_tag(tag_id: int, db: Session = Depends(get_db)):
|
||||
"""Delete a tag"""
|
||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
||||
if not tag:
|
||||
raise HTTPException(status_code=404, detail="Tag not found")
|
||||
|
||||
db.delete(tag)
|
||||
db.commit()
|
||||
|
||||
return {"message": f"Tag '{tag.name}' deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/search", response_model=List[TagResponse])
|
||||
async def search_tags(
|
||||
query: str = Query(..., min_length=1, description="Search query"),
|
||||
limit: int = Query(default=100, le=1000),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Search tags by name or description"""
|
||||
tags = db.query(Tag).filter(
|
||||
(Tag.name.ilike(f"%{query}%")) | (Tag.description.ilike(f"%{query}%"))
|
||||
).order_by(Tag.name).limit(limit).all()
|
||||
|
||||
return tags
|
||||
@@ -1,7 +1,8 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.config import settings
|
||||
from app.models.artifact import Base
|
||||
from app.models.artifact import Base as ArtifactBase
|
||||
from app.models.tag import Base as TagBase
|
||||
|
||||
engine = create_engine(settings.database_url)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
@@ -9,7 +10,8 @@ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def init_db():
|
||||
"""Initialize database tables"""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
ArtifactBase.metadata.create_all(bind=engine)
|
||||
TagBase.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
|
||||
31
app/main.py
31
app/main.py
@@ -1,9 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from app.api.artifacts import router as artifacts_router
|
||||
from app.api.seed import router as seed_router
|
||||
from app.api.tags import router as tags_router
|
||||
from app.database import init_db
|
||||
from app.config import settings
|
||||
import logging
|
||||
@@ -38,11 +37,9 @@ app.add_middleware(
|
||||
# Include routers
|
||||
app.include_router(artifacts_router)
|
||||
app.include_router(seed_router)
|
||||
app.include_router(tags_router)
|
||||
|
||||
# Mount static files
|
||||
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
|
||||
if os.path.exists(static_dir):
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
# Note: Frontend is now served separately as an Angular application
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -69,19 +66,15 @@ async def api_root():
|
||||
|
||||
@app.get("/")
|
||||
async def ui_root():
|
||||
"""Serve the UI"""
|
||||
index_path = os.path.join(static_dir, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
return FileResponse(index_path)
|
||||
else:
|
||||
return {
|
||||
"message": "Test Artifact Data Lake API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"ui": "UI not found. Serving API only.",
|
||||
"deployment_mode": settings.deployment_mode,
|
||||
"storage_backend": settings.storage_backend
|
||||
}
|
||||
"""API root - Frontend is served separately"""
|
||||
return {
|
||||
"message": "Test Artifact Data Lake API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"frontend": "Frontend is served separately on port 4200 (development) or via reverse proxy (production)",
|
||||
"deployment_mode": settings.deployment_mode,
|
||||
"storage_backend": settings.storage_backend
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
|
||||
21
app/models/tag.py
Normal file
21
app/models/tag.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, String, Integer, DateTime, Text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
__tablename__ = "tags"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False, index=True)
|
||||
description = Column(Text)
|
||||
color = Column(String(7)) # Hex color code, e.g., #FF5733
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Tag(id={self.id}, name='{self.name}')>"
|
||||
28
app/schemas/tag.py
Normal file
28
app/schemas/tag.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class TagBase(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Tag name")
|
||||
description: Optional[str] = Field(None, description="Tag description")
|
||||
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code (e.g., #FF5733)")
|
||||
|
||||
|
||||
class TagCreate(TagBase):
|
||||
pass
|
||||
|
||||
|
||||
class TagUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Tag name")
|
||||
description: Optional[str] = Field(None, description="Tag description")
|
||||
color: Optional[str] = Field(None, pattern="^#[0-9A-Fa-f]{6}$", description="Hex color code")
|
||||
|
||||
|
||||
class TagResponse(TagBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user