]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
add support for falcon as server framework
[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         args.phpcgi_path = Path(kwargs['phpcgi_path'])
104         args.project_dir = Path(args.project_dir).resolve()
105
106         if 'cli_args' not in kwargs:
107             logging.basicConfig(stream=sys.stderr,
108                                 format='%(asctime)s: %(message)s',
109                                 datefmt='%Y-%m-%d %H:%M:%S',
110                                 level=max(4 - args.verbose, 1) * 10)
111
112         args.config = Configuration(args.project_dir,
113                                     environ=kwargs.get('environ', os.environ))
114         args.config.set_libdirs(module=kwargs['module_dir'],
115                                 osm2pgsql=kwargs['osm2pgsql_path'])
116
117         log = logging.getLogger()
118         log.warning('Using project directory: %s', str(args.project_dir))
119
120         try:
121             return args.command.run(args)
122         except UsageError as exception:
123             if log.isEnabledFor(logging.DEBUG):
124                 raise # use Python's exception printing
125             log.fatal('FATAL: %s', exception)
126
127         # If we get here, then execution has failed in some way.
128         return 1
129
130
131 # Subcommand classes
132 #
133 # Each class needs to implement two functions: add_args() adds the CLI parameters
134 # for the subfunction, run() executes the subcommand.
135 #
136 # The class documentation doubles as the help text for the command. The
137 # first line is also used in the summary when calling the program without
138 # a subcommand.
139 #
140 # No need to document the functions each time.
141 # pylint: disable=C0111
142 class QueryExport:
143     """\
144     Export addresses as CSV file from the database.
145     """
146
147     def add_args(self, parser: argparse.ArgumentParser) -> None:
148         group = parser.add_argument_group('Output arguments')
149         group.add_argument('--output-type', default='street',
150                            choices=('continent', 'country', 'state', 'county',
151                                     'city', 'suburb', 'street', 'path'),
152                            help='Type of places to output (default: street)')
153         group.add_argument('--output-format',
154                            default='street;suburb;city;county;state;country',
155                            help=("Semicolon-separated list of address types "
156                                  "(see --output-type). Multiple ranks can be "
157                                  "merged into one column by simply using a "
158                                  "comma-separated list."))
159         group.add_argument('--output-all-postcodes', action='store_true',
160                            help=("List all postcodes for address instead of "
161                                  "just the most likely one"))
162         group.add_argument('--language',
163                            help=("Preferred language for output "
164                                  "(use local name, if omitted)"))
165         group = parser.add_argument_group('Filter arguments')
166         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
167                            help='Export only objects within country')
168         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
169                            help='Export only children of this OSM node')
170         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
171                            help='Export only children of this OSM way')
172         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
173                            help='Export only children of this OSM relation')
174
175
176     def run(self, args: NominatimArgs) -> int:
177         params: List[Union[int, str]] = [
178                              '--output-type', args.output_type,
179                              '--output-format', args.output_format]
180         if args.output_all_postcodes:
181             params.append('--output-all-postcodes')
182         if args.language:
183             params.extend(('--language', args.language))
184         if args.restrict_to_country:
185             params.extend(('--restrict-to-country', args.restrict_to_country))
186         if args.restrict_to_osm_node:
187             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
188         if args.restrict_to_osm_way:
189             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
190         if args.restrict_to_osm_relation:
191             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
192
193         return run_legacy_script('export.php', *params, config=args.config)
194
195
196 class AdminServe:
197     """\
198     Start a simple web server for serving the API.
199
200     This command starts a built-in webserver to serve the website
201     from the current project directory. This webserver is only suitable
202     for testing and development. Do not use it in production setups!
203
204     There are different webservers available. The default 'php' engine
205     runs the classic PHP frontend. 'sanic' and 'falcon' are Python servers
206     which run the new Python frontend code. This is highly experimental
207     at the moment and may not include the full API.
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         group.add_argument('--engine', default='php',
217                            choices=('php', 'sanic', 'falcon'),
218                            help='Webserver framework to run. (default: php)')
219
220
221     def run(self, args: NominatimArgs) -> int:
222         if args.engine == 'php':
223             run_php_server(args.server, args.project_dir / 'website')
224         else:
225             server_info = args.server.split(':', 1)
226             host = server_info[0]
227             if len(server_info) > 1:
228                 if not server_info[1].isdigit():
229                     raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
230                 port = int(server_info[1])
231             else:
232                 port = 8088
233
234             if args.engine == 'sanic':
235                 import nominatim.server.sanic.server
236
237                 app = nominatim.server.sanic.server.get_application(args.project_dir)
238                 app.run(host=host, port=port, debug=True)
239             elif args.engine == 'falcon':
240                 import uvicorn
241                 import nominatim.server.falcon.server
242
243                 app = nominatim.server.falcon.server.get_application(args.project_dir)
244                 uvicorn.run(app, host=host, port=port)
245
246         return 0
247
248
249 def get_set_parser(**kwargs: Any) -> CommandlineParser:
250     """\
251     Initializes the parser and adds various subcommands for
252     nominatim cli.
253     """
254     parser = CommandlineParser('nominatim', nominatim.__doc__)
255
256     parser.add_subcommand('import', clicmd.SetupAll())
257     parser.add_subcommand('freeze', clicmd.SetupFreeze())
258     parser.add_subcommand('replication', clicmd.UpdateReplication())
259
260     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
261
262     parser.add_subcommand('add-data', clicmd.UpdateAddData())
263     parser.add_subcommand('index', clicmd.UpdateIndex())
264     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
265
266     parser.add_subcommand('admin', clicmd.AdminFuncs())
267
268     parser.add_subcommand('export', QueryExport())
269     parser.add_subcommand('serve', AdminServe())
270
271     if kwargs.get('phpcgi_path'):
272         parser.add_subcommand('search', clicmd.APISearch())
273         parser.add_subcommand('reverse', clicmd.APIReverse())
274         parser.add_subcommand('lookup', clicmd.APILookup())
275         parser.add_subcommand('details', clicmd.APIDetails())
276         parser.add_subcommand('status', clicmd.APIStatus())
277     else:
278         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
279
280     return parser
281
282
283 def nominatim(**kwargs: Any) -> int:
284     """\
285     Command-line tools for importing, updating, administrating and
286     querying the Nominatim database.
287     """
288     parser = get_set_parser(**kwargs)
289
290     return parser.run(**kwargs)