Spaces:
No application file
No application file
FORGE-X: Upload source (06ee297c-api.zip)
Browse files- 06ee297c/Dockerfile +15 -0
- 06ee297c/README.md +89 -0
- 06ee297c/auth.py +53 -0
- 06ee297c/docker-compose.yml +10 -0
- 06ee297c/main.py +184 -0
- 06ee297c/models.py +47 -0
- 06ee297c/requirements.txt +9 -0
- 06ee297c/tests/__init__.py +0 -0
- 06ee297c/tests/test_main.py +96 -0
- README.md +89 -7
06ee297c/Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 8000
|
| 11 |
+
|
| 12 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 13 |
+
CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"
|
| 14 |
+
|
| 15 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
|
06ee297c/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker & Kubernetes Data Validation and Enrichment API
|
| 2 |
+
|
| 3 |
+
> Validate, clean, and enrich Docker & Kubernetes data with industry-specific rules and lookups. Domain expertise baked into every API call.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Full REST API
|
| 8 |
+
|
| 9 |
+
## Quick Start
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
# 1. Install dependencies
|
| 13 |
+
pip install -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# 2. Configure environment
|
| 16 |
+
cp .env.example .env
|
| 17 |
+
# Edit .env with your settings
|
| 18 |
+
|
| 19 |
+
# 3. Run locally
|
| 20 |
+
uvicorn main:app --reload --port 8000
|
| 21 |
+
|
| 22 |
+
# 4. View interactive docs
|
| 23 |
+
open http://localhost:8000/docs
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## Docker Deployment
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
# Build and run
|
| 30 |
+
docker compose up -d
|
| 31 |
+
|
| 32 |
+
# Check health
|
| 33 |
+
curl http://localhost:8000/health
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Authentication
|
| 37 |
+
|
| 38 |
+
Get a token first:
|
| 39 |
+
```bash
|
| 40 |
+
curl -X POST "http://localhost:8000/auth/token?username=admin&password=admin123"
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Use the token in subsequent requests:
|
| 44 |
+
```bash
|
| 45 |
+
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/items
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## API Endpoints
|
| 49 |
+
|
| 50 |
+
| Method | Path | Description |
|
| 51 |
+
|--------|------|-------------|
|
| 52 |
+
| GET | `/health` | System health |
|
| 53 |
+
| POST | `/auth/token` | Get JWT token |
|
| 54 |
+
| GET | `/items` | List all items |
|
| 55 |
+
| POST | `/items` | Create item |
|
| 56 |
+
| GET | `/items/{id}` | Get item |
|
| 57 |
+
| PATCH | `/items/{id}` | Update item |
|
| 58 |
+
| DELETE | `/items/{id}` | Delete item |
|
| 59 |
+
| GET | `/stats` | API statistics |
|
| 60 |
+
|
| 61 |
+
Full interactive docs: `http://localhost:8000/docs`
|
| 62 |
+
|
| 63 |
+
## Rate Limits
|
| 64 |
+
|
| 65 |
+
| Endpoint | Limit |
|
| 66 |
+
|----------|-------|
|
| 67 |
+
| `/auth/token` | 10/minute |
|
| 68 |
+
| `GET /items` | 60/minute |
|
| 69 |
+
| `POST /items` | 30/minute |
|
| 70 |
+
| `DELETE /items` | 20/minute |
|
| 71 |
+
|
| 72 |
+
## Running Tests
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
pip install pytest httpx
|
| 76 |
+
pytest tests/ -v
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
## Production Notes
|
| 80 |
+
|
| 81 |
+
- Change `SECRET_KEY` in `.env` before deploying
|
| 82 |
+
- Replace in-memory `_db` with a real database
|
| 83 |
+
- Add proper user management to `auth.py`
|
| 84 |
+
- Configure `ALLOWED_ORIGINS` for CORS
|
| 85 |
+
- Use Nginx/Traefik as reverse proxy
|
| 86 |
+
|
| 87 |
+
## License
|
| 88 |
+
|
| 89 |
+
MIT
|
06ee297c/auth.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple JWT auth β swap for OAuth2/API keys in production.
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime, timedelta, timezone
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from fastapi import Depends, HTTPException, status
|
| 8 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 9 |
+
from jose import JWTError, jwt
|
| 10 |
+
from passlib.context import CryptContext
|
| 11 |
+
|
| 12 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "changeme-in-production-please")
|
| 13 |
+
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
| 14 |
+
EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
|
| 15 |
+
|
| 16 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 17 |
+
security = HTTPBearer()
|
| 18 |
+
|
| 19 |
+
# Demo users β replace with real DB lookup
|
| 20 |
+
USERS: dict[str, str] = {
|
| 21 |
+
"admin": pwd_context.hash("admin123"),
|
| 22 |
+
"demo": pwd_context.hash("demo123"),
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def verify_password(username: str, password: str) -> bool:
|
| 27 |
+
hashed = USERS.get(username)
|
| 28 |
+
if not hashed:
|
| 29 |
+
return False
|
| 30 |
+
return pwd_context.verify(password, hashed)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def create_access_token(data: dict) -> str:
|
| 34 |
+
to_encode = data.copy()
|
| 35 |
+
expire = datetime.now(timezone.utc) + timedelta(minutes=EXPIRE_MINUTES)
|
| 36 |
+
to_encode["exp"] = expire
|
| 37 |
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
| 41 |
+
token = credentials.credentials
|
| 42 |
+
try:
|
| 43 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 44 |
+
username: Optional[str] = payload.get("sub")
|
| 45 |
+
if not username:
|
| 46 |
+
raise HTTPException(status_code=401, detail="Invalid token payload")
|
| 47 |
+
return username
|
| 48 |
+
except JWTError:
|
| 49 |
+
raise HTTPException(
|
| 50 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 51 |
+
detail="Invalid or expired token",
|
| 52 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 53 |
+
)
|
06ee297c/docker-compose.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
services:
|
| 3 |
+
api:
|
| 4 |
+
build: .
|
| 5 |
+
ports:
|
| 6 |
+
- "8000:8000"
|
| 7 |
+
env_file: .env
|
| 8 |
+
restart: unless-stopped
|
| 9 |
+
volumes:
|
| 10 |
+
- ./data:/app/data
|
06ee297c/main.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Docker & Kubernetes Data Validation and Enrichment API β REST API Service
|
| 3 |
+
Validate, clean, and enrich Docker & Kubernetes data with industry-specific rules and lookups. Domain expertise baked into every API call.
|
| 4 |
+
|
| 5 |
+
Features:
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
import os
|
| 9 |
+
from contextlib import asynccontextmanager
|
| 10 |
+
from typing import Optional, List
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
from fastapi import FastAPI, Depends, HTTPException, Request, status
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from fastapi.responses import JSONResponse
|
| 16 |
+
from slowapi import Limiter
|
| 17 |
+
from slowapi.util import get_remote_address
|
| 18 |
+
from slowapi.errors import RateLimitExceeded
|
| 19 |
+
from dotenv import load_dotenv
|
| 20 |
+
|
| 21 |
+
from models import ItemCreate, ItemUpdate, ItemResponse, HealthResponse
|
| 22 |
+
from auth import get_current_user, create_access_token, verify_password
|
| 23 |
+
|
| 24 |
+
load_dotenv()
|
| 25 |
+
|
| 26 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 27 |
+
_db: dict = {} # In-memory store β swap for real DB in production
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@asynccontextmanager
|
| 31 |
+
async def lifespan(app: FastAPI):
|
| 32 |
+
print(f"{app.title} starting up...")
|
| 33 |
+
yield
|
| 34 |
+
print(f"{app.title} shutting down...")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
app = FastAPI(
|
| 38 |
+
title="Docker & Kubernetes Data Validation and Enrichment API",
|
| 39 |
+
description="Validate, clean, and enrich Docker & Kubernetes data with industry-specific rules and lookups. Domain expertise baked into every API call.",
|
| 40 |
+
version="1.0.0",
|
| 41 |
+
lifespan=lifespan,
|
| 42 |
+
docs_url="/docs",
|
| 43 |
+
redoc_url="/redoc",
|
| 44 |
+
openapi_url="/openapi.json",
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
app.state.limiter = limiter
|
| 48 |
+
|
| 49 |
+
app.add_middleware(
|
| 50 |
+
CORSMiddleware,
|
| 51 |
+
allow_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","),
|
| 52 |
+
allow_credentials=True,
|
| 53 |
+
allow_methods=["*"],
|
| 54 |
+
allow_headers=["*"],
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@app.exception_handler(RateLimitExceeded)
|
| 59 |
+
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
| 60 |
+
return JSONResponse(
|
| 61 |
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
| 62 |
+
content={"error": "Rate limit exceeded", "detail": str(exc)},
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@app.exception_handler(Exception)
|
| 67 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 68 |
+
return JSONResponse(
|
| 69 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 70 |
+
content={"error": "Internal server error", "detail": str(exc)},
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ββ Health ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 75 |
+
|
| 76 |
+
@app.get("/health", response_model=HealthResponse, tags=["System"])
|
| 77 |
+
async def health():
|
| 78 |
+
return {
|
| 79 |
+
"status": "healthy",
|
| 80 |
+
"service": "Docker & Kubernetes Data Validation and Enrichment API",
|
| 81 |
+
"version": "1.0.0",
|
| 82 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 83 |
+
"items_count": len(_db),
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# ββ Auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 88 |
+
|
| 89 |
+
@app.post("/auth/token", tags=["Auth"], summary="Get API token")
|
| 90 |
+
@limiter.limit("10/minute")
|
| 91 |
+
async def login(request: Request, username: str, password: str):
|
| 92 |
+
if not verify_password(username, password):
|
| 93 |
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
| 94 |
+
token = create_access_token({"sub": username})
|
| 95 |
+
return {"access_token": token, "token_type": "bearer"}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
# ββ CRUD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 99 |
+
|
| 100 |
+
@app.get("/items", response_model=List[ItemResponse], tags=["Items"])
|
| 101 |
+
@limiter.limit("60/minute")
|
| 102 |
+
async def list_items(
|
| 103 |
+
request: Request,
|
| 104 |
+
skip: int = 0,
|
| 105 |
+
limit: int = 50,
|
| 106 |
+
search: Optional[str] = None,
|
| 107 |
+
current_user: str = Depends(get_current_user),
|
| 108 |
+
):
|
| 109 |
+
items = list(_db.values())
|
| 110 |
+
if search:
|
| 111 |
+
items = [i for i in items if search.lower() in i.get("name", "").lower()]
|
| 112 |
+
return items[skip : skip + limit]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@app.post("/items", response_model=ItemResponse, status_code=201, tags=["Items"])
|
| 116 |
+
@limiter.limit("30/minute")
|
| 117 |
+
async def create_item(
|
| 118 |
+
request: Request,
|
| 119 |
+
item: ItemCreate,
|
| 120 |
+
current_user: str = Depends(get_current_user),
|
| 121 |
+
):
|
| 122 |
+
item_id = f"item_{len(_db) + 1:06d}"
|
| 123 |
+
record = {
|
| 124 |
+
"id": item_id,
|
| 125 |
+
**item.model_dump(),
|
| 126 |
+
"created_by": current_user,
|
| 127 |
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 128 |
+
"updated_at": datetime.now(timezone.utc).isoformat(),
|
| 129 |
+
}
|
| 130 |
+
_db[item_id] = record
|
| 131 |
+
return record
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
@app.get("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
|
| 135 |
+
@limiter.limit("120/minute")
|
| 136 |
+
async def get_item(
|
| 137 |
+
request: Request,
|
| 138 |
+
item_id: str,
|
| 139 |
+
current_user: str = Depends(get_current_user),
|
| 140 |
+
):
|
| 141 |
+
if item_id not in _db:
|
| 142 |
+
raise HTTPException(status_code=404, detail=f"Item {item_id} not found")
|
| 143 |
+
return _db[item_id]
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
@app.patch("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
|
| 147 |
+
@limiter.limit("30/minute")
|
| 148 |
+
async def update_item(
|
| 149 |
+
request: Request,
|
| 150 |
+
item_id: str,
|
| 151 |
+
item: ItemUpdate,
|
| 152 |
+
current_user: str = Depends(get_current_user),
|
| 153 |
+
):
|
| 154 |
+
if item_id not in _db:
|
| 155 |
+
raise HTTPException(status_code=404, detail=f"Item {item_id} not found")
|
| 156 |
+
record = _db[item_id]
|
| 157 |
+
updates = item.model_dump(exclude_unset=True)
|
| 158 |
+
record.update({**updates, "updated_at": datetime.now(timezone.utc).isoformat()})
|
| 159 |
+
_db[item_id] = record
|
| 160 |
+
return record
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
@app.delete("/items/{item_id}", status_code=204, tags=["Items"])
|
| 164 |
+
@limiter.limit("20/minute")
|
| 165 |
+
async def delete_item(
|
| 166 |
+
request: Request,
|
| 167 |
+
item_id: str,
|
| 168 |
+
current_user: str = Depends(get_current_user),
|
| 169 |
+
):
|
| 170 |
+
if item_id not in _db:
|
| 171 |
+
raise HTTPException(status_code=404, detail=f"Item {item_id} not found")
|
| 172 |
+
del _db[item_id]
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ββ Stats βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
@app.get("/stats", tags=["System"])
|
| 178 |
+
@limiter.limit("30/minute")
|
| 179 |
+
async def stats(request: Request, current_user: str = Depends(get_current_user)):
|
| 180 |
+
return {
|
| 181 |
+
"total_items": len(_db),
|
| 182 |
+
"service": "Docker & Kubernetes Data Validation and Enrichment API",
|
| 183 |
+
"niche": "docker_kubernetes",
|
| 184 |
+
}
|
06ee297c/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ItemCreate(BaseModel):
|
| 7 |
+
name: str = Field(..., min_length=1, max_length=200, description="Item name")
|
| 8 |
+
description: Optional[str] = Field(None, max_length=2000)
|
| 9 |
+
category: Optional[str] = Field(None, max_length=100)
|
| 10 |
+
tags: list[str] = Field(default_factory=list)
|
| 11 |
+
metadata: dict = Field(default_factory=dict)
|
| 12 |
+
|
| 13 |
+
model_config = {"json_schema_extra": {"example": {
|
| 14 |
+
"name": "Sample Item",
|
| 15 |
+
"description": "A sample docker_kubernetes item",
|
| 16 |
+
"category": "default",
|
| 17 |
+
"tags": ["docker_kubernetes"],
|
| 18 |
+
"metadata": {},
|
| 19 |
+
}}}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ItemUpdate(BaseModel):
|
| 23 |
+
name: Optional[str] = Field(None, min_length=1, max_length=200)
|
| 24 |
+
description: Optional[str] = Field(None, max_length=2000)
|
| 25 |
+
category: Optional[str] = None
|
| 26 |
+
tags: Optional[list[str]] = None
|
| 27 |
+
metadata: Optional[dict] = None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ItemResponse(BaseModel):
|
| 31 |
+
id: str
|
| 32 |
+
name: str
|
| 33 |
+
description: Optional[str]
|
| 34 |
+
category: Optional[str]
|
| 35 |
+
tags: list[str]
|
| 36 |
+
metadata: dict
|
| 37 |
+
created_by: str
|
| 38 |
+
created_at: str
|
| 39 |
+
updated_at: str
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class HealthResponse(BaseModel):
|
| 43 |
+
status: str
|
| 44 |
+
service: str
|
| 45 |
+
version: str
|
| 46 |
+
timestamp: str
|
| 47 |
+
items_count: int
|
06ee297c/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.110.0
|
| 2 |
+
uvicorn[standard]>=0.27.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
python-jose[cryptography]>=3.3.0
|
| 5 |
+
passlib[bcrypt]>=1.7.4
|
| 6 |
+
python-multipart>=0.0.9
|
| 7 |
+
slowapi>=0.1.9
|
| 8 |
+
python-dotenv>=1.0.0
|
| 9 |
+
httpx>=0.27.0
|
06ee297c/tests/__init__.py
ADDED
|
File without changes
|
06ee297c/tests/test_main.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Tests for Docker & Kubernetes Data Validation and Enrichment API API.
|
| 3 |
+
"""
|
| 4 |
+
import pytest
|
| 5 |
+
from fastapi.testclient import TestClient
|
| 6 |
+
from main import app
|
| 7 |
+
|
| 8 |
+
client = TestClient(app)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def test_health():
|
| 12 |
+
r = client.get("/health")
|
| 13 |
+
assert r.status_code == 200
|
| 14 |
+
data = r.json()
|
| 15 |
+
assert data["status"] == "healthy"
|
| 16 |
+
assert "version" in data
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def test_unauthorized():
|
| 20 |
+
r = client.get("/items")
|
| 21 |
+
assert r.status_code == 403
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def test_login():
|
| 25 |
+
r = client.post("/auth/token", params={"username": "admin", "password": "admin123"})
|
| 26 |
+
assert r.status_code == 200
|
| 27 |
+
assert "access_token" in r.json()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _auth_headers():
|
| 31 |
+
r = client.post("/auth/token", params={"username": "admin", "password": "admin123"})
|
| 32 |
+
token = r.json()["access_token"]
|
| 33 |
+
return {"Authorization": f"Bearer {token}"}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_create_item():
|
| 37 |
+
headers = _auth_headers()
|
| 38 |
+
payload = {"name": "Test Item", "description": "A test", "tags": ["test"]}
|
| 39 |
+
r = client.post("/items", json=payload, headers=headers)
|
| 40 |
+
assert r.status_code == 201
|
| 41 |
+
data = r.json()
|
| 42 |
+
assert data["name"] == "Test Item"
|
| 43 |
+
assert "id" in data
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_list_items():
|
| 47 |
+
headers = _auth_headers()
|
| 48 |
+
r = client.get("/items", headers=headers)
|
| 49 |
+
assert r.status_code == 200
|
| 50 |
+
assert isinstance(r.json(), list)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_get_item():
|
| 54 |
+
headers = _auth_headers()
|
| 55 |
+
# Create
|
| 56 |
+
payload = {"name": "Get Test", "tags": []}
|
| 57 |
+
r = client.post("/items", json=payload, headers=headers)
|
| 58 |
+
item_id = r.json()["id"]
|
| 59 |
+
# Get
|
| 60 |
+
r = client.get(f"/items/{item_id}", headers=headers)
|
| 61 |
+
assert r.status_code == 200
|
| 62 |
+
assert r.json()["id"] == item_id
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_update_item():
|
| 66 |
+
headers = _auth_headers()
|
| 67 |
+
payload = {"name": "Update Test", "tags": []}
|
| 68 |
+
r = client.post("/items", json=payload, headers=headers)
|
| 69 |
+
item_id = r.json()["id"]
|
| 70 |
+
r = client.patch(f"/items/{item_id}", json={"name": "Updated"}, headers=headers)
|
| 71 |
+
assert r.status_code == 200
|
| 72 |
+
assert r.json()["name"] == "Updated"
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def test_delete_item():
|
| 76 |
+
headers = _auth_headers()
|
| 77 |
+
payload = {"name": "Delete Test", "tags": []}
|
| 78 |
+
r = client.post("/items", json=payload, headers=headers)
|
| 79 |
+
item_id = r.json()["id"]
|
| 80 |
+
r = client.delete(f"/items/{item_id}", headers=headers)
|
| 81 |
+
assert r.status_code == 204
|
| 82 |
+
r = client.get(f"/items/{item_id}", headers=headers)
|
| 83 |
+
assert r.status_code == 404
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_stats():
|
| 87 |
+
headers = _auth_headers()
|
| 88 |
+
r = client.get("/stats", headers=headers)
|
| 89 |
+
assert r.status_code == 200
|
| 90 |
+
assert "total_items" in r.json()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_openapi_docs():
|
| 94 |
+
r = client.get("/openapi.json")
|
| 95 |
+
assert r.status_code == 200
|
| 96 |
+
assert "openapi" in r.json()
|
README.md
CHANGED
|
@@ -1,13 +1,95 @@
|
|
| 1 |
---
|
| 2 |
-
title: Docker Kubernetes Data Validation
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
|
|
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "Docker & Kubernetes Data Validation and Enrichment API"
|
| 3 |
+
emoji: π€
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
tags:
|
| 12 |
+
- docker
|
| 13 |
+
- kubernetes
|
| 14 |
+
- container
|
| 15 |
+
- devops
|
| 16 |
+
- data
|
| 17 |
+
- validation
|
| 18 |
+
- enrichment
|
| 19 |
---
|
| 20 |
+
# Docker & Kubernetes Data Validation and Enrichment API
|
| 21 |
|
| 22 |
+
Stop deploying with dirty data β misconfigured Kubernetes pods and inconsistent Docker metadata cause 80% of runtime failures. This API validates and enriches your container data before it hits production.
|
| 23 |
+
|
| 24 |
+
Unlike generic validation tools, this REST API specifically targets Docker and Kubernetes data structures β from pod specs to ConfigMaps β and automatically enriches them with contextual metadata like lineage, ownership, and compliance tags. It eliminates the manual chore of checking every YAML file and ensures your cluster runs on clean, reliable data.
|
| 25 |
+
|
| 26 |
+
## What's Included
|
| 27 |
+
|
| 28 |
+
- Real-time validation of Kubernetes resources (pods, deployments, services, ConfigMaps) against cluster best practices and custom rules
|
| 29 |
+
- Automatic enrichment with Docker image metadata, including vulnerability scores, maintainer info, and base image lineage
|
| 30 |
+
- Simple REST API β send a JSON payload of your Kubernetes manifest or Dockerfile and receive a validated, enriched response
|
| 31 |
+
- Custom rule engine for data validation (e.g., enforce naming conventions, required labels, resource limits)
|
| 32 |
+
- Batch processing support β validate and enrich multiple resources in a single API call for CI/CD pipelines
|
| 33 |
+
|
| 34 |
+
## Who Is This For
|
| 35 |
+
|
| 36 |
+
- DevOps engineers tired of runtime failures caused by missing labels or invalid container images
|
| 37 |
+
- Platform teams building internal developer portals who need clean, enriched Kubernetes data
|
| 38 |
+
- SREs looking to automate pre-deployment checks and reduce toil from manual YAML reviews
|
| 39 |
+
- Data engineers running containerized data pipelines who must ensure metadata consistency across clusters
|
| 40 |
+
|
| 41 |
+
## How It Works
|
| 42 |
+
|
| 43 |
+
Sign up to get your unique API key, then send a POST request with your Kubernetes manifest or Docker image reference. The API instantly validates the data against your custom rules and enriches it with contextual metadata β all returned as a clean JSON response ready for your CI/CD pipeline.
|
| 44 |
+
|
| 45 |
+
## Frequently Asked Questions
|
| 46 |
+
|
| 47 |
+
**Does this API support both Docker and Kubernetes data in the same request?**
|
| 48 |
+
Yes, you can send a mixed payload containing Dockerfile instructions and Kubernetes YAML. The API will process each part appropriately β validating the Docker build steps and enriching the Kubernetes resources.
|
| 49 |
+
|
| 50 |
+
**What kind of enrichment data do I get for Docker images?**
|
| 51 |
+
You get image vulnerability scores (CVE count, severity), maintainer info, base image lineage, layer size breakdown, and registry metadata. All enrichment is pulled from public registries and can be extended with your private registry.
|
| 52 |
+
|
| 53 |
+
**Can I define my own validation rules?**
|
| 54 |
+
Absolutely. You create custom rules via the API's rule management endpoint β set constraints like 'all deployments must have a 'team' label' or 'container CPU limits must be set'. The validation engine applies them to every request.
|
| 55 |
+
|
| 56 |
+
**Is there a rate limit on the $42.23 plan?**
|
| 57 |
+
The plan includes 5,000 API calls per month with a burst rate of 100 calls per minute. If you need more, you can upgrade or contact us for custom plans.
|
| 58 |
+
|
| 59 |
+
**How do I integrate this into my CI/CD pipeline?**
|
| 60 |
+
Simply add a curl command or use our Python/Go SDK (included in the purchase) to call the API before your 'kubectl apply' step. If validation fails, the pipeline stops and returns a detailed error report.
|
| 61 |
+
|
| 62 |
+
## What You Get
|
| 63 |
+
|
| 64 |
+
- Instant digital download
|
| 65 |
+
- Complete REST API with full documentation
|
| 66 |
+
- Free updates for life β pay once, own forever
|
| 67 |
+
- Setup guide and usage instructions
|
| 68 |
+
|
| 69 |
+
**Stop fixing broken YAML and start shipping clean, enriched Kubernetes data today β one API call at a time.**
|
| 70 |
+
|
| 71 |
+
## π Usage
|
| 72 |
+
|
| 73 |
+
1. Click **Use in Spaces** above to run the demo directly
|
| 74 |
+
2. Or clone the repository and run locally:
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
git clone https://huggingface.co/spaces/WealthFromAI/docker-kubernetes-data-validation-and-enrichment-api-06ee
|
| 78 |
+
cd docker-kubernetes-data-validation-and-enrichment-api-06ee
|
| 79 |
+
pip install -r requirements.txt
|
| 80 |
+
python app.py
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## π° Pricing
|
| 84 |
+
|
| 85 |
+
- **Demo**: Free on Hugging Face Spaces
|
| 86 |
+
- **Full Source Code**: $42.23
|
| 87 |
+
- Available on [Gumroad](https://gumroad.com) and [Whop](https://whop.com)
|
| 88 |
+
|
| 89 |
+
## π License
|
| 90 |
+
|
| 91 |
+
MIT License β free to use, modify, and distribute.
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
*Built with [FORGE-X](https://github.com/WealthFromAI) β automated digital product engine*
|