mrok 0.4.5__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 (39) hide show
  1. mrok/agent/devtools/inspector/app.py +2 -2
  2. mrok/agent/sidecar/app.py +64 -16
  3. mrok/agent/sidecar/main.py +5 -5
  4. mrok/agent/ziticorn.py +2 -2
  5. mrok/cli/commands/__init__.py +2 -2
  6. mrok/cli/commands/admin/register/extensions.py +2 -4
  7. mrok/cli/commands/admin/register/instances.py +11 -5
  8. mrok/cli/commands/{proxy → frontend}/__init__.py +1 -1
  9. mrok/cli/commands/{proxy → frontend}/run.py +4 -4
  10. mrok/constants.py +4 -0
  11. mrok/controller/openapi/examples.py +13 -0
  12. mrok/frontend/__init__.py +3 -0
  13. mrok/frontend/app.py +75 -0
  14. mrok/{proxy → frontend}/main.py +4 -10
  15. mrok/proxy/__init__.py +0 -3
  16. mrok/proxy/app.py +160 -57
  17. mrok/proxy/backend.py +43 -0
  18. mrok/{http → proxy}/config.py +3 -3
  19. mrok/{datastructures.py → proxy/datastructures.py} +43 -10
  20. mrok/proxy/exceptions.py +22 -0
  21. mrok/proxy/lifespan.py +10 -0
  22. mrok/{master.py → proxy/master.py} +35 -38
  23. mrok/{metrics.py → proxy/metrics.py} +37 -49
  24. mrok/{http → proxy}/middlewares.py +47 -26
  25. mrok/proxy/streams.py +45 -0
  26. mrok/proxy/types.py +15 -0
  27. mrok/{http → proxy}/utils.py +1 -1
  28. {mrok-0.4.5.dist-info → mrok-0.5.0.dist-info}/METADATA +2 -5
  29. {mrok-0.4.5.dist-info → mrok-0.5.0.dist-info}/RECORD +35 -31
  30. mrok/http/__init__.py +0 -0
  31. mrok/http/forwarder.py +0 -338
  32. mrok/http/lifespan.py +0 -39
  33. mrok/http/types.py +0 -43
  34. /mrok/{http → proxy}/constants.py +0 -0
  35. /mrok/{http → proxy}/protocol.py +0 -0
  36. /mrok/{http → proxy}/server.py +0 -0
  37. {mrok-0.4.5.dist-info → mrok-0.5.0.dist-info}/WHEEL +0 -0
  38. {mrok-0.4.5.dist-info → mrok-0.5.0.dist-info}/entry_points.txt +0 -0
  39. {mrok-0.4.5.dist-info → mrok-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mrok
3
- Version: 0.4.5
3
+ Version: 0.5.0
4
4
  Summary: MPT Extensions OpenZiti Orchestrator
5
5
  Author: SoftwareOne AG
6
6
  License: Apache License
@@ -206,7 +206,6 @@ License: Apache License
206
206
  limitations under the License.
207
207
  License-File: LICENSE.txt
208
208
  Requires-Python: <4,>=3.12
209
- Requires-Dist: aiocache<0.13.0,>=0.12.3
210
209
  Requires-Dist: asn1crypto<2.0.0,>=1.5.1
211
210
  Requires-Dist: cryptography<46.0.0,>=45.0.7
212
211
  Requires-Dist: dynaconf<4.0.0,>=3.2.11
@@ -214,14 +213,12 @@ Requires-Dist: fastapi-pagination<0.15.0,>=0.14.1
214
213
  Requires-Dist: fastapi[standard]<0.120.0,>=0.119.0
215
214
  Requires-Dist: gunicorn<24.0.0,>=23.0.0
216
215
  Requires-Dist: hdrhistogram<0.11.0,>=0.10.3
217
- Requires-Dist: httptools<0.8.0,>=0.7.1
218
- Requires-Dist: httpx<0.29.0,>=0.28.1
216
+ Requires-Dist: httpcore<2.0.0,>=1.0.9
219
217
  Requires-Dist: openziti<2.0.0,>=1.3.1
220
218
  Requires-Dist: psutil<8.0.0,>=7.1.3
221
219
  Requires-Dist: pydantic<3.0.0,>=2.11.7
222
220
  Requires-Dist: pyfiglet<2.0.0,>=1.0.4
223
221
  Requires-Dist: pyjwt<3.0.0,>=2.10.1
224
- Requires-Dist: pytest-textual-snapshot<2.0.0,>=1.1.0
225
222
  Requires-Dist: pyyaml<7.0.0,>=6.0.2
226
223
  Requires-Dist: pyzmq<28.0.0,>=27.1.0
227
224
  Requires-Dist: rich<15.0.0,>=14.1.0
@@ -1,26 +1,24 @@
1
1
  mrok/__init__.py,sha256=D1PUs3KtMCqG4bFLceVNG62L3RN53NS95uSCNXpgvzs,181
2
2
  mrok/conf.py,sha256=_5Z-A5LyojQeY8J7W8C0QidsmrPl99r9qKYEoMf4kcI,840
3
- mrok/datastructures.py,sha256=gp8KF2JoNOxIRzYStVZLKL_XVDbcIVSIDnmpQo4FNt0,4067
3
+ mrok/constants.py,sha256=UTGYqs3DgEd_SN-k0JK10ekmHVWQaaARtdh1Y-0JG_s,122
4
4
  mrok/errors.py,sha256=ruNMDFr2_0ezCGXuCG1OswCEv-bHOIzMMd02J_0ABcs,37
5
5
  mrok/logging.py,sha256=ZMWn0w4fJ-F_g-L37H_GM14BSXAIF2mFF_ougX5S7mg,2856
6
- mrok/master.py,sha256=XuketJZuB1YWdbTs819pjLum7Qfv232F9ZCxdwRztCQ,8340
7
- mrok/metrics.py,sha256=asweK_7xiV5MtkDkvbEm9Tktqrl2KHM8VflF0AkNGI0,4036
8
6
  mrok/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- mrok/agent/ziticorn.py,sha256=marXGcr6CbDdiNi8V3CZJhg8YCUbKw6ySuO3-0-zf8g,900
7
+ mrok/agent/ziticorn.py,sha256=iENhronSKhPIhXhTMj7pElaLZgeEsgEVTKp_1yMa3Dk,907
10
8
  mrok/agent/devtools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
9
  mrok/agent/devtools/__main__.py,sha256=R8ezbW7hCik5r45U3w2TgiTubg9SlbVsWA-bapILJXU,781
12
10
  mrok/agent/devtools/inspector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
11
  mrok/agent/devtools/inspector/__main__.py,sha256=HeYcRf1bjXPji2LKMPCcTU61afrRH2P1RqnFmHClRTc,524
14
- mrok/agent/devtools/inspector/app.py,sha256=_pzxemMqIunE5EdMq5amjqpOGsMWIOw17GgiCtRAi6Q,16464
12
+ mrok/agent/devtools/inspector/app.py,sha256=E1WKcc5k7afq6xq_efEpOk9g5CaXmYjU1kNACZrYYyg,16486
15
13
  mrok/agent/devtools/inspector/server.py,sha256=C4uD6_1psSHMjJLUDCMPGvKdQYKaEwYTw27NAbwuuA0,636
16
14
  mrok/agent/sidecar/__init__.py,sha256=DrjJGhqFyxsVODW06KI20Wpr6HsD2lD6qFCKUXc7GIE,59
17
- mrok/agent/sidecar/app.py,sha256=1p6qWkXVq78zcJ2dhCYlw8CqfwPsgEtu07Lp5csK3Iw,874
18
- mrok/agent/sidecar/main.py,sha256=h31wynUCcFmRckvqLHtH97w1QgMv4fzcmYjhRPUobxY,1076
15
+ mrok/agent/sidecar/app.py,sha256=8gLB9tgxzC4l6CBYB9MW9A6-v2cjJHx-xVNwAnCD7hI,2506
16
+ mrok/agent/sidecar/main.py,sha256=m3aKhOHwIEsXDeTMynHmv41JnBIjv-QUgQ4uw1rrsDA,1068
19
17
  mrok/cli/__init__.py,sha256=mtFEa8IeS1x6Gm4dUYoSnAxyEzNqbUVSmWxtuZUMR84,61
20
18
  mrok/cli/main.py,sha256=DFcYPwDskXi8SKAgEsuP4GMFzaniIf_6bZaSDWvYKDk,2724
21
19
  mrok/cli/rich.py,sha256=P3Dyu8EArUR9_0j7DPK7LRx85TWdYdZ1SaJzD_S1ZCE,511
22
20
  mrok/cli/utils.py,sha256=m_olScdIUGks5IoC6p2F9D6CQIucWZ7LHyrvwm2bkJw,106
23
- mrok/cli/commands/__init__.py,sha256=jihISOj3ZZQ_dn0rogXrJOx6b283KLRUfTw9USQgAhI,134
21
+ mrok/cli/commands/__init__.py,sha256=-UOGzh38oWX7fPeI2nc5I9z8LylRdQAt868q4G6rNGk,140
24
22
  mrok/cli/commands/admin/__init__.py,sha256=WU49jpMF9p18UONjYywWEFzjF57zLpLKJ0qAZvrzcR4,414
25
23
  mrok/cli/commands/admin/bootstrap.py,sha256=iOnHctYajgcHrG_Idjn5Y7VVSaWYRIhdgqKSw9TWq9I,1680
26
24
  mrok/cli/commands/admin/utils.py,sha256=wQ-qQJGFyhikMJY_CWT-G6sTEIZb-LUdj1AUZisLPBw,1363
@@ -28,8 +26,8 @@ mrok/cli/commands/admin/list/__init__.py,sha256=kjCMcpn1gopcrQaaHxfFh8Kyngldepnl
28
26
  mrok/cli/commands/admin/list/extensions.py,sha256=16fhDB5ucL8su2WQnSaQ1E6MhgC4vkP9-nuHAcPpzyE,4405
29
27
  mrok/cli/commands/admin/list/instances.py,sha256=kaqeyidwUxgYqfaHXqp2m76rm5h2ErBsYyZcNeaBRwY,5912
30
28
  mrok/cli/commands/admin/register/__init__.py,sha256=5Jb_bc2L47MEpQIrOcquzduTFWQ01Jd1U1MpqaR-Ekw,209
31
- mrok/cli/commands/admin/register/extensions.py,sha256=p1qX5gSQX1IGpOQjO2MJzbc09v1ebdFuPo94QzJErKk,1485
32
- mrok/cli/commands/admin/register/instances.py,sha256=XB6uAchc7Rm8uAu7o3-oHaN_rS8CCIBf0QKWZGW86fI,1940
29
+ mrok/cli/commands/admin/register/extensions.py,sha256=dxciVA_S31rZSm0A7lkecn2mI9TMlWDhcJTgwgNXbM4,1460
30
+ mrok/cli/commands/admin/register/instances.py,sha256=raF57jPUTryWdvNqGCosth1C-8jjv9IbA0UuNbDel3A,2220
33
31
  mrok/cli/commands/admin/unregister/__init__.py,sha256=-GjjCPX1pISbWmJK6GpKO3ijGsDQb21URjU1hNu99O4,215
34
32
  mrok/cli/commands/admin/unregister/extensions.py,sha256=GR3Iwzeksk_R0GkgmCSG7iHRcUrI7ABqDi25Gbes64Y,1016
35
33
  mrok/cli/commands/admin/unregister/instances.py,sha256=-28wL8pTXTWHVHtw93y8-dqi-Dlf0OZOnlBCKOyGo80,1138
@@ -44,8 +42,8 @@ mrok/cli/commands/agent/run/sidecar.py,sha256=Tj5inAeSX1E3yCVs2q4P3sP3trvvwk2lYM
44
42
  mrok/cli/commands/controller/__init__.py,sha256=2xw-YVN0akiLiuGUU3XbYyZZ0ugOjQ6XhtTkzEKSmMA,161
45
43
  mrok/cli/commands/controller/openapi.py,sha256=QLjVao9UkB2vBaGkFi_q_jrlg4Np4ldMRwDIJsrJ7A8,1175
46
44
  mrok/cli/commands/controller/run.py,sha256=yl1p7oRHhQINWWjUKlRHtMIWUCV0KsxYdyVyazhX834,2406
47
- mrok/cli/commands/proxy/__init__.py,sha256=Y9oluemsuWSaykDniLVsI2cyTurcEO3_GJDHgf-7BdU,120
48
- mrok/cli/commands/proxy/run.py,sha256=QzKAjNCib-SS8IrGGHOxDjTtgTQxfeeeqmI3LaIkiLo,1293
45
+ mrok/cli/commands/frontend/__init__.py,sha256=0kK37yG6qs7yAa8TYlKZUA-nHrWsO4y5CjbVkXafnuk,123
46
+ mrok/cli/commands/frontend/run.py,sha256=4w9B662_7ASWuN_skWWWXXQyTcp0G4I-FzvfjL7Yiu0,1305
49
47
  mrok/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
48
  mrok/controller/app.py,sha256=XxCIB7N1YE52vSYfvGW2UPgEEOZ9jxDMe2l9D2SfXi8,1866
51
49
  mrok/controller/auth.py,sha256=hYa0OPJ5X0beGxRP6qbxwJOVXj5TmzHjmam2OjTBKn4,2704
@@ -55,24 +53,30 @@ mrok/controller/dependencies/__init__.py,sha256=voewk6gjkA0OarL6HFmfT_RLqBns0Fpl
55
53
  mrok/controller/dependencies/conf.py,sha256=2Pa8fxJHkZ29q6UL-w6hUP_wr7WnNELfw5LlzWg1Tec,162
56
54
  mrok/controller/dependencies/ziti.py,sha256=fYoxeJb4s6p2_3gxbExbFSRabjpvp_gZMBb3ocXZV3Y,702
57
55
  mrok/controller/openapi/__init__.py,sha256=U1dw45w76CcoQagyqg_FXdMuJF3qJZZM6wG8TeTe3Zo,101
58
- mrok/controller/openapi/examples.py,sha256=ZI0BP7L6sI0z7Mq1I3uc2UrweGpzpPeGSIuf1bUKkgg,1419
56
+ mrok/controller/openapi/examples.py,sha256=-Mwj41veIeydYaRSxcihN02g_jMuUrtvMfTjs08Dzf8,1852
59
57
  mrok/controller/openapi/utils.py,sha256=Kn55ISAWlMJNwrJTum7iFrBvJvr81To76pCK8W-s79Q,1114
60
58
  mrok/controller/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
59
  mrok/controller/routes/extensions.py,sha256=zoY4sNz_BIZcbly6WXM7Rbpn2jmB89njS_0xdJkoKfs,9192
62
60
  mrok/controller/routes/instances.py,sha256=v-fn_F6JHbDZ4YUNCIZzClgHp6aC1Eu5HB7k7qBG5pk,2202
63
- mrok/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- mrok/http/config.py,sha256=k73-4hBo6jag1RpyZagJLtTCL6EQoebZaX8Vv-CMN_k,2050
65
- mrok/http/constants.py,sha256=ao5gI2HFBWmrdd2Yc6XFK_RGaHk-omxI4AqvfIiGes8,409
66
- mrok/http/forwarder.py,sha256=DakD9hrrCWAzB1B_4SgQaQaEHcDYLLI9WaYs5F0O36I,12977
67
- mrok/http/lifespan.py,sha256=UdbOqjWZsHzJJjX0CTd2hY96Jpk5QWtdHJEzPG6Z4hQ,1288
68
- mrok/http/middlewares.py,sha256=SGo4EwhTId2uJx1aMuqGbNy7MXgZlDEdZI0buzBYVv0,5011
69
- mrok/http/protocol.py,sha256=ap8jbLUvgbAH81ZJZCBkQiYR7mkV_eL3rpfwEkoE8sU,392
70
- mrok/http/server.py,sha256=Mj7C85fc-DXp-WTBWaOd7ag808oliLmFBH5bf-G2FHg,370
71
- mrok/http/types.py,sha256=XpNrvbfpANKvmjOBYtLF1FmDHoJF3z_MIMQHXoJlvmE,1302
72
- mrok/http/utils.py,sha256=sOixYu3R9-nNoMFYdifrreYvcFRIHYVtb6AAmtVzaLE,2125
73
- mrok/proxy/__init__.py,sha256=vWXyImroqM1Eq8e_oFPBup8VJ3reyp8SVjFTbLzRkI8,51
74
- mrok/proxy/app.py,sha256=kWMg4oi0WtwHS0CD4iOK2-dghBzu8ya7aRIH-jn-61g,2436
75
- mrok/proxy/main.py,sha256=ZXpticE6J4FABaslDB_8J5qklPsf3e7xIFSZmcPAAjQ,1588
61
+ mrok/frontend/__init__.py,sha256=SN3LoFwAye18lfJ8OKNNS-7kLc2A9OxPGIEIEYYtAOA,54
62
+ mrok/frontend/app.py,sha256=We7z5xrbxvOuLsmWgsqfoECZ8FTWOtlw80a14g_8p9M,2632
63
+ mrok/frontend/main.py,sha256=kaje2kfS902TNQwwKn12WEr4N7hKHPg_NwczXAdplwY,1445
64
+ mrok/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
+ mrok/proxy/app.py,sha256=mqb07evKlGUzgqEy-LMzjyeIg1VjUFu83pZeBA4zVyM,5976
66
+ mrok/proxy/backend.py,sha256=T8DDA733CqbFWp5lrf_tKQK_VDgcERKTfesvxofl3AY,1493
67
+ mrok/proxy/config.py,sha256=aUTx0-dNAT7U0UEp5bX7-IO7p3HS082QUocAVq2NUWk,2053
68
+ mrok/proxy/constants.py,sha256=ao5gI2HFBWmrdd2Yc6XFK_RGaHk-omxI4AqvfIiGes8,409
69
+ mrok/proxy/datastructures.py,sha256=SDwhWpJTJrwK9hamHb_Og5PxKXG6VGOwVeoPxtaQLnU,5173
70
+ mrok/proxy/exceptions.py,sha256=61OhdihQNdnBUqAI9mbBkXD1yWg-6Eftk7EhWCU1VF0,642
71
+ mrok/proxy/lifespan.py,sha256=9qevhD_5Y0f8fGTh2axdfWx7v1K4vnWtiUNyJLesOHE,262
72
+ mrok/proxy/master.py,sha256=Houudf2eYZkP5Ri8JtmFeIlWtMuTvybpPaMeIPNyLRg,8484
73
+ mrok/proxy/metrics.py,sha256=NPhEbHoRdZ7YLKJPxmLc-sCFmos_L0iZwO0ufUs4SNg,3654
74
+ mrok/proxy/middlewares.py,sha256=XHOSQPCXAqUWU2lg1iNkIfTWcfCOJrhEJ_bhVpX-ToQ,5504
75
+ mrok/proxy/protocol.py,sha256=ap8jbLUvgbAH81ZJZCBkQiYR7mkV_eL3rpfwEkoE8sU,392
76
+ mrok/proxy/server.py,sha256=Mj7C85fc-DXp-WTBWaOd7ag808oliLmFBH5bf-G2FHg,370
77
+ mrok/proxy/streams.py,sha256=KxiEFiyAWlFQ7t-t8PZHfDveysBk4Wjo2La-aL7XghQ,1371
78
+ mrok/proxy/types.py,sha256=oET5CXqcBvk4kf_giNojZ9kfF5wygy3Z-MvcDd38LMU,555
79
+ mrok/proxy/utils.py,sha256=OxX6pJv_Wh_KgWx95YeJ3YeuSgwm2tsd00897P3fxys,2126
76
80
  mrok/ziti/__init__.py,sha256=20OWMiexRhOovZOX19zlX87-V78QyWnEnSZfyAftUdE,263
77
81
  mrok/ziti/api.py,sha256=KvGiT9d4oSgC3JbFWLDQyuHcLX2HuZJoJ8nHmWtCDkY,16154
78
82
  mrok/ziti/bootstrap.py,sha256=QIDhlkIxPW2QRuumFq2D1WDbD003P5f3z24pAUsyeBI,2696
@@ -81,8 +85,8 @@ mrok/ziti/errors.py,sha256=yYCbVDwktnR0AYduqtynIjo73K3HOhIrwA_vQimvEd4,368
81
85
  mrok/ziti/identities.py,sha256=1BcwfqAJHMBhc3vRaf0aLaIkoHskj5Xe2Lsq2lO9Vs8,6735
82
86
  mrok/ziti/pki.py,sha256=o2tySqHC8-7bvFuI2Tqxg9vX6H6ZSxWxfP_9x29e19M,1954
83
87
  mrok/ziti/services.py,sha256=zR1PEBYwXVou20iJK4euh0ZZFAo9UB8PZk8f6SDmiUE,3194
84
- mrok-0.4.5.dist-info/METADATA,sha256=jomSUZzuiMTTuC3T3zzisYfaFnevrSbQJ7y1-sM6lgU,15836
85
- mrok-0.4.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
86
- mrok-0.4.5.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
87
- mrok-0.4.5.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
88
- mrok-0.4.5.dist-info/RECORD,,
88
+ mrok-0.5.0.dist-info/METADATA,sha256=-rLLRvaOAveuVDJR_ZLNWohgU9EdIM1jgrnH6LoqgHc,15705
89
+ mrok-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
90
+ mrok-0.5.0.dist-info/entry_points.txt,sha256=tloXwvU1uJicBJR2h-8HoVclPgwJWDwuREMHN8Zq-nU,38
91
+ mrok-0.5.0.dist-info/licenses/LICENSE.txt,sha256=6PaICaoA3yNsZKLv5G6OKqSfLSoX7MakYqTDgJoTCBs,11346
92
+ mrok-0.5.0.dist-info/RECORD,,
mrok/http/__init__.py DELETED
File without changes
mrok/http/forwarder.py DELETED
@@ -1,338 +0,0 @@
1
- import abc
2
- import asyncio
3
- import logging
4
-
5
- from mrok.http.types import ASGIReceive, ASGISend, Scope, StreamReader, StreamWriter
6
-
7
- logger = logging.getLogger("mrok.proxy")
8
-
9
-
10
- class BackendNotFoundError(Exception):
11
- pass
12
-
13
-
14
- class ForwardAppBase(abc.ABC):
15
- """Generic HTTP forwarder base class.
16
-
17
- Subclasses must implement `select_backend(scope)` to return an
18
- (asyncio.StreamReader, asyncio.StreamWriter) pair connected to the
19
- desired backend. The base class implements the HTTP/1.1 framing
20
- and streaming logic (requests and responses).
21
- """
22
-
23
- def __init__(
24
- self,
25
- read_chunk_size: int = 65536,
26
- lifespan_timeout: float = 10.0,
27
- ) -> None:
28
- self._read_chunk_size: int = int(read_chunk_size)
29
- self._lifespan_timeout = lifespan_timeout
30
-
31
- async def handle_lifespan(self, receive: ASGIReceive, send: ASGISend) -> None:
32
- while True:
33
- event = await receive()
34
- etype = event.get("type")
35
-
36
- if etype == "lifespan.startup":
37
- try:
38
- await asyncio.wait_for(self.startup(), self._lifespan_timeout)
39
- except TimeoutError:
40
- logger.exception("Lifespan startup timed out")
41
- await send({"type": "lifespan.startup.failed", "message": "startup timeout"})
42
- continue
43
- except Exception as e:
44
- logger.exception("Exception during lifespan startup")
45
- await send({"type": "lifespan.startup.failed", "message": str(e)})
46
- continue
47
- await send({"type": "lifespan.startup.complete"})
48
-
49
- elif etype == "lifespan.shutdown":
50
- try:
51
- await asyncio.wait_for(self.shutdown(), self._lifespan_timeout)
52
- except TimeoutError:
53
- logger.exception("Lifespan shutdown timed out")
54
- await send({"type": "lifespan.shutdown.failed", "message": "shutdown timeout"})
55
- return
56
- except Exception as exc:
57
- logger.exception("Exception during lifespan shutdown")
58
- await send({"type": "lifespan.shutdown.failed", "message": str(exc)})
59
- return
60
- await send({"type": "lifespan.shutdown.complete"})
61
- return
62
-
63
- @abc.abstractmethod
64
- async def select_backend(
65
- self,
66
- scope: Scope,
67
- headers: dict[str, str],
68
- ) -> tuple[StreamReader, StreamWriter] | tuple[None, None]:
69
- """Return (reader, writer) connected to the target backend."""
70
-
71
- async def startup(self):
72
- return
73
-
74
- async def shutdown(self):
75
- return
76
-
77
- async def __call__(self, scope: Scope, receive: ASGIReceive, send: ASGISend) -> None:
78
- """ASGI callable entry point.
79
-
80
- Delegates to smaller helper methods for readability. Subclasses only
81
- need to implement backend selection.
82
- """
83
- scope_type = scope.get("type")
84
- if scope_type == "lifespan":
85
- await self.handle_lifespan(receive, send)
86
- return
87
-
88
- if scope.get("type") != "http":
89
- await send({"type": "http.response.start", "status": 500, "headers": []})
90
- await send({"type": "http.response.body", "body": b"Unsupported"})
91
- return
92
-
93
- method = scope.get("method", "GET")
94
- path_qs = self.format_path(scope)
95
-
96
- headers = list(scope.get("headers", []))
97
- headers = self.ensure_host_header(headers, scope)
98
- reader, writer = await self.select_backend(
99
- scope, {k[0].decode().lower(): k[1].decode() for k in headers}
100
- )
101
-
102
- if not (reader and writer):
103
- await send({"type": "http.response.start", "status": 502, "headers": []})
104
- await send({"type": "http.response.body", "body": b"Bad Gateway"})
105
- return
106
-
107
- use_chunked = self.ensure_chunked_if_needed(headers)
108
-
109
- await self.write_request_line_and_headers(writer, method, path_qs, headers)
110
-
111
- await self.stream_request_body(receive, writer, use_chunked)
112
-
113
- status_line = await reader.readline()
114
- if not status_line:
115
- await send({"type": "http.response.start", "status": 502, "headers": []})
116
- await send({"type": "http.response.body", "body": b"Bad Gateway"})
117
- writer.close()
118
- await writer.wait_closed()
119
- return
120
-
121
- status, headers_out, raw_headers = await self.read_status_and_headers(reader, status_line)
122
-
123
- await send({"type": "http.response.start", "status": status, "headers": headers_out})
124
-
125
- await self.stream_response_body(reader, send, raw_headers)
126
-
127
- writer.close()
128
- await writer.wait_closed()
129
-
130
- def format_path(self, scope: Scope) -> str:
131
- raw_path = scope.get("raw_path")
132
- if raw_path:
133
- return raw_path.decode()
134
- q = scope.get("query_string", b"")
135
- path = scope.get("path", "/")
136
- path_qs = path
137
- if q:
138
- path_qs += "?" + q.decode()
139
- return path_qs
140
-
141
- def ensure_host_header(
142
- self, headers: list[tuple[bytes, bytes]], scope: Scope
143
- ) -> list[tuple[bytes, bytes]]:
144
- if any(n.lower() == b"host" for n, _ in headers):
145
- return headers
146
- server = scope.get("server")
147
- if server:
148
- host = f"{server[0]}:{server[1]}" if server[1] else server[0]
149
- headers.append((b"host", host.encode()))
150
- return headers
151
-
152
- def ensure_chunked_if_needed(self, headers: list[tuple[bytes, bytes]]) -> bool:
153
- has_content_length = any(n.lower() == b"content-length" for n, _ in headers)
154
- has_transfer_encoding = any(n.lower() == b"transfer-encoding" for n, _ in headers)
155
- if not has_content_length and not has_transfer_encoding:
156
- headers.append((b"transfer-encoding", b"chunked"))
157
- return True
158
- return False
159
-
160
- async def write_request_line_and_headers(
161
- self,
162
- writer: StreamWriter,
163
- method: str,
164
- path_qs: str,
165
- headers: list[tuple[bytes, bytes]],
166
- ) -> None:
167
- writer.write(f"{method} {path_qs} HTTP/1.1\r\n".encode())
168
- for name, value in headers:
169
- if name.lower() == b"expect":
170
- continue
171
- writer.write(name + b": " + value + b"\r\n")
172
- writer.write(b"\r\n")
173
- await writer.drain()
174
-
175
- async def stream_request_body(
176
- self, receive: ASGIReceive, writer: StreamWriter, use_chunked: bool
177
- ) -> None:
178
- if use_chunked:
179
- await self.stream_request_chunked(receive, writer)
180
- return
181
-
182
- await self.stream_request_until_end(receive, writer)
183
-
184
- async def stream_request_chunked(self, receive: ASGIReceive, writer: StreamWriter) -> None:
185
- """Send request body to backend using HTTP/1.1 chunked encoding."""
186
- while True:
187
- event = await receive()
188
- if event["type"] == "http.request":
189
- body = event.get("body", b"") or b""
190
- if body:
191
- writer.write(f"{len(body):X}\r\n".encode())
192
- writer.write(body)
193
- writer.write(b"\r\n")
194
- await writer.drain()
195
- if not event.get("more_body", False):
196
- break
197
- elif event["type"] == "http.disconnect":
198
- writer.close()
199
- return
200
-
201
- writer.write(b"0\r\n\r\n")
202
- await writer.drain()
203
-
204
- async def stream_request_until_end(self, receive: ASGIReceive, writer: StreamWriter) -> None:
205
- """Send request body to backend when content length/transfer-encoding
206
- already provided (no chunking).
207
- """
208
- while True:
209
- event = await receive()
210
- if event["type"] == "http.request":
211
- body = event.get("body", b"") or b""
212
- if body:
213
- writer.write(body)
214
- await writer.drain()
215
- if not event.get("more_body", False):
216
- break
217
- elif event["type"] == "http.disconnect":
218
- writer.close()
219
- return
220
-
221
- async def read_status_and_headers(
222
- self, reader: StreamReader, first_line: bytes
223
- ) -> tuple[int, list[tuple[bytes, bytes]], dict[bytes, bytes]]:
224
- parts = first_line.decode(errors="ignore").split(" ", 2)
225
- status = int(parts[1]) if len(parts) >= 2 and parts[1].isdigit() else 502
226
- headers: list[tuple[bytes, bytes]] = []
227
- raw_headers: dict[bytes, bytes] = {}
228
- while True:
229
- line = await reader.readline()
230
- if line in (b"\r\n", b"\n", b""):
231
- break
232
- i = line.find(b":")
233
- if i == -1:
234
- continue
235
- name = line[:i].strip().lower()
236
- value = line[i + 1 :].strip()
237
- headers.append((name, value))
238
- raw_headers[name] = value
239
-
240
- return status, headers, raw_headers
241
-
242
- def is_chunked(self, te_value: bytes) -> bool:
243
- """Return True if transfer-encoding header tokens include 'chunked'."""
244
- if not te_value:
245
- return False
246
- # split on commas, strip spaces and check tokens
247
- tokens = [t.strip() for t in te_value.split(b",")]
248
- return any(t.lower() == b"chunked" for t in tokens)
249
-
250
- def parse_content_length(self, cl_value: bytes | None) -> int | None:
251
- """Parse Content-Length header value to int, or return None if invalid."""
252
- if cl_value is None:
253
- return None
254
- try:
255
- return int(cl_value)
256
- except Exception:
257
- return None
258
-
259
- async def drain_trailers(self, reader: StreamReader) -> None:
260
- """Consume trailer header lines until an empty line is encountered."""
261
- while True:
262
- trailer = await reader.readline()
263
- if trailer in (b"\r\n", b"\n", b""):
264
- break
265
-
266
- async def stream_response_chunked(self, reader: StreamReader, send: ASGISend) -> None:
267
- """Read chunked-encoded response from reader, decode and forward to ASGI send."""
268
- while True:
269
- size_line = await reader.readline()
270
- if not size_line:
271
- break
272
- size_str = size_line.split(b";", 1)[0].strip()
273
- try:
274
- size = int(size_str, 16)
275
- except Exception:
276
- break
277
- if size == 0:
278
- # consume trailers
279
- await self.drain_trailers(reader)
280
- break
281
- try:
282
- chunk = await reader.readexactly(size)
283
- except Exception:
284
- break
285
- # consume the CRLF after the chunk
286
- try:
287
- await reader.readexactly(2)
288
- except Exception:
289
- logger.warning("failed to read CRLF after chunk from backend")
290
- await send({"type": "http.response.body", "body": chunk, "more_body": True})
291
-
292
- await send({"type": "http.response.body", "body": b"", "more_body": False})
293
-
294
- async def stream_response_with_content_length(
295
- self, reader: StreamReader, send: ASGISend, content_length: int
296
- ) -> None:
297
- """Read exactly content_length bytes and forward to ASGI send events."""
298
- remaining = content_length
299
- sent_final = False
300
- while remaining > 0:
301
- to_read = min(self._read_chunk_size, remaining)
302
- chunk = await reader.read(to_read)
303
- if not chunk:
304
- break
305
- remaining -= len(chunk)
306
- more = remaining > 0
307
- await send({"type": "http.response.body", "body": chunk, "more_body": more})
308
- if not more:
309
- sent_final = True
310
-
311
- if not sent_final:
312
- await send({"type": "http.response.body", "body": b"", "more_body": False})
313
-
314
- async def stream_response_until_eof(self, reader: StreamReader, send: ASGISend) -> None:
315
- """Read from reader until EOF and forward chunks to ASGI send events."""
316
- while True:
317
- chunk = await reader.read(self._read_chunk_size)
318
- if not chunk:
319
- break
320
- await send({"type": "http.response.body", "body": chunk, "more_body": True})
321
- await send({"type": "http.response.body", "body": b"", "more_body": False})
322
-
323
- async def stream_response_body(
324
- self, reader: StreamReader, send: ASGISend, raw_headers: dict[bytes, bytes]
325
- ) -> None:
326
- te = raw_headers.get(b"transfer-encoding", b"").lower()
327
- cl = raw_headers.get(b"content-length")
328
-
329
- if self.is_chunked(te):
330
- await self.stream_response_chunked(reader, send)
331
- return
332
-
333
- content_length = self.parse_content_length(cl)
334
- if content_length is not None:
335
- await self.stream_response_with_content_length(reader, send, content_length)
336
- return
337
-
338
- await self.stream_response_until_eof(reader, send)
mrok/http/lifespan.py DELETED
@@ -1,39 +0,0 @@
1
- import logging
2
- from collections.abc import Awaitable, Callable
3
-
4
- from uvicorn.config import Config
5
- from uvicorn.lifespan.on import LifespanOn
6
-
7
- AsyncCallback = Callable[[], Awaitable[None]]
8
-
9
-
10
- class MrokLifespan(LifespanOn):
11
- def __init__(self, config: Config) -> None:
12
- super().__init__(config)
13
- self.logger = logging.getLogger("mrok.proxy")
14
-
15
-
16
- class LifespanWrapper:
17
- def __init__(
18
- self, app, on_startup: AsyncCallback | None = None, on_shutdown: AsyncCallback | None = None
19
- ):
20
- self.app = app
21
- self.on_startup = on_startup
22
- self.on_shutdown = on_shutdown
23
-
24
- async def __call__(self, scope, receive, send):
25
- if scope["type"] == "lifespan":
26
- while True:
27
- event = await receive()
28
- if event["type"] == "lifespan.startup":
29
- if self.on_startup:
30
- await self.on_startup()
31
- await send({"type": "lifespan.startup.complete"})
32
-
33
- elif event["type"] == "lifespan.shutdown":
34
- if self.on_shutdown:
35
- await self.on_shutdown()
36
- await send({"type": "lifespan.shutdown.complete"})
37
- break
38
- else:
39
- await self.app(scope, receive, send)
mrok/http/types.py DELETED
@@ -1,43 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from collections.abc import Awaitable, Callable, MutableMapping
5
- from typing import Any, Protocol
6
-
7
- from mrok.datastructures import HTTPRequest, HTTPResponse
8
-
9
- Scope = MutableMapping[str, Any]
10
- Message = MutableMapping[str, Any]
11
-
12
- ASGIReceive = Callable[[], Awaitable[Message]]
13
- ASGISend = Callable[[Message], Awaitable[None]]
14
- ASGIApp = Callable[[Scope, ASGIReceive, ASGISend], Awaitable[None]]
15
- RequestCompleteCallback = Callable[[HTTPRequest], Awaitable | None]
16
- ResponseCompleteCallback = Callable[[HTTPResponse], Awaitable | None]
17
-
18
-
19
- class StreamReaderWrapper(Protocol):
20
- async def read(self, n: int = -1) -> bytes: ...
21
- async def readexactly(self, n: int) -> bytes: ...
22
- async def readline(self) -> bytes: ...
23
- def at_eof(self) -> bool: ...
24
-
25
- @property
26
- def underlying(self) -> asyncio.StreamReader: ...
27
-
28
-
29
- class StreamWriterWrapper(Protocol):
30
- def write(self, data: bytes) -> None: ...
31
- async def drain(self) -> None: ...
32
- def close(self) -> None: ...
33
- async def wait_closed(self) -> None: ...
34
-
35
- @property
36
- def transport(self): ...
37
-
38
- @property
39
- def underlying(self) -> asyncio.StreamWriter: ...
40
-
41
-
42
- StreamReader = StreamReaderWrapper | asyncio.StreamReader
43
- StreamWriter = StreamWriterWrapper | asyncio.StreamWriter
File without changes
File without changes
File without changes
File without changes