Test Failed
Push — master ( ef441d...0acd10 )
by Heiko 'riot'
04:41 queued 10s
created

isomer.tool.dev.events()   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 9
nop 0
dl 0
loc 15
rs 9.95
c 0
b 0
f 0
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# Isomer - The distributed application framework
5
# ==============================================
6
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU Affero General Public License for more details.
17
#
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22
"""
23
24
Module: Dev
25
===========
26
27
A collection of developer support tools.
28
29
"""
30
31
import time
32
import pkg_resources
33
import click
34
import os
35
import shutil
36
37
from pprint import pprint
38
from click_didyoumean import DYMGroup
39
from collections import OrderedDict, namedtuple
40
from operator import attrgetter
41
42
from isomer.tool import log, ask, debug, verbose, warn
43
from isomer.misc.std import std_table
44
from isomer.tool.templates import write_template_file
45
from isomer.error import abort
46
from isomer.ui.store.inventory import populate_store, get_inventory
47
48
paths = [
49
    "isomer",
50
    "isomer/{plugin_name}",
51
    "isomer-frontend/{plugin_name}/scripts/controllers",
52
    "isomer-frontend/{plugin_name}/views",
53
]
54
55
templates = {
56
    "setup_file": ("setup.py.template", "setup.py"),
57
    "package_file": ("package.json.template", "package.json"),
58
    "component": ("component.py.template", "isomer/{plugin_name}/{plugin_name}.py"),
59
    "module_init": ("init.py.template", "isomer/__init__.py"),
60
    "package_init": ("init.py.template", "isomer/{plugin_name}/__init__.py"),
61
    "schemata": ("schemata.py.template", "isomer/{plugin_name}/schemata.py"),
62
    "controller": (
63
        "controller.js.template",
64
        "isomer-frontend/{plugin_name}/scripts/controllers/{" "plugin_name}.js",
65
    ),
66
    "view": (
67
        "view.html.template",
68
        "isomer-frontend/{plugin_name}/views/{plugin_name}.html",
69
    ),
70
}
71
72
questions = OrderedDict(
73
    {
74
        "plugin_name": "plugin",
75
        "author_name": u"author",
76
        "author_email": u"[email protected]",
77
        "description": u"Description",
78
        "long_description": u"Very long description, use \\n to get multilines.",
79
        "version": "0.0.1",
80
        "github_url": "isomeric/example",
81
        "license": "GPLv3",
82
        "keywords": "Isomer example plugin",
83
    }
84
)
85
86
info_header = """The manage command guides you through setting up a new isomer
87
package.
88
It provides basic setup. If you need dependencies or have other special
89
needs, edit the resulting files by hand.
90
91
You can press Ctrl-C any time to cancel this process.
92
93
See iso create-module --help for more details.
94
"""
95
96
97
def _augment_info(info):
98
    """Fill out the template information"""
99
100
    info["description_header"] = "=" * len(info["description"])
101
    info["component_name"] = info["plugin_name"].capitalize()
102
    info["year"] = time.localtime().tm_year
103
    info["license_longtext"] = ""
104
105
    info["keyword_list"] = u""
106
    for keyword in info["keywords"].split(" "):
107
        print(keyword)
108
        info["keyword_list"] += u"'" + str(keyword) + u"', "
109
    print(info["keyword_list"])
110
    if len(info["keyword_list"]) > 0:
111
        # strip last comma
112
        info["keyword_list"] = info["keyword_list"][:-2]
113
114
    return info
115
116
117
def _construct_module(info, target):
118
    """Build a module from templates and user supplied information"""
119
120
    for path in paths:
121
        real_path = os.path.abspath(os.path.join(target, path.format(**info)))
122
        log("Making directory '%s'" % real_path)
123
        os.makedirs(real_path)
124
125
    # pprint(info)
126
    for item in templates.values():
127
        source = os.path.join("dev/templates", item[0])
128
        filename = os.path.abspath(os.path.join(target, item[1].format(**info)))
129
        log("Creating file from template '%s'" % filename, emitter="MANAGE")
130
        write_template_file(source, filename, info)
131
132
133
def _ask_questionnaire():
134
    """Asks questions to fill out a Isomer plugin template"""
135
136
    answers = {}
137
    print(info_header)
138
    pprint(questions.items())
139
140
    for question, default in questions.items():
141
        response = ask(question, default, str(type(default)), show_hint=True)
142
        if type(default) == bytes and type(response) != str:
