codeshift 0.3.6__py3-none-any.whl → 0.4.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.
Files changed (34) hide show
  1. codeshift/__init__.py +2 -2
  2. codeshift/cli/__init__.py +1 -1
  3. codeshift/cli/commands/__init__.py +1 -1
  4. codeshift/cli/commands/auth.py +5 -5
  5. codeshift/cli/commands/scan.py +2 -5
  6. codeshift/cli/commands/upgrade.py +2 -7
  7. codeshift/cli/commands/upgrade_all.py +1 -1
  8. codeshift/cli/main.py +2 -2
  9. codeshift/migrator/llm_migrator.py +8 -12
  10. codeshift/utils/__init__.py +1 -1
  11. codeshift/utils/api_client.py +11 -11
  12. codeshift/utils/cache.py +1 -1
  13. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/METADATA +2 -18
  14. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/RECORD +18 -34
  15. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/licenses/LICENSE +1 -1
  16. codeshift/api/__init__.py +0 -1
  17. codeshift/api/auth.py +0 -182
  18. codeshift/api/config.py +0 -73
  19. codeshift/api/database.py +0 -215
  20. codeshift/api/main.py +0 -103
  21. codeshift/api/models/__init__.py +0 -55
  22. codeshift/api/models/auth.py +0 -108
  23. codeshift/api/models/billing.py +0 -92
  24. codeshift/api/models/migrate.py +0 -42
  25. codeshift/api/models/usage.py +0 -116
  26. codeshift/api/routers/__init__.py +0 -5
  27. codeshift/api/routers/auth.py +0 -440
  28. codeshift/api/routers/billing.py +0 -395
  29. codeshift/api/routers/migrate.py +0 -304
  30. codeshift/api/routers/usage.py +0 -291
  31. codeshift/api/routers/webhooks.py +0 -289
  32. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/WHEEL +0 -0
  33. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/entry_points.txt +0 -0
  34. {codeshift-0.3.6.dist-info → codeshift-0.4.0.dist-info}/top_level.txt +0 -0
