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
|
@@ -1,61 +1,45 @@
|
|
|
1
|
-
codeshift/__init__.py,sha256=
|
|
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/
|
|
5
|
-
codeshift/
|
|
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=
|
|
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=
|
|
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=
|
|
29
|
-
codeshift/cli/commands/upgrade.py,sha256=
|
|
30
|
-
codeshift/cli/commands/upgrade_all.py,sha256=
|
|
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=
|
|
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=
|
|
40
|
-
codeshift/knowledge_base/libraries/aiohttp.yaml,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
79
|
-
codeshift/utils/api_client.py,sha256=
|
|
80
|
-
codeshift/utils/cache.py,sha256=
|
|
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/
|
|
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.
|
|
87
|
-
codeshift-0.
|
|
88
|
-
codeshift-0.
|
|
89
|
-
codeshift-0.
|
|
90
|
-
codeshift-0.
|
|
91
|
-
codeshift-0.
|
|
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,,
|
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
|