143
            response = response.decode("utf-8")
144
        answers[question] = response
145
146
    return answers
147
148
149
@click.group(
150
    cls=DYMGroup,
151
    short_help="Developer support operations"
152
)
153
def dev():
154
    """[GROUP] Developer support operations"""
155
156
157
@dev.command(short_help="generate async-api definition")
158
@click.option(
159
    "--filename",
160
    "-f",
161
    help="Filename to write output",
162
    default="",
163
    type=click.types.Path(exists=False),
164
)
165
@click.pass_context
166
def generate_api(ctx, filename):
167
    """Generate and output isomer async api definition"""
168
169
    _ = get_components("")
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable get_components does not seem to be defined.
Loading history...
170
171
    api = generate_asyncapi()
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable generate_asyncapi does not seem to be defined.
Loading history...
172
173
    if filename == "":
174
        print(api)
175
    else:
176
        with open(filename, "w") as f:
177
            dump(api, f, indent=4)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable dump does not seem to be defined.
Loading history...
178
179
180
@dev.command(short_help="list known events")
181
def events():
182
    """List all known authorized and anonymous events"""
183
184
    from isomer.events.system import (
185
        get_anonymous_events,
186
        get_user_events,
187
        populate_user_events,
188
    )
189
190
    populate_user_events()
191
192
    event_list = {**get_user_events(), **get_anonymous_events()}
193
194
    log("Events:\n", event_list, pretty=True)
195
196
197
@dev.command(short_help="export schemata")
198
@click.option("--output-path", "-o", help="output path", type=click.types.Path())
199
@click.option(
200
    "--format",
201
    "-f",
202
    help="Specify format",
203
    default="jsonschema",
204
    type=click.types.Choice(["jsonschema", "typescript"]),
205
)
206
@click.option(
207
    "--no-entity-mode",
208
    "-n",
209
    help="Do not use ngrx-auto-entity mode (removes key and does not uses classes)",
210
    default=False,
211
    is_flag=True,
212
)
213
@click.option(
214
    "--include-meta",
215
    "-m",
216
    is_flag=True,
217
    default=False,
218
    help="Include meta properties like permissions",
219
)
220
@click.argument("schemata", nargs=-1)
221
@click.pass_context
222
def export_schemata(ctx, output_path, format, no_entity_mode, include_meta, schemata):
223
    """Utility function for exporting known schemata to various formats
224
225
    Exporting to typescript requires the "json-schema-to-typescript" tool.
226
    You can install it via:
227
228
        npm -g install json-schema-to-typescript
229
    """
230
231
    # TODO: This one is rather ugly to edit, should the need arise..
232
    banner = (
233
        "/* tslint:disable */\n/**\n* This file was automatically generated "
234
        "by Isomer's command line tool using:\n"
235
        " * 'iso dev export-schemata -f typescript' "
236
        "- using json-schema-to-typescript.\n"
237
        " * DO NOT MODIFY IT BY HAND. Instead, modify the source isomer object "
238
        "file,\n* and run the iso tool schemata exporter again, to regenerate this file.\n*/"
239
    )
240
241
    # TODO: This should be employable to automatically generate
242
    #  typescript definitions inside a modules frontend part as part
243
    #  of the development cycle.
244
245
    from isomer import database, schemastore
246
247
    database.initialize(ctx.obj["dbhost"], ctx.obj["dbname"], ignore_fail=True)
248
249
    if len(schemata) == 0:
250
        schemata = database.objectmodels.keys()
251
252
    if output_path is not None:
253
        stdout = False
254
        if not os.path.exists(output_path):
255
            abort("Output Path doesn't exist.")
256
    else:
257
        stdout = True
258
259
    for item in schemata:
260
        if item not in schemastore.schemastore:
261
            log("Schema not registered:", item, lvl=warn)
262
            continue
263
264
        schema = schemastore.schemastore[item]["schema"]
265
266
        if not include_meta:
267
            if "perms" in schema["properties"]:
268
                del schema["properties"]["perms"]
269
            if "roles_create" in schema:
270
                del schema["roles_create"]
271
            if "required" in schema:
272
                del schema["required"]
273
274
        if format == "jsonschema":
275
            log("Generating json schema of", item)
276
277
            if stdout:
278
                print(json.dumps(schema, indent=4))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable json does not seem to be defined.
Loading history...
279
            else:
280
                with open(os.path.join(output_path, item + ".json"), "w") as f:
281
                    json.dump(schema, f, indent=4)
