Skip to content

Commit

Permalink
Merge pull request #22 from Otoru/users/vitoru/create-cli
Browse files Browse the repository at this point in the history
Users/vitoru/create cli
  • Loading branch information
Otoru authored Aug 22, 2024
2 parents 8a3710c + 547fc71 commit 43dab5a
Show file tree
Hide file tree
Showing 22 changed files with 783 additions and 154 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ venv.bak/
.dmypy.json
dmypy.json
.pyre/
test
.vscode
4 changes: 0 additions & 4 deletions .gitpod.Dockerfile

This file was deleted.

11 changes: 0 additions & 11 deletions .gitpod.yml

This file was deleted.

1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

Genesis is a python library designed to build applications (with asyncio) that work with freeswitch through ESL.

[![Gitpod badge](https://img.shields.io/badge/Gitpod-ready%20to%20code-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/Otoru/Genesis)
[![Tests badge](https://github.com/Otoru/Genesis/actions/workflows/tests.yml/badge.svg)](https://github.com/Otoru/Genesis/actions/workflows/tests.yml)
[![Build badge](https://github.com/Otoru/Genesis/actions/workflows/pypi.yml/badge.svg)](https://github.com/Otoru/Genesis/actions/workflows/pypi.yml)
[![License badge](https://img.shields.io/github/license/otoru/Genesis.svg)](https://github.com/Otoru/Genesis/blob/master/LICENSE.md)
Expand Down
5 changes: 1 addition & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ At first, it is necessary to make it very clear where our library is supported
| -------------- | ------------------ |
| 3.12.x | :white_check_mark: |
| 3.11.x | :white_check_mark: |
| 3.10.x | :white_check_mark: |
| 3.9.x | :warning: |
| 3.8.x | :warning: |
| 3.7.x | :warning: |
| 3.10.x | :warning: |

## Reporting a Vulnerability

Expand Down
8 changes: 5 additions & 3 deletions genesis/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import importlib.metadata

from .consumer import Consumer, filtrate
from .outbound import Outbound, Session
from .outbound import Outbound, Session, ESLEvent
from .inbound import Inbound

__all__ = ["Inbound", "Consumer", "filtrate", "Outbound", "Session"]
__version__ = "0.3.0"
__all__ = ["Inbound", "Consumer", "filtrate", "Outbound", "Session", "ESLEvent"]
__version__ = importlib.metadata.version("genesis")
3 changes: 3 additions & 0 deletions genesis/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from genesis.cli import app

app(prog_name="genesis")
44 changes: 44 additions & 0 deletions genesis/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
CLI module for Genesis.
------------------------
This module contains the CLI commands for Genesis.
"""

import importlib.metadata
from typing import Annotated, Union

import typer
from rich import print

from genesis.cli.consumer import consumer
from genesis.cli.outbound import outbound


app = typer.Typer(rich_markup_mode="rich")
app.add_typer(consumer, name="consumer", short_help="Run you ESL events consumer.")
app.add_typer(outbound, name="outbound", short_help="Run you outbound services.")


def version(show: bool) -> None:
"""Show the version and exit."""
if show:
version = importlib.metadata.version("genesis")
print(f"Genesis version: [green]{version}[/green]")
raise typer.Exit()


@app.callback()
def callback(
version: Annotated[
Union[bool, None],
typer.Option("--version", help="Show the version and exit.", callback=version),
] = None,
) -> None:
"""
Genesis - [blue]FreeSWITCH Event Socket protocol[/blue] implementation with [bold]asyncio[/bold].
Run yours freeswitch apps without any external dependencies.
ℹ️ Read more in the docs: [link]https://github.com/Otoru/Genesis/wiki[/link].
"""
211 changes: 211 additions & 0 deletions genesis/cli/consumer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
from typing import Annotated, Union
from pathlib import Path
import importlib
import logging
import asyncio

import typer
from rich import print
from rich.panel import Panel
from rich.padding import Padding

from genesis.cli import watcher
from genesis.logger import logger
from genesis.consumer import Consumer
from genesis.cli.exceptions import CLIExcpetion
from genesis.cli.utils import complete_log_levels
from genesis.cli.discover import get_import_string

consumer = typer.Typer(rich_markup_mode="rich")


async def _run_with_reload(app: Consumer, path: Path) -> None:
loop = asyncio.get_running_loop()
queue = asyncio.Queue()

async def consume(queue: asyncio.Queue) -> None:
await app.start()
async for event in watcher.EventIterator(queue):
if event:
logger.info(f"File changed: {event.src_path}")
await app.stop()
logger.info("App stopped, restarting...")
await app.start()

observer = watcher.factory(path, queue, loop)

observer.start()
asyncio.run_coroutine_threadsafe(queue.put(None), loop)

try:
await consume(queue)
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
observer.stop()


def _run(
path: Union[Path, None] = None,
host: str = "127.0.0.1",
port: int = 8021,
reload: bool = True,
app: Union[str, None] = None,
loglevel: str = "info",
password: str = "ClueCon",
) -> None:
try:
import_string = get_import_string(Consumer, path=path, app_name=app)

panel = Panel(
f"[dim]ESL dial address:[/dim] [link]esl://{host}:{port}[/link]",
title="Genesis Consumer app",
expand=False,
padding=(1, 2),
style="black on yellow" if reload else "green",
)

print(Padding(panel, 1))

module_str, attr_str = import_string.split(":")
module = importlib.import_module(module_str)
app: Consumer = getattr(module, attr_str)

app.host = host
app.port = port
app.password = password

logger.info(f"Setting log level to [bold]{loglevel.upper()}[/bold]")
levels = logging.getLevelNamesMapping()
logger.setLevel(levels.get(loglevel.upper(), logging.INFO))

if reload:
asyncio.run(_run_with_reload(app, path))
else:
asyncio.run(app.start())

except CLIExcpetion as e:
logger.error(e)
raise typer.Exit(1)


@consumer.command()
def dev(
path: Annotated[
Path,
typer.Argument(
help="A path to a Python file or package directory.", metavar="PATH"
),
],
*,
host: Annotated[
str,
typer.Option(help="The host to connect on.", envvar="ESL_HOST"),
] = "127.0.0.1",
port: Annotated[
int,
typer.Option(help="The port to connect on.", envvar="ESL_PORT"),
] = 8021,
password: Annotated[
str,
typer.Option(
help="The password to authenticate on host.", envvar="ESL_PASSWORD"
),
] = "ClueCon",
app: Annotated[
Union[str, None],
typer.Option(
help="Variable that contains the [bold]Consumer[/bold] app in the imported module or package.",
envvar="ESL_APP_NAME",
),
] = None,
loglevel: Annotated[
str,
typer.Option(
help="The log level to use.",
envvar="ESL_LOG_LEVEL",
show_default=True,
case_sensitive=False,
autocompletion=complete_log_levels,
),
] = "info",
):
"""
Run a [bold]Consumer[/bold] genesis app in [yellow]development[/yellow] mode. 🧪
It automatically detects the Python module or package that needs to be imported based on the file or directory path passed.
It detects the [bold]Consumer[/bold] app object to use based on the app name passed.
By default it looks in the module or package for an object named [blue]app[/blue].
Otherwise, it uses the first [bold]Consumer[/bold] app found in the imported module or package.
"""
_run(
path=path,
host=host,
port=port,
password=password,
app=app,
reload=True,
loglevel=loglevel,
)


@consumer.command()
def run(
path: Annotated[
Path,
typer.Argument(
help="A path to a Python file or package directory.", metavar="PATH"
),
],
*,
host: Annotated[
str,
typer.Option(help="The host to connect on.", envvar="ESL_HOST"),
] = "127.0.0.1",
port: Annotated[
int,
typer.Option(help="The port to connect on.", envvar="ESL_PORT"),
] = 8021,
password: Annotated[
str,
typer.Option(
help="The password to authenticate on host.", envvar="ESL_PASSWORD"
),
] = "ClueCon",
app: Annotated[
Union[str, None],
typer.Option(
help="Variable that contains the [bold]Consumer[/bold] app in the imported module or package.",
envvar="ESL_APP_NAME",
),
] = None,
loglevel: Annotated[
str,
typer.Option(
help="The log level to use.",
envvar="ESL_LOG_LEVEL",
show_default=True,
case_sensitive=False,
autocompletion=complete_log_levels,
),
] = "info",
):
"""
Run a [bold]Consumer[/bold] genesis app in [green]production[/green] mode. 🚀
It automatically detects the Python module or package that needs to be imported based on the file or directory path passed.
It detects the [bold]Consumer[/bold] app object to use based on the app name passed.
By default it looks in the module or package for an object named [blue]app[/blue].
Otherwise, it uses the first [bold]Consumer[/bold] app found in the imported module or package.
"""
_run(
path=path,
host=host,
port=port,
password=password,
app=app,
reload=False,
loglevel=loglevel,
)
Loading

0 comments on commit 43dab5a

Please sign in to comment.