codeshift 0.3.7__py3-none-any.whl → 0.5.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.
- codeshift/__init__.py +2 -2
- codeshift/cli/__init__.py +1 -1
- codeshift/cli/commands/__init__.py +1 -1
- codeshift/cli/commands/auth.py +46 -30
- codeshift/cli/commands/scan.py +2 -5
- codeshift/cli/commands/upgrade.py +69 -61
- codeshift/cli/commands/upgrade_all.py +1 -1
- codeshift/cli/main.py +2 -2
- codeshift/knowledge/generator.py +6 -0
- codeshift/knowledge_base/libraries/aiohttp.yaml +3 -3
- codeshift/knowledge_base/libraries/httpx.yaml +4 -4
- codeshift/knowledge_base/libraries/pytest.yaml +1 -1
- codeshift/knowledge_base/models.py +1 -0
- codeshift/migrator/llm_migrator.py +8 -12
- codeshift/migrator/transforms/marshmallow_transformer.py +50 -0
- codeshift/migrator/transforms/pydantic_v1_to_v2.py +191 -22
- codeshift/scanner/code_scanner.py +22 -2
- codeshift/utils/__init__.py +1 -1
- codeshift/utils/api_client.py +155 -15
- codeshift/utils/cache.py +1 -1
- codeshift/utils/credential_store.py +393 -0
- codeshift/utils/llm_client.py +111 -9
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/METADATA +4 -16
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/RECORD +28 -43
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/licenses/LICENSE +1 -1
- codeshift/api/__init__.py +0 -1
- codeshift/api/auth.py +0 -182
- codeshift/api/config.py +0 -73
- codeshift/api/database.py +0 -215
- codeshift/api/main.py +0 -103
- codeshift/api/models/__init__.py +0 -55
- codeshift/api/models/auth.py +0 -108
- codeshift/api/models/billing.py +0 -92
- codeshift/api/models/migrate.py +0 -42
- codeshift/api/models/usage.py +0 -116
- codeshift/api/routers/__init__.py +0 -5
- codeshift/api/routers/auth.py +0 -440
- codeshift/api/routers/billing.py +0 -395
- codeshift/api/routers/migrate.py +0 -304
- codeshift/api/routers/usage.py +0 -291
- codeshift/api/routers/webhooks.py +0 -289
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/WHEEL +0 -0
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/entry_points.txt +0 -0
- {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/top_level.txt +0 -0
codeshift/api/main.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""FastAPI application for Codeshift billing API."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import AsyncGenerator
|
|
4
|
-
from contextlib import asynccontextmanager
|
|
5
|
-
|
|
6
|
-
from fastapi import FastAPI, Request
|
|
7
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
-
from fastapi.responses import JSONResponse
|
|
9
|
-
|
|
10
|
-
from codeshift import __version__
|
|
11
|
-
from codeshift.api.config import get_settings
|
|
12
|
-
from codeshift.api.routers import auth, billing, migrate, usage, webhooks
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@asynccontextmanager
|
|
16
|
-
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
17
|
-
"""Application lifespan handler."""
|
|
18
|
-
# Startup
|
|
19
|
-
settings = get_settings()
|
|
20
|
-
print(f"Codeshift API starting (environment: {settings.environment})")
|
|
21
|
-
yield
|
|
22
|
-
# Shutdown
|
|
23
|
-
print("Codeshift API shutting down")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
app = FastAPI(
|
|
27
|
-
title="Codeshift API",
|
|
28
|
-
description="Billing and authentication API for Codeshift CLI",
|
|
29
|
-
version=__version__,
|
|
30
|
-
lifespan=lifespan,
|
|
31
|
-
docs_url="/docs" if not get_settings().is_production else None,
|
|
32
|
-
redoc_url="/redoc" if not get_settings().is_production else None,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# CORS configuration
|
|
36
|
-
app.add_middleware(
|
|
37
|
-
CORSMiddleware,
|
|
38
|
-
allow_origins=[
|
|
39
|
-
"https://codeshift.dev",
|
|
40
|
-
"https://www.codeshift.dev",
|
|
41
|
-
"http://localhost:3000", # Local development
|
|
42
|
-
],
|
|
43
|
-
allow_credentials=True,
|
|
44
|
-
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
45
|
-
allow_headers=["*"],
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Include routers
|
|
50
|
-
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
|
51
|
-
app.include_router(billing.router, prefix="/billing", tags=["Billing"])
|
|
52
|
-
app.include_router(migrate.router, tags=["Migration"])
|
|
53
|
-
app.include_router(usage.router, prefix="/usage", tags=["Usage"])
|
|
54
|
-
app.include_router(webhooks.router, prefix="/webhooks", tags=["Webhooks"])
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@app.get("/")
|
|
58
|
-
async def root() -> dict:
|
|
59
|
-
"""API root endpoint."""
|
|
60
|
-
return {
|
|
61
|
-
"name": "Codeshift API",
|
|
62
|
-
"version": __version__,
|
|
63
|
-
"status": "healthy",
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@app.get("/health")
|
|
68
|
-
async def health_check() -> dict:
|
|
69
|
-
"""Health check endpoint."""
|
|
70
|
-
return {"status": "healthy", "version": __version__}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@app.exception_handler(Exception)
|
|
74
|
-
async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
75
|
-
"""Global exception handler."""
|
|
76
|
-
settings = get_settings()
|
|
77
|
-
|
|
78
|
-
# Log the error
|
|
79
|
-
print(f"Unhandled exception: {exc}")
|
|
80
|
-
|
|
81
|
-
# Return appropriate response based on environment
|
|
82
|
-
if settings.is_production:
|
|
83
|
-
return JSONResponse(
|
|
84
|
-
status_code=500,
|
|
85
|
-
content={"detail": "Internal server error"},
|
|
86
|
-
)
|
|
87
|
-
else:
|
|
88
|
-
return JSONResponse(
|
|
89
|
-
status_code=500,
|
|
90
|
-
content={"detail": str(exc), "type": type(exc).__name__},
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# For running with uvicorn directly
|
|
95
|
-
if __name__ == "__main__":
|
|
96
|
-
import uvicorn
|
|
97
|
-
|
|
98
|
-
uvicorn.run(
|
|
99
|
-
"codeshift.api.main:app",
|
|
100
|
-
host="0.0.0.0",
|
|
101
|
-
port=8000,
|
|
102
|
-
reload=True,
|
|
103
|
-
)
|
codeshift/api/models/__init__.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Pydantic models for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
from codeshift.api.models.auth import (
|
|
4
|
-
APIKey,
|
|
5
|
-
APIKeyCreate,
|
|
6
|
-
APIKeyResponse,
|
|
7
|
-
TokenResponse,
|
|
8
|
-
UserInfo,
|
|
9
|
-
)
|
|
10
|
-
from codeshift.api.models.billing import (
|
|
11
|
-
CheckoutSessionRequest,
|
|
12
|
-
CheckoutSessionResponse,
|
|
13
|
-
PortalSessionResponse,
|
|
14
|
-
SubscriptionInfo,
|
|
15
|
-
TierInfo,
|
|
16
|
-
)
|
|
17
|
-
from codeshift.api.models.migrate import (
|
|
18
|
-
ExplainChangeRequest,
|
|
19
|
-
ExplainChangeResponse,
|
|
20
|
-
MigrateCodeRequest,
|
|
21
|
-
MigrateCodeResponse,
|
|
22
|
-
)
|
|
23
|
-
from codeshift.api.models.usage import (
|
|
24
|
-
QuotaInfo,
|
|
25
|
-
UsageEvent,
|
|
26
|
-
UsageEventCreate,
|
|
27
|
-
UsageResponse,
|
|
28
|
-
UsageSummary,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
__all__ = [
|
|
32
|
-
# Auth models
|
|
33
|
-
"APIKey",
|
|
34
|
-
"APIKeyCreate",
|
|
35
|
-
"APIKeyResponse",
|
|
36
|
-
"TokenResponse",
|
|
37
|
-
"UserInfo",
|
|
38
|
-
# Billing models
|
|
39
|
-
"CheckoutSessionRequest",
|
|
40
|
-
"CheckoutSessionResponse",
|
|
41
|
-
"PortalSessionResponse",
|
|
42
|
-
"SubscriptionInfo",
|
|
43
|
-
"TierInfo",
|
|
44
|
-
# Migrate models
|
|
45
|
-
"ExplainChangeRequest",
|
|
46
|
-
"ExplainChangeResponse",
|
|
47
|
-
"MigrateCodeRequest",
|
|
48
|
-
"MigrateCodeResponse",
|
|
49
|
-
# Usage models
|
|
50
|
-
"QuotaInfo",
|
|
51
|
-
"UsageEvent",
|
|
52
|
-
"UsageEventCreate",
|
|
53
|
-
"UsageResponse",
|
|
54
|
-
"UsageSummary",
|
|
55
|
-
]
|
codeshift/api/models/auth.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Authentication models for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, EmailStr, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class UserInfo(BaseModel):
|
|
9
|
-
"""User profile information."""
|
|
10
|
-
|
|
11
|
-
id: str
|
|
12
|
-
email: EmailStr
|
|
13
|
-
full_name: str | None = None
|
|
14
|
-
tier: str = "free"
|
|
15
|
-
stripe_customer_id: str | None = None
|
|
16
|
-
created_at: datetime
|
|
17
|
-
|
|
18
|
-
class Config:
|
|
19
|
-
from_attributes = True
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class APIKeyCreate(BaseModel):
|
|
23
|
-
"""Request to create a new API key."""
|
|
24
|
-
|
|
25
|
-
name: str = Field(default="CLI Key", min_length=1, max_length=100)
|
|
26
|
-
scopes: list[str] = Field(default=["read", "write"])
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class APIKey(BaseModel):
|
|
30
|
-
"""API key information (without the secret)."""
|
|
31
|
-
|
|
32
|
-
id: str
|
|
33
|
-
name: str
|
|
34
|
-
key_prefix: str
|
|
35
|
-
scopes: list[str]
|
|
36
|
-
revoked: bool = False
|
|
37
|
-
last_used_at: datetime | None = None
|
|
38
|
-
expires_at: datetime | None = None
|
|
39
|
-
created_at: datetime
|
|
40
|
-
|
|
41
|
-
class Config:
|
|
42
|
-
from_attributes = True
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class APIKeyResponse(BaseModel):
|
|
46
|
-
"""Response when creating a new API key (includes full key once)."""
|
|
47
|
-
|
|
48
|
-
id: str
|
|
49
|
-
name: str
|
|
50
|
-
key: str # Full API key - only shown once
|
|
51
|
-
key_prefix: str
|
|
52
|
-
scopes: list[str]
|
|
53
|
-
created_at: datetime
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class TokenResponse(BaseModel):
|
|
57
|
-
"""Response for authentication token."""
|
|
58
|
-
|
|
59
|
-
access_token: str
|
|
60
|
-
token_type: str = "bearer"
|
|
61
|
-
expires_in: int
|
|
62
|
-
refresh_token: str | None = None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class LoginRequest(BaseModel):
|
|
66
|
-
"""Request for CLI login."""
|
|
67
|
-
|
|
68
|
-
email: EmailStr
|
|
69
|
-
password: str = Field(min_length=6)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class LoginResponse(BaseModel):
|
|
73
|
-
"""Response for CLI login."""
|
|
74
|
-
|
|
75
|
-
api_key: str
|
|
76
|
-
user: UserInfo
|
|
77
|
-
message: str = "Login successful"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class DeviceCodeRequest(BaseModel):
|
|
81
|
-
"""Request to initiate device code flow."""
|
|
82
|
-
|
|
83
|
-
client_id: str = "codeshift-cli"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class DeviceCodeResponse(BaseModel):
|
|
87
|
-
"""Response with device code for authentication."""
|
|
88
|
-
|
|
89
|
-
device_code: str
|
|
90
|
-
user_code: str
|
|
91
|
-
verification_uri: str
|
|
92
|
-
expires_in: int = 900 # 15 minutes
|
|
93
|
-
interval: int = 5 # Polling interval in seconds
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class DeviceTokenRequest(BaseModel):
|
|
97
|
-
"""Request to exchange device code for token."""
|
|
98
|
-
|
|
99
|
-
device_code: str
|
|
100
|
-
client_id: str = "codeshift-cli"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class RegisterRequest(BaseModel):
|
|
104
|
-
"""Request for new user registration."""
|
|
105
|
-
|
|
106
|
-
email: EmailStr
|
|
107
|
-
password: str = Field(min_length=8, description="Password must be at least 8 characters")
|
|
108
|
-
full_name: str | None = Field(default=None, max_length=100)
|
codeshift/api/models/billing.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""Billing models for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TierInfo(BaseModel):
|
|
9
|
-
"""Information about a pricing tier."""
|
|
10
|
-
|
|
11
|
-
name: str
|
|
12
|
-
display_name: str
|
|
13
|
-
price_monthly: int # In cents
|
|
14
|
-
files_per_month: int
|
|
15
|
-
llm_calls_per_month: int
|
|
16
|
-
features: list[str]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SubscriptionInfo(BaseModel):
|
|
20
|
-
"""Current subscription information."""
|
|
21
|
-
|
|
22
|
-
tier: str
|
|
23
|
-
status: str # active, canceled, past_due, etc.
|
|
24
|
-
stripe_subscription_id: str | None = None
|
|
25
|
-
current_period_start: datetime | None = None
|
|
26
|
-
current_period_end: datetime | None = None
|
|
27
|
-
cancel_at_period_end: bool = False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class CheckoutSessionRequest(BaseModel):
|
|
31
|
-
"""Request to create a Stripe checkout session."""
|
|
32
|
-
|
|
33
|
-
tier: str = Field(..., pattern="^(pro|unlimited)$")
|
|
34
|
-
success_url: str | None = None
|
|
35
|
-
cancel_url: str | None = None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class CheckoutSessionResponse(BaseModel):
|
|
39
|
-
"""Response with Stripe checkout session."""
|
|
40
|
-
|
|
41
|
-
checkout_url: str
|
|
42
|
-
session_id: str
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class PortalSessionResponse(BaseModel):
|
|
46
|
-
"""Response with Stripe billing portal URL."""
|
|
47
|
-
|
|
48
|
-
portal_url: str
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class PriceInfo(BaseModel):
|
|
52
|
-
"""Stripe price information."""
|
|
53
|
-
|
|
54
|
-
id: str
|
|
55
|
-
product_id: str
|
|
56
|
-
unit_amount: int
|
|
57
|
-
currency: str
|
|
58
|
-
recurring_interval: str
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class InvoiceInfo(BaseModel):
|
|
62
|
-
"""Invoice information."""
|
|
63
|
-
|
|
64
|
-
id: str
|
|
65
|
-
status: str
|
|
66
|
-
amount_due: int
|
|
67
|
-
amount_paid: int
|
|
68
|
-
currency: str
|
|
69
|
-
created: datetime
|
|
70
|
-
due_date: datetime | None = None
|
|
71
|
-
hosted_invoice_url: str | None = None
|
|
72
|
-
pdf_url: str | None = None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class PaymentMethodInfo(BaseModel):
|
|
76
|
-
"""Payment method information."""
|
|
77
|
-
|
|
78
|
-
id: str
|
|
79
|
-
type: str # card, bank_account, etc.
|
|
80
|
-
card_brand: str | None = None
|
|
81
|
-
card_last4: str | None = None
|
|
82
|
-
card_exp_month: int | None = None
|
|
83
|
-
card_exp_year: int | None = None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class BillingOverview(BaseModel):
|
|
87
|
-
"""Complete billing overview for a user."""
|
|
88
|
-
|
|
89
|
-
subscription: SubscriptionInfo
|
|
90
|
-
tier_info: TierInfo
|
|
91
|
-
payment_method: PaymentMethodInfo | None = None
|
|
92
|
-
upcoming_invoice: InvoiceInfo | None = None
|
codeshift/api/models/migrate.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Migration models for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class MigrateCodeRequest(BaseModel):
|
|
9
|
-
"""Request to migrate code using LLM."""
|
|
10
|
-
|
|
11
|
-
code: str = Field(..., description="Source code to migrate")
|
|
12
|
-
library: str = Field(..., description="Library being upgraded (e.g., 'pydantic')")
|
|
13
|
-
from_version: str = Field(..., description="Current version (e.g., '1.10.0')")
|
|
14
|
-
to_version: str = Field(..., description="Target version (e.g., '2.5.0')")
|
|
15
|
-
context: str | None = Field(None, description="Optional context about the migration")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class MigrateCodeResponse(BaseModel):
|
|
19
|
-
"""Response from LLM migration."""
|
|
20
|
-
|
|
21
|
-
success: bool
|
|
22
|
-
migrated_code: str
|
|
23
|
-
original_code: str
|
|
24
|
-
error: str | None = None
|
|
25
|
-
usage: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
-
cached: bool = False
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ExplainChangeRequest(BaseModel):
|
|
30
|
-
"""Request to explain a migration change."""
|
|
31
|
-
|
|
32
|
-
original_code: str = Field(..., description="Original code before migration")
|
|
33
|
-
transformed_code: str = Field(..., description="Transformed code after migration")
|
|
34
|
-
library: str = Field(..., description="Library being upgraded")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class ExplainChangeResponse(BaseModel):
|
|
38
|
-
"""Response with explanation of changes."""
|
|
39
|
-
|
|
40
|
-
success: bool
|
|
41
|
-
explanation: str | None = None
|
|
42
|
-
error: str | None = None
|
codeshift/api/models/usage.py
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
"""Usage tracking models for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from pydantic import BaseModel, Field
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class UsageEventCreate(BaseModel):
|
|
10
|
-
"""Request to record a usage event."""
|
|
11
|
-
|
|
12
|
-
event_type: str = Field(..., pattern="^(file_migrated|llm_call|scan|apply)$")
|
|
13
|
-
library: str | None = None
|
|
14
|
-
quantity: int = Field(default=1, ge=1)
|
|
15
|
-
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class UsageEvent(BaseModel):
|
|
19
|
-
"""A recorded usage event."""
|
|
20
|
-
|
|
21
|
-
id: str
|
|
22
|
-
user_id: str
|
|
23
|
-
event_type: str
|
|
24
|
-
library: str | None = None
|
|
25
|
-
quantity: int
|
|
26
|
-
metadata: dict[str, Any]
|
|
27
|
-
billing_period: str
|
|
28
|
-
created_at: datetime
|
|
29
|
-
|
|
30
|
-
class Config:
|
|
31
|
-
from_attributes = True
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class UsageSummary(BaseModel):
|
|
35
|
-
"""Summary of usage for a billing period."""
|
|
36
|
-
|
|
37
|
-
billing_period: str
|
|
38
|
-
files_migrated: int = 0
|
|
39
|
-
llm_calls: int = 0
|
|
40
|
-
scans: int = 0
|
|
41
|
-
applies: int = 0
|
|
42
|
-
total_events: int = 0
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class QuotaInfo(BaseModel):
|
|
46
|
-
"""Current quota information for a user."""
|
|
47
|
-
|
|
48
|
-
tier: str
|
|
49
|
-
billing_period: str
|
|
50
|
-
|
|
51
|
-
# Current usage
|
|
52
|
-
files_migrated: int = 0
|
|
53
|
-
llm_calls: int = 0
|
|
54
|
-
|
|
55
|
-
# Limits
|
|
56
|
-
files_limit: int
|
|
57
|
-
llm_calls_limit: int
|
|
58
|
-
|
|
59
|
-
# Calculated fields
|
|
60
|
-
files_remaining: int
|
|
61
|
-
llm_calls_remaining: int
|
|
62
|
-
files_percentage: float
|
|
63
|
-
llm_calls_percentage: float
|
|
64
|
-
|
|
65
|
-
@classmethod
|
|
66
|
-
def from_usage(
|
|
67
|
-
cls,
|
|
68
|
-
tier: str,
|
|
69
|
-
billing_period: str,
|
|
70
|
-
files_migrated: int,
|
|
71
|
-
llm_calls: int,
|
|
72
|
-
files_limit: int,
|
|
73
|
-
llm_calls_limit: int,
|
|
74
|
-
) -> "QuotaInfo":
|
|
75
|
-
"""Create QuotaInfo from usage data."""
|
|
76
|
-
files_remaining = max(0, files_limit - files_migrated)
|
|
77
|
-
llm_calls_remaining = max(0, llm_calls_limit - llm_calls)
|
|
78
|
-
|
|
79
|
-
return cls(
|
|
80
|
-
tier=tier,
|
|
81
|
-
billing_period=billing_period,
|
|
82
|
-
files_migrated=files_migrated,
|
|
83
|
-
llm_calls=llm_calls,
|
|
84
|
-
files_limit=files_limit,
|
|
85
|
-
llm_calls_limit=llm_calls_limit,
|
|
86
|
-
files_remaining=files_remaining,
|
|
87
|
-
llm_calls_remaining=llm_calls_remaining,
|
|
88
|
-
files_percentage=round(files_migrated / files_limit * 100, 1) if files_limit > 0 else 0,
|
|
89
|
-
llm_calls_percentage=(
|
|
90
|
-
round(llm_calls / llm_calls_limit * 100, 1) if llm_calls_limit > 0 else 0
|
|
91
|
-
),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class UsageResponse(BaseModel):
|
|
96
|
-
"""Response for usage queries."""
|
|
97
|
-
|
|
98
|
-
quota: QuotaInfo
|
|
99
|
-
recent_events: list[UsageEvent] = Field(default_factory=list)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class QuotaCheckRequest(BaseModel):
|
|
103
|
-
"""Request to check if an operation is within quota."""
|
|
104
|
-
|
|
105
|
-
event_type: str = Field(..., pattern="^(file_migrated|llm_call|scan|apply)$")
|
|
106
|
-
quantity: int = Field(default=1, ge=1)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class QuotaCheckResponse(BaseModel):
|
|
110
|
-
"""Response for quota check."""
|
|
111
|
-
|
|
112
|
-
allowed: bool
|
|
113
|
-
current_usage: int
|
|
114
|
-
limit: int
|
|
115
|
-
remaining: int
|
|
116
|
-
message: str | None = None
|