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,61 +1,45 @@
1
- codeshift/__init__.py,sha256=rDp65Hj8u9ubLT3_gkhrKfdSBIlXKBQays3n_eLXl0s,202
1
+ codeshift/__init__.py,sha256=L-ImO-zi7CvCuKCl_PYICUgTDppPXgYDaUoIQWukRzU,202
2
2
  codeshift/analyzer/__init__.py,sha256=m61k8rtOyHQNoPLDeoe0S9WSy9syvCyMgLZ07ojpoNU,188
3
3
  codeshift/analyzer/risk_assessor.py,sha256=nKplyymbsqcbsZyDLVZ2zukpQlPD5KIvO-FBJ4AZFYc,13422
4
- codeshift/api/__init__.py,sha256=jlBP8FeFfOkITfWFAK9Wqc0UHt6kyYAqVXzfvT_EQMk,29
5
- codeshift/api/auth.py,sha256=37wWEr4rvGbEB4HQIxI-m-uWa6HgjHQdCWCaaDwOpOA,5565
6
- codeshift/api/config.py,sha256=QYCSKT9t4D_mSQHIsEmaFXvQhy7yZW7EJNUqmRB8T5E,1989
7
- codeshift/api/database.py,sha256=VIsau8ccQ1ZRQAeQWF85GsdkqaDabTOnMlpgiJcLMLU,7171
8
- codeshift/api/main.py,sha256=Q6rVpUhEeX-4KBbUXliUgQ5RGTjHKPyiPggVfZVHmvg,2877
9
- codeshift/api/models/__init__.py,sha256=IXaCQUxh0hF03nh_3-fP0vpYdRab-RsI5LtKv9okELA,1143
10
- codeshift/api/models/auth.py,sha256=vbSspwn1UR0km_nC4dD08JCib56IDFQwAQqXiHZ6_t4,2435
11
- codeshift/api/models/billing.py,sha256=6Q0RMiYYqOAAvOPzJqnJ6FgV0mylPD0_GiguocO-Uo0,2107
12
- codeshift/api/models/migrate.py,sha256=XkFQ3bZWlXngnkbE3im3eZo5WnoPt1BCHpKSp38PIMY,1353
13
- codeshift/api/models/usage.py,sha256=YuP3XM9UsHE6OWHcXHY1jTWDirtln5Sx-0-VaDo6p8M,2911
14
- codeshift/api/routers/__init__.py,sha256=kh9jmNdH0v_A70KN2GbAb4zhHBnl992pK3m_aEDYtEw,171
15
- codeshift/api/routers/auth.py,sha256=1mJ3Hx7ARuZGA9iuglOUDXE8khScQfjMcL5naLE6MfM,13106
16
- codeshift/api/routers/billing.py,sha256=imG1W89cv204IojrGePd9-wXEIavsGZBa1_Obzuu9Qc,12322
17
- codeshift/api/routers/migrate.py,sha256=2FddO-XAHOVIoGGh7nGwVCd17jYl_FY-x7YnjKlYnxc,8945
18
- codeshift/api/routers/usage.py,sha256=sLaefM9T-3Kw-OGKOIERS_Z3btZTDx8Mxu-6zAuvF-w,9197
19
- codeshift/api/routers/webhooks.py,sha256=oHn-Fie4KxbPnfP7hdSe8yfp8JHNDkdyBBj2kGnbnfA,8733
20
- codeshift/cli/__init__.py,sha256=khf471pmz_J7-MGnarVWvmM0mCKFvdXpaPSJgx-KvD4,87
21
- codeshift/cli/main.py,sha256=iHNfLciYfq2F_QF-7hBFQvT7ezbIFg0zPJesWoKPT3Y,6983
4
+ codeshift/cli/__init__.py,sha256=rf0c8oMqzYktdLH8ga2sA7rS5GsJse8dGfIbfW4DDXo,87
5
+ codeshift/cli/main.py,sha256=iLXy2QMLXoiM88sn-XEHk9vJncB-WeKfEq0eps3ZHig,6983
22
6
  codeshift/cli/package_manager.py,sha256=K7spYHSQ6VoHlrzM3L9B2JoxBTe_9gLd57P6ME0PsSI,2807
