mrok 0.2.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. mrok/agent/devtools/__init__.py +0 -0
  2. mrok/agent/devtools/__main__.py +34 -0
  3. mrok/agent/devtools/inspector/__init__.py +0 -0
  4. mrok/agent/devtools/inspector/__main__.py +25 -0
  5. mrok/agent/devtools/inspector/app.py +556 -0
  6. mrok/agent/devtools/inspector/server.py +18 -0
  7. mrok/agent/sidecar/app.py +9 -10
  8. mrok/agent/sidecar/main.py +35 -16
  9. mrok/agent/ziticorn.py +27 -18
  10. mrok/cli/commands/__init__.py +2 -1
  11. mrok/cli/commands/admin/list/instances.py +24 -4
  12. mrok/cli/commands/admin/register/extensions.py +2 -2
  13. mrok/cli/commands/admin/register/instances.py +3 -3
  14. mrok/cli/commands/admin/unregister/extensions.py +2 -2
  15. mrok/cli/commands/admin/unregister/instances.py +2 -2
  16. mrok/cli/commands/agent/__init__.py +2 -0
  17. mrok/cli/commands/agent/dev/__init__.py +7 -0
  18. mrok/cli/commands/agent/dev/console.py +25 -0
  19. mrok/cli/commands/agent/dev/web.py +37 -0
  20. mrok/cli/commands/agent/run/asgi.py +35 -16
  21. mrok/cli/commands/agent/run/sidecar.py +29 -13
  22. mrok/cli/commands/agent/utils.py +5 -0
  23. mrok/cli/commands/controller/run.py +1 -5
  24. mrok/cli/commands/proxy/__init__.py +6 -0
  25. mrok/cli/commands/proxy/run.py +49 -0
  26. mrok/cli/utils.py +5 -0
  27. mrok/conf.py +6 -0
  28. mrok/controller/auth.py +2 -2
  29. mrok/controller/routes/extensions.py +9 -7
  30. mrok/datastructures.py +159 -0
  31. mrok/http/config.py +3 -6
  32. mrok/http/constants.py +22 -0
  33. mrok/http/forwarder.py +62 -23
  34. mrok/http/lifespan.py +29 -0
  35. mrok/http/middlewares.py +143 -0
  36. mrok/http/types.py +43 -0
  37. mrok/http/utils.py +90 -0
  38. mrok/logging.py +22 -0
  39. mrok/master.py +269 -0
  40. mrok/metrics.py +139 -0
  41. mrok/proxy/__init__.py +3 -0
  42. mrok/proxy/app.py +73 -0
  43. mrok/proxy/dataclasses.py +12 -0
  44. mrok/proxy/main.py +58 -0
  45. mrok/proxy/streams.py +124 -0
  46. mrok/proxy/types.py +12 -0
  47. mrok/proxy/ziti.py +173 -0
  48. mrok/ziti/identities.py +50 -20
  49. mrok/ziti/services.py +8 -8
  50. {mrok-0.2.3.dist-info → mrok-0.4.0.dist-info}/METADATA +7 -1
  51. mrok-0.4.0.dist-info/RECORD +92 -0
  52. {mrok-0.2.3.dist-info → mrok-0.4.0.dist-info}/WHEEL +1 -1
  53. mrok/http/master.py +0 -132
  54. mrok-0.2.3.dist-info/RECORD +0 -66
  55. {mrok-0.2.3.dist-info → mrok-0.4.0.dist-info}/entry_points.txt +0 -0
  56. {mrok-0.2.3.dist-info → mrok-0.4.0.dist-info}/licenses/LICENSE.txt +0 -0
