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.
Files changed (44) 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 +46 -30
  5. codeshift/cli/commands/scan.py +2 -5
  6. codeshift/cli/commands/upgrade.py +69 -61
  7. codeshift/cli/commands/upgrade_all.py +1 -1
  8. codeshift/cli/main.py +2 -2
  9. codeshift/knowledge/generator.py +6 -0
  10. codeshift/knowledge_base/libraries/aiohttp.yaml +3 -3
  11. codeshift/knowledge_base/libraries/httpx.yaml +4 -4
  12. codeshift/knowledge_base/libraries/pytest.yaml +1 -1
  13. codeshift/knowledge_base/models.py +1 -0
  14. codeshift/migrator/llm_migrator.py +8 -12
  15. codeshift/migrator/transforms/marshmallow_transformer.py +50 -0
  16. codeshift/migrator/transforms/pydantic_v1_to_v2.py +191 -22
  17. codeshift/scanner/code_scanner.py +22 -2
  18. codeshift/utils/__init__.py +1 -1
  19. codeshift/utils/api_client.py +155 -15
  20. codeshift/utils/cache.py +1 -1
  21. codeshift/utils/credential_store.py +393 -0
  22. codeshift/utils/llm_client.py +111 -9
  23. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/METADATA +4 -16
  24. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/RECORD +28 -43
  25. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/licenses/LICENSE +1 -1
  26. codeshift/api/__init__.py +0 -1
  27. codeshift/api/auth.py +0 -182
  28. codeshift/api/config.py +0 -73
  29. codeshift/api/database.py +0 -215
  30. codeshift/api/main.py +0 -103
  31. codeshift/api/models/__init__.py +0 -55
  32. codeshift/api/models/auth.py +0 -108
  33. codeshift/api/models/billing.py +0 -92
  34. codeshift/api/models/migrate.py +0 -42
  35. codeshift/api/models/usage.py +0 -116
  36. codeshift/api/routers/__init__.py +0 -5
  37. codeshift/api/routers/auth.py +0 -440
  38. codeshift/api/routers/billing.py +0 -395
  39. codeshift/api/routers/migrate.py +0 -304
  40. codeshift/api/routers/usage.py +0 -291
  41. codeshift/api/routers/webhooks.py +0 -289
  42. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/WHEEL +0 -0
  43. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/entry_points.txt +0 -0
  44. {codeshift-0.3.7.dist-info → codeshift-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,440 +0,0 @@
1
- """Authentication router for the PyResolve API."""
2
-
3
- import secrets
4
- from datetime import datetime, timedelta, timezone
5
-
6
- from fastapi import APIRouter, HTTPException, status
7
-
8
- from codeshift.api.auth import (
9
- CurrentUser,
10
- generate_api_key,
11
- )
12
- from codeshift.api.database import get_database
13
- from codeshift.api.models.auth import (
14
- APIKey,
15
- APIKeyCreate,
16
- APIKeyResponse,
17
- DeviceCodeRequest,
18
- DeviceCodeResponse,
19
- DeviceTokenRequest,
20
- LoginRequest,
21
- LoginResponse,
22
- RegisterRequest,
23
- UserInfo,
24
- )
25
-
26
- router = APIRouter()
27
-
28
- # In-memory storage for device codes (in production, use Redis)
29
- _device_codes: dict[str, dict] = {}
30
-
31
-
32
- @router.get("/me", response_model=UserInfo)
33
- async def get_current_user_info(user: CurrentUser) -> UserInfo:
34
- """Get current authenticated user information."""
35
- db = get_database()
36
- profile = db.get_profile_by_id(user.user_id)
37
-
38
- if not profile:
39
- raise HTTPException(
40
- status_code=status.HTTP_404_NOT_FOUND,
41
- detail="User profile not found",
42
- )
43
-
44
- return UserInfo(
45
- id=profile["id"],
46
- email=profile["email"],
47
- full_name=profile.get("full_name"),
48
- tier=profile.get("tier", "free"),
49
- stripe_customer_id=profile.get("stripe_customer_id"),
50
- created_at=profile["created_at"],
51
- )
52
-
53
-
54
- @router.post("/login", response_model=LoginResponse)
55
- async def login(request: LoginRequest) -> LoginResponse:
56
- """Login with email and password, returns an API key.
57
-
58
- This endpoint authenticates against Supabase Auth and creates
59
- a new API key for CLI usage.
60
- """
61
- from codeshift.api.database import get_supabase_anon_client
62
-
63
- client = get_supabase_anon_client()
64
-
65
- try:
66
- # Authenticate with Supabase
67
- auth_response = client.auth.sign_in_with_password(
68
- {"email": request.email, "password": request.password}
69
- )
70
- except Exception as e:
71
- raise HTTPException(
72
- status_code=status.HTTP_401_UNAUTHORIZED,
73
- detail="Invalid email or password",
74
- ) from e
75
-
76
- if not auth_response.user:
77
- raise HTTPException(
78
- status_code=status.HTTP_401_UNAUTHORIZED,
79
- detail="Invalid email or password",
80
- )
81
-
82
- user_id = auth_response.user.id
83
-
84
- # Get or create profile
85
- db = get_database()
86
- profile = db.get_profile_by_id(user_id)
87
-
88
- if not profile:
89
- raise HTTPException(
90
- status_code=status.HTTP_404_NOT_FOUND,
91
- detail="User profile not found",
92
- )
93
-
94
- # Generate a new API key
95
- full_key, key_prefix, key_hash = generate_api_key()
96
-
97
- # Store the API key
98
- db.create_api_key(
99
- user_id=user_id,
100
- key_prefix=key_prefix,
101
- key_hash=key_hash,
102
- name="CLI Login Key",
103
- )
104
-
105
- return LoginResponse(
106
- api_key=full_key,
107
- user=UserInfo(
108
- id=profile["id"],
109
- email=profile["email"],
110
- full_name=profile.get("full_name"),
111
- tier=profile.get("tier", "free"),
112
- stripe_customer_id=profile.get("stripe_customer_id"),
113
- created_at=profile["created_at"],
114
- ),
115
- )
116
-
117
-
118
- @router.post("/register", response_model=LoginResponse)
119
- async def register(request: RegisterRequest) -> LoginResponse:
120
- """Register a new user account.
121
-
122
- Creates a new user in Supabase Auth and returns an API key for CLI usage.
123
- The profile is automatically created by a database trigger.
124
- """
125
- from codeshift.api.database import get_supabase_anon_client
126
-
127
- client = get_supabase_anon_client()
128
-
129
- try:
130
- # Create user in Supabase Auth
131
- auth_response = client.auth.sign_up(
132
- {
133
- "email": request.email,
134
- "password": request.password,
135
- "options": {"data": {"full_name": request.full_name} if request.full_name else {}},
136
- }
137
- )
138
- except Exception as e:
139
- error_msg = str(e).lower()
140
- if "already registered" in error_msg or "already exists" in error_msg:
141
- raise HTTPException(
142
- status_code=status.HTTP_409_CONFLICT,
143
- detail="An account with this email already exists",
144
- ) from e
145
- raise HTTPException(
146
- status_code=status.HTTP_400_BAD_REQUEST,
147
- detail="Registration failed. Please try again.",
148
- ) from e
149
-
150
- if not auth_response.user:
151
- raise HTTPException(
152
- status_code=status.HTTP_400_BAD_REQUEST,
153
- detail="Registration failed. Please try again.",
154
- )
155
-
156
- user_id = auth_response.user.id
157
-
158
- # Wait briefly for the trigger to create the profile
159
- import asyncio
160
-
161
- await asyncio.sleep(0.5)
162
-
163
- # Get the profile (created by database trigger)
164
- db = get_database()
165
- profile = db.get_profile_by_id(user_id)
166
-
167
- if not profile:
168
- # Profile wasn't created by trigger - create it manually
169
- try:
170
- db.client.table("profiles").insert(
171
- {
172
- "id": user_id,
173
- "email": request.email,
174
- "full_name": request.full_name or "",
175
- "tier": "free",
176
- }
177
- ).execute()
178
- profile = db.get_profile_by_id(user_id)
179
- except Exception:
180
- pass
181
-
182
- if not profile:
183
- raise HTTPException(
184
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
185
- detail="Account created but profile setup failed. Please try logging in.",
186
- )
187
-
188
- # Generate a new API key
189
- full_key, key_prefix, key_hash = generate_api_key()
190
-
191
- # Store the API key
192
- db.create_api_key(
193
- user_id=user_id,
194
- key_prefix=key_prefix,
195
- key_hash=key_hash,
196
- name="CLI Registration Key",
197
- )
198
-
199
- return LoginResponse(
200
- api_key=full_key,
201
- user=UserInfo(
202
- id=profile["id"],
203
- email=profile["email"],
204
- full_name=profile.get("full_name"),
205
- tier=profile.get("tier", "free"),
206
- stripe_customer_id=profile.get("stripe_customer_id"),
207
- created_at=profile["created_at"],
208
- ),
209
- message="Registration successful",
210
- )
211
-
212
-
213
- @router.post("/device/code", response_model=DeviceCodeResponse)
214
- async def request_device_code(request: DeviceCodeRequest) -> DeviceCodeResponse:
215
- """Request a device code for CLI authentication.
216
-
217
- This initiates the device code flow for CLI authentication.
218
- The user will receive a code to enter in their browser.
219
- """
220
- # Generate codes
221
- device_code = secrets.token_urlsafe(32)
222
- user_code = "".join(secrets.choice("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") for _ in range(8))
223
- user_code = f"{user_code[:4]}-{user_code[4:]}" # Format: XXXX-XXXX
224
-
225
- # Store for later verification
226
- _device_codes[device_code] = {
227
- "user_code": user_code,
228
- "client_id": request.client_id,
229
- "expires_at": datetime.now(timezone.utc) + timedelta(minutes=15),
230
- "user_id": None, # Will be set when user authenticates
231
- "status": "pending",
232
- }
233
-
234
- return DeviceCodeResponse(
235
- device_code=device_code,
236
- user_code=user_code,
237
- verification_uri="https://codeshift.dev/device",
238
- expires_in=900,
239
- interval=5,
240
- )
241
-
242
-
243
- @router.post("/device/token", response_model=LoginResponse)
244
- async def exchange_device_code(request: DeviceTokenRequest) -> LoginResponse:
245
- """Exchange a device code for an API key.
246
-
247
- The CLI polls this endpoint until the user completes authentication.
248
- """
249
- device_data = _device_codes.get(request.device_code)
250
-
251
- if not device_data:
252
- raise HTTPException(
253
- status_code=status.HTTP_400_BAD_REQUEST,
254
- detail="Invalid device code",
255
- )
256
-
257
- # Check expiration
258
- if datetime.now(timezone.utc) > device_data["expires_at"]:
259
- del _device_codes[request.device_code]
260
- raise HTTPException(
261
- status_code=status.HTTP_400_BAD_REQUEST,
262
- detail="Device code expired",
263
- )
264
-
265
- # Check status
266
- if device_data["status"] == "pending":
267
- raise HTTPException(
268
- status_code=status.HTTP_428_PRECONDITION_REQUIRED,
269
- detail="authorization_pending",
270
- )
271
-
272
- if device_data["status"] == "denied":
273
- del _device_codes[request.device_code]
274
- raise HTTPException(
275
- status_code=status.HTTP_403_FORBIDDEN,
276
- detail="User denied the request",
277
- )
278
-
279
- if device_data["status"] != "approved":
280
- raise HTTPException(
281
- status_code=status.HTTP_400_BAD_REQUEST,
282
- detail="Invalid device code status",
283
- )
284
-
285
- # Get user info
286
- user_id = device_data["user_id"]
287
- db = get_database()
288
- profile = db.get_profile_by_id(user_id)
289
-
290
- if not profile:
291
- raise HTTPException(
292
- status_code=status.HTTP_404_NOT_FOUND,
293
- detail="User profile not found",
294
- )
295
-
296
- # Generate API key
297
- full_key, key_prefix, key_hash = generate_api_key()
298
-
299
- db.create_api_key(
300
- user_id=user_id,
301
- key_prefix=key_prefix,
302
- key_hash=key_hash,
303
- name="CLI Device Key",
304
- )
305
-
306
- # Clean up device code
307
- del _device_codes[request.device_code]
308
-
309
- return LoginResponse(
310
- api_key=full_key,
311
- user=UserInfo(
312
- id=profile["id"],
313
- email=profile["email"],
314
- full_name=profile.get("full_name"),
315
- tier=profile.get("tier", "free"),
316
- stripe_customer_id=profile.get("stripe_customer_id"),
317
- created_at=profile["created_at"],
318
- ),
319
- )
320
-
321
-
322
- @router.post("/device/approve")
323
- async def approve_device_code(user_code: str, user: CurrentUser) -> dict:
324
- """Approve a device code (called from web UI after user logs in)."""
325
- # Find the device code by user code
326
- for device_code, data in _device_codes.items():
327
- if data["user_code"] == user_code.upper().replace("-", "").strip():
328
- if datetime.now(timezone.utc) > data["expires_at"]:
329
- del _device_codes[device_code]
330
- raise HTTPException(
331
- status_code=status.HTTP_400_BAD_REQUEST,
332
- detail="Device code expired",
333
- )
334
-
335
- data["user_id"] = user.user_id
336
- data["status"] = "approved"
337
- return {"message": "Device approved successfully"}
338
-
339
- raise HTTPException(
340
- status_code=status.HTTP_404_NOT_FOUND,
341
- detail="Invalid user code",
342
- )
343
-
344
-
345
- @router.get("/keys", response_model=list[APIKey])
346
- async def list_api_keys(user: CurrentUser) -> list[APIKey]:
347
- """List all API keys for the current user."""
348
- db = get_database()
349
-
350
- result = (
351
- db.client.table("api_keys")
352
- .select("*")
353
- .eq("user_id", user.user_id)
354
- .eq("revoked", False)
355
- .order("created_at", desc=True)
356
- .execute()
357
- )
358
-
359
- return [
360
- APIKey(
361
- id=key["id"],
362
- name=key["name"],
363
- key_prefix=key["key_prefix"],
364
- scopes=key.get("scopes", []),
365
- revoked=key["revoked"],
366
- last_used_at=key.get("last_used_at"),
367
- expires_at=key.get("expires_at"),
368
- created_at=key["created_at"],
369
- )
370
- for key in result.data
371
- ]
372
-
373
-
374
- @router.post("/keys", response_model=APIKeyResponse)
375
- async def create_api_key(request: APIKeyCreate, user: CurrentUser) -> APIKeyResponse:
376
- """Create a new API key."""
377
- db = get_database()
378
-
379
- # Generate the key
380
- full_key, key_prefix, key_hash = generate_api_key()
381
-
382
- # Store it
383
- result = db.create_api_key(
384
- user_id=user.user_id,
385
- key_prefix=key_prefix,
386
- key_hash=key_hash,
387
- name=request.name,
388
- scopes=request.scopes,
389
- )
390
-
391
- return APIKeyResponse(
392
- id=result["id"],
393
- name=request.name,
394
- key=full_key, # Only time the full key is returned
395
- key_prefix=key_prefix,
396
- scopes=request.scopes,
397
- created_at=result["created_at"],
398
- )
399
-
400
-
401
- @router.delete("/keys/{key_id}")
402
- async def revoke_api_key(key_id: str, user: CurrentUser) -> dict:
403
- """Revoke an API key."""
404
- db = get_database()
405
-
406
- # Verify ownership
407
- result = db.client.table("api_keys").select("user_id").eq("id", key_id).execute()
408
-
409
- if not result.data:
410
- raise HTTPException(
411
- status_code=status.HTTP_404_NOT_FOUND,
412
- detail="API key not found",
413
- )
414
-
415
- if result.data[0]["user_id"] != user.user_id:
416
- raise HTTPException(
417
- status_code=status.HTTP_403_FORBIDDEN,
418
- detail="Not authorized to revoke this key",
419
- )
420
-
421
- # Revoke
422
- success = db.revoke_api_key(key_id)
423
-
424
- if not success:
425
- raise HTTPException(
426
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
427
- detail="Failed to revoke key",
428
- )
429
-
430
- return {"message": "API key revoked successfully"}
431
-
432
-
433
- @router.post("/logout")
434
- async def logout(user: CurrentUser) -> dict:
435
- """Logout by revoking the current API key."""
436
- if user.api_key_id:
437
- db = get_database()
438
- db.revoke_api_key(user.api_key_id)
439
-
440
- return {"message": "Logged out successfully"}