282
283
        elif format == "typescript":
284
            log("Generating typescript annotations of", item)
285
286
            success, result = run_process(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable run_process does not seem to be defined.
Loading history...
287
                output_path,
288
                [
289
                    "json2ts",
290
                    "--bannerComment",
291
                    banner,
292
                ],
293
                stdin=json.dumps(schema).encode("utf-8"),
294
            )
295
            typescript = result.output.decode("utf-8")
296
297
            if no_entity_mode is False:
298
                typescript = (
299
                        "import { Entity, Key } from '@briebug/ngrx-auto-entity';\n"
300
                        + typescript
301
                )
302
                typescript = typescript.replace("uuid", "@Key uuid")
303
                typescript = typescript.replace(
304
                    "export interface",
305
                    "@Entity({modelName: '%s'})\n" "export class" % item,
306
                )
307
308
            if stdout:
309
                print(typescript)
310
            else:
311
                with open(os.path.join(output_path, item + ".ts"), "w") as f:
312
                    f.write(typescript)
313
314
    finish(ctx)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable finish does not seem to be defined.
Loading history...
315
316
317
@dev.command(short_help="create starterkit module")
318
@click.option(
319
    "--clear-target",
320
    "--clear",
321
    help="Clears already existing target",
322
    default=False,
323
    is_flag=True,
324
)
325
@click.option(
326
    "--target",
327
    help="Create module in the given folder (uses ./ if omitted)",
328
    default=".",
329
    metavar="<folder>",
330
)
331
def create_module(clear_target, target):
332
    """Creates a new template Isomer plugin module"""
333
334
    if os.path.exists(target):
335
        if clear_target:
336
            shutil.rmtree(target)
337
        else:
338
            log("Target exists! Use --clear to delete it first.", emitter="MANAGE")
339
            abort(2)
340
341
    done = False
342
    info = None
343
344
    while not done:
345
        info = _ask_questionnaire()
346
        pprint(info)
347
        done = ask("Is the above correct", default="y", data_type="bool")
348
349
    augmented_info = _augment_info(info)
350
351
    log("Constructing module %(plugin_name)s" % info)
352
    _construct_module(augmented_info, target)
353
354
355
@dev.command(short_help="List setuptools installed component information")
356
@click.option(
357
    "--base",
358
    "-b",
359
    is_flag=True,
360
    default=False,
361
    help="Also list isomer-base (integrated) modules",
362
)
363
@click.option(
364
    "--sails",
365
    "-s",
366
    is_flag=True,
367
    default=False,
368
    help="Also list isomer-sails (integrated) modules",
369
)
370
@click.option(
371
    "--schemata",
372
    is_flag=True,
373
    default=False,
374
    help="Also list registered schemata"
375
)
376
@click.option(
377
    "--management",
378
    is_flag=True,
379
    default=False,
380
    help="Also list registered management commands"
381
)
382
@click.option(
383
    "--provisions",
384
    is_flag=True,
385
    default=False,
386
    help="Also list registered provisions"
387
)
388
@click.option(
389
    "--frontend-only",
390
    "-f",
391
    is_flag=True,
392
    default=False,
393
    help="Only list modules with a frontend",
394
)
395
@click.option(
396
    "--frontend-list",
397
    "-l",
398
    is_flag=True,
399
    default=False,
400
    help="List files in frontend per module",
401
)
402
@click.option(
403
    "--directory", "-d", is_flag=True, default=False, help="Show directory of module"
404
)
405
@click.option(
406
    "--sort-key",
407
    "-k",
408
    default="package",
409
    type=click.Choice(
410
        ["name", "package", "classname", "location", "frontend", "group"]),
411
)
412
@click.option(
413
    "--all",
414
    "-a",
415
    is_flag=True,
416
    default=False,
417
    help="List all registered entrypoints"
418
)
419
@click.option(
420
    "--filter",
421
    "-f",
422
    default="",
423
    type=str,
424
    help="Filter table by string"
425
)
426
@click.option("--long", is_flag=True, default=False, help="Show full table")
427
def entrypoints(base, sails, schemata, management, provisions, frontend_only,
428
                frontend_list, directory, sort_key, all, filter, long):
429
    """Display list of entrypoints and diagnose module loading problems."""
430
431
    log("Showing entrypoints:")
432
433
    full_component = namedtuple(
434
        "Component", ["name", "package", "classname", "location", "frontend", "group"]
435
    )