codeshift/api/config.py DELETED
@@ -1,73 +0,0 @@
1
- """API configuration settings."""
2
-
3
- from functools import lru_cache
4
-
5
- from pydantic_settings import BaseSettings
6
-
7
-
8
- class APISettings(BaseSettings):
9
- """Configuration settings for the Codeshift API."""
10
-
11
- # Supabase
12
- supabase_url: str = ""
13
- supabase_anon_key: str = ""
14
- supabase_service_role_key: str = ""
15
-
16
- # Stripe
17
- stripe_secret_key: str = ""
18
- stripe_webhook_secret: str = ""
19
- stripe_price_id_pro: str = ""
20
- stripe_price_id_unlimited: str = ""
21
-
22
- # Anthropic (for server-side LLM calls)
23
- anthropic_api_key: str = ""
24
-
25
- # API settings
26
- codeshift_api_url: str = "https://py-resolve.replit.app"
27
- api_key_prefix: str = "cs_"
28
-
29
- # Tier quotas
30
- tier_free_files: int = 100
31
- tier_free_llm_calls: int = 50
32
- tier_pro_files: int = 1000
33
- tier_pro_llm_calls: int = 500
34
- tier_unlimited_files: int = 999999999
35
- tier_unlimited_llm_calls: int = 999999999
36
-
37
- # Environment
38
- environment: str = "development"
39
-
40
- model_config = {
41
- "env_prefix": "",
42
- "env_file": ".env",
43
- "extra": "ignore",
44
- }
45
-
46
- @property
47
- def is_production(self) -> bool:
48
- """Check if running in production."""
49
- return self.environment == "production"
50
-
51
- def get_tier_limits(self, tier: str) -> dict[str, int]:
52
- """Get quota limits for a tier."""
53
- limits = {
54
- "free": {
55
- "files_per_month": self.tier_free_files,
56
- "llm_calls_per_month": self.tier_free_llm_calls,
57
- },
58
- "pro": {
59
- "files_per_month": self.tier_pro_files,
60
- "llm_calls_per_month": self.tier_pro_llm_calls,
61
- },
62
- "unlimited": {
63
- "files_per_month": self.tier_unlimited_files,
64
- "llm_calls_per_month": self.tier_unlimited_llm_calls,
65
- },
66
- }
67
- return limits.get(tier, limits["free"])
68
-
69
-
70
- @lru_cache
71
- def get_settings() -> APISettings:
72
- """Get cached API settings."""
73
- return APISettings()
codeshift/api/database.py DELETED
@@ -1,215 +0,0 @@
1
- """Supabase database client and operations."""
2
-
3
- from datetime import datetime, timezone
4
- from typing import Any, Optional, cast
5
-
6
- from codeshift.api.config import get_settings
7
- from supabase import Client as SupabaseClient
8
- from supabase import create_client
9
-
10
-
11
- def get_supabase_client() -> "SupabaseClient":
12
- """Get a Supabase client instance."""
13
- settings = get_settings()
14
- return create_client(
15
- settings.supabase_url,
16
- settings.supabase_service_role_key,
17
- )
18
-
19
-
20
- def get_supabase_anon_client() -> "SupabaseClient":
21
- """Get a Supabase client with anon key (for user-facing operations)."""
22
- settings = get_settings()
23
- return create_client(
24
- settings.supabase_url,
25
- settings.supabase_anon_key,
26
- )
27
-
28
-
29
- class Database:
30
- """Database operations wrapper."""
31
-
32
- def __init__(self, client: Optional["SupabaseClient"] = None):
33
- """Initialize with optional client, otherwise use service role client."""
34
- self._client = client
35
-
36
- @property
37
- def client(self) -> "SupabaseClient":
38
- """Get or create the Supabase client."""
39
- if self._client is None:
40
- self._client = get_supabase_client()
41
- return self._client
42
-
43
- # Profile operations
44
- def get_profile_by_id(self, user_id: str) -> dict | None:
45
- """Get a user profile by ID."""
46
- result = self.client.table("profiles").select("*").eq("id", user_id).execute()
47
- return result.data[0] if result.data else None
48
-
49
- def get_profile_by_email(self, email: str) -> dict | None:
50
- """Get a user profile by email."""
51
- result = self.client.table("profiles").select("*").eq("email", email).execute()
52
- return result.data[0] if result.data else None
53
-
54
- def update_profile(self, user_id: str, data: dict) -> dict | None:
55
- """Update a user profile."""
56
- result = self.client.table("profiles").update(data).eq("id", user_id).execute()
57
- return result.data[0] if result.data else None
58
-
59
- def update_profile_tier(
60
- self, user_id: str, tier: str, stripe_customer_id: str | None = None
61
- ) -> dict | None:
62
- """Update a user's tier and optionally their Stripe customer ID."""
63
- data = {"tier": tier, "updated_at": datetime.now(timezone.utc).isoformat()}
64
- if stripe_customer_id:
65
- data["stripe_customer_id"] = stripe_customer_id
66
- return self.update_profile(user_id, data)
67
-
68
- # API key operations
69
- def get_api_key_by_hash(self, key_hash: str) -> dict | None:
70
- """Get an API key by its hash."""
71
- result = (
72
- self.client.table("api_keys")
73
- .select("*, profiles(*)")
74
- .eq("key_hash", key_hash)
75
- .eq("revoked", False)
76
- .execute()
77
- )
78
- return result.data[0] if result.data else None
79
-
80
- def get_api_key_by_prefix(self, key_prefix: str) -> dict | None:
81
- """Get an API key by its prefix."""
82
- result = (
83
- self.client.table("api_keys")
84
- .select("*, profiles(*)")
85
- .eq("key_prefix", key_prefix)
86
- .eq("revoked", False)
87
- .execute()
88
- )
89
- return result.data[0] if result.data else None
90
-
91
- def create_api_key(
92
- self,
93
- user_id: str,
94
- key_prefix: str,
95
- key_hash: str,
96
- name: str = "CLI Key",
97
- scopes: list[str] | None = None,
98
- ) -> dict[str, Any]:
99
- """Create a new API key."""
100
- data = {
101
- "user_id": user_id,
102
- "key_prefix": key_prefix,
103
- "key_hash": key_hash,
104
- "name": name,
105
- "scopes": scopes or ["read", "write"],
106
- }
107
- result = self.client.table("api_keys").insert(data).execute()
108
- return cast(dict[str, Any], result.data[0])
109
-
110
- def revoke_api_key(self, key_id: str) -> bool:
111
- """Revoke an API key."""
112
- result = (
113
- self.client.table("api_keys")
114
- .update({"revoked": True, "revoked_at": datetime.now(timezone.utc).isoformat()})
115
- .eq("id", key_id)
116
- .execute()
117
- )
118
- return bool(result.data)
119
-
120
- def update_api_key_last_used(self, key_id: str) -> None:
121
- """Update the last_used_at timestamp for an API key."""
122
- self.client.table("api_keys").update(
123
- {"last_used_at": datetime.now(timezone.utc).isoformat()}
124
- ).eq("id", key_id).execute()
125
-
126
- # Usage event operations
127
- def record_usage_event(
128
- self,
129
- user_id: str,
130
- event_type: str,
131
- library: str | None = None,
132
- quantity: int = 1,
133
- metadata: dict[str, Any] | None = None,
134
- ) -> dict[str, Any]:
135
- """Record a usage event."""
136
- now = datetime.now(timezone.utc)
137
- data = {
138
- "user_id": user_id,
139
- "event_type": event_type,
140
- "library": library,
141
- "quantity": quantity,
142
- "metadata": metadata or {},
143
- "billing_period": now.strftime("%Y-%m"),
144
- "created_at": now.isoformat(),
145
- }
146
- result = self.client.table("usage_events").insert(data).execute()
147
- return cast(dict[str, Any], result.data[0])
148
-
149
- def get_usage_for_period(
150
- self, user_id: str, billing_period: str | None = None
151
- ) -> dict[str, int]:
152
- """Get usage summary for a billing period."""
153
- if billing_period is None:
154
- billing_period = datetime.now(timezone.utc).strftime("%Y-%m")
155
-
156
- result = (
157
- self.client.table("usage_events")
158
- .select("event_type, quantity")
159
- .eq("user_id", user_id)
160
- .eq("billing_period", billing_period)
161
- .execute()
162
- )
163
-
164
- # Aggregate by event type
165
- usage: dict[str, int] = {}
166
- for event in result.data:
167
- event_type = event["event_type"]
168
- usage[event_type] = usage.get(event_type, 0) + event["quantity"]
169
-
170
- return usage
171
-
172
- def get_usage_events(
173
- self,
174
- user_id: str,
175
- billing_period: str | None = None,
176
- event_type: str | None = None,
177
- limit: int = 100,
178
- ) -> list[dict[str, Any]]:
179
- """Get detailed usage events."""
180
- if billing_period is None:
181
- billing_period = datetime.now(timezone.utc).strftime("%Y-%m")
182
-
183
- query = (
184
- self.client.table("usage_events")
185
- .select("*")
186
- .eq("user_id", user_id)
187
- .eq("billing_period", billing_period)
188
- )
189
-
190
- if event_type:
191
- query = query.eq("event_type", event_type)
192
-
193
- result = query.order("created_at", desc=True).limit(limit).execute()
194
- return cast(list[dict[str, Any]], result.data)
195
-
196
- def get_user_quota(self, user_id: str) -> dict[str, int] | None:
197
- """Get quota information for a user.
198
-
199
- Returns:
200
- Dict with llm_calls and file_migrated counts, or None if error.
201
- """
202
- billing_period = datetime.now(timezone.utc).strftime("%Y-%m")
203
- return self.get_usage_for_period(user_id, billing_period)
204
-
205
-
206
- # Singleton instance
207
- _db: Database | None = None
208
-
209
-
210
- def get_database() -> Database:
211
- """Get the database singleton."""
212
- global _db
213
- if _db is None:
214
- _db = Database()
215
- return _db
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
- )
@@ -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
- ]
@@ -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)
@@ -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
@@ -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