diff --git a/backend/app/config.py b/backend/app/config.py index f36dbf5..4d7be94 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index 974b946..352987b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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 diff --git a/backend/app/seed.py b/backend/app/seed.py new file mode 100644 index 0000000..e7fea9a --- /dev/null +++ b/backend/app/seed.py @@ -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")