436
    component = namedtuple("Component", ["name", "package", "frontend", "group"])
437
    results = []
438
439
    from pkg_resources import iter_entry_points
440
441
    entry_points = {
442
        "components": iter_entry_points(group="isomer.components", name=None)
443
    }
444
445
    if all:
446
        sails = base = schemata = management = provisions = True
447
448
    if sails:
449
        entry_points["sails"] = iter_entry_points(group="isomer.sails", name=None)
450
    if base:
451
        entry_points["base"] = iter_entry_points(group="isomer.base", name=None)
452
    if schemata:
453
        entry_points["schemata"] = iter_entry_points(group="isomer.schemata", name=None)
454
    if management:
455
        entry_points["management"] = iter_entry_points(group="isomer.management",
456
                                                       name=None)
457
    if provisions:
458
        entry_points["provisions"] = iter_entry_points(group="isomer.provisions",
459
                                                       name=None)
460
461
    log("Entrypoints:", entry_points, pretty=True, lvl=verbose)
462
    try:
463
        for key, iterator in entry_points.items():
464
            for entry_point in iterator:
465
                log("Entrypoint Group:", key, entry_point, pretty=True, lvl=debug)
466
467
                try:
468
                    name = entry_point.name
469
                    package = entry_point.dist.project_name
470
                    log("Package:", package, pretty=True, lvl=debug)
471
                    location = entry_point.dist.location
472
                    try:
473
                        loaded = entry_point.load()
474
                    except pkg_resources.DistributionNotFound as e:
475
                        log(
476
                            "Required distribution not found:",
477
                            e,
478
                            pretty=True,
479
                            exc=True,
480
                            lvl=warn,
481
                        )
482
                        continue
483
484
                    log(
485
                        "Entry point: ",
486
                        entry_point,
487
                        name,
488
                        entry_point.resolve(),
489
                        location,
490
                        lvl=debug,
491
                    )
492
493
                    log("Loaded: ", loaded, lvl=debug)
494
495
                    try:
496
                        pkg = pkg_resources.Requirement.parse(package)
497
                        frontend = pkg_resources.resource_listdir(pkg, "frontend")
498
                        log("Frontend resources found:", frontend, lvl=debug)
499
                    except Exception as e:
500
                        log(
501
                            "Exception during frontend resource lookup:",
502
                            e,
503
                            lvl=debug,
504
                            exc=True,
505
                        )
506
                        frontend = None
507
508
                    if frontend not in (None, []):
509
                        log("Contains frontend parts", lvl=debug)
510
                        if not frontend_list:
511
                            frontend = "[X]"
512
                    else:
513
                        frontend = "[ ]"
514
515
                    if filter != "":
516
                        if filter not in name and filter not in package and filter not in location:
517
                            continue
518
519
                    if long:
520
                        if key in ("schemata", "provisions"):
521
                            classname = "[SCHEMA]"
522
                        else:
523
                            classname = repr(loaded).lstrip("<class '").rstrip("'>")
524
525
                        result = full_component(
526
                            frontend=frontend,
527
                            name=name,
528
                            package=package,
529
                            classname=classname,
530
                            location=location if directory else "use -d",
531
                            group=key,
532
                        )
533
                    else:
534
                        result = component(
535
                            frontend=frontend, name=name, package=package, group=key
536
                        )
537
538
                    if not frontend_only or frontend:
539
                        results.append(result)
540
                except ImportError as e:
541
                    log("Exception while iterating entrypoints:", e, type(e), lvl=warn,
542
                        exc=True)
543
    except ModuleNotFoundError as e:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ModuleNotFoundError does not seem to be defined.
Loading history...
544
        log("Module could not be loaded:", e, exc=True)
545
546
    results = sorted(results, key=attrgetter(sort_key))
547
548
    table = std_table(results)
549
    log("\n%s" % table)
550
551
552
@dev.command(short_help="Grab and show software store inventory")
553
@click.option(
554
    "--source",
555
    help="Specify a different source than official Isomer",
556
    default="https://store.isomer.eu/simple",
557
    metavar="<url>",
558
)
559
def store_inventory(source):
560
    """List available pacakages"""
561
562
    store = populate_store(source)
563
564
    log(store, pretty=True)
565
566
567
@dev.command(short_help="Show local inventory")
568
@click.pass_context
569
def local_inventory(ctx):
570
    """List installed pacakages"""
571
572
    inventory = get_inventory(ctx)
573
574
    log(inventory, pretty=True)
575