mapchete-hub-cli 2025.8.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapchete_hub_cli/__init__.py +22 -0
- mapchete_hub_cli/cli/__init__.py +4 -0
- mapchete_hub_cli/cli/cancel.py +55 -0
- mapchete_hub_cli/cli/clean.py +79 -0
- mapchete_hub_cli/cli/execute.py +186 -0
- mapchete_hub_cli/cli/job.py +146 -0
- mapchete_hub_cli/cli/jobs.py +80 -0
- mapchete_hub_cli/cli/main.py +51 -0
- mapchete_hub_cli/cli/options.py +285 -0
- mapchete_hub_cli/cli/processes.py +44 -0
- mapchete_hub_cli/cli/progress.py +65 -0
- mapchete_hub_cli/cli/retry.py +67 -0
- mapchete_hub_cli/cli/show_remote_version.py +12 -0
- mapchete_hub_cli/cli/test.py +77 -0
- mapchete_hub_cli/client.py +436 -0
- mapchete_hub_cli/enums.py +43 -0
- mapchete_hub_cli/exceptions.py +26 -0
- mapchete_hub_cli/job.py +299 -0
- mapchete_hub_cli/log.py +14 -0
- mapchete_hub_cli/parser.py +78 -0
- mapchete_hub_cli/time.py +61 -0
- mapchete_hub_cli/types.py +8 -0
- mapchete_hub_cli-2025.8.0.dist-info/METADATA +165 -0
- mapchete_hub_cli-2025.8.0.dist-info/RECORD +27 -0
- mapchete_hub_cli-2025.8.0.dist-info/WHEEL +5 -0
- mapchete_hub_cli-2025.8.0.dist-info/entry_points.txt +14 -0
- mapchete_hub_cli-2025.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from mapchete_hub_cli.client import (
|
|
2
|
+
COMMANDS,
|
|
3
|
+
DEFAULT_TIMEOUT,
|
|
4
|
+
JOB_STATUSES,
|
|
5
|
+
MHUB_CLI_ZONES_WAIT_TILES_COUNT,
|
|
6
|
+
MHUB_CLI_ZONES_WAIT_TIME_SECONDS,
|
|
7
|
+
Client,
|
|
8
|
+
Job,
|
|
9
|
+
load_mapchete_config,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Client",
|
|
14
|
+
"Job",
|
|
15
|
+
"COMMANDS",
|
|
16
|
+
"DEFAULT_TIMEOUT",
|
|
17
|
+
"JOB_STATUSES",
|
|
18
|
+
"MHUB_CLI_ZONES_WAIT_TILES_COUNT",
|
|
19
|
+
"MHUB_CLI_ZONES_WAIT_TIME_SECONDS",
|
|
20
|
+
"load_mapchete_config",
|
|
21
|
+
]
|
|
22
|
+
__version__ = "2025.8.0"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mapchete_hub_cli.cli import options
|
|
6
|
+
from mapchete_hub_cli.client import Client, Jobs
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command(short_help="Cancel jobs.")
|
|
12
|
+
@options.opt_job_ids
|
|
13
|
+
@options.opt_output_path
|
|
14
|
+
@options.opt_status
|
|
15
|
+
@options.opt_command
|
|
16
|
+
@options.opt_since_no_default
|
|
17
|
+
@options.opt_until
|
|
18
|
+
@options.opt_job_name
|
|
19
|
+
@options.opt_force
|
|
20
|
+
@options.opt_debug
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def cancel(ctx, job_ids, debug=False, force=False, **kwargs):
|
|
23
|
+
"""Cancel jobs and their follow-up jobs if batch was submitted."""
|
|
24
|
+
client = Client(**ctx.obj)
|
|
25
|
+
try:
|
|
26
|
+
kwargs.update(from_date=kwargs.pop("since"), to_date=kwargs.pop("until"))
|
|
27
|
+
|
|
28
|
+
if job_ids:
|
|
29
|
+
jobs = Jobs.from_jobs([client.job(job_id) for job_id in job_ids])
|
|
30
|
+
|
|
31
|
+
else:
|
|
32
|
+
if all([v is None for v in kwargs.values()]): # pragma: no cover
|
|
33
|
+
click.echo(ctx.get_help())
|
|
34
|
+
raise click.UsageError(
|
|
35
|
+
"Please either provide one or more job IDs or other search values."
|
|
36
|
+
)
|
|
37
|
+
jobs = client.jobs(**kwargs)
|
|
38
|
+
|
|
39
|
+
unfinished_jobs = jobs.unfinished_jobs(msg_writer=click.echo)
|
|
40
|
+
|
|
41
|
+
if not unfinished_jobs: # pragma: no cover
|
|
42
|
+
click.echo("No revokable jobs found.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for job in unfinished_jobs:
|
|
46
|
+
click.echo(job.job_id)
|
|
47
|
+
if force or click.confirm(
|
|
48
|
+
f"Do you really want to cancel {len(unfinished_jobs)} job(s)?", abort=True
|
|
49
|
+
):
|
|
50
|
+
unfinished_jobs.cancel(msg_writer=click.echo)
|
|
51
|
+
|
|
52
|
+
except Exception as e: # pragma: no cover
|
|
53
|
+
if debug:
|
|
54
|
+
raise
|
|
55
|
+
raise click.ClickException(e)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mapchete_hub_cli.cli import options
|
|
6
|
+
from mapchete_hub_cli.client import Client
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command(short_help="Abort stalled jobs.")
|
|
12
|
+
@click.pass_context
|
|
13
|
+
@click.option(
|
|
14
|
+
"--inactive-since",
|
|
15
|
+
type=click.STRING,
|
|
16
|
+
default="5h",
|
|
17
|
+
help="Time since jobs have been inactive.",
|
|
18
|
+
show_default=True,
|
|
19
|
+
)
|
|
20
|
+
@click.option(
|
|
21
|
+
"--pending-since",
|
|
22
|
+
type=click.STRING,
|
|
23
|
+
default="3d",
|
|
24
|
+
help="Time since jobs have been pending.",
|
|
25
|
+
show_default=True,
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--skip-dashboard-check", is_flag=True, help="Skip dashboard availability check."
|
|
29
|
+
)
|
|
30
|
+
@click.option("--retry", is_flag=True, help="Retry instead of cancel stalled jobs.")
|
|
31
|
+
@options.opt_use_old_image
|
|
32
|
+
@options.opt_force
|
|
33
|
+
@options.opt_debug
|
|
34
|
+
def clean(
|
|
35
|
+
ctx: click.Context,
|
|
36
|
+
inactive_since: str = "5h",
|
|
37
|
+
pending_since: str = "3d",
|
|
38
|
+
skip_dashboard_check: bool = False,
|
|
39
|
+
retry: bool = False,
|
|
40
|
+
use_old_image: bool = False,
|
|
41
|
+
force: bool = False,
|
|
42
|
+
debug: bool = False,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Checks for probably stalled jobs and offers to cancel or retry them.
|
|
46
|
+
|
|
47
|
+
The check looks at three properties:\n
|
|
48
|
+
- jobs which are pending for too long\n
|
|
49
|
+
- jobs which are parsing|initializing|running but have been inactive for too long\n
|
|
50
|
+
- jobs which are running, have a scheduler but scheduler dashboard is not available\n
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
stalled_jobs = Client(**ctx.obj).stalled_jobs(
|
|
54
|
+
inactive_since=inactive_since,
|
|
55
|
+
pending_since=pending_since,
|
|
56
|
+
check_inactive_dashboard=not skip_dashboard_check,
|
|
57
|
+
msg_writer=click.echo,
|
|
58
|
+
)
|
|
59
|
+
if stalled_jobs: # pragma: no cover
|
|
60
|
+
click.echo(f"found {len(stalled_jobs)} potentially stalled jobs:")
|
|
61
|
+
for job in stalled_jobs:
|
|
62
|
+
click.echo(job.job_id)
|
|
63
|
+
if force or click.confirm(
|
|
64
|
+
f"Do you really want to cancel {'and retry ' if retry else ''}{len(stalled_jobs)} job(s)?",
|
|
65
|
+
abort=True,
|
|
66
|
+
):
|
|
67
|
+
if retry:
|
|
68
|
+
stalled_jobs.cancel_and_retry(
|
|
69
|
+
msg_writer=click.echo, use_old_image=use_old_image
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
stalled_jobs.cancel(msg_writer=click.echo)
|
|
73
|
+
else:
|
|
74
|
+
click.echo("No stalled jobs found.")
|
|
75
|
+
|
|
76
|
+
except Exception as exc: # pragma: no cover
|
|
77
|
+
if debug:
|
|
78
|
+
raise
|
|
79
|
+
raise click.ClickException(str(exc))
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
from typing import List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from mapchete_hub_cli.cli import options
|
|
7
|
+
from mapchete_hub_cli.cli.progress import show_progress_bar
|
|
8
|
+
from mapchete_hub_cli.client import Client
|
|
9
|
+
from mapchete_hub_cli.parser import load_mapchete_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(help="Execute a process.")
|
|
13
|
+
@options.arg_mapchete_files
|
|
14
|
+
@options.opt_zoom
|
|
15
|
+
@options.opt_area
|
|
16
|
+
@options.opt_area_crs
|
|
17
|
+
@options.opt_zones_within_area
|
|
18
|
+
@options.opt_bounds
|
|
19
|
+
@options.opt_point
|
|
20
|
+
@options.opt_tile
|
|
21
|
+
@options.opt_overwrite
|
|
22
|
+
@options.opt_verbose
|
|
23
|
+
@options.opt_dask_specs
|
|
24
|
+
@options.opt_dask_max_submitted_tasks
|
|
25
|
+
@options.opt_dask_chunksize
|
|
26
|
+
@options.opt_dask_no_task_graph
|
|
27
|
+
@options.opt_debug
|
|
28
|
+
@options.opt_job_name
|
|
29
|
+
@options.opt_make_zones
|
|
30
|
+
@options.opt_full_zones
|
|
31
|
+
@options.opt_zones_wait_count
|
|
32
|
+
@options.opt_zones_wait_seconds
|
|
33
|
+
@options.opt_zone
|
|
34
|
+
@options.opt_force
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def execute(
|
|
37
|
+
ctx: click.Context,
|
|
38
|
+
mapchete_files: List[str],
|
|
39
|
+
bounds: Optional[Tuple[float, float, float, float]] = None,
|
|
40
|
+
overwrite: bool = False,
|
|
41
|
+
verbose: bool = False,
|
|
42
|
+
debug: bool = False,
|
|
43
|
+
dask_no_task_graph: bool = False,
|
|
44
|
+
dask_max_submitted_tasks: int = 1000,
|
|
45
|
+
dask_chunksize: int = 100,
|
|
46
|
+
make_zones_on_zoom: Optional[int] = None,
|
|
47
|
+
full_zones: int = False,
|
|
48
|
+
zones_wait_count: int = 100,
|
|
49
|
+
zones_wait_seconds: float = 1.0,
|
|
50
|
+
job_name: Optional[str] = None,
|
|
51
|
+
zone: Optional[str] = None,
|
|
52
|
+
force: bool = False,
|
|
53
|
+
area: Optional[str] = None,
|
|
54
|
+
zones_within_area: bool = False,
|
|
55
|
+
**kwargs,
|
|
56
|
+
):
|
|
57
|
+
"""Execute a process."""
|
|
58
|
+
dask_settings = dict(
|
|
59
|
+
process_graph=not dask_no_task_graph,
|
|
60
|
+
max_submitted_tasks=dask_max_submitted_tasks,
|
|
61
|
+
chunksize=dask_chunksize,
|
|
62
|
+
)
|
|
63
|
+
client = Client(**ctx.obj)
|
|
64
|
+
|
|
65
|
+
for mapchete_file in mapchete_files:
|
|
66
|
+
try:
|
|
67
|
+
if make_zones_on_zoom is not None and (
|
|
68
|
+
bounds is None and area is None
|
|
69
|
+
): # pragma: no cover
|
|
70
|
+
raise click.UsageError(
|
|
71
|
+
"--make-zones-on-zoom requires --bounds and/or --area"
|
|
72
|
+
)
|
|
73
|
+
elif make_zones_on_zoom is not None or zone is not None:
|
|
74
|
+
try:
|
|
75
|
+
from mapchete.config.parse import guess_geometry
|
|
76
|
+
from mapchete.tile import BufferedTilePyramid, BufferedTile
|
|
77
|
+
except ImportError: # pragma: no cover
|
|
78
|
+
raise ImportError(
|
|
79
|
+
"please install mapchete_hub_cli[zones] extra for this feature."
|
|
80
|
+
)
|
|
81
|
+
tp = BufferedTilePyramid(
|
|
82
|
+
load_mapchete_config(mapchete_file)["pyramid"]["grid"]
|
|
83
|
+
)
|
|
84
|
+
zones: List[BufferedTile] = []
|
|
85
|
+
if zone:
|
|
86
|
+
zones = [tp.tile(*zone)]
|
|
87
|
+
else:
|
|
88
|
+
if bounds:
|
|
89
|
+
zones = [
|
|
90
|
+
tile_zone
|
|
91
|
+
for tile_zone in tp.tiles_from_bounds(
|
|
92
|
+
bounds, make_zones_on_zoom
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
if area:
|
|
96
|
+
geometry, crs = guess_geometry(area)
|
|
97
|
+
if crs != tp.crs: # pragma: no cover
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"area CRS ({crs}) must be the same as BufferedTilePyramid CRS ({tp.crs})"
|
|
100
|
+
)
|
|
101
|
+
zones = zones or [
|
|
102
|
+
tile_zone
|
|
103
|
+
for tile_zone in tp.tiles_from_geom(
|
|
104
|
+
geometry, make_zones_on_zoom
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
if zones_within_area:
|
|
109
|
+
zones = [
|
|
110
|
+
tile_zone
|
|
111
|
+
for tile_zone in zones
|
|
112
|
+
if tile_zone.bbox.within(geometry)
|
|
113
|
+
]
|
|
114
|
+
else:
|
|
115
|
+
zones = [
|
|
116
|
+
tile_zone
|
|
117
|
+
for tile_zone in zones
|
|
118
|
+
if tile_zone.bbox.intersects(geometry)
|
|
119
|
+
]
|
|
120
|
+
if (
|
|
121
|
+
force
|
|
122
|
+
or len(zones) == 1
|
|
123
|
+
or click.confirm(
|
|
124
|
+
f"Do you really want to submit {len(zones)} jobs?", abort=True
|
|
125
|
+
)
|
|
126
|
+
):
|
|
127
|
+
for tile_zone in zones:
|
|
128
|
+
zone_job_name = (
|
|
129
|
+
f"{job_name}-{tile_zone.zoom}-{tile_zone.row}-{tile_zone.col}"
|
|
130
|
+
if job_name
|
|
131
|
+
else None
|
|
132
|
+
)
|
|
133
|
+
process_bounds = (
|
|
134
|
+
bounds_intersection(bounds, tile_zone.bounds)
|
|
135
|
+
if (bounds and not full_zones)
|
|
136
|
+
else tile_zone.bounds
|
|
137
|
+
)
|
|
138
|
+
if len(zones) >= zones_wait_count: # pragma: no cover
|
|
139
|
+
sleep(zones_wait_seconds)
|
|
140
|
+
job = client.start_job(
|
|
141
|
+
command="execute",
|
|
142
|
+
config=mapchete_file,
|
|
143
|
+
params=dict(
|
|
144
|
+
kwargs,
|
|
145
|
+
bounds=process_bounds,
|
|
146
|
+
mode="overwrite" if overwrite else "continue",
|
|
147
|
+
dask_settings=dask_settings,
|
|
148
|
+
job_name=zone_job_name,
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
click.echo(job.job_id)
|
|
152
|
+
else:
|
|
153
|
+
job = client.start_job(
|
|
154
|
+
command="execute",
|
|
155
|
+
config=mapchete_file,
|
|
156
|
+
params=dict(
|
|
157
|
+
kwargs,
|
|
158
|
+
bounds=bounds,
|
|
159
|
+
mode="overwrite" if overwrite else "continue",
|
|
160
|
+
dask_settings=dask_settings,
|
|
161
|
+
job_name=job_name,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
if verbose: # pragma: no cover
|
|
165
|
+
click.echo(f"job {job.job_id} {job.status}")
|
|
166
|
+
if job.properties.get("dask_dashboard_link"):
|
|
167
|
+
click.echo(
|
|
168
|
+
f"dask dashboard: {job.properties.get('dask_dashboard_link')}"
|
|
169
|
+
)
|
|
170
|
+
show_progress_bar(job, disable=debug)
|
|
171
|
+
else:
|
|
172
|
+
click.echo(job.job_id)
|
|
173
|
+
|
|
174
|
+
except Exception as e: # pragma: no cover
|
|
175
|
+
if debug:
|
|
176
|
+
raise
|
|
177
|
+
raise click.ClickException(e)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def bounds_intersection(bounds1, bounds2):
|
|
181
|
+
return (
|
|
182
|
+
max([bounds1[0], bounds2[0]]),
|
|
183
|
+
max([bounds1[1], bounds2[1]]),
|
|
184
|
+
min([bounds1[2], bounds2[2]]),
|
|
185
|
+
min([bounds1[3], bounds2[3]]),
|
|
186
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import oyaml as yaml
|
|
5
|
+
|
|
6
|
+
from mapchete_hub_cli.cli import options
|
|
7
|
+
from mapchete_hub_cli.client import Client, Job
|
|
8
|
+
from mapchete_hub_cli.enums import Status
|
|
9
|
+
from mapchete_hub_cli.time import pretty_time, pretty_time_since, str_to_date
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(short_help="Show job status.")
|
|
13
|
+
@click.argument("job_id", type=click.STRING)
|
|
14
|
+
@options.opt_geojson
|
|
15
|
+
@options.opt_metadata_items
|
|
16
|
+
@click.option("--traceback", is_flag=True, help="Print only traceback if available.")
|
|
17
|
+
@click.option("--show-config", is_flag=True, help="Print Mapchete config.")
|
|
18
|
+
@click.option("--show-params", is_flag=True, help="Print Mapchete parameters.")
|
|
19
|
+
@click.option("--show-process", is_flag=True, help="Print Mapchete process.")
|
|
20
|
+
@options.opt_debug
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def job(
|
|
23
|
+
ctx,
|
|
24
|
+
job_id,
|
|
25
|
+
geojson=False,
|
|
26
|
+
show_config=False,
|
|
27
|
+
show_params=False,
|
|
28
|
+
show_process=False,
|
|
29
|
+
traceback=False,
|
|
30
|
+
debug=False,
|
|
31
|
+
metadata_items=None,
|
|
32
|
+
**_,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Show job status.
|
|
36
|
+
|
|
37
|
+
JOB_ID can either be a valid job ID or :last:, in which case the CLI will automatically
|
|
38
|
+
determine the most recently updated job.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
client = Client(**ctx.obj)
|
|
42
|
+
job = client.job(job_id)
|
|
43
|
+
if geojson: # pragma: no cover
|
|
44
|
+
click.echo(job.to_json())
|
|
45
|
+
return
|
|
46
|
+
elif show_config:
|
|
47
|
+
click.echo(yaml.dump(job.properties["mapchete"]["config"], indent=2))
|
|
48
|
+
return
|
|
49
|
+
elif show_params:
|
|
50
|
+
for k, v in job.properties["mapchete"]["params"].items():
|
|
51
|
+
click.echo(
|
|
52
|
+
f"{k}: {', '.join(map(str, v)) if v else None}"
|
|
53
|
+
) if isinstance(v, list) else click.echo(f"{k}: {v}")
|
|
54
|
+
return
|
|
55
|
+
elif show_process:
|
|
56
|
+
process = job.properties["mapchete"]["config"].get("process")
|
|
57
|
+
process = process if isinstance(process, list) else [process]
|
|
58
|
+
for line in process:
|
|
59
|
+
click.echo(line)
|
|
60
|
+
return
|
|
61
|
+
elif traceback: # pragma: no cover
|
|
62
|
+
click.echo(job.properties.get("exception"))
|
|
63
|
+
click.echo(job.properties.get("traceback"))
|
|
64
|
+
print_job_details(job, metadata_items=metadata_items, verbose=True)
|
|
65
|
+
except Exception as e: # pragma: no cover
|
|
66
|
+
if debug:
|
|
67
|
+
raise
|
|
68
|
+
raise click.ClickException(e)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
status_colors = {
|
|
72
|
+
Status.pending: "blue",
|
|
73
|
+
Status.parsing: "yellow",
|
|
74
|
+
Status.initializing: "yellow",
|
|
75
|
+
Status.running: "yellow",
|
|
76
|
+
Status.retrying: "yellow",
|
|
77
|
+
Status.done: "green",
|
|
78
|
+
Status.failed: "red",
|
|
79
|
+
Status.cancelled: "magenta",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def print_job_details(
|
|
84
|
+
job: Job, metadata_items: Optional[List[str]] = None, verbose: bool = False
|
|
85
|
+
):
|
|
86
|
+
try:
|
|
87
|
+
color = status_colors[job.status]
|
|
88
|
+
except KeyError: # pragma: no cover
|
|
89
|
+
color = "white"
|
|
90
|
+
|
|
91
|
+
mapchete_config = job.properties.get("mapchete", {}).get("config", {})
|
|
92
|
+
|
|
93
|
+
# job ID and job status
|
|
94
|
+
click.echo(click.style(f"{job.job_id}", fg=color, bold=True))
|
|
95
|
+
|
|
96
|
+
if verbose:
|
|
97
|
+
# job name
|
|
98
|
+
click.echo(f"job name: {job.properties.get('job_name')}")
|
|
99
|
+
|
|
100
|
+
# status
|
|
101
|
+
click.echo(click.style(f"status: {job.status}"))
|
|
102
|
+
|
|
103
|
+
# exception
|
|
104
|
+
click.echo(click.style(f"exception: {job.properties.get('exception')}"))
|
|
105
|
+
|
|
106
|
+
# progress
|
|
107
|
+
current = job.properties.get("current_progress", 0)
|
|
108
|
+
total = job.properties.get("total_progress", 100)
|
|
109
|
+
progress = round(100 * current / total, 2) if total else 0.0
|
|
110
|
+
click.echo(f"progress: {progress}%")
|
|
111
|
+
|
|
112
|
+
# dask_dashboard_link
|
|
113
|
+
click.echo(f"dask dashboard: {job.properties.get('dask_dashboard_link')}")
|
|
114
|
+
|
|
115
|
+
# command
|
|
116
|
+
click.echo(f"command: {job.properties.get('command')}")
|
|
117
|
+
|
|
118
|
+
# output path
|
|
119
|
+
click.echo(f"output path: {mapchete_config.get('output', {}).get('path')}")
|
|
120
|
+
|
|
121
|
+
# bounds
|
|
122
|
+
click.echo(f"bounds: {job.bounds}")
|
|
123
|
+
|
|
124
|
+
# started, updated, finished time
|
|
125
|
+
for time_property in ["started", "updated", "finished"]:
|
|
126
|
+
click.echo(
|
|
127
|
+
f"{time_property}: {job.properties.get(time_property, 'unknown')}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# runtime
|
|
131
|
+
runtime = job.properties.get("runtime", "unknown")
|
|
132
|
+
click.echo(f"runtime: {pretty_time(runtime) if runtime else None}")
|
|
133
|
+
|
|
134
|
+
# last received update
|
|
135
|
+
last_update = job.properties.get("updated", "unknown")
|
|
136
|
+
click.echo(
|
|
137
|
+
f"last received update: {pretty_time_since(str_to_date(last_update))} ago"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if metadata_items:
|
|
141
|
+
for i in metadata_items:
|
|
142
|
+
click.echo(f"{i}: {job.properties.get(i)}")
|
|
143
|
+
|
|
144
|
+
if verbose or metadata_items:
|
|
145
|
+
# append newline
|
|
146
|
+
click.echo("")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mapchete_hub_cli.cli import options
|
|
6
|
+
from mapchete_hub_cli.cli.job import print_job_details
|
|
7
|
+
from mapchete_hub_cli.client import Client
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(short_help="Show current jobs.")
|
|
13
|
+
@options.opt_output_path
|
|
14
|
+
@options.opt_status
|
|
15
|
+
@options.opt_command
|
|
16
|
+
@options.opt_since
|
|
17
|
+
@options.opt_until
|
|
18
|
+
@options.opt_job_name
|
|
19
|
+
@options.opt_unique_by_job_name
|
|
20
|
+
@options.opt_sort_by
|
|
21
|
+
@options.opt_bounds
|
|
22
|
+
@options.opt_geojson
|
|
23
|
+
@options.opt_metadata_items
|
|
24
|
+
@options.opt_verbose
|
|
25
|
+
@options.opt_debug
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def jobs(
|
|
28
|
+
ctx,
|
|
29
|
+
geojson=False,
|
|
30
|
+
verbose=False,
|
|
31
|
+
sort_by=None,
|
|
32
|
+
debug=False,
|
|
33
|
+
metadata_items=None,
|
|
34
|
+
**kwargs,
|
|
35
|
+
):
|
|
36
|
+
"""Show current jobs."""
|
|
37
|
+
|
|
38
|
+
def _sort_jobs(jobs, sort_by=None):
|
|
39
|
+
if sort_by == "status":
|
|
40
|
+
return list(
|
|
41
|
+
sorted(
|
|
42
|
+
jobs,
|
|
43
|
+
key=lambda x: (
|
|
44
|
+
x.to_dict()["properties"]["status"],
|
|
45
|
+
x.to_dict()["properties"]["updated"],
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
elif sort_by in ["started", "runtime"]:
|
|
50
|
+
return list(
|
|
51
|
+
sorted(jobs, key=lambda x: x.to_dict()["properties"][sort_by] or 0.0)
|
|
52
|
+
)
|
|
53
|
+
elif sort_by == "progress":
|
|
54
|
+
|
|
55
|
+
def _get_progress(job):
|
|
56
|
+
properties = job.to_dict().get("properties", {})
|
|
57
|
+
current = properties.get("current_progress")
|
|
58
|
+
total = properties.get("total_progress")
|
|
59
|
+
return 100 * current / total if total else 0.0
|
|
60
|
+
|
|
61
|
+
return list(sorted(jobs, key=lambda x: _get_progress(x)))
|
|
62
|
+
|
|
63
|
+
kwargs.update(from_date=kwargs.pop("since"), to_date=kwargs.pop("until"))
|
|
64
|
+
try:
|
|
65
|
+
client = Client(**ctx.obj)
|
|
66
|
+
jobs = client.jobs(**kwargs)
|
|
67
|
+
if geojson:
|
|
68
|
+
click.echo(jobs.to_json())
|
|
69
|
+
else:
|
|
70
|
+
# sort by status and then by timestamp
|
|
71
|
+
jobs = _sort_jobs(jobs, sort_by=sort_by)
|
|
72
|
+
logger.debug(jobs)
|
|
73
|
+
if verbose:
|
|
74
|
+
click.echo(f"{len(jobs)} jobs found. \n")
|
|
75
|
+
for i in jobs:
|
|
76
|
+
print_job_details(i, metadata_items=metadata_items, verbose=verbose)
|
|
77
|
+
except Exception as e: # pragma: no cover
|
|
78
|
+
if debug:
|
|
79
|
+
raise
|
|
80
|
+
raise click.ClickException(e)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click_plugins import with_plugins
|
|
5
|
+
|
|
6
|
+
from mapchete_hub_cli import DEFAULT_TIMEOUT, __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
entry_points = metadata.entry_points()
|
|
10
|
+
commands = entry_points.select(group="mapchete_hub_cli.cli.commands")
|
|
11
|
+
|
|
12
|
+
host_options = dict(host_ip="0.0.0.0", port=5000)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@with_plugins(commands)
|
|
16
|
+
@click.version_option(version=__version__, message="%(version)s")
|
|
17
|
+
@click.group(help="Process control on Mapchete Hub.")
|
|
18
|
+
@click.option(
|
|
19
|
+
"--host",
|
|
20
|
+
"-h",
|
|
21
|
+
type=click.STRING,
|
|
22
|
+
nargs=1,
|
|
23
|
+
default=f"{host_options['host_ip']}:{host_options['port']}",
|
|
24
|
+
help="Address and port of mhub endpoint",
|
|
25
|
+
show_default=True,
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--timeout",
|
|
29
|
+
type=click.INT,
|
|
30
|
+
default=DEFAULT_TIMEOUT,
|
|
31
|
+
help="Time in seconds to wait for server response.",
|
|
32
|
+
show_default=True,
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--user",
|
|
36
|
+
"-u",
|
|
37
|
+
type=click.STRING,
|
|
38
|
+
help="Username for basic auth. (Or set MHUB_USER env variable.)",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--password",
|
|
42
|
+
"-p",
|
|
43
|
+
type=click.STRING,
|
|
44
|
+
help="Password for basic auth. (Or set MHUB_PASSWORD env variable.)",
|
|
45
|
+
)
|
|
46
|
+
@click.pass_context
|
|
47
|
+
def mhub(ctx, host, **kwargs):
|
|
48
|
+
"""Main command group."""
|
|
49
|
+
host = host if host.startswith("http") else f"http://{host}"
|
|
50
|
+
host = host if host.endswith("/") else f"{host}/"
|
|
51
|
+
ctx.obj = dict(host=host, **kwargs)
|