File without changes
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """mrok agent devtools CLI entrypoint.
3
+
4
+ Provides a small CLI that accepts an optional subscriber port and
5
+ invokes the `run` function with that port.
6
+ """
7
+
8
+ import argparse
9
+ from typing import Any
10
+
11
+
12
+ def run(port: int) -> None:
13
+ """Run the devtools agent using the given subscriber port.
14
+
15
+ This is a stub for the runtime function. Implementation goes here.
16
+ """
17
+ pass
18
+
19
+
20
+ def main(argv: Any = None) -> None:
21
+ parser = argparse.ArgumentParser(description="mrok devtools agent")
22
+ parser.add_argument(
23
+ "-p",
24
+ "--subscriber-port",
25
+ type=int,
26
+ default=50001,
27
+ help="Port for subscriber (default: 50001)",
28
+ )
29
+ args = parser.parse_args(argv)
30
+ run(args.subscriber_port)
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
File without changes
@@ -0,0 +1,25 @@
1
+ import argparse
2
+ from typing import Any
3
+
4
+ from mrok.agent.devtools.inspector.app import InspectorApp
5
+
6
+
7
+ def run(port: int) -> None:
8
+ app = InspectorApp(port)
9
+ app.run()
10
+
11
+
12
+ def main(argv: Any = None) -> None:
13
+ parser = argparse.ArgumentParser(description="mrok devtools agent")
14
+ parser.add_argument(
15
+ "-p",
16
+ "--subscriber-port",
17
+ type=int,
18
+ default=50001,
19
+ help="Port for subscriber (default: 50001)",
20
+ )
21
+ args = parser.parse_args(argv)
22
+ run(args.subscriber_port)
23
+
24
+
25
+ main()
@@ -0,0 +1,556 @@
1
+ import json
2
+ from collections import OrderedDict, deque
3
+ from typing import Literal
4
+ from uuid import uuid4
5
+
6
+ import zmq.asyncio
7
+ from rich.table import Table
8
+ from textual import on, work
9
+ from textual.app import App, ComposeResult
10
+ from textual.containers import VerticalScroll
11
+ from textual.events import Resize
12
+ from textual.widgets import (
13
+ Collapsible,
14
+ DataTable,
15
+ Digits,
16
+ Header,
17
+ Label,
18
+ Placeholder,
19
+ Sparkline,
20
+ Static,
21
+ TabbedContent,
22
+ TabPane,
23
+ TextArea,
24
+ Tree,
25
+ )
26
+ from textual.widgets.data_table import ColumnKey
27
+ from textual.worker import get_current_worker
28
+
29
+ from mrok import __version__
30
+ from mrok.datastructures import Event, HTTPHeaders, HTTPResponse, Meta, WorkerMetrics
31
+
32
+
33
+ def build_tree(node, data):
34
+ if isinstance(data, dict):
35
+ for key, value in data.items():
36
+ child = node.add(str(key))
37
+ build_tree(child, value)
38
+ elif isinstance(data, list):
39
+ for index, value in enumerate(data):
40
+ child = node.add(f"[{index}]")
41
+ build_tree(child, value)
42
+ else:
43
+ node.add(repr(data))
44
+
45
+
46
+ def hexdump(data, width=16):
47
+ lines = []
48
+ for i in range(0, len(data), width):
49
+ chunk = data[i : i + width]
50
+ hex_part = " ".join(f"{b:02x}" for b in chunk)
51
+ ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk)
52
+ lines.append(f"{hex_part:<{width * 3}} {ascii_part}")
53
+ return "\n".join(lines)
54
+
55
+
56
+ class Counter(Digits):
57
+ DEFAULT_CSS = """
58
+ Counter {
59
+ text-align: center;
60
+ border: round #00BBFF;
61
+ }
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ counter_name: str,
67
+ value: str = "",
68
+ *,
69
+ name: str | None = None,
70
+ id: str | None = None,
71
+ classes: str | None = None,
72
+ disabled: bool = False,
73
+ ) -> None:
74
+ self.counter_name = counter_name
75
+ super().__init__(
76
+ value,
77
+ name=name,
78
+ id=id,
79
+ classes=classes,
80
+ disabled=disabled,
81
+ )
82
+
83
+ def on_mount(self) -> None:
84
+ self.border_title = self.counter_name
85
+
86
+
87
+ class HostInfo(Static):
88
+ BORDER_TITLE = "Process"
89
+ DEFAULT_CSS = """
90
+ HostInfo {
91
+ border: round #00BBFF;
92
+ layout: grid;
93
+ grid-size: 2 2;
94
+ grid-gutter: 0;
95
+ grid-columns: 1fr 2fr;
96
+ height: 100%;
97
+ width: 25;
98
+ content-align: center middle;
99
+ }
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ content="",
105
+ *,
106
+ expand=False,
107
+ shrink=False,
108
+ markup=True,
109
+ name=None,
110
+ id=None,
111
+ classes=None,
112
+ disabled=False,
113
+ ) -> None:
114
+ super().__init__(
115
+ content,
116
+ expand=expand,
117
+ shrink=shrink,
118
+ markup=markup,
119
+ name=name,
120
+ id=id,
121
+ classes=classes,
122
+ disabled=disabled,
123
+ )
124
+ self.mem_values: deque = deque([0] * 100, maxlen=100)
125
+ self.cpu_values: deque = deque([0] * 100, maxlen=100)
126
+
127
+ def compose(self) -> ComposeResult:
128
+ yield Label("CPU")
129
+ yield Sparkline(self.cpu_values, id="cpu")
130
+ yield Label("Memory")
131
+ yield Sparkline(self.mem_values, id="mem")
132
+
133
+ def update_info(self, cpu: int, mem: int):
134
+ self.cpu_values.append(cpu)
135
+ self.mem_values.append(mem)
136
+ self.query_one("#cpu", Sparkline).data = self.cpu_values
137
+ self.query_one("#mem", Sparkline).data = self.mem_values
138
+
139
+
140
+ class InfoPanel(Static):
141
+ BORDER_TITLE = "Info"
142
+
143
+ DEFAULT_CSS = """
144
+ InfoPanel {
145
+ background: black;
146
+ height: 100%;
147
+ border: round #0088FF;
148
+ }
149
+ """
150
+
151
+ def __init__(
152
+ self,
153
+ content="",
154
+ *,
155
+ expand=False,
156
+ shrink=False,
157
+ markup=True,
158
+ name=None,
159
+ id=None,
160
+ classes=None,
161
+ disabled=False,
162
+ ) -> None:
163
+ super().__init__(
164
+ content,
165
+ expand=expand,
166
+ shrink=shrink,
167
+ markup=markup,
168
+ name=name,
169
+ id=id,
170
+ classes=classes,
171
+ disabled=disabled,
172
+ )
173
+
174
+ def compose(self) -> ComposeResult:
175
+ yield DataTable(show_header=False, cursor_type="none")
176
+
177
+ def on_mount(self) -> None:
178
+ table = self.query_one(DataTable)
179
+ table.add_columns("Label", "Value")
180
+
181
+ # if len(self.workers_metrics):
182
+ # hinfo = self.query_one(HostInfo)
183
+ # hinfo.update_info(
184
+ # cpu=int(mean([m.process.cpu for m in self.workers_metrics.values()])),
185
+ # mem=int(mean([m.process.mem for m in self.workers_metrics.values()])),
186
+ # )
187
+
188
+ def update_meta(self, meta: Meta) -> None:
189
+ table = self.query_one(DataTable)
190
+ if len(table.rows) == 0:
191
+ table.add_row("URL", f"https://{meta.extension}.{meta.domain}")
192
+ table.add_row("Extension ID", meta.extension.upper())
193
+ table.add_row("Instance ID", meta.instance.upper())
194
+ table.add_row("mrok version ID", __version__)
195
+ self.loading = False
196
+
197
+
198
+ class Requests(Static):
199
+ BORDER_TITLE = "Requests"
200
+ DEFAULT_CSS = """
201
+ Requests {
202
+ background: black;
203
+ height: 100%;
204
+ border: round #0088FF;
205
+ }
206
+ """
207
+
208
+ def compose(self) -> ComposeResult:
209
+ # give the table an id so CSS can target it; enable zebra stripes
210
+ yield DataTable(id="requests", zebra_stripes=True, cursor_type="row")
211
+
212
+ def on_mount(self) -> None:
213
+ table = self.query_one(DataTable)
214
+ table.add_column(
215
+ "Method",
216
+ width=7,
217
+ )
218
+ table.add_column(
219
+ "Status",
220
+ width=6,
221
+ )
222
+ table.add_column("Path", width=25, key="path")
223
+ table.add_column(
224
+ "Duration (ms)",
225
+ width=13,
226
+ )
227
+
228
+
229
+ class Details(Static):
230
+ BORDER_TITLE = "Details"
231
+
232
+ def compose(self) -> ComposeResult:
233
+ with TabbedContent():
234
+ with TabPane("Headers"):
235
+ with VerticalScroll():
236
+ with Collapsible(title="General", collapsed=False):
237
+ yield DataTable(id="general-data", show_header=False, show_cursor=False)
238
+
239
+ with Collapsible(
240
+ title="Request Headers", collapsed=False, id="request-headers"
241
+ ):
242
+ yield Static(id="request-headers-table")
243
+ with Collapsible(
244
+ title="Response Headers", collapsed=False, id="response-headers"
245
+ ):
246
+ yield Static(id="response-headers-table")
247
+ with TabPane("Payload"):
248
+ yield Placeholder("payload")
249
+ with TabPane("Preview"):
250
+ yield Tree("Preview")
251
+ with TabPane("Response"):
252
+ yield TextArea(id="raw-response", read_only=True)
253
+
254
+ def update_headers(self, type: Literal["request", "response"], headers: HTTPHeaders):
255
+ collapsible = self.query_one(f"#{type}-headers", Collapsible)
256
+ collapsible.title = (
257
+ f"{type.capitalize()} Headers ({len(headers)})"
258
+ if len(headers)
259
+ else f"{type.capitalize()} Headers"
260
+ )
261
+ if len(headers):
262
+ headers_table = Table(show_header=False, box=None)
263
+ for k, v in headers.items():
264
+ headers_table.add_row(f"[orange3]{k}[/]", v)
265
+ self.query_one(f"#{type}-headers-table", Static).content = headers_table
266
+ else:
267
+ self.query_one(f"#{type}-headers-table", Static).content = ""
268
+
269
+ def update_preview(self, response: HTTPResponse):
270
+ tree = self.query_one(Tree)
271
+ tree.clear()
272
+ if (
273
+ response.headers.get("Content-Type", "").startswith("application/json")
274
+ and response.body
275
+ ):
276
+ build_tree(tree.root, json.loads(response.body.decode()))
277
+
278
+ def update_response(self, response: HTTPResponse):
279
+ text_area = self.query_one("#raw-response", TextArea)
280
+ text_area.clear()
281
+ if response.body:
282
+ text_area.text = response.body.decode()
283
+
284
+ def update_info(self, response: HTTPResponse):
285
+ general_table = self.query_one("#general-data", DataTable)
286
+ general_table.clear()
287
+ url = response.request.url
288
+ if response.request.query_string:
289
+ url = f"{url}?{response.request.query_string.decode()}"
290
+ general_table.add_row("Request Url", url)
291
+ general_table.add_row("Request Method", self.format_method(response.request.method))
292
+ general_table.add_row("Status Code", self.format_status(response.status))
293
+ self.update_headers("request", response.request.headers)
294
+ self.update_headers("response", response.headers)
295
+ self.update_preview(response)
296
+ self.update_response(response)
297
+
298
+ def format_status(self, status: int) -> str:
299
+ if status < 400:
300
+ return f"🟢 [green]{status}[/]"
301
+ elif status < 500:
302
+ return f"🟠 [orange3]{status}[/]"
303
+ else:
304
+ return f"🔴 [red]{status}[/]"
305
+
306
+ def format_method(self, method: str) -> str:
307
+ method = method.upper()
308
+ if method in ["POST", "PUT", "PATCH"]:
309
+ return f"[orange3]{method}[/]"
310
+ elif method == "DELETE":
311
+ return f"[red]{method}[/]"
312
+ else:
313
+ return f"[blue]{method}[/]"
314
+
315
+ def on_mount(self) -> None:
316
+ general_table = self.query_one("#general-data", DataTable)
317
+ general_table.add_columns("Label", "Value")
318
+
319
+
320
+ class LeftPanel(Static):
321
+ DEFAULT_CSS = """
322
+ LeftPanel {
323
+ layout: grid;
324
+ grid-size: 1 3;
325
+ grid-rows: 1fr 2fr 4fr;
326
+ grid-columns: 1fr;
327
+ background: black;
328
+ height: 100%;
329
+ width: 100%;
330
+ }
331
+ Details {
332
+ background: black;
333
+ height: 100%;
334
+ border: round #0088FF;
335
+ }
336
+ """
337
+
338
+ def compose(self):
339
+ yield InfoPanel()
340
+ yield Requests()
341
+ yield Details()
342
+
343
+
344
+ class MetricsPanel(Static):
345
+ BORDER_TITLE = "Metrics"
346
+ DEFAULT_CSS = """
347
+ MetricsPanel {
348
+ background: black;
349
+ layout: grid;
350
+ grid-size: 1 4;
351
+ grid-gutter: 0;
352
+ content-align: center top;
353
+ border: round #0088FF;
354
+ height: 100%;
355
+ grid-rows: auto auto auto auto;
356
+ }
357
+ #requests-rps {
358
+ color: #0088FF;
359
+ }
360
+ #requests-ok {
361
+ color: green;
362
+ }
363
+ #requests-ko {
364
+ color: red;
365
+ }
366
+ """
367
+
368
+ def __init__(
369
+ self,
370
+ content="",
371
+ *,
372
+ expand=False,
373
+ shrink=False,
374
+ markup=True,
375
+ name=None,
376
+ id=None,
377
+ classes=None,
378
+ disabled=False,
379
+ ) -> None:
380
+ super().__init__(
381
+ content,
382
+ expand=expand,
383
+ shrink=shrink,
384
+ markup=markup,
385
+ name=name,
386
+ id=id,
387
+ classes=classes,
388
+ disabled=disabled,
389
+ )
390
+ self.workers_metrics: dict[str, WorkerMetrics] = {}
391
+
392
+ def compose(self) -> ComposeResult:
393
+ yield Counter("RPS", id="requests-rps")
394
+ yield Counter("Total", id="requests-total")
395
+ yield Counter("Success", id="requests-ok")
396
+ yield Counter("Failed", id="requests-ko")
397
+
398
+ def on_mount(self) -> None:
399
+ self.set_interval(5.0, self.update_stats)
400
+
401
+ def update_stats(self) -> None:
402
+ self.query_one("#requests-rps", Digits).update(
403
+ str(sum(m.requests.rps for m in self.workers_metrics.values()))
404
+ )
405
+ self.query_one("#requests-total", Digits).update(
406
+ str(sum(m.requests.total for m in self.workers_metrics.values()))
407
+ )
408
+ self.query_one("#requests-ok", Digits).update(
409
+ str(sum(m.requests.successful for m in self.workers_metrics.values()))
410
+ )
411
+ self.query_one("#requests-ko", Digits).update(
412
+ str(sum(m.requests.failed for m in self.workers_metrics.values()))
413
+ )
414
+ self.loading = False
415
+
416
+ # if len(self.workers_metrics):
417
+ # hinfo = self.query_one(HostInfo)
418
+ # hinfo.update_info(
419
+ # cpu=int(mean([m.process.cpu for m in self.workers_metrics.values()])),
420
+ # mem=int(mean([m.process.mem for m in self.workers_metrics.values()])),
421
+ # )
422
+
423
+ def update_metrics(self, metrics: WorkerMetrics) -> None:
424
+ self.workers_metrics[metrics.worker_id] = metrics
425
+
426
+
427
+ class InspectorApp(App):
428
+ TITLE = "mrok Dev Console"
429
+ CSS = """
430
+ Screen {
431
+ layout: grid;
432
+ grid-size: 2 1;
433
+ grid-columns: 5fr 1fr;
434
+ background: black;
435
+ }
436
+ DataTable {
437
+ background: black;
438
+ color: #dddddd;
439
+ height: auto;
440
+ max-height: 100%;
441
+ }
442
+
443
+ /* Header row: white title text, neon underline */
444
+ DataTable > .datatable--header {
445
+ text-style: bold;
446
+ background: black;
447
+ color: white;
448
+ border-bottom: solid #0088FF;
449
+ }
450
+
451
+ /* Cells inherit neon text on black */
452
+ DataTable .cell {
453
+ color: #0088FF;
454
+ background: black;
455
+ }
456
+
457
+ /* Zebra stripes: even rows slightly lighter than black */
458
+ DataTable > .datatable--even-row {
459
+ background: rgb(10,10,14);
460
+ }
461
+ DataTable > .datatable--odd-row {
462
+ background: black;
463
+ }
464
+
465
+ /* Hover and focus styles */
466
+ DataTable > .datatable--hover {
467
+ background: rgb(6,12,20);
468
+ }
469
+ DataTable:focus > .datatable--cursor {
470
+ background: rgb(2,40,70);
471
+ color: white;
472
+ }
473
+
474
+ /* Fixed columns/rows use a muted neon background */
475
+ DataTable > .datatable--fixed {
476
+ background: rgb(6,6,10);
477
+ color: #00CFFF;
478
+ }
479
+ Collapsible {
480
+ background: black;
481
+ border: round #00BBFF;
482
+ }
483
+ """
484
+
485
+ def __init__(
486
+ self,
487
+ subscriber_port: int,
488
+ max_requests: int = 1000,
489
+ driver_class=None,
490
+ css_path=None,
491
+ watch_css=False,
492
+ ansi_color=False,
493
+ ):
494
+ super().__init__(driver_class, css_path, watch_css, ansi_color)
495
+ self.ctx = zmq.asyncio.Context()
496
+ self.socket = self.ctx.socket(zmq.SUB)
497
+ self.socket.connect(f"tcp://127.0.0.1:{subscriber_port}")
498
+ self.max_requests = max_requests
499
+ self.requests: OrderedDict = OrderedDict()
500
+
501
+ def compose(self) -> ComposeResult:
502
+ yield Header()
503
+ yield LeftPanel()
504
+ yield MetricsPanel()
505
+
506
+ async def on_mount(self):
507
+ self.query_one(InfoPanel).loading = True
508
+ self.query_one(MetricsPanel).loading = True
509
+ self.socket.subscribe("")
510
+ self.consumer()
511
+
512
+ @work(exclusive=True)
513
+ async def consumer(self):
514
+ worker = get_current_worker()
515
+ requests_table = self.query_one("#requests", DataTable)
516
+ while not worker.is_cancelled:
517
+ try:
518
+ self.log("Waiting for event")
519
+ event = Event.model_validate_json(await self.socket.recv_string())
520
+ self.log(f"Event received: {event}")
521
+ if event.type == "status":
522
+ info_widget = self.query_one(InfoPanel)
523
+ info_widget.update_meta(event.data.meta)
524
+ metrics_widget = self.query_one(MetricsPanel)
525
+ metrics_widget.update_metrics(event.data.metrics)
526
+ continue
527
+
528
+ response = event.data
529
+ if len(self.requests) == self.max_requests:
530
+ self.requests.popitem(last=False)
531
+ req_id = str(uuid4())
532
+ self.requests[req_id] = response
533
+ requests_table.clear()
534
+ for key, response in reversed(self.requests.items()):
535
+ requests_table.add_row(
536
+ response.request.method,
537
+ response.status,
538
+ response.request.url,
539
+ response.duration * 1000,
540
+ key=key,
541
+ )
542
+ except Exception as e:
543
+ self.log(e)
544
+
545
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
546
+ if not event.row_key:
547
+ return
548
+ response = self.requests[event.row_key]
549
+ details = self.query_one(Details)
550
+ details.update_info(response)
551
+
552
+ @on(Resize)
553
+ def resize_requests_table(self, event: Resize):
554
+ width = event.size.width
555
+ table = self.query_one("#requests", DataTable)
556
+ table.columns[ColumnKey("path")].width = width - 69
@@ -0,0 +1,18 @@
1
+ from aiohttp import web
2
+ from textual_serve.server import Server
3
+
4
+
5
+ class InspectorServer(Server):
6
+ def __init__(self, port: int = 7777, subscriber_port: int = 50001):
7
+ self.subscriber_port = subscriber_port
8
+ super().__init__(
9
+ f"python -m mrok.agent.devtools.inspector -p {subscriber_port}",
10
+ port=port,
11
+ )
12
+
13
+ async def on_startup(self, app: web.Application) -> None:
14
+ self.console.print(
15
+ f"Serving mrok inspector web app on {self.public_url} "
16
+ f"(subscriber port {self.subscriber_port})"
17
+ )
18
+ self.console.print("\n[cyan]Press Ctrl+C to quit")
mrok/agent/sidecar/app.py CHANGED
@@ -1,30 +1,29 @@
1
1
  import asyncio