23
7
  codeshift/cli/quota.py,sha256=zBiY3zqCGEyxbS3vnoQdgBld1emMQzLc0_5cUOWy9U8,5989
24
- codeshift/cli/commands/__init__.py,sha256=phw462By4_CzFoJQUdON73qF5-5Mv45GwUEOX_5SbLc,218
8
+ codeshift/cli/commands/__init__.py,sha256=Kbs7DFUWOXkw5-9jiiR03cuUJeo5byusLr_Y3cKFE3E,218
25
9
  codeshift/cli/commands/apply.py,sha256=JqUiu6I5WD25677SXpOejKxLWNIUQUKrbOQ_JbpoEak,11213
26
- codeshift/cli/commands/auth.py,sha256=uXUtNEtAH17vhSlANfcFR5pCP8NQmFJ7Uw0bN30rYlM,28066
10
+ codeshift/cli/commands/auth.py,sha256=bCGF9aEz-5PLsp1rOkHZ8A3GRtd9NtpC3tePlJ3ZO6M,28638
27
11
  codeshift/cli/commands/diff.py,sha256=4LjrVRu4lhj-aOBvClOnEnN0l2nZEU5S1_qzYoXL4dQ,6357
28
- codeshift/cli/commands/scan.py,sha256=JXR3MMKWOh1dlAEJ-OXh-EjCDBrBMxqzI6snZcBirqU,11386
29
- codeshift/cli/commands/upgrade.py,sha256=_V1BP15BJR6OZHg7jdq90kfdUpjquf_FoWNNEWNzrnI,15874
30
- codeshift/cli/commands/upgrade_all.py,sha256=v-fz9jWCzJWWW6d0QXTdOfd2M54okI9AA6qv7qxLPiE,18964
12
+ codeshift/cli/commands/scan.py,sha256=Eia3xW6sVZSiKtxd7JXyjIcOUsxduLGOuFjBhhp1ut0,11373
13
+ codeshift/cli/commands/upgrade.py,sha256=15SPmu09oi7Gu7KLvyDLbcF-YndkEoa0sPhlMZ-zsuo,15853
14
+ codeshift/cli/commands/upgrade_all.py,sha256=FimS7SYJeYkQvhrWACLQG4QiyyynCgi9SapUb8Ax0Ik,18964
31
15
  codeshift/knowledge/__init__.py,sha256=_YwrLgjvsJQuYajfnIhUQqFeircF0MfkI9zJBPZTupc,1221
32
16
  codeshift/knowledge/cache.py,sha256=aEx9aNDfrzCYMzlPRBzBOYiFIAGDcZJfQvcXroa5vsA,4837
33
- codeshift/knowledge/generator.py,sha256=WVCF0ULYmVQRHBiK6BTyS6FuArSCj-UDWzPJLujVbxU,7304
17
+ codeshift/knowledge/generator.py,sha256=t30Rf8ByFxjz3wUk8Dq5fWGcL2MLS_rP4Xik4OspAUs,7386
34
18
  codeshift/knowledge/models.py,sha256=tvM6dnZJ00nJttIDMYxKlc8fYadgCdP2bqMVTA7o3HY,5157
35
19
  codeshift/knowledge/parser.py,sha256=uWm8IjOYoV06Sx5_odyrLB93XL1GNRHdzuNVSFM-fMA,8493
36
20
  codeshift/knowledge/sources.py,sha256=4GA4z4gHADCAfeBTF3dAO2S6H4s9leUKoxKPK2Vcopk,12280
37
21
  codeshift/knowledge_base/__init__.py,sha256=-xiDpdKrizX-5FeA8NjnHxASkkv44BwiMORTYF29Vj4,368
