Compare commits
1 Commits
feature/pr
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4026978bc |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,11 +1,3 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
.Python
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
|
||||
# Binaries
|
||||
/bin/
|
||||
*.exe
|
||||
|
||||
@@ -3,6 +3,9 @@ from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Environment
|
||||
env: str = "development" # "development" or "production"
|
||||
|
||||
# Server
|
||||
server_host: str = "0.0.0.0"
|
||||
server_port: int = 8080
|
||||
@@ -28,6 +31,14 @@ class Settings(BaseSettings):
|
||||
sslmode = f"?sslmode={self.database_sslmode}" if self.database_sslmode else ""
|
||||
return f"postgresql://{self.database_user}:{self.database_password}@{self.database_host}:{self.database_port}/{self.database_dbname}{sslmode}"
|
||||
|
||||
@property
|
||||
def is_development(self) -> bool:
|
||||
return self.env.lower() == "development"
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.env.lower() == "production"
|
||||
|
||||
class Config:
|
||||
env_prefix = "ORCHARD_"
|
||||
case_sensitive = False
|
||||
|
||||
@@ -2,19 +2,35 @@ from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .config import get_settings
|
||||
from .database import init_db
|
||||
from .database import init_db, SessionLocal
|
||||
from .routes import router
|
||||
from .seed import seed_database
|
||||
|
||||
settings = get_settings()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup: initialize database
|
||||
init_db()
|
||||
|
||||
# Seed test data in development mode
|
||||
if settings.is_development:
|
||||
logger.info(f"Running in {settings.env} mode - checking for seed data")
|
||||
db = SessionLocal()
|
||||
try:
|
||||
seed_database(db)
|
||||
finally:
|
||||
db.close()
|
||||
else:
|
||||
logger.info(f"Running in {settings.env} mode - skipping seed data")
|
||||
|
||||
yield
|
||||
# Shutdown: cleanup if needed
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, func
|
||||
from sqlalchemy import or_
|
||||
from typing import List, Optional
|
||||
import math
|
||||
import re
|
||||
|
||||
from .database import get_db
|
||||
@@ -17,7 +16,6 @@ from .schemas import (
|
||||
UploadResponse,
|
||||
ConsumerResponse,
|
||||
HealthResponse,
|
||||
PaginatedResponse, PaginationMeta,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
@@ -41,44 +39,13 @@ def health_check():
|
||||
|
||||
|
||||
# Project routes
|
||||
@router.get("/api/v1/projects", response_model=PaginatedResponse[ProjectResponse])
|
||||
def list_projects(
|
||||
request: Request,
|
||||
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 project name"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@router.get("/api/v1/projects", response_model=List[ProjectResponse])
|
||||
def list_projects(request: Request, db: Session = Depends(get_db)):
|
||||
user_id = get_user_id(request)
|
||||
|
||||
# Base query - filter by access
|
||||
query = db.query(Project).filter(
|
||||
projects = db.query(Project).filter(
|
||||
or_(Project.is_public == True, Project.created_by == user_id)
|
||||
)
|
||||
|
||||
# Apply search filter (case-insensitive)
|
||||
if search:
|
||||
query = query.filter(func.lower(Project.name).contains(search.lower()))
|
||||
|
||||
# Get total count before pagination
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * limit
|
||||
projects = query.order_by(Project.name).offset(offset).limit(limit).all()
|
||||
|
||||
# Calculate total pages
|
||||
total_pages = math.ceil(total / limit) if total > 0 else 1
|
||||
|
||||
return PaginatedResponse(
|
||||
items=projects,
|
||||
pagination=PaginationMeta(
|
||||
page=page,
|
||||
limit=limit,
|
||||
total=total,
|
||||
total_pages=total_pages,
|
||||
),
|
||||
)
|
||||
).order_by(Project.name).all()
|
||||
return projects
|
||||
|
||||
|
||||
@router.post("/api/v1/projects", response_model=ProjectResponse)
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Generic, TypeVar
|
||||
from typing import Optional, List
|
||||
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):
|
||||
|
||||
222
backend/app/seed.py
Normal file
222
backend/app/seed.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Test data seeding for development environment.
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import Project, Package, Artifact, Tag, Upload
|
||||
from .storage import get_storage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Test data definitions
|
||||
TEST_PROJECTS = [
|
||||
{
|
||||
"name": "frontend-libs",
|
||||
"description": "Shared frontend libraries and components",
|
||||
"is_public": True,
|
||||
"packages": [
|
||||
{
|
||||
"name": "ui-components",
|
||||
"description": "Reusable UI component library",
|
||||
},
|
||||
{
|
||||
"name": "design-tokens",
|
||||
"description": "Design system tokens and variables",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "backend-services",
|
||||
"description": "Backend microservices and shared utilities",
|
||||
"is_public": True,
|
||||
"packages": [
|
||||
{
|
||||
"name": "auth-lib",
|
||||
"description": "Authentication and authorization library",
|
||||
},
|
||||
{
|
||||
"name": "common-utils",
|
||||
"description": "Common utility functions",
|
||||
},
|
||||
{
|
||||
"name": "api-client",
|
||||
"description": "Generated API client library",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "mobile-apps",
|
||||
"description": "Mobile application builds and assets",
|
||||
"is_public": True,
|
||||
"packages": [
|
||||
{
|
||||
"name": "ios-release",
|
||||
"description": "iOS release builds",
|
||||
},
|
||||
{
|
||||
"name": "android-release",
|
||||
"description": "Android release builds",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "internal-tools",
|
||||
"description": "Internal development tools (private)",
|
||||
"is_public": False,
|
||||
"packages": [
|
||||
{
|
||||
"name": "dev-scripts",
|
||||
"description": "Development automation scripts",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# Sample artifacts to create (content, tags)
|
||||
TEST_ARTIFACTS = [
|
||||
{
|
||||
"project": "frontend-libs",
|
||||
"package": "ui-components",
|
||||
"content": b"/* UI Components v1.0.0 */\nexport const Button = () => {};\nexport const Input = () => {};\n",
|
||||
"filename": "ui-components-1.0.0.js",
|
||||
"content_type": "application/javascript",
|
||||
"tags": ["v1.0.0", "latest"],
|
||||
},
|
||||
{
|
||||
"project": "frontend-libs",
|
||||
"package": "ui-components",
|
||||
"content": b"/* UI Components v1.1.0 */\nexport const Button = () => {};\nexport const Input = () => {};\nexport const Modal = () => {};\n",
|
||||
"filename": "ui-components-1.1.0.js",
|
||||
"content_type": "application/javascript",
|
||||
"tags": ["v1.1.0"],
|
||||
},
|
||||
{
|
||||
"project": "frontend-libs",
|
||||
"package": "design-tokens",
|
||||
"content": b'{"colors": {"primary": "#007bff", "secondary": "#6c757d"}, "spacing": {"sm": "8px", "md": "16px"}}',
|
||||
"filename": "tokens.json",
|
||||
"content_type": "application/json",
|
||||
"tags": ["v1.0.0", "latest"],
|
||||
},
|
||||
{
|
||||
"project": "backend-services",
|
||||
"package": "common-utils",
|
||||
"content": b"# Common Utils\n\ndef format_date(dt):\n return dt.isoformat()\n\ndef slugify(text):\n return text.lower().replace(' ', '-')\n",
|
||||
"filename": "utils-2.0.0.py",
|
||||
"content_type": "text/x-python",
|
||||
"tags": ["v2.0.0", "stable", "latest"],
|
||||
},
|
||||
{
|
||||
"project": "backend-services",
|
||||
"package": "auth-lib",
|
||||
"content": b"package auth\n\nfunc ValidateToken(token string) bool {\n return len(token) > 0\n}\n",
|
||||
"filename": "auth-lib-1.0.0.go",
|
||||
"content_type": "text/x-go",
|
||||
"tags": ["v1.0.0", "latest"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def is_database_empty(db: Session) -> bool:
|
||||
"""Check if the database has any projects."""
|
||||
return db.query(Project).first() is None
|
||||
|
||||
|
||||
def seed_database(db: Session) -> None:
|
||||
"""Seed the database with test data."""
|
||||
if not is_database_empty(db):
|
||||
logger.info("Database already has data, skipping seed")
|
||||
return
|
||||
|
||||
logger.info("Seeding database with test data...")
|
||||
storage = get_storage()
|
||||
|
||||
# Create projects and packages
|
||||
project_map = {}
|
||||
package_map = {}
|
||||
|
||||
for project_data in TEST_PROJECTS:
|
||||
project = Project(
|
||||
name=project_data["name"],
|
||||
description=project_data["description"],
|
||||
is_public=project_data["is_public"],
|
||||
created_by="seed-user",
|
||||
)
|
||||
db.add(project)
|
||||
db.flush() # Get the ID
|
||||
project_map[project_data["name"]] = project
|
||||
|
||||
for package_data in project_data["packages"]:
|
||||
package = Package(
|
||||
project_id=project.id,
|
||||
name=package_data["name"],
|
||||
description=package_data["description"],
|
||||
)
|
||||
db.add(package)
|
||||
db.flush()
|
||||
package_map[(project_data["name"], package_data["name"])] = package
|
||||
|
||||
logger.info(f"Created {len(project_map)} projects and {len(package_map)} packages")
|
||||
|
||||
# Create artifacts and tags
|
||||
artifact_count = 0
|
||||
tag_count = 0
|
||||
|
||||
for artifact_data in TEST_ARTIFACTS:
|
||||
project = project_map[artifact_data["project"]]
|
||||
package = package_map[(artifact_data["project"], artifact_data["package"])]
|
||||
|
||||
content = artifact_data["content"]
|
||||
sha256_hash = hashlib.sha256(content).hexdigest()
|
||||
size = len(content)
|
||||
s3_key = f"fruits/{sha256_hash[:2]}/{sha256_hash[2:4]}/{sha256_hash}"
|
||||
|
||||
# Store in S3
|
||||
try:
|
||||
storage.client.put_object(
|
||||
Bucket=storage.bucket,
|
||||
Key=s3_key,
|
||||
Body=content,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to store artifact in S3: {e}")
|
||||
continue
|
||||
|
||||
# Create artifact record
|
||||
artifact = Artifact(
|
||||
id=sha256_hash,
|
||||
size=size,
|
||||
content_type=artifact_data["content_type"],
|
||||
original_name=artifact_data["filename"],
|
||||
created_by="seed-user",
|
||||
s3_key=s3_key,
|
||||
ref_count=len(artifact_data["tags"]),
|
||||
)
|
||||
db.add(artifact)
|
||||
|
||||
# Create upload record
|
||||
upload = Upload(
|
||||
artifact_id=sha256_hash,
|
||||
package_id=package.id,
|
||||
original_name=artifact_data["filename"],
|
||||
uploaded_by="seed-user",
|
||||
)
|
||||
db.add(upload)
|
||||
artifact_count += 1
|
||||
|
||||
# Create tags
|
||||
for tag_name in artifact_data["tags"]:
|
||||
tag = Tag(
|
||||
package_id=package.id,
|
||||
name=tag_name,
|
||||
artifact_id=sha256_hash,
|
||||
created_by="seed-user",
|
||||
)
|
||||
db.add(tag)
|
||||
tag_count += 1
|
||||
|
||||
db.commit()
|
||||
logger.info(f"Created {artifact_count} artifacts and {tag_count} tags")
|
||||
logger.info("Database seeding complete")
|
||||
Reference in New Issue
Block a user