Skip to content

Commit 2692940

Browse files
Add Typer CLI #23
1 parent bab30cd commit 2692940

File tree

11 files changed

+245
-11
lines changed

11 files changed

+245
-11
lines changed

minos/api_gateway/rest/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from .coordinator import (
1111
MicroserviceCallCoordinator,
1212
)
13+
from .launchers import (
14+
EntrypointLauncher,
15+
)
1316
from .service import (
1417
ApiGatewayRestService,
1518
)

minos/api_gateway/rest/cli.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Copyright (C) 2021 Clariteia SL
3+
4+
This file is part of minos framework.
5+
6+
Minos framework can not be copied and/or distributed without the express permission of Clariteia SL.
7+
"""
8+
from pathlib import (
9+
Path,
10+
)
11+
from typing import (
12+
Optional,
13+
)
14+
15+
import typer
16+
17+
from minos.api_gateway.common import (
18+
MinosConfig,
19+
)
20+
from minos.api_gateway.rest.launchers import (
21+
EntrypointLauncher,
22+
)
23+
from minos.api_gateway.rest.service import (
24+
ApiGatewayRestService,
25+
)
26+
27+
app = typer.Typer()
28+
29+
30+
@app.command("start")
31+
def start(
32+
file_path: Optional[Path] = typer.Argument(
33+
"config.yml", help="API Gateway configuration file.", envvar="MINOS_API_GATEWAY_CONFIG_FILE_PATH"
34+
)
35+
): # pragma: no cover
36+
"""Start Api Gateway services."""
37+
38+
try:
39+
config = MinosConfig(file_path)
40+
except Exception as exc:
41+
typer.echo(f"Error loading config: {exc!r}")
42+
raise typer.Exit(code=1)
43+
44+
services = (ApiGatewayRestService(config=config),)
45+
try:
46+
EntrypointLauncher(config=config, services=services).launch()
47+
except Exception as exc:
48+
typer.echo(f"Error launching Api Gateway: {exc!r}")
49+
raise typer.Exit(code=1)
50+
51+
typer.echo("Api Gateway is up and running!\n")
52+
53+
54+
@app.command("status")
55+
def status():
56+
"""Get the Api Gateway status."""
57+
raise NotImplementedError
58+
59+
60+
@app.command("stop")
61+
def stop():
62+
"""Stop the Api Gateway."""
63+
raise NotImplementedError
64+
65+
66+
def main(): # pragma: no cover
67+
"""CLI's main function."""
68+
app()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Copyright (C) 2021 Clariteia SL
3+
4+
This file is part of minos framework.
5+
6+
Minos framework can not be copied and/or distributed without the express permission of Clariteia SL.
7+
"""
8+
9+
import logging
10+
from typing import (
11+
NoReturn,
12+
)
13+
14+
from aiomisc import (
15+
entrypoint,
16+
)
17+
from aiomisc.entrypoint import (
18+
Entrypoint,
19+
)
20+
from cached_property import (
21+
cached_property,
22+
)
23+
24+
from minos.api_gateway.common import (
25+
MinosConfig,
26+
)
27+
28+
logger = logging.getLogger(__name__)
29+
30+
31+
class EntrypointLauncher:
32+
"""EntryPoint Launcher class."""
33+
34+
def __init__(self, config: MinosConfig, services: tuple, *args, **kwargs):
35+
self.config = config
36+
self.services = services
37+
38+
def launch(self) -> NoReturn:
39+
"""Launch a new execution and keeps running forever..
40+
41+
:return: This method does not return anything.
42+
"""
43+
logger.info("Starting API Gateway...")
44+
with self.entrypoint as loop:
45+
logger.info("API Gateway is up and running!")
46+
loop.run_forever()
47+
48+
@cached_property
49+
def entrypoint(self) -> Entrypoint:
50+
"""Entrypoint instance.
51+
52+
:return: An ``Entrypoint`` instance.
53+
"""
54+
55+
return entrypoint(*self.services) # pragma: no cover

poetry.lock

Lines changed: 39 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ include = [
3232
[tool.poetry.dependencies]
3333
python = "^3.9"
3434
minos-apigateway-common = "^0.0.4"
35+
typer = "^0.3.2"
36+
cached-property = "^1.5.2"
3537

3638
[tool.poetry.dev-dependencies]
3739
black = "^19.10b"
@@ -49,3 +51,6 @@ m2r2 = "^0.2.7"
4951
[build-system]
5052
requires = ["poetry-core>=1.0.0"]
5153
build-backend = "poetry.core.masonry.api"
54+
55+
[tool.poetry.scripts]
56+
api_gateway = "minos.api_gateway.rest.cli:main"

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ include_trailing_comma = True
4545
force_grid_wrap = 1
4646
use_parentheses = True
4747
line_length = 120
48+
49+
[options.entry_points]
50+
console_scripts =
51+
api_gateway = minos.api_gateway.rest.cli:main
File renamed without changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import unittest
2+
from unittest.mock import (
3+
PropertyMock,
4+
patch,
5+
)
6+
7+
from typer.testing import (
8+
CliRunner,
9+
)
10+
11+
from minos.api_gateway.common import (
12+
MinosConfig,
13+
)
14+
from minos.api_gateway.rest.cli import (
15+
app,
16+
)
17+
from minos.api_gateway.rest.launchers import (
18+
EntrypointLauncher,
19+
)
20+
from tests.utils import (
21+
BASE_PATH,
22+
FakeEntrypoint,
23+
)
24+
25+
runner = CliRunner()
26+
27+
28+
class Foo:
29+
def __init__(self, **kwargs):
30+
self.kwargs = kwargs
31+
32+
33+
class TestCli(unittest.TestCase):
34+
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
35+
36+
def setUp(self):
37+
self.config = MinosConfig(self.CONFIG_FILE_PATH)
38+
self.services = ["a", "b", Foo]
39+
self.launcher = EntrypointLauncher(config=self.config, services=self.services)
40+
41+
def test_app_ko(self):
42+
path = f"{BASE_PATH}/non_existing_config.yml"
43+
result = runner.invoke(app, ["start", path])
44+
self.assertEqual(result.exit_code, 1)
45+
self.assertTrue("Error loading config" in result.stdout)
46+
47+
def test_launch(self):
48+
entrypoint = FakeEntrypoint()
49+
with patch("minos.api_gateway.rest.launchers.EntrypointLauncher.entrypoint", new_callable=PropertyMock) as mock:
50+
mock.return_value = entrypoint
51+
self.launcher.launch()
52+
self.assertEqual(1, entrypoint.call_count)

tests/test_api_gateway/test_rest/test_coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
class TestRestCoordinator(AioHTTPTestCase):
24-
CONFIG_FILE_PATH = BASE_PATH / "test_config.yml"
24+
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
2525

2626
async def get_application(self):
2727
"""

tests/test_api_gateway/test_rest/test_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919

2020
class TestRestService(AioHTTPTestCase):
21-
CONFIG_FILE_PATH = BASE_PATH / "test_config.yml"
21+
CONFIG_FILE_PATH = BASE_PATH / "config.yml"
2222

2323
async def get_application(self):
2424
"""

0 commit comments

Comments
 (0)