38
22
  codeshift/knowledge_base/loader.py,sha256=xMOSJEtnlzA1sGHGqeWdwzv2rF-LvVfSgEx9Q5WMTGk,3484
39
- codeshift/knowledge_base/models.py,sha256=OwA9ts14aMW-PfWBi8GFcwHIGls5ERur7FCQXYcIU7k,3816
40
- codeshift/knowledge_base/libraries/aiohttp.yaml,sha256=0JjP5BQdW172MmUE0U-xeCeavO0PN8_a1hM6W7zqQLI,6469
23
+ codeshift/knowledge_base/models.py,sha256=YQ-Voe3y6O4Pge88YBsqM0Q64797-_7_lkyvmXArTeU,3836
24
+ codeshift/knowledge_base/libraries/aiohttp.yaml,sha256=LB-uyxpgghc6n4I7hKctvIvKsW-bbXo8HQQORCkDBFs,6472
41
25
  codeshift/knowledge_base/libraries/attrs.yaml,sha256=iih_l9GaETP-FTy0ktqgOHlcUIiXaIT7TInfquV8XHo,5776
42
26
  codeshift/knowledge_base/libraries/celery.yaml,sha256=jE0gvUv5VpzgIp8Pmgh_pL64SJoXpMZojTuxtczMgQg,8455
43
27
  codeshift/knowledge_base/libraries/click.yaml,sha256=JoGFDVzV_49ZWp0It0VTPjrfQOXBoDJXlEzuJxwUSFQ,6689
44
28
  codeshift/knowledge_base/libraries/django.yaml,sha256=F5FGfiZBOBAiHcTrUhNfjt4DUoa8MaNZCPCyFgSeFu8,12224
45
29
  codeshift/knowledge_base/libraries/fastapi.yaml,sha256=i6dXGbEP1bn2k06XgqzgvqE2Wle_NZRhUBTk06KhW3Y,6419
46
30
  codeshift/knowledge_base/libraries/flask.yaml,sha256=HETNUj5nrsCMH8sFwN1zPmUXGfcGWo5KhCDY4-hilR0,9401
47
- codeshift/knowledge_base/libraries/httpx.yaml,sha256=Pgr13Vs3O8jKNu0Ttga9cfaQpYwPkjNHzvCbAvu-itg,6516
31
+ codeshift/knowledge_base/libraries/httpx.yaml,sha256=goeaxr40vYglOIjHtm1M8zmzw8SafJhX77KA6kVAKRI,6520
48
32
  codeshift/knowledge_base/libraries/marshmallow.yaml,sha256=1K3VrNGsjka7bfHMOwUrlvBv-j2tgLOg7MDGflWDDZM,8307
49
33
  codeshift/knowledge_base/libraries/numpy.yaml,sha256=kpV1BNXTmLE9Lbki6caLHKpRzLYpPDkc-e3NtELtH1Y,13020
50
34
  codeshift/knowledge_base/libraries/pandas.yaml,sha256=cgu-dAaXIVyZivRr7swh3Df-GEpVfcxh5-J-6ow_pLc,10366
51
35
  codeshift/knowledge_base/libraries/pydantic.yaml,sha256=nTleM2JaabzrcJoMPrIYhPbV93Gl3GZ1uQnQSeHBw_w,8150
52
- codeshift/knowledge_base/libraries/pytest.yaml,sha256=I9_hz3_nqFxZVFoRJL2x7AP30bQxXHM1T6aU0PyEvQs,6601
36
+ codeshift/knowledge_base/libraries/pytest.yaml,sha256=6mSeeETC4yF4s1J22UIhhhplvYUOIGkDWo5ZSuq4nCw,6602
53
37
  codeshift/knowledge_base/libraries/requests.yaml,sha256=8Msjlt8v4mCEmTIdyvlpgtWsCXarpPqXNMPFlFd_Ojc,9035
