Passed
Pull Request — master (#1309)
by
unknown
03:13
created

ocrd.cli.resmgr   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 136
dl 0
loc 189
rs 9.6
c 0
b 0
f 0

6 Functions

Rating   Name   Duplication   Size   Complexity  
A resmgr_cli() 0 6 1
C migrate() 0 34 10
A list_installed() 0 9 2
A list_available() 0 12 2
A print_resources() 0 6 3
F download() 0 81 17
1
"""
2
OCR-D CLI: management of processor resources
3
4
.. click:: ocrd.cli.resmgr:resmgr_cli
5
    :prog: ocrd resmgr
6
    :nested: full
7
"""
8
import sys
9
from pathlib import Path
10
from shutil import which
11
from yaml import safe_load, safe_dump
12
13
import requests
14
import click
15
16
from ocrd_utils import (
17
    directory_size,
18
    getLogger,
19
    get_moduledir,
20
    get_ocrd_tool_json,
21
    resource_filename,
22
    initLogging,
23
    RESOURCE_LOCATIONS,
24
    RESOURCE_TYPES
25
)
26
from ocrd.constants import RESOURCE_USER_LIST_COMMENT
27
28
from ..resource_manager import OcrdResourceManager
29
30
31
def print_resources(executable, reslist, resmgr):
32
    print(f"{executable}")
33
    for resdict in reslist:
34
        res_loc = resmgr.resource_dir_to_location(resdict['path']) if 'path' in resdict else ''
35
        print(f"- {resdict['name']} @ {res_loc} ({resdict['url']})\n  {resdict['description']}")
36
    print()
37
38
39
@click.group("resmgr")
40
def resmgr_cli():
41
    """
42
    Managing processor resources
43
    """
44
    initLogging()
45
46
47
@resmgr_cli.command('list-available')
48
@click.option('-D', '--no-dynamic', is_flag=True, default=False,
49
              help="Whether to skip looking into each processor's --dump-{json,module-dir} for module-level resources")
50
@click.option('-e', '--executable', metavar='EXEC', default='ocrd-*',
51
              help='Show only resources for executable beginning with EXEC', )
52
def list_available(executable, no_dynamic):
53
    """
54
    List available resources
55
    """
56
    resmgr = OcrdResourceManager()
57
    for executable, reslist in resmgr.list_available(executable=executable, dynamic=not no_dynamic):
58
        print_resources(executable, reslist, resmgr)
59
60
61
@resmgr_cli.command('list-installed')
62
@click.option('-e', '--executable', help='Show only resources for executable EXEC', metavar='EXEC')
63
def list_installed(executable=None):
64
    """
65
    List installed resources
66
    """
67
    resmgr = OcrdResourceManager()
68
    for executable, reslist in resmgr.list_installed(executable):
69
        print_resources(executable, reslist, resmgr)
70
71
72
@resmgr_cli.command('download')
73
@click.option('-n', '--any-url', default='', help='URL of unregistered resource to download/copy from')
74
@click.option('-D', '--no-dynamic', default=False, is_flag=True,
75
              help="Whether to skip looking into each processor's --dump-{json,module-dir} for module-level resources")
76
@click.option('-t', '--resource-type', type=click.Choice(RESOURCE_TYPES), default='file',
77
              help='Type of resource',)
78
@click.option('-P', '--path-in-archive', default='.', help='Path to extract in case of archive type')
79
@click.option('-a', '--allow-uninstalled', is_flag=True,
80
              help="Allow installing resources for uninstalled processors",)
81
@click.option('-o', '--overwrite', help='Overwrite existing resources', is_flag=True)
82
@click.option('-l', '--location', type=click.Choice(RESOURCE_LOCATIONS), default='data',
83
              help="Where to store resources - defaults to first location in processor's 'resource_locations' "
84
                   "list or finally 'data'")
85
@click.argument('executable', required=True)
86
@click.argument('name', required=False)
87
def download(any_url, no_dynamic, resource_type, path_in_archive, allow_uninstalled, overwrite, location, executable,
88
             name):
89
    """
90
    Download resource NAME for processor EXECUTABLE.
91
92
    NAME is the name of the resource made available by downloading or copying.
93
94
    If NAME is '*' (asterisk), then download all known registered resources for this processor.
95
96
    If ``--any-url=URL`` or ``-n URL`` is given, then URL is accepted regardless of registered resources for ``NAME``.
97
    (This can be used for unknown resources or for replacing registered resources.)
98
99
    If ``--resource-type`` is set to `archive`, then that archive gets unpacked after download,
100
    and its ``--path-in-archive`` will subsequently be renamed to NAME.
101
    """
102
    log = getLogger('ocrd.cli.resmgr')
103
    resmgr = OcrdResourceManager()
104
    if executable != '*' and not name:
105
        log.error(f"Unless EXECUTABLE ('{executable}') is the '*' wildcard, NAME is required")
106
        sys.exit(1)
107
    elif executable == '*':
108
        executable = None
109
    if name == '*':
110
        name = None
111
    if executable and not which(executable):
112
        if not allow_uninstalled:
113
            log.error(f"Executable '{executable}' is not installed. "
114
                      f"To download resources anyway, use the -a/--allow-uninstalled flag")
115
            sys.exit(1)
116
        else:
117
            log.info(f"Executable '{executable}' is not installed, but downloading resources anyway")
118
    reslist = resmgr.list_available(executable=executable, dynamic=not no_dynamic, name=name)
119
    if not any(r[1] for r in reslist):
120
        log.info(f"No resources {name} found in registry for executable {executable}")
121
        if executable and name:
122
            reslist = [(executable, [{
123
                'url': any_url or '???',
124
                'name': name,
125
                'type': resource_type,
126
                'path_in_archive': path_in_archive}]
127
            )]
128
    for this_executable, this_reslist in reslist:
129
        resource_locations = get_ocrd_tool_json(this_executable)['resource_locations']
130
        if not location:
131
            location = resource_locations[0]
132
        elif location not in resource_locations:
133
            log.warning(f"The selected --location {location} is not in the {this_executable}'s resource search path, "
134
                        f"refusing to install to invalid location. Instead installing to: {resource_locations[0]}")
135
        res_dest_dir = resmgr.build_resource_dest_dir(location=location, executable=this_executable)
136
        for res_dict in this_reslist:
137
            try:
138
                fpath = resmgr.handle_resource(
139
                    res_dict=res_dict,
140
                    executable=this_executable,
141
                    dest_dir=res_dest_dir,
142
                    any_url=any_url,
143
                    overwrite=overwrite,
144
                    resource_type=resource_type,
145
                    path_in_archive=path_in_archive
146
                )
147
                if not fpath:
148
                    continue
149
            except FileExistsError as exc:
150
                log.info(str(exc))
151
            usage = res_dict.get('parameter_usage', 'as-is')
152
            log.info(f"Use in parameters as '{resmgr.parameter_usage(res_dict['name'], usage)}'")
153
154
155
@resmgr_cli.command('migrate')
156
@click.argument('migration', type=click.Choice(['2.37.0']))
157
def migrate(migration):
158
    """
159
    Update the configuration after updating core to MIGRATION
160
    """
161
    resmgr = OcrdResourceManager(skip_init=True)
162
    log = getLogger('ocrd.resmgr.migrate')
163
    if not resmgr.user_list.exists():
164
        log.info(f'No configuration file found at {resmgr.user_list}, nothing to do')
165
    if migration == '2.37.0':
166
        backup_file = resmgr.user_list.with_suffix(f'.yml.before-{migration}')
167
        yaml_in_str = resmgr.user_list.read_text()
168
        log.info(f'Backing {resmgr.user_list} to {backup_file}')
169
        backup_file.write_text(yaml_in_str)
170
        log.info(f'Applying migration {migration} to {resmgr.user_list}')
171
        yaml_in = safe_load(yaml_in_str)
172
        yaml_out = {}
173
        for executable, reslist_in in yaml_in.items():
174
            yaml_out[executable] = []
175
            for resdict_in in reslist_in:
176
                resdict_out = {}
177
                for k_in, v_in in resdict_in.items():
178
                    k_out, v_out = k_in, v_in
179
                    if k_in == 'type' and v_in in ['github-dir', 'tarball']:
180
                        if v_in == 'github-dir':
181
                            v_out = 'directory'
182
                        elif v_in == 'tarball':
183
                            v_out = 'directory'
184
                    resdict_out[k_out] = v_out
185
                yaml_out[executable].append(resdict_out)
186
        resmgr.user_list.write_text(
187
            RESOURCE_USER_LIST_COMMENT + '\n# migrated with ocrd resmgr migrate {migration}\n' + safe_dump(yaml_out))
188
        log.info(f'Applied migration {migration} to {resmgr.user_list}')
189