Passed
Push — main ( ebde94...acc836 )
by Eran
01:54
created

graphinate.cli   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 124
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 82
dl 0
loc 124
rs 10
c 0
b 0
f 0

5 Functions

Rating   Name   Duplication   Size   Complexity  
C import_from_string() 0 34 9
A server() 0 16 1
A save() 0 19 5
A _get_kwargs() 0 2 1
A cli() 0 4 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A GraphModelType.convert() 0 11 3
1
import importlib
2
import json
3
from pathlib import Path
4
from types import ModuleType
5
from typing import Any
6
7
import click
8
from strawberry import Schema
9
10
from . import GraphModel, builders, graphql
11
from .renderers.graphql import DEFAULT_PORT
12
13
14
def _get_kwargs(ctx: click.Context) -> dict:
15
    return dict([item.strip('--').split('=') for item in ctx.args if item.startswith("--")])  # NOSONAR
16
17
18
def import_from_string(import_str: str) -> GraphModel:
19
    """Import an object from a string reference {module-name}:{variable-name}
20
    For example, if `model: GraphModel = GraphModel(...)` is a variable defined in an app.py file,
21
     then the reference would be app:model.
22
    """
23
24
    if not isinstance(import_str, str):
25
        raise ImportFromStringError(f"{import_str} is not a string")
26
27
    module_name, _, attrs_names_str = import_str.partition(':')
28
    if not module_name or not attrs_names_str:
29
        message = f"Import string '{import_str}' must be in format '<module>:<attribute>'."
30
        raise ImportFromStringError(message)
31
32
    try:
33
        module: ModuleType = importlib.import_module(module_name)
34
    except ModuleNotFoundError as exc:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ModuleNotFoundError does not seem to be defined.
Loading history...
35
        if exc.name != module_name:
36
            raise exc from None
37
        message = f"Could not import module '{module_name}'."
38
        raise ImportFromStringError(message) from exc
39
40
    instance_candidate: ModuleType | GraphModel = module
41
    try:
42
        for attr_name in attrs_names_str.split('.'):
43
            instance_candidate = getattr(instance_candidate, attr_name)
44
    except AttributeError as e:
45
        message = f"Attribute '{attrs_names_str}' not found in import string reference '{import_str}'."
46
        raise ImportFromStringError(message) from e
47
48
    if isinstance(instance_candidate, GraphModel):
49
        return instance_candidate
50
    else:
51
        raise ImportFromStringError(f"GraphModel instance cannot be determined from reference '{import_str}'")
52
53
54
class ImportFromStringError(Exception):
55
    pass
56
57
58
class GraphModelType(click.ParamType):
59
    name = "MODEL"
60
61
    def convert(self,
62
                value: Any,
63
                param: click.Parameter | None,
64
                ctx: click.Context) -> GraphModel:  # type: ignore[override]
65
        if isinstance(value, GraphModel):
66
            return value
67
68
        try:
69
            return import_from_string(value)
70
        except Exception as e:
71
            self.fail(str(e))
72
73
74
model_option = click.option('-m', '--model',
75
                            type=GraphModelType(),
76
                            help="A GraphModel instance reference {module-name}:{GraphModel-instance-variable-name}"
77
                                 " For example given a var `model=GraphModel()` defined in app.py file, then the"
78
                                 " reference would be app:model")
79
80
81
@click.group()
82
@click.pass_context
83
def cli(ctx: click.Context) -> None:
84
    ctx.ensure_object(dict)
85
86
87
@cli.command()
88
@model_option
89
@click.pass_context
90
def save(ctx: click.Context, model: GraphModel) -> None:
91
    file_path = Path(f"{model.name}.d3_graph.json")
92
93
    if file_path.is_absolute():
94
        raise click.ClickException("Please provide a relative file path for saving the graph.")
95
96
    if file_path.parent != Path('.'):
97
        raise click.ClickException("Saving to subdirectories is not supported. Please provide a file name only.")
98
99
    if file_path.exists():
100
        click.confirm(f"The file '{file_path}' already exists. Do you want to overwrite it?", abort=True)
101
102
    kwargs = _get_kwargs(ctx)
103
    with open(file_path, mode='w') as fp:
104
        graph = builders.D3Builder(model, **kwargs).build()
105
        json.dump(graph, fp=fp, default=str, **kwargs)
106
107
108
@cli.command()
109
@model_option
110
@click.option('-p', '--port', type=int, default=DEFAULT_PORT, help='Port number.')
111
@click.option('-b', '--browse', type=bool, default=False, help='Open server address in browser.')
112
@click.pass_context
113
def server(ctx: click.Context, model: GraphModel, port: int, browse: bool) -> None:
114
    message = """
115
     ██████╗ ██████╗  █████╗ ██████╗ ██╗  ██╗██╗███╗   ██╗ █████╗ ████████╗███████╗
116
    ██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║  ██║██║████╗  ██║██╔══██╗╚══██╔══╝██╔════╝
117
    ██║  ███╗██████╔╝███████║██████╔╝███████║██║██╔██╗ ██║███████║   ██║   █████╗
118
    ██║   ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║██║██║╚██╗██║██╔══██║   ██║   ██╔══╝
119
    ╚██████╔╝██║  ██║██║  ██║██║     ██║  ██║██║██║ ╚████║██║  ██║   ██║   ███████╗
120
     ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝   ╚═╝   ╚══════╝"""
121
    click.echo(message)
122
    schema: Schema = builders.GraphQLBuilder(model).build()
123
    graphql.server(schema, port=port, browse=browse, **_get_kwargs(ctx))
124