54
38
  codeshift/knowledge_base/libraries/sqlalchemy.yaml,sha256=qEk1Nc2ovgCiBLPLsBrcsWM_KZaJn8MlHotDyZkzX9w,9717
55
39
  codeshift/migrator/__init__.py,sha256=V5ATKxw4hV6Ec3g78IeHkP92-VM4l6OzH0gi-lnU09w,467
56
40
  codeshift/migrator/ast_transforms.py,sha256=fyySqSIFVHS8jTFledkbGWQ-_zcC5-S_rsv4sPu9jEA,7190
57
41
  codeshift/migrator/engine.py,sha256=QkxkAFIewmSOwFW041BtPBqEfv7x8nVH8zsS-bSB1Do,14572
58
- codeshift/migrator/llm_migrator.py,sha256=dyxVNu8Rf0FPM7Q_4TXt8f5nbbMqOHOIc_zbMsRm3RU,10373
42
+ codeshift/migrator/llm_migrator.py,sha256=qWqb3Gzj4khDhlTmlHbpFplQ8zTq2c_ev52qL9Yeg90,10356
59
43
  codeshift/migrator/transforms/__init__.py,sha256=WWeJSr4dOKXwl8XmoCBhkYTxd4e0ENEwfMoLNwkGYsc,807
60
44
  codeshift/migrator/transforms/aiohttp_transformer.py,sha256=-7N9xYeoDSOy4iUujKU_0CtTfQS2Mx4P9lKEvriN6YQ,23297
61
45
  codeshift/migrator/transforms/attrs_transformer.py,sha256=_Bgg-oLUzvTENZnH1yIORH2rV9F2vJinnyRwQpBalXM,24128
@@ -65,27 +49,28 @@ codeshift/migrator/transforms/django_transformer.py,sha256=21gHStxJNHRj8x_pVfRB8
65
49
  codeshift/migrator/transforms/fastapi_transformer.py,sha256=axFZhCdMo3gAtHPxboSDhx56RasMjREcfaI5aweivfA,7495
66
50
  codeshift/migrator/transforms/flask_transformer.py,sha256=VcBlXDlhwgsk_1S2fyi3QsRSOUeE_DohMPjXbp4DY_A,20031
67
51
  codeshift/migrator/transforms/httpx_transformer.py,sha256=fxqmnJhWdUHuY0a9uXQZ2MFdF9kdxjm6c3UDgeMiFP0,16952
68
- codeshift/migrator/transforms/marshmallow_transformer.py,sha256=tLXX7Vko8OJQ9E1oSDooqGU-bjIgaC7-ZsK_aixPhrk,18339
52
+ codeshift/migrator/transforms/marshmallow_transformer.py,sha256=1XLd_KRU3QDs4HAs_L-w0UEqDOTChYhbmf1-3OJfVF4,20827
69
53
  codeshift/migrator/transforms/numpy_transformer.py,sha256=y4lJcAiT3UK8ZtZptCoSkli0UGNzhAyfUIf_I1F7WSE,15165
70
54
  codeshift/migrator/transforms/pandas_transformer.py,sha256=93WoeY9FOLChAWKr9xTbkQCpOS1kwajzNfWqfrNE-bY,8905
71
- codeshift/migrator/transforms/pydantic_v1_to_v2.py,sha256=J89WJF_gOeg0FUB0G2X5OCUt31zbCrGmhsBojqDgqX0,26879
55
+ codeshift/migrator/transforms/pydantic_v1_to_v2.py,sha256=Zo-KiQwyShowD2m0X1kiohtvbxpafRIsXxdFmvCm6Pk,35260
72
56
  codeshift/migrator/transforms/pytest_transformer.py,sha256=zcNoY0HWONvT2jpAoP01Nk-N-drwVLttTHbOZgb6HlM,13724
