WealthFromAI commited on
Commit
ce3bece
·
verified ·
1 Parent(s): 7fb2dbb

FORGE-X: Upload source (ccc35f8f-api.zip)

Browse files
README.md CHANGED
@@ -1,13 +1,98 @@
1
  ---
2
- title: Startup Fundraising Vc Document Generation Api Ccc3
3
- emoji: 📚
4
- colorFrom: indigo
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
 
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "Startup Fundraising & VC Document Generation API"
3
+ emoji: 🚀
4
+ colorFrom: purple
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 4.44.0
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ tags:
12
+ - startup
13
+ - fundraising
14
+ - venture-capital
15
+ - pitch
16
+ - data
17
+ - validation
18
+ - enrichment
19
+ - document
20
+ - generation
21
+ - startup-fundraising
22
  ---
23
+ # Startup Fundraising & VC Document Generation API
24
 
25
+ Stop wasting weeks on pitch decks and term sheets—your investors won't wait, but your API can deliver in seconds.
26
+
27
+ This API automates the entire document generation process for startup fundraising, from pitch decks to legal term sheets, so you can focus on closing rounds instead of formatting slides. It's the only solution that combines speed, accuracy, and seamless integration into your existing workflow, making it the best startup fundraising REST API on the market.
28
+
29
+ ## What's Included
30
+
31
+ - Generate professional pitch decks with customizable templates and investor-ready data
32
+ - Create legally compliant term sheets, cap tables, and investor update letters
33
+ - Automate document versioning and track changes for audit trails
34
+ - Integrate with your CRM, email, or fundraising platform via simple REST endpoints
35
+ - Support for multiple document formats (PDF, DOCX, HTML) and branding customization
36
+
37
+ ## Who Is This For
38
+
39
+ - Founders raising seed or Series A rounds who need to produce documents fast
40
+ - Startup lawyers and legal teams automating document generation for multiple clients
41
+ - Accelerators and incubators managing batch fundraising materials
42
+ - Fundraising platforms and SaaS tools embedding document generation as a feature
43
+
44
+ ## How It Works
45
+
46
+ Sign up for an API key, then send a POST request with your startup's data (e.g., company name, financials, team info) and the document type. The API returns a ready-to-download file in seconds. No manual templates, no complex setup—just code and close.
47
+
48
+ ## Frequently Asked Questions
49
+
50
+ **Can I customize the document templates?**
51
+ Yes, you can pass custom CSS, branding colors, and logos via API parameters, or use our template editor to create your own.
52
+
53
+ **Is the API compliant with legal standards for term sheets?**
54
+ Our templates are based on standard VC term sheet formats (NVCA, Y Combinator) and can be reviewed by your legal team. We recommend final legal review before signing.
55
+
56
+ **What programming languages does the API support?**
57
+ Any language that can make HTTP requests—Python, JavaScript, Ruby, PHP, Go, etc. We provide SDKs for Python and Node.js.
58
+
59
+ **How many documents can I generate per month?**
60
+ The $42.23 plan includes 500 document generations per month. Higher tiers are available for larger volumes.
61
+
62
+ **Can I integrate this with my existing fundraising CRM?**
63
+ Yes, the REST API is designed for easy integration. We have pre-built connectors for HubSpot, Salesforce, and Notion.
64
+
65
+ ## What You Get
66
+
67
+ - Instant digital download
68
+ - Complete REST API with full documentation
69
+ - Free updates for life — pay once, own forever
70
+ - Setup guide and usage instructions
71
+
72
+ **Get the Startup Fundraising & VC Document Generation API now and close your round faster—no more manual document delays.**
73
+
74
+ ## 🚀 Usage
75
+
76
+ 1. Click **Use in Spaces** above to run the demo directly
77
+ 2. Or clone the repository and run locally:
78
+
79
+ ```bash
80
+ git clone https://huggingface.co/spaces/WealthFromAI/startup-fundraising-vc-document-generation-api-ccc3
81
+ cd startup-fundraising-vc-document-generation-api-ccc3
82
+ pip install -r requirements.txt
83
+ python app.py
84
+ ```
85
+
86
+ ## 💰 Pricing
87
+
88
+ - **Demo**: Free on Hugging Face Spaces
89
+ - **Full Source Code**: $42.23
90
+ - Available on [Gumroad](https://gumroad.com) and [Whop](https://whop.com)
91
+
92
+ ## 📄 License
93
+
94
+ MIT License — free to use, modify, and distribute.
95
+
96
+ ---
97
+
98
+ *Built with [FORGE-X](https://github.com/WealthFromAI) — automated digital product engine*
ccc35f8f/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"]
ccc35f8f/README.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Startup Fundraising & VC Document Generation API
2
+
3
+ > Generate PDFs, contracts, reports, and forms from templates with dynamic Startup Fundraising & VC data. Embed document generation in any app.
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
ccc35f8f/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
+ )
ccc35f8f/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
ccc35f8f/main.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Startup Fundraising & VC Document Generation API — REST API Service
3
+ Generate PDFs, contracts, reports, and forms from templates with dynamic Startup Fundraising & VC data. Embed document generation in any app.
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="Startup Fundraising & VC Document Generation API",
39
+ description="Generate PDFs, contracts, reports, and forms from templates with dynamic Startup Fundraising & VC data. Embed document generation in any app.",
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": "Startup Fundraising & VC Document Generation 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": "Startup Fundraising & VC Document Generation API",
183
+ "niche": "startup_fundraising",
184
+ }
ccc35f8f/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 startup_fundraising item",
16
+ "category": "default",
17
+ "tags": ["startup_fundraising"],
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
ccc35f8f/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
ccc35f8f/tests/__init__.py ADDED
File without changes
ccc35f8f/tests/test_main.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for Startup Fundraising & VC Document Generation 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()