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