73
57
  codeshift/migrator/transforms/requests_transformer.py,sha256=r2hqawzdvsjblUHPoYU1tQ-miVqi1EEVsBPLXpzZz_s,12537
74
58
  codeshift/migrator/transforms/sqlalchemy_transformer.py,sha256=rgoIk4_iDCuydZhy9svdiNlZPnmI0sQuJB5F3f7LH3Y,34286
75
59
  codeshift/scanner/__init__.py,sha256=GFx9yMPZVuxBC8mGOPZoINsCsJgHV4TSjiV4KSF3fPU,300
76
- codeshift/scanner/code_scanner.py,sha256=YGuHVI1FN0h8cGSARFlF5duFd8WBCJUSVMcqCbsjqEQ,12859
60
+ codeshift/scanner/code_scanner.py,sha256=YBbmUrFn8Qyep4A52iqLcgySMvYQr49GsnmV8kpdxEw,13935
77
61
  codeshift/scanner/dependency_parser.py,sha256=Vd-wbgcG2trgLN7wntbNrGwuXvamn3u7B5SGvORdPiY,15372
78
- codeshift/utils/__init__.py,sha256=dNtKUzFwoMxuEl41d_V0Zggv5lprZnOowB8Qt_THwbg,148
79
- codeshift/utils/api_client.py,sha256=RUbKiox9dc_xc4z7IcrZ7plObYkkaKFa7Dw4qi75KGw,8010
80
- codeshift/utils/cache.py,sha256=WHwNToQVmzhLgmHvt9nWUlzDFmVLBp_Sax7R4rrQfOg,8793
62
+ codeshift/utils/__init__.py,sha256=8G28m1UBDdEqF_G8GN6qRFWhpjDhiXJmFd9gSgIvkQc,148
63
+ codeshift/utils/api_client.py,sha256=PupRgfFrfXFfAG44s7p32XhtvYUxwmKucPhFKt5-kIE,12641
64
+ codeshift/utils/cache.py,sha256=9vPjU54S48iKy4CDvcjgz0u9eeIx7aUAHCdk3coRF6w,8793
81
65
  codeshift/utils/config.py,sha256=8x-rEh4q99K0HvT4ZQHDQAeUT8Tc_HATkZOomBGVyIA,2454
82
- codeshift/utils/llm_client.py,sha256=WkT3KftJi7rsj8MXH4MVJvznugicb2XpkKqnqRET1eo,6369
66
+ codeshift/utils/credential_store.py,sha256=Yq5UDrYSwEYN5sXsNNoFxX3BG3V2qVMNrY4bNAJHapM,13366
67
+ codeshift/utils/llm_client.py,sha256=NNVBJIl0dbxU9PMOJuSdDCTvRZFNetgJIkmjVSjEM0c,10115
83
68
  codeshift/validator/__init__.py,sha256=WRQSfJ7eLJdjR2_f_dXSaBtfawkvu1Dlu20Gh76D12c,280
84
69
  codeshift/validator/syntax_checker.py,sha256=FJeLIqhNhV7_Xj2RskHScJZks6A9fybaqv5Z1-MGDfo,5343
85
70
  codeshift/validator/test_runner.py,sha256=VX0OqkuI3AJxOUzRW2_BEjdDsMw1N4a0od-pPbSF6O8,6760