2
2
  import logging
3
- from collections.abc import Awaitable, Callable
4
3
  from pathlib import Path
5
- from typing import Any
6
4
 
7
5
  from mrok.http.forwarder import ForwardAppBase
6
+ from mrok.http.types import Scope, StreamReader, StreamWriter
8
7
 
9
- logger = logging.getLogger("mrok.proxy")
10
-
11
- Scope = dict[str, Any]
12
- ASGIReceive = Callable[[], Awaitable[dict[str, Any]]]
13
- ASGISend = Callable[[dict[str, Any]], Awaitable[None]]
8
+ logger = logging.getLogger("mrok.agent")
14
9
 
15
10
 
16
11
  class ForwardApp(ForwardAppBase):
17
12
  def __init__(
18
- self, target_address: str | Path | tuple[str, int], read_chunk_size: int = 65536
13
+ self,
14
+ target_address: str | Path | tuple[str, int],
15
+ read_chunk_size: int = 65536,
19
16
  ) -> None:
20
- super().__init__(read_chunk_size=read_chunk_size)
17
+ super().__init__(
18
+ read_chunk_size=read_chunk_size,
19
+ )
21
20
  self._target_address = target_address
22
21
 
23
22
  async def select_backend(
24
23
  self,
25
24
  scope: Scope,
26
25
  headers: dict[str, str],
27
- ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter] | tuple[None, None]:
26
+ ) -> tuple[StreamReader, StreamWriter] | tuple[None, None]:
28
27
  if isinstance(self._target_address, tuple):
29
28
  return await asyncio.open_connection(*self._target_address)
30
29
  return await asyncio.open_unix_connection(str(self._target_address))