quantlix 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +1 -0
- api/auth.py +81 -0
- api/config.py +61 -0
- api/db.py +38 -0
- api/disposable_domains.py +499 -0
- api/email.py +202 -0
- api/main.py +83 -0
- api/models.py +142 -0
- api/queue.py +23 -0
- api/rate_limit.py +178 -0
- api/routes/__init__.py +1 -0
- api/routes/auth.py +477 -0
- api/routes/billing.py +234 -0
- api/routes/deploy.py +34 -0
- api/routes/health.py +20 -0
- api/routes/jobs.py +43 -0
- api/routes/run.py +76 -0
- api/routes/status.py +63 -0
- api/routes/usage.py +143 -0
- api/schemas.py +252 -0
- api/usage_service.py +82 -0
- cli/__init__.py +1 -0
- cli/main.py +367 -0
- orchestrator/__init__.py +1 -0
- orchestrator/config.py +28 -0
- orchestrator/inference_client.py +38 -0
- orchestrator/k8s.py +124 -0
- orchestrator/main.py +43 -0
- orchestrator/worker.py +192 -0
- quantlix-0.1.0.dist-info/METADATA +107 -0
- quantlix-0.1.0.dist-info/RECORD +39 -0
- quantlix-0.1.0.dist-info/WHEEL +5 -0
- quantlix-0.1.0.dist-info/entry_points.txt +2 -0
- quantlix-0.1.0.dist-info/licenses/LICENSE +21 -0
- quantlix-0.1.0.dist-info/top_level.txt +4 -0
- sdk/__init__.py +1 -0
- sdk/python/__init__.py +24 -0
- sdk/quantlix/__init__.py +21 -0
- sdk/quantlix/client.py +311 -0
api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Quantlix API."""
|
api/auth.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""API key and password authentication."""
|
|
2
|
+
import hashlib
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import bcrypt
|
|
6
|
+
from fastapi import Depends, HTTPException, status
|
|
7
|
+
from fastapi.security import APIKeyHeader
|
|
8
|
+
from sqlalchemy import select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from api.db import get_db
|
|
12
|
+
from api.models import APIKey, User
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def hash_password(password: str) -> str:
|
|
19
|
+
"""Hash password with bcrypt."""
|
|
20
|
+
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
24
|
+
"""Verify password against hash."""
|
|
25
|
+
return bcrypt.checkpw(plain.encode(), hashed.encode())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def hash_api_key(key: str) -> str:
|
|
29
|
+
"""Hash API key for storage (never store plain)."""
|
|
30
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_user_from_api_key(
|
|
34
|
+
api_key: Annotated[str | None, Depends(api_key_header)],
|
|
35
|
+
db: Annotated[AsyncSession, Depends(get_db)],
|
|
36
|
+
) -> User:
|
|
37
|
+
"""Resolve API key to user. Raises 401 if invalid or missing."""
|
|
38
|
+
if not api_key or not api_key.strip():
|
|
39
|
+
raise HTTPException(
|
|
40
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
41
|
+
detail="Missing API key. Provide X-API-Key header.",
|
|
42
|
+
)
|
|
43
|
+
key_hash = hash_api_key(api_key.strip())
|
|
44
|
+
result = await db.execute(
|
|
45
|
+
select(APIKey).where(APIKey.key_hash == key_hash)
|
|
46
|
+
)
|
|
47
|
+
api_key_row = result.scalar_one_or_none()
|
|
48
|
+
if not api_key_row:
|
|
49
|
+
raise HTTPException(
|
|
50
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
51
|
+
detail="Invalid API key.",
|
|
52
|
+
)
|
|
53
|
+
result = await db.execute(select(User).where(User.id == api_key_row.user_id))
|
|
54
|
+
user = result.scalar_one()
|
|
55
|
+
return user
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def get_current_api_key(
|
|
59
|
+
api_key: Annotated[str | None, Depends(api_key_header)],
|
|
60
|
+
db: Annotated[AsyncSession, Depends(get_db)],
|
|
61
|
+
) -> APIKey:
|
|
62
|
+
"""Resolve API key header to APIKey row. Raises 401 if invalid. For rotate/revoke flows."""
|
|
63
|
+
if not api_key or not api_key.strip():
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
66
|
+
detail="Missing API key. Provide X-API-Key header.",
|
|
67
|
+
)
|
|
68
|
+
key_hash = hash_api_key(api_key.strip())
|
|
69
|
+
result = await db.execute(select(APIKey).where(APIKey.key_hash == key_hash))
|
|
70
|
+
api_key_row = result.scalar_one_or_none()
|
|
71
|
+
if not api_key_row:
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
74
|
+
detail="Invalid API key.",
|
|
75
|
+
)
|
|
76
|
+
return api_key_row
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Type aliases for route dependencies
|
|
80
|
+
CurrentUser = Annotated[User, Depends(get_user_from_api_key)]
|
|
81
|
+
CurrentAPIKey = Annotated[APIKey, Depends(get_current_api_key)]
|
api/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Application configuration."""
|
|
2
|
+
from pydantic import ConfigDict
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Settings(BaseSettings):
|
|
7
|
+
model_config = ConfigDict(
|
|
8
|
+
extra="ignore",
|
|
9
|
+
env_file=".env",
|
|
10
|
+
env_file_encoding="utf-8",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
postgres_host: str = "localhost"
|
|
14
|
+
postgres_port: int = 5432
|
|
15
|
+
postgres_user: str = "cloud"
|
|
16
|
+
postgres_password: str = "cloud_secret"
|
|
17
|
+
postgres_db: str = "cloud"
|
|
18
|
+
redis_url: str = "redis://localhost:6379/0"
|
|
19
|
+
minio_endpoint: str = "localhost:9000"
|
|
20
|
+
minio_access_key: str = "minioadmin"
|
|
21
|
+
minio_secret_key: str = "minioadmin"
|
|
22
|
+
minio_bucket: str = "models"
|
|
23
|
+
jwt_secret: str = "dev-secret-change-me"
|
|
24
|
+
|
|
25
|
+
# Email (Sweego). Set to False to disable all email until domain is verified.
|
|
26
|
+
# Use SWEEGO_API_KEY for HTTP API (recommended in K8s; avoids SMTP port blocking).
|
|
27
|
+
# Otherwise use SMTP with smtp_user/smtp_password.
|
|
28
|
+
email_enabled: bool = True
|
|
29
|
+
sweego_api_key: str = "" # When set, use Sweego HTTP API instead of SMTP
|
|
30
|
+
sweego_auth_type: str = "api_token" # "api_token" (Api-Token), "api_key" (Api-Key), or "bearer" (Authorization: Bearer)
|
|
31
|
+
smtp_host: str = "smtp.sweego.io"
|
|
32
|
+
smtp_port: int = 587
|
|
33
|
+
smtp_user: str = ""
|
|
34
|
+
smtp_password: str = ""
|
|
35
|
+
smtp_from_email: str = "support@quantlix.ai"
|
|
36
|
+
smtp_from_name: str = "Quantlix"
|
|
37
|
+
app_base_url: str = "https://api.quantlix.ai" # For verification links
|
|
38
|
+
portal_base_url: str = "https://app.quantlix.ai" # For Stripe redirects
|
|
39
|
+
dev_return_verification_link: bool = False # If True, include verification link in signup response (for local testing)
|
|
40
|
+
|
|
41
|
+
# Usage limits (0 = unlimited)
|
|
42
|
+
usage_limit_tokens_per_month: int = 0
|
|
43
|
+
usage_limit_compute_seconds_per_month: float = 0.0
|
|
44
|
+
|
|
45
|
+
# CORS (comma-separated extra origins, e.g. for Vercel: https://quantlix.vercel.app)
|
|
46
|
+
cors_origins: str = ""
|
|
47
|
+
|
|
48
|
+
# Stripe
|
|
49
|
+
stripe_secret_key: str = ""
|
|
50
|
+
stripe_webhook_secret: str = ""
|
|
51
|
+
stripe_price_id_starter: str = "" # Price ID for Starter €9/mo
|
|
52
|
+
stripe_price_id_pro: str = "" # Price ID for Pro plan (e.g. price_xxx)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def database_url(self) -> str:
|
|
56
|
+
return (
|
|
57
|
+
f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
|
|
58
|
+
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
settings = Settings()
|
api/db.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Database setup and session management."""
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
5
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
6
|
+
|
|
7
|
+
from api.config import settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Base(DeclarativeBase):
|
|
11
|
+
"""SQLAlchemy declarative base."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
engine = create_async_engine(
|
|
15
|
+
settings.database_url,
|
|
16
|
+
echo=False,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
async_session_maker = async_sessionmaker(
|
|
20
|
+
engine,
|
|
21
|
+
class_=AsyncSession,
|
|
22
|
+
expire_on_commit=False,
|
|
23
|
+
autocommit=False,
|
|
24
|
+
autoflush=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
29
|
+
"""Dependency for async DB session."""
|
|
30
|
+
async with async_session_maker() as session:
|
|
31
|
+
try:
|
|
32
|
+
yield session
|
|
33
|
+
await session.commit()
|
|
34
|
+
except Exception:
|
|
35
|
+
await session.rollback()
|
|
36
|
+
raise
|
|
37
|
+
finally:
|
|
38
|
+
await session.close()
|