]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
make Python frontend default and PHP optional
[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) 2023 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
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_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.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 AdminServe:
143     """\
144     Start a simple web server for serving the API.
145
146     This command starts a built-in webserver to serve the website
147     from the current project directory. This webserver is only suitable
148     for testing and development. Do not use it in production setups!
149
150     There are different webservers available. The default 'php' engine
151     runs the classic PHP frontend. The other engines are Python servers
152     which run the new Python frontend code. This is highly experimental
153     at the moment and may not include the full API.
154
155     By the default, the webserver can be accessed at: http://127.0.0.1:8088
156     """
157
158     def add_args(self, parser: argparse.ArgumentParser) -> None:
159         group = parser.add_argument_group('Server arguments')
160         group.add_argument('--server', default='127.0.0.1:8088',
161                            help='The address the server will listen to.')
162         group.add_argument('--engine', default='falcon',
163                            choices=('php', 'falcon', 'starlette'),
164                            help='Webserver framework to run. (default: falcon)')
165
166
167     def run(self, args: NominatimArgs) -> int:
168         if args.engine == 'php':
169             if args.config.lib_dir.php is None:
170                 raise UsageError("PHP frontend not configured.")
171             run_php_server(args.server, args.project_dir / 'website')
172         else:
173             import uvicorn # pylint: disable=import-outside-toplevel
174             server_info = args.server.split(':', 1)
175             host = server_info[0]
176             if len(server_info) > 1:
177                 if not server_info[1].isdigit():
178                     raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
179                 port = int(server_info[1])
180             else:
181                 port = 8088
182
183             server_module = importlib.import_module(f'nominatim.server.{args.engine}.server')
184
185             app = server_module.get_application(args.project_dir)
186             uvicorn.run(app, host=host, port=port)
187
188         return 0
189
190
191 def get_set_parser() -> CommandlineParser:
192     """\
193     Initializes the parser and adds various subcommands for
194     nominatim cli.
195     """
196     parser = CommandlineParser('nominatim', nominatim.__doc__)
197
198     parser.add_subcommand('import', clicmd.SetupAll())
199     parser.add_subcommand('freeze', clicmd.SetupFreeze())
200     parser.add_subcommand('replication', clicmd.UpdateReplication())
201
202     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
203
204     parser.add_subcommand('add-data', clicmd.UpdateAddData())
205     parser.add_subcommand('index', clicmd.UpdateIndex())
206     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
207
208     parser.add_subcommand('admin', clicmd.AdminFuncs())
209
210     parser.add_subcommand('export', clicmd.QueryExport())
211     parser.add_subcommand('convert', clicmd.ConvertDB())
212     parser.add_subcommand('serve', AdminServe())
213
214     parser.add_subcommand('search', clicmd.APISearch())
215     parser.add_subcommand('reverse', clicmd.APIReverse())
216     parser.add_subcommand('lookup', clicmd.APILookup())
217     parser.add_subcommand('details', clicmd.APIDetails())
218     parser.add_subcommand('status', clicmd.APIStatus())
219
220     return parser
221
222
223 def nominatim(**kwargs: Any) -> int:
224     """\
225     Command-line tools for importing, updating, administrating and
226     querying the Nominatim database.
227     """
228     return get_set_parser().run(**kwargs)