mrok 0.1.6__py3-none-any.whl → 0.1.7__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 (63) hide show
  1. mrok/__init__.py +6 -0
  2. mrok/agent/__init__.py +0 -0
  3. mrok/agent/sidecar/__init__.py +3 -0
  4. mrok/agent/sidecar/app.py +30 -0
  5. mrok/agent/sidecar/main.py +27 -0
  6. mrok/agent/ziticorn.py +29 -0
  7. mrok/cli/__init__.py +3 -0
  8. mrok/cli/commands/__init__.py +7 -0
  9. mrok/cli/commands/admin/__init__.py +10 -0
  10. mrok/cli/commands/admin/bootstrap.py +58 -0
  11. mrok/cli/commands/admin/register/__init__.py +8 -0
  12. mrok/cli/commands/admin/register/extensions.py +46 -0
  13. mrok/cli/commands/admin/register/instances.py +60 -0
  14. mrok/cli/commands/admin/unregister/__init__.py +8 -0
  15. mrok/cli/commands/admin/unregister/extensions.py +33 -0
  16. mrok/cli/commands/admin/unregister/instances.py +34 -0
  17. mrok/cli/commands/admin/utils.py +23 -0
  18. mrok/cli/commands/agent/__init__.py +6 -0
  19. mrok/cli/commands/agent/run/__init__.py +7 -0
  20. mrok/cli/commands/agent/run/asgi.py +49 -0
  21. mrok/cli/commands/agent/run/sidecar.py +54 -0
  22. mrok/cli/commands/controller/__init__.py +7 -0
  23. mrok/cli/commands/controller/openapi.py +47 -0
  24. mrok/cli/commands/controller/run.py +87 -0
  25. mrok/cli/main.py +97 -0
  26. mrok/cli/rich.py +18 -0
  27. mrok/conf.py +32 -0
  28. mrok/controller/__init__.py +0 -0
  29. mrok/controller/app.py +62 -0
  30. mrok/controller/auth.py +87 -0
  31. mrok/controller/dependencies/__init__.py +4 -0
  32. mrok/controller/dependencies/conf.py +7 -0
  33. mrok/controller/dependencies/ziti.py +27 -0
  34. mrok/controller/openapi/__init__.py +3 -0
  35. mrok/controller/openapi/examples.py +44 -0
  36. mrok/controller/openapi/utils.py +35 -0
  37. mrok/controller/pagination.py +79 -0
  38. mrok/controller/routes.py +294 -0
  39. mrok/controller/schemas.py +67 -0
  40. mrok/errors.py +2 -0
  41. mrok/http/__init__.py +0 -0
  42. mrok/http/config.py +65 -0
  43. mrok/http/forwarder.py +299 -0
  44. mrok/http/lifespan.py +10 -0
  45. mrok/http/master.py +90 -0
  46. mrok/http/protocol.py +11 -0
  47. mrok/http/server.py +14 -0
  48. mrok/logging.py +76 -0
  49. mrok/ziti/__init__.py +15 -0
  50. mrok/ziti/api.py +467 -0
  51. mrok/ziti/bootstrap.py +71 -0
  52. mrok/ziti/constants.py +6 -0
  53. mrok/ziti/errors.py +25 -0
  54. mrok/ziti/identities.py +161 -0
  55. mrok/ziti/pki.py +52 -0
  56. mrok/ziti/services.py +87 -0
  57. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/METADATA +7 -9
  58. mrok-0.1.7.dist-info/RECORD +61 -0
  59. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/WHEEL +1 -2
  60. mrok-0.1.6.dist-info/RECORD +0 -6
  61. mrok-0.1.6.dist-info/top_level.txt +0 -1
  62. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/entry_points.txt +0 -0
  63. {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/licenses/LICENSE.txt +0 -0
mrok/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("mrok")
5
+ except PackageNotFoundError: # pragma: no cover
6
+ __version__ = "0.0.0.dev0"
mrok/agent/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from mrok.agent.sidecar.main import run
2
+
3
+ __all__ = ["run"]
@@ -0,0 +1,30 @@
1
+ import asyncio
2
+ import logging
3
+ from collections.abc import Awaitable, Callable
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from mrok.http.forwarder import ForwardAppBase
8
+
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]]
14
+
15
+
16
+ class ForwardApp(ForwardAppBase):
17
+ def __init__(
18
+ self, target_address: str | Path | tuple[str, int], read_chunk_size: int = 65536
19
+ ) -> None:
20
+ super().__init__(read_chunk_size=read_chunk_size)
21
+ self._target_address = target_address
22
+
23
+ async def select_backend(
24
+ self,
25
+ scope: Scope,
26
+ headers: dict[str, str],
27
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter] | tuple[None, None]:
28
+ if isinstance(self._target_address, tuple):
29
+ return await asyncio.open_connection(*self._target_address)
30
+ return await asyncio.open_unix_connection(str(self._target_address))
@@ -0,0 +1,27 @@
1
+ import asyncio
2
+ import contextlib
3
+ from functools import partial
4
+ from pathlib import Path
5
+
6
+ from mrok.agent.sidecar.app import ForwardApp
7
+ from mrok.http.config import MrokBackendConfig
8
+ from mrok.http.master import Master
9
+ from mrok.http.server import MrokServer
10
+
11
+
12
+ def run_sidecar(identity_file: str, target_addr: str | Path | tuple[str, int]):
13
+ config = MrokBackendConfig(ForwardApp(target_addr), identity_file)
14
+ server = MrokServer(config)
15
+ with contextlib.suppress(KeyboardInterrupt, asyncio.CancelledError):
16
+ server.run()
17
+
18
+
19
+ def run(
20
+ identity_file: str,
21
+ target_addr: str | Path | tuple[str, int],
22
+ workers=4,
23
+ reload=False,
24
+ ):
25
+ start_fn = partial(run_sidecar, identity_file, target_addr)
26
+ master = Master(start_fn, workers=workers, reload=reload)
27
+ master.run()
mrok/agent/ziticorn.py ADDED
@@ -0,0 +1,29 @@
1
+ import asyncio
2
+ import contextlib
3
+ import os
4
+ from functools import partial
5
+
6
+ from mrok.http.config import ASGIApplication, MrokBackendConfig
7
+ from mrok.http.master import Master
8
+ from mrok.http.server import MrokServer
9
+
10
+
11
+ def run_ziticorn(app: ASGIApplication | str, identity_file: str):
12
+ import sys
13
+
14
+ sys.path.insert(0, os.getcwd())
15
+ config = MrokBackendConfig(app, identity_file)
16
+ server = MrokServer(config)
17
+ with contextlib.suppress(KeyboardInterrupt, asyncio.CancelledError):
18
+ server.run()
19
+
20
+
21
+ def run(
22
+ app: ASGIApplication | str,
23
+ identity_file: str,
24
+ workers: int = 4,
25
+ reload: bool = False,
26
+ ):
27
+ start_fn = partial(run_ziticorn, app, identity_file)
28
+ master = Master(start_fn, workers=workers, reload=reload)
29
+ master.run()
mrok/cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from mrok.cli.main import app, run
2
+
3
+ __all__ = ["app", "run"]
@@ -0,0 +1,7 @@
1
+ from mrok.cli.commands import admin, agent, controller
2
+
3
+ __all__ = [
4
+ "admin",
5
+ "agent",
6
+ "controller",
7
+ ]
@@ -0,0 +1,10 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin import bootstrap
4
+ from mrok.cli.commands.admin.register import app as register_app
5
+ from mrok.cli.commands.admin.unregister import app as unregister_app
6
+
7
+ app = typer.Typer(help="mrok administrative commands.")
8
+ app.add_typer(register_app)
9
+ app.add_typer(unregister_app)
10
+ bootstrap.register(app)
@@ -0,0 +1,58 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Annotated, Any
6
+
7
+ import typer
8
+
9
+ from mrok.cli.commands.admin.utils import parse_tags
10
+ from mrok.conf import Settings
11
+ from mrok.ziti.api import TagsType, ZitiClientAPI, ZitiManagementAPI
12
+ from mrok.ziti.bootstrap import bootstrap_identity
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def bootstrap(
18
+ settings: Settings, forced: bool, tags: TagsType | None
19
+ ) -> tuple[str, dict[str, Any] | None]:
20
+ async with ZitiManagementAPI(settings) as mgmt_api, ZitiClientAPI(settings) as client_api:
21
+ return await bootstrap_identity(
22
+ mgmt_api,
23
+ client_api,
24
+ settings.proxy.identity,
25
+ settings.proxy.mode,
26
+ forced,
27
+ tags,
28
+ )
29
+
30
+
31
+ def register(app: typer.Typer) -> None:
32
+ @app.command("bootstrap")
33
+ def run_bootstrap(
34
+ ctx: typer.Context,
35
+ identity_file: Path = typer.Argument(
36
+ Path("proxy_identity.json"),
37
+ help="Path to identity output file",
38
+ writable=True,
39
+ ),
40
+ forced: bool = typer.Option(
41
+ False,
42
+ "--force",
43
+ help="Regenerate identity even if it already exists",
44
+ ),
45
+ tags: Annotated[
46
+ list[str] | None,
47
+ typer.Option(
48
+ "--tag",
49
+ "-t",
50
+ help="Add tag",
51
+ show_default=True,
52
+ ),
53
+ ] = None,
54
+ ):
55
+ """Run the mrok bootstrap."""
56
+ _, identity_json = asyncio.run(bootstrap(ctx.obj, forced, parse_tags(tags)))
57
+ if identity_json:
58
+ json.dump(identity_json, identity_file.open("w"))
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin.register import extensions, instances
4
+
5
+ app = typer.Typer(name="register", help="Register resources into OpenZiti.")
6
+
7
+ extensions.register(app)
8
+ instances.register(app)
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+ import re
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich import print
7
+
8
+ from mrok.cli.commands.admin.utils import parse_tags
9
+ from mrok.conf import Settings
10
+ from mrok.ziti.api import ZitiManagementAPI
11
+ from mrok.ziti.services import register_extension
12
+
13
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
14
+
15
+
16
+ async def do_register(settings: Settings, extension_id: str, tags: list[str] | None):
17
+ async with ZitiManagementAPI(settings) as api:
18
+ await register_extension(settings, api, extension_id, tags=parse_tags(tags))
19
+
20
+
21
+ def validate_extension_id(extension_id: str) -> str:
22
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
23
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
24
+ return extension_id
25
+
26
+
27
+ def register(app: typer.Typer) -> None:
28
+ @app.command("extension")
29
+ def register_extension(
30
+ ctx: typer.Context,
31
+ extension_id: str = typer.Argument(
32
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
33
+ ),
34
+ tags: Annotated[
35
+ list[str] | None,
36
+ typer.Option(
37
+ "--tag",
38
+ "-t",
39
+ help="Add tag",
40
+ show_default=True,
41
+ ),
42
+ ] = None,
43
+ ):
44
+ """Register a new Extension in OpenZiti (service)."""
45
+ asyncio.run(do_register(ctx.obj, extension_id, tags))
46
+ print(f"🍻 [green]Extension [bold]{extension_id}[/bold] registered.[/green]")
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from mrok.cli.commands.admin.utils import parse_tags
10
+ from mrok.conf import Settings
11
+ from mrok.ziti.api import ZitiClientAPI, ZitiManagementAPI
12
+ from mrok.ziti.identities import register_instance
13
+
14
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
15
+
16
+
17
+ async def do_register(
18
+ settings: Settings, extension_id: str, instance_id: str, tags: list[str] | None
19
+ ):
20
+ async with ZitiManagementAPI(settings) as mgmt_api, ZitiClientAPI(settings) as client_api:
21
+ return await register_instance(
22
+ mgmt_api, client_api, extension_id, instance_id, tags=parse_tags(tags)
23
+ )
24
+
25
+
26
+ def validate_extension_id(extension_id: str):
27
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
28
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
29
+ return extension_id
30
+
31
+
32
+ def register(app: typer.Typer) -> None:
33
+ @app.command("instance")
34
+ def register_instance(
35
+ ctx: typer.Context,
36
+ extension_id: str = typer.Argument(
37
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
38
+ ),
39
+ instance_id: str = typer.Argument(..., help="Instance ID"),
40
+ output: Path = typer.Argument(
41
+ ...,
42
+ file_okay=True,
43
+ dir_okay=False,
44
+ writable=True,
45
+ resolve_path=True,
46
+ help="Output file (default: stdout)",
47
+ ),
48
+ tags: Annotated[
49
+ list[str] | None,
50
+ typer.Option(
51
+ "--tag",
52
+ "-t",
53
+ help="Add tag",
54
+ show_default=True,
55
+ ),
56
+ ] = None,
57
+ ):
58
+ """Register a new Extension Instance in OpenZiti (identity)."""
59
+ _, identity_file = asyncio.run(do_register(ctx.obj, extension_id, instance_id, tags))
60
+ json.dump(identity_file, output.open("w"))
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.admin.unregister import extensions, instances
4
+
5
+ app = typer.Typer(name="unregister", help="Unregister resources from OpenZiti.")
6
+
7
+ extensions.register(app)
8
+ instances.register(app)
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import re
3
+
4
+ import typer
5
+
6
+ from mrok.conf import Settings
7
+ from mrok.ziti.api import ZitiManagementAPI
8
+ from mrok.ziti.services import unregister_extension
9
+
10
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
11
+
12
+
13
+ async def do_unregister(settings: Settings, extension_id: str):
14
+ async with ZitiManagementAPI(settings) as api:
15
+ await unregister_extension(settings, api, extension_id)
16
+
17
+
18
+ def validate_extension_id(extension_id: str):
19
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
20
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
21
+ return extension_id
22
+
23
+
24
+ def register(app: typer.Typer) -> None:
25
+ @app.command("extension")
26
+ def unregister_extension(
27
+ ctx: typer.Context,
28
+ extension_id: str = typer.Argument(
29
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
30
+ ),
31
+ ):
32
+ """Unregister a new Extension in OpenZiti (service)."""
33
+ asyncio.run(do_unregister(ctx.obj, extension_id))
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+ import re
3
+
4
+ import typer
5
+
6
+ from mrok.conf import Settings
7
+ from mrok.ziti.api import ZitiManagementAPI
8
+ from mrok.ziti.identities import unregister_instance
9
+
10
+ RE_EXTENSION_ID = re.compile(r"(?i)EXT-\d{4}-\d{4}")
11
+
12
+
13
+ async def do_unregister(settings: Settings, extension_id: str, instance_id: str):
14
+ async with ZitiManagementAPI(settings) as api:
15
+ await unregister_instance(api, extension_id, instance_id)
16
+
17
+
18
+ def validate_extension_id(extension_id: str):
19
+ if not RE_EXTENSION_ID.fullmatch(extension_id):
20
+ raise typer.BadParameter("ext_id must match EXT-xxxx-yyyy (case-insensitive)")
21
+ return extension_id
22
+
23
+
24
+ def register(app: typer.Typer) -> None:
25
+ @app.command("instance")
26
+ def unregister_instance(
27
+ ctx: typer.Context,
28
+ extension_id: str = typer.Argument(
29
+ ..., callback=validate_extension_id, help="Extension ID in format EXT-xxxx-yyyy"
30
+ ),
31
+ instance_id: str = typer.Argument(..., help="Instance ID"),
32
+ ):
33
+ """Register a new Extension Instance in OpenZiti (identity)."""
34
+ asyncio.run(do_unregister(ctx.obj, extension_id, instance_id))
@@ -0,0 +1,23 @@
1
+ import typer
2
+
3
+ from mrok.ziti.api import TagsType
4
+
5
+
6
+ def parse_tags(pairs: list[str] | None) -> TagsType | None:
7
+ if not pairs:
8
+ return None
9
+
10
+ result: dict[str, str | bool | None] = {}
11
+ for item in pairs:
12
+ if "=" not in item:
13
+ raise typer.BadParameter(f"Invalid format {item!r}, expected key=value")
14
+ key, raw = item.split("=", 1)
15
+ raw_lower = raw.strip().lower()
16
+ if raw_lower in ("true", "false"):
17
+ val: str | bool | None = raw_lower == "true"
18
+ elif raw == "":
19
+ val = None
20
+ else:
21
+ val = raw
22
+ result[key.strip()] = val
23
+ return result
@@ -0,0 +1,6 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.agent.run import app as run_app
4
+
5
+ app = typer.Typer(help="mrok agent commands.")
6
+ app.add_typer(run_app)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.agent.run import asgi, sidecar
4
+
5
+ app = typer.Typer(name="run", help="Run mrok agent.")
6
+ asgi.register(app)
7
+ sidecar.register(app)
@@ -0,0 +1,49 @@
1
+ import multiprocessing
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ # from app.logging import get_logging_config
8
+ from mrok.agent import ziticorn
9
+
10
+
11
+ def number_of_workers():
12
+ return (multiprocessing.cpu_count() * 2) + 1
13
+
14
+
15
+ default_workers = number_of_workers()
16
+
17
+
18
+ def register(app: typer.Typer) -> None:
19
+ @app.command("asgi")
20
+ def run_asgi(
21
+ app: str = typer.Argument(
22
+ ...,
23
+ help="ASGI application",
24
+ ),
25
+ identity_file: Path = typer.Argument(
26
+ ...,
27
+ help="Identity json file",
28
+ ),
29
+ workers: Annotated[
30
+ int,
31
+ typer.Option(
32
+ "--workers",
33
+ "-w",
34
+ help=f"Number of workers. Default: {default_workers}",
35
+ show_default=True,
36
+ ),
37
+ ] = default_workers,
38
+ reload: Annotated[
39
+ bool,
40
+ typer.Option(
41
+ "--reload",
42
+ "-r",
43
+ help="Enable auto-reload. Default: False",
44
+ show_default=True,
45
+ ),
46
+ ] = False,
47
+ ):
48
+ """Run an ASGI application exposing it through OpenZiti network."""
49
+ ziticorn.run(app, str(identity_file), workers=workers, reload=reload)
@@ -0,0 +1,54 @@
1
+ import multiprocessing
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from mrok.agent import sidecar
8
+
9
+
10
+ def number_of_workers():
11
+ return (multiprocessing.cpu_count() * 2) + 1
12
+
13
+
14
+ default_workers = number_of_workers()
15
+
16
+
17
+ def register(app: typer.Typer) -> None:
18
+ @app.command("sidecar")
19
+ def run_sidecar(
20
+ identity_file: Path = typer.Argument(
21
+ ...,
22
+ help="Identity json file",
23
+ ),
24
+ target: Path = typer.Argument(
25
+ ...,
26
+ help="Target service (host:port or path to unix domain socket)",
27
+ ),
28
+ workers: Annotated[
29
+ int,
30
+ typer.Option(
31
+ "--workers",
32
+ "-w",
33
+ help=f"Number of workers. Default: {default_workers}",
34
+ show_default=True,
35
+ ),
36
+ ] = default_workers,
37
+ reload: Annotated[
38
+ bool,
39
+ typer.Option(
40
+ "--reload",
41
+ "-r",
42
+ help="Enable auto-reload. Default: False",
43
+ show_default=True,
44
+ ),
45
+ ] = False,
46
+ ):
47
+ """Run a Sidecar Proxy to expose a web application through OpenZiti."""
48
+ if ":" in str(target):
49
+ host, port = str(target).split(":", 1)
50
+ target_addr = (host or "127.0.0.1", int(port))
51
+ else:
52
+ target_addr = str(target) # type: ignore
53
+
54
+ sidecar.run(str(identity_file), target_addr, workers=workers, reload=reload)
@@ -0,0 +1,7 @@
1
+ import typer
2
+
3
+ from mrok.cli.commands.controller import openapi, run
4
+
5
+ app = typer.Typer(help="mrok controller commands.")
6
+ run.register(app)
7
+ openapi.register(app)
@@ -0,0 +1,47 @@
1
+ import json
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ import yaml
8
+
9
+ from mrok.controller.openapi import generate_openapi_spec
10
+
11
+
12
+ class OutputFormat(str, Enum):
13
+ json = "json"
14
+ yaml = "yaml"
15
+
16
+
17
+ def register(app: typer.Typer) -> None:
18
+ @app.command("openapi")
19
+ def generate_spec(
20
+ ctx: typer.Context,
21
+ output: Annotated[
22
+ Path | None,
23
+ typer.Option(
24
+ "--output",
25
+ "-o",
26
+ help="Output file",
27
+ ),
28
+ ] = Path("mrok_openapi_spec.yml"),
29
+ output_format: Annotated[
30
+ OutputFormat,
31
+ typer.Option(
32
+ "--output-format",
33
+ "-f",
34
+ help="Output file format",
35
+ ),
36
+ ] = OutputFormat.yaml,
37
+ ):
38
+ """
39
+ Generates the mrok controller OpenAPI spec file.
40
+ """
41
+ from mrok.controller.app import app
42
+
43
+ dump_fn = json.dump if output_format == OutputFormat.json else yaml.dump
44
+ spec = generate_openapi_spec(app, ctx.obj)
45
+
46
+ with open(output, "w") as f: # type: ignore
47
+ dump_fn(spec, f, indent=2)
@@ -0,0 +1,87 @@
1
+ import multiprocessing
2
+ from collections.abc import Callable
3
+ from typing import Annotated, Any
4
+
5
+ import typer
6
+ from gunicorn.app.base import BaseApplication
7
+
8
+ from mrok.controller.app import app as asgi_app
9
+ from mrok.logging import get_logging_config
10
+
11
+
12
+ def number_of_workers():
13
+ return (multiprocessing.cpu_count() * 2) + 1
14
+
15
+
16
+ class StandaloneApplication(BaseApplication): # pragma: no cover
17
+ def __init__(self, application: Callable, options: dict[str, Any] | None = None):
18
+ self.options = options or {}
19
+ self.application = application
20
+ super().__init__()
21
+
22
+ def load_config(self):
23
+ config = {
24
+ key: value
25
+ for key, value in self.options.items()
26
+ if key in self.cfg.settings and value is not None
27
+ }
28
+ for key, value in config.items():
29
+ self.cfg.set(key.lower(), value)
30
+
31
+ def load(self):
32
+ return self.application
33
+
34
+
35
+ default_workers = number_of_workers()
36
+
37
+
38
+ def register(app: typer.Typer) -> None:
39
+ @app.command("run")
40
+ def run_controller(
41
+ ctx: typer.Context,
42
+ host: Annotated[
43
+ str,
44
+ typer.Option(
45
+ "--host",
46
+ "-h",
47
+ help="Host to bind to. Default: 127.0.0.1",
48
+ show_default=True,
49
+ ),
50
+ ] = "127.0.0.1",
51
+ port: Annotated[
52
+ int,
53
+ typer.Option(
54
+ "--port",
55
+ "-p",
56
+ help="Port to bind to. Default: 8000",
57
+ show_default=True,
58
+ ),
59
+ ] = 8000,
60
+ workers: Annotated[
61
+ int,
62
+ typer.Option(
63
+ "--workers",
64
+ "-w",
65
+ help=f"Number of workers. Default: {default_workers}",
66
+ show_default=True,
67
+ ),
68
+ ] = default_workers,
69
+ dev: Annotated[
70
+ bool,
71
+ typer.Option(
72
+ "--reload",
73
+ "-r",
74
+ help="Enable auto-reload. Default: False",
75
+ show_default=True,
76
+ ),
77
+ ] = False,
78
+ ):
79
+ """Run the mrok controller with Gunicorn and Uvicorn workers."""
80
+ options = {
81
+ "bind": f"{host}:{port}",
82
+ "workers": workers,
83
+ "worker_class": "uvicorn_worker.UvicornWorker",
84
+ "logconfig_dict": get_logging_config(ctx.obj),
85
+ "reload": dev,
86
+ }
87
+ StandaloneApplication(asgi_app, options).run()