]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
Merge pull request #2770 from lonvia/typed-python
[nominatim.git] / nominatim / cli.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Command-line interface to the Nominatim functions for import, update,
9 database administration and querying.
10 """
11 from typing import Optional, Any, List, Union
12 import logging
13 import os
14 import sys
15 import argparse
16 from pathlib import Path
17
18 from nominatim.config import Configuration
19 from nominatim.tools.exec_utils import run_legacy_script, run_php_server
20 from nominatim.errors import UsageError
21 from nominatim import clicmd
22 from nominatim import version
23 from nominatim.clicmd.args import NominatimArgs, Subcommand
24
25 LOG = logging.getLogger()
26
27 class CommandlineParser:
28     """ Wraps some of the common functions for parsing the command line
29         and setting up subcommands.
30     """
31     def __init__(self, prog: str, description: Optional[str]):
32         self.parser = argparse.ArgumentParser(
33             prog=prog,
34             description=description,
35             formatter_class=argparse.RawDescriptionHelpFormatter)
36
37         self.subs = self.parser.add_subparsers(title='available commands',
38                                                dest='subcommand')
39
40         # Global arguments that only work if no sub-command given
41         self.parser.add_argument('--version', action='store_true',
42                                  help='Print Nominatim version and exit')
43
44         # Arguments added to every sub-command
45         self.default_args = argparse.ArgumentParser(add_help=False)
46         group = self.default_args.add_argument_group('Default arguments')
47         group.add_argument('-h', '--help', action='help',
48                            help='Show this help message and exit')
49         group.add_argument('-q', '--quiet', action='store_const', const=0,
50                            dest='verbose', default=1,
51                            help='Print only error messages')
52         group.add_argument('-v', '--verbose', action='count', default=1,
53                            help='Increase verboseness of output')
54         group.add_argument('--project-dir', metavar='DIR', default='.',
55                            help='Base directory of the Nominatim installation (default:.)')
56         group.add_argument('-j', '--threads', metavar='NUM', type=int,
57                            help='Number of parallel threads to use')
58
59
60     def nominatim_version_text(self) -> str:
61         """ Program name and version number as string
62         """
63         text = f'Nominatim version {version.version_str()}'
64         if version.GIT_COMMIT_HASH is not None:
65             text += f' ({version.GIT_COMMIT_HASH})'
66         return text
67
68
69     def add_subcommand(self, name: str, cmd: Subcommand) -> None:
70         """ Add a subcommand to the parser. The subcommand must be a class
71             with a function add_args() that adds the parameters for the
72             subcommand and a run() function that executes the command.
73         """
74         assert cmd.__doc__ is not None
75
76         parser = self.subs.add_parser(name, parents=[self.default_args],
77                                       help=cmd.__doc__.split('\n', 1)[0],
78                                       description=cmd.__doc__,
79                                       formatter_class=argparse.RawDescriptionHelpFormatter,
80                                       add_help=False)
81         parser.set_defaults(command=cmd)
82         cmd.add_args(parser)
83
84
85     def run(self, **kwargs: Any) -> int:
86         """ Parse the command line arguments of the program and execute the
87             appropriate subcommand.
88         """
89         args = NominatimArgs()
90         try:
91             self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
92         except SystemExit:
93             return 1
94
95         if args.version:
96             print(self.nominatim_version_text())
97             return 0
98
99         if args.subcommand is None:
100             self.parser.print_help()
101             return 1
102
103         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'sqllib_dir',
104                     'data_dir', 'config_dir', 'phpcgi_path'):
105             setattr(args, arg, Path(kwargs[arg]))
106         args.project_dir = Path(args.project_dir).resolve()
107
108         if 'cli_args' not in kwargs:
109             logging.basicConfig(stream=sys.stderr,
110                                 format='%(asctime)s: %(message)s',
111                                 datefmt='%Y-%m-%d %H:%M:%S',
112                                 level=max(4 - args.verbose, 1) * 10)
113
114         args.config = Configuration(args.project_dir, args.config_dir,
115                                     environ=kwargs.get('environ', os.environ))
116         args.config.set_libdirs(module=args.module_dir,
117                                 osm2pgsql=args.osm2pgsql_path,
118                                 php=args.phplib_dir,
119                                 sql=args.sqllib_dir,
120                                 data=args.data_dir)
121
122         log = logging.getLogger()
123         log.warning('Using project directory: %s', str(args.project_dir))
124
125         try:
126             return args.command.run(args)
127         except UsageError as exception:
128             if log.isEnabledFor(logging.DEBUG):
129                 raise # use Python's exception printing
130             log.fatal('FATAL: %s', exception)
131
132         # If we get here, then execution has failed in some way.
133         return 1
134
135
136 # Subcommand classes
137 #
138 # Each class needs to implement two functions: add_args() adds the CLI parameters
139 # for the subfunction, run() executes the subcommand.
140 #
141 # The class documentation doubles as the help text for the command. The
142 # first line is also used in the summary when calling the program without
143 # a subcommand.
144 #
145 # No need to document the functions each time.
146 # pylint: disable=C0111
147 class QueryExport:
148     """\
149     Export addresses as CSV file from the database.
150     """
151
152     def add_args(self, parser: argparse.ArgumentParser) -> None:
153         group = parser.add_argument_group('Output arguments')
154         group.add_argument('--output-type', default='street',
155                            choices=('continent', 'country', 'state', 'county',
156                                     'city', 'suburb', 'street', 'path'),
157                            help='Type of places to output (default: street)')
158         group.add_argument('--output-format',
159                            default='street;suburb;city;county;state;country',
160                            help=("Semicolon-separated list of address types "
161                                  "(see --output-type). Multiple ranks can be "
162                                  "merged into one column by simply using a "
163                                  "comma-separated list."))
164         group.add_argument('--output-all-postcodes', action='store_true',
165                            help=("List all postcodes for address instead of "
166                                  "just the most likely one"))
167         group.add_argument('--language',
168                            help=("Preferred language for output "
169                                  "(use local name, if omitted)"))
170         group = parser.add_argument_group('Filter arguments')
171         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
172                            help='Export only objects within country')
173         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
174                            help='Export only children of this OSM node')
175         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
176                            help='Export only children of this OSM way')
177         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
178                            help='Export only children of this OSM relation')
179
180
181     def run(self, args: NominatimArgs) -> int:
182         params: List[Union[int, str]] = [
183                              '--output-type', args.output_type,
184                              '--output-format', args.output_format]
185         if args.output_all_postcodes:
186             params.append('--output-all-postcodes')
187         if args.language:
188             params.extend(('--language', args.language))
189         if args.restrict_to_country:
190             params.extend(('--restrict-to-country', args.restrict_to_country))
191         if args.restrict_to_osm_node:
192             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
193         if args.restrict_to_osm_way:
194             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
195         if args.restrict_to_osm_relation:
196             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
197
198         return run_legacy_script('export.php', *params, nominatim_env=args)
199
200
201 class AdminServe:
202     """\
203     Start a simple web server for serving the API.
204
205     This command starts the built-in PHP webserver to serve the website
206     from the current project directory. This webserver is only suitable
207     for testing and development. Do not use it in production setups!
208
209     By the default, the webserver can be accessed at: http://127.0.0.1:8088
210     """
211
212     def add_args(self, parser: argparse.ArgumentParser) -> None:
213         group = parser.add_argument_group('Server arguments')
214         group.add_argument('--server', default='127.0.0.1:8088',
215                            help='The address the server will listen to.')
216
217
218     def run(self, args: NominatimArgs) -> int:
219         run_php_server(args.server, args.project_dir / 'website')
220         return 0
221
222
223 def get_set_parser(**kwargs: Any) -> CommandlineParser:
224     """\
225     Initializes the parser and adds various subcommands for
226     nominatim cli.
227     """
228     parser = CommandlineParser('nominatim', nominatim.__doc__)
229
230     parser.add_subcommand('import', clicmd.SetupAll())
231     parser.add_subcommand('freeze', clicmd.SetupFreeze())
232     parser.add_subcommand('replication', clicmd.UpdateReplication())
233
234     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
235
236     parser.add_subcommand('add-data', clicmd.UpdateAddData())
237     parser.add_subcommand('index', clicmd.UpdateIndex())
238     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
239
240     parser.add_subcommand('admin', clicmd.AdminFuncs())
241
242     parser.add_subcommand('export', QueryExport())
243     parser.add_subcommand('serve', AdminServe())
244
245     if kwargs.get('phpcgi_path'):
246         parser.add_subcommand('search', clicmd.APISearch())
247         parser.add_subcommand('reverse', clicmd.APIReverse())
248         parser.add_subcommand('lookup', clicmd.APILookup())
249         parser.add_subcommand('details', clicmd.APIDetails())
250         parser.add_subcommand('status', clicmd.APIStatus())
251     else:
252         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
253
254     return parser
255
256
257 def nominatim(**kwargs: Any) -> int:
258     """\
259     Command-line tools for importing, updating, administrating and
260     querying the Nominatim database.
261     """
262     parser = get_set_parser(**kwargs)
263
264     return parser.run(**kwargs)