86
- codeshift-0.3.7.dist-info/licenses/LICENSE,sha256=SrjIFBfm7GQh90Giv7mEiPnYdgp1wsp0RnOTT6HkeVw,1066
87
- codeshift-0.3.7.dist-info/METADATA,sha256=CwpDXX4P62C0Cgq5cR5cVneNXQeEkJCyoNyj8P0TOY0,17312
88
- codeshift-0.3.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
89
- codeshift-0.3.7.dist-info/entry_points.txt,sha256=AlJ8V7a2pNyu-9UiRKUWiTMIJtaYAUnlg53Y-wFHiK0,53
90
- codeshift-0.3.7.dist-info/top_level.txt,sha256=Ct42mtGs5foZ4MyYSksd5rXP0qFhWSZz8Y8mON0EEds,10
91
- codeshift-0.3.7.dist-info/RECORD,,
71
+ codeshift-0.5.0.dist-info/licenses/LICENSE,sha256=mHKnse9JK19WRK76lYEwKB9nJWyzMzRpG4gkUtbTyac,1066
72
+ codeshift-0.5.0.dist-info/METADATA,sha256=xzJZhOOKl7VZMhw-2qQBXfVhSAVmGJIcbpWoZFt_xOw,16841
73
+ codeshift-0.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
74
+ codeshift-0.5.0.dist-info/entry_points.txt,sha256=AlJ8V7a2pNyu-9UiRKUWiTMIJtaYAUnlg53Y-wFHiK0,53
75
+ codeshift-0.5.0.dist-info/top_level.txt,sha256=Ct42mtGs5foZ4MyYSksd5rXP0qFhWSZz8Y8mON0EEds,10
76
+ codeshift-0.5.0.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 PyResolve
3
+ Copyright (c) 2024 Codeshift
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
codeshift/api/__init__.py DELETED
@@ -1 +0,0 @@
1
- """PyResolve Billing API."""
codeshift/api/auth.py DELETED
@@ -1,182 +0,0 @@
1
- """Authentication utilities and dependencies for the PyResolve API."""
2
-
3
- import hashlib
4
- import secrets
5
- from collections.abc import Awaitable, Callable
6
- from typing import Annotated
7
-
8
- from fastapi import Depends, HTTPException, Security, status
9
- from fastapi.security import APIKeyHeader
10
-
11
- from codeshift.api.config import get_settings
12
- from codeshift.api.database import get_database
13
-
14
- # API Key header scheme
15
- api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
16
-
17
-
18
- def generate_api_key() -> tuple[str, str, str]:
19
- """Generate a new API key.
20
-
21
- Returns:
22
- Tuple of (full_key, key_prefix, key_hash)
23
- """
24
- settings = get_settings()
25
-
26
- # Generate 32 random bytes (256 bits of entropy)
27
- key_suffix = secrets.token_urlsafe(32)
28
-
29
- # Create the full key with prefix
30
- full_key = f"{settings.api_key_prefix}{key_suffix}"
31
-
32
- # Get prefix for identification (first 12 chars including prefix)
33
- key_prefix = full_key[:12]
34
-
35
- # Hash the full key for storage
36
- key_hash = hash_api_key(full_key)
37
-
38
- return full_key, key_prefix, key_hash
39
-
40
-
41
- def hash_api_key(api_key: str) -> str:
42
- """Hash an API key using SHA-256."""
43
- return hashlib.sha256(api_key.encode()).hexdigest()
44
-
45
-
46
- class AuthenticatedUser:
47
- """Authenticated user context."""
48
-
49
- def __init__(
50
- self,
51
- user_id: str,
52
- email: str,
53
- tier: str,
54
- api_key_id: str | None = None,
55
- scopes: list[str] | None = None,
56
- ):
57
- self.user_id = user_id
58
- self.email = email
59
- self.tier = tier
60
- self.api_key_id = api_key_id
61
- self.scopes = scopes or []
62
-
63
- def has_scope(self, scope: str) -> bool:
64
- """Check if user has a specific scope."""
65
- return scope in self.scopes or "admin" in self.scopes
66
-
67
-
68
- async def get_current_user(
69
- api_key: Annotated[str | None, Security(api_key_header)] = None,
70
- ) -> AuthenticatedUser:
71
- """Validate API key and return the authenticated user.
72
-
73
- Raises:
74
- HTTPException: If API key is invalid or missing
75
- """
76
- if not api_key:
77
- raise HTTPException(
78
- status_code=status.HTTP_401_UNAUTHORIZED,
79
- detail="Missing API key",
80
- headers={"WWW-Authenticate": "ApiKey"},
81
- )
82
-
83
- # Hash the provided key
84
- key_hash = hash_api_key(api_key)
85
-
86
- # Look up the key in the database
87
- db = get_database()
88
- api_key_record = db.get_api_key_by_hash(key_hash)
89
-
90
- if not api_key_record:
91
- raise HTTPException(
92
- status_code=status.HTTP_401_UNAUTHORIZED,
93
- detail="Invalid API key",
94
- headers={"WWW-Authenticate": "ApiKey"},
95
- )
96
-
97
- # Check if key is expired
98
- if api_key_record.get("expires_at"):
99
- from datetime import datetime, timezone
100
-
101
- expires_at = api_key_record["expires_at"]
102
- if isinstance(expires_at, str):
103
- expires_at = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
104
- if expires_at < datetime.now(timezone.utc):
105
- raise HTTPException(
106
- status_code=status.HTTP_401_UNAUTHORIZED,
107
- detail="API key has expired",
108
- headers={"WWW-Authenticate": "ApiKey"},
109
- )
110
-
111
- # Update last used timestamp
112
- db.update_api_key_last_used(api_key_record["id"])
113
-
114
- # Get profile data
115
- profile = api_key_record.get("profiles", {})
116
-
117
- return AuthenticatedUser(
118
- user_id=api_key_record["user_id"],
119
- email=profile.get("email", ""),
120
- tier=profile.get("tier", "free"),
121
- api_key_id=api_key_record["id"],
122
- scopes=api_key_record.get("scopes", []),
123
- )
124
-
125
-
126
- async def get_optional_user(
127
- api_key: Annotated[str | None, Security(api_key_header)] = None,
128
- ) -> AuthenticatedUser | None:
129
- """Get the current user if authenticated, otherwise return None.
130
-
131
- This is useful for endpoints that work both authenticated and unauthenticated.
132
- """
133
- if not api_key:
134
- return None
135
-
136
- try:
137
- return await get_current_user(api_key)
138
- except HTTPException:
139
- return None
140
-
141
-
142
- def require_scope(scope: str) -> Callable[..., Awaitable[AuthenticatedUser]]:
143
- """Dependency that requires a specific scope."""
144
-
145
- async def check_scope(
146
- user: Annotated[AuthenticatedUser, Depends(get_current_user)],
147
- ) -> AuthenticatedUser:
148
- if not user.has_scope(scope):
149
- raise HTTPException(
150
- status_code=status.HTTP_403_FORBIDDEN,
151
- detail=f"Scope '{scope}' required",
152
- )
153
- return user
154
-
155
- return check_scope
156
-
157
-
158
- def require_tier(minimum_tier: str) -> Callable[..., Awaitable[AuthenticatedUser]]:
159
- """Dependency that requires a minimum tier."""
160
- tier_levels = {"free": 0, "pro": 1, "unlimited": 2, "enterprise": 3}
161
-
162
- async def check_tier(
163
- user: Annotated[AuthenticatedUser, Depends(get_current_user)],
164
- ) -> AuthenticatedUser:
165
- user_level = tier_levels.get(user.tier, 0)
166
- required_level = tier_levels.get(minimum_tier, 0)
167
-
168
- if user_level < required_level:
169
- raise HTTPException(
170
- status_code=status.HTTP_403_FORBIDDEN,
171
- detail=f"This feature requires {minimum_tier} tier or higher",
172
- )
173
- return user
174
-
175
- return check_tier
176
-
177
-
178
- # Type aliases for dependency injection
179
- CurrentUser = Annotated[AuthenticatedUser, Depends(get_current_user)]
180
- OptionalUser = Annotated[AuthenticatedUser | None, Depends(get_optional_user)]
181
- ProUser = Annotated[AuthenticatedUser, Depends(require_tier("pro"))]
182
- UnlimitedUser = Annotated[AuthenticatedUser, Depends(require_tier("unlimited"))]
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