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.
@@ -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,4 @@
1
+ from mapchete_hub_cli.cli.main import mhub
2
+
3
+
4
+ __all__ = ["mhub"]
@@ -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)