]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/cli.py
restrict use of os.environ in Configuration
[nominatim.git] / src / nominatim_db / cli.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2024 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 import asyncio
18 from pathlib import Path
19
20 from .config import Configuration
21 from .errors import UsageError
22 from .tools.exec_utils import run_php_server
23 from . import clicmd
24 from . import version
25 from .clicmd.args import NominatimArgs, Subcommand
26
27 LOG = logging.getLogger()
28
29 class CommandlineParser:
30     """ Wraps some of the common functions for parsing the command line
31         and setting up subcommands.
32     """
33     def __init__(self, prog: str, description: Optional[str]):
34         self.parser = argparse.ArgumentParser(
35             prog=prog,
36             description=description,
37             formatter_class=argparse.RawDescriptionHelpFormatter)
38
39         self.subs = self.parser.add_subparsers(title='available commands',
40                                                dest='subcommand')
41
42         # Global arguments that only work if no sub-command given
43         self.parser.add_argument('--version', action='store_true',
44                                  help='Print Nominatim version and exit')
45
46         # Arguments added to every sub-command
47         self.default_args = argparse.ArgumentParser(add_help=False)
48         group = self.default_args.add_argument_group('Default arguments')
49         group.add_argument('-h', '--help', action='help',
50                            help='Show this help message and exit')
51         group.add_argument('-q', '--quiet', action='store_const', const=0,
52                            dest='verbose', default=1,
53                            help='Print only error messages')
54         group.add_argument('-v', '--verbose', action='count', default=1,
55                            help='Increase verboseness of output')
56         group.add_argument('--project-dir', metavar='DIR', default='.',
57                            help='Base directory of the Nominatim installation (default:.)')
58         group.add_argument('-j', '--threads', metavar='NUM', type=int,
59                            help='Number of parallel threads to use')
60
61
62     def nominatim_version_text(self) -> str:
63         """ Program name and version number as string
64         """
65         text = f'Nominatim version {version.NOMINATIM_VERSION!s}'
66         if version.GIT_COMMIT_HASH is not None:
67             text += f' ({version.GIT_COMMIT_HASH})'
68         return text
69
70
71     def add_subcommand(self, name: str, cmd: Subcommand) -> None:
72         """ Add a subcommand to the parser. The subcommand must be a class
73             with a function add_args() that adds the parameters for the
74             subcommand and a run() function that executes the command.
75         """
76         assert cmd.__doc__ is not None
77
78         parser = self.subs.add_parser(name, parents=[self.default_args],
79                                       help=cmd.__doc__.split('\n', 1)[0],
80                                       description=cmd.__doc__,
81                                       formatter_class=argparse.RawDescriptionHelpFormatter,
82                                       add_help=False)
83         parser.set_defaults(command=cmd)
84         cmd.add_args(parser)
85
86
87     def run(self, **kwargs: Any) -> int:
88         """ Parse the command line arguments of the program and execute the
89             appropriate subcommand.
90         """
91         args = NominatimArgs()
92         try:
93             self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
94         except SystemExit:
95             return 1
96
97         if args.version:
98             print(self.nominatim_version_text())
99             return 0
100
101         if args.subcommand is None:
102             self.parser.print_help()
103             return 1
104
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             ret = args.command.run(args)
123
124             if args.config.TOKENIZER == 'legacy':
125                 log.warning('WARNING: the "legacy" tokenizer is deprecated '
126                             'and will be removed in Nominatim 5.0.')
127
128             return ret
129         except UsageError as exception:
130             if log.isEnabledFor(logging.DEBUG):
131                 raise # use Python's exception printing
132             log.fatal('FATAL: %s', exception)
133
134         # If we get here, then execution has failed in some way.
135         return 1
136
137
138 # Subcommand classes
139 #
140 # Each class needs to implement two functions: add_args() adds the CLI parameters
141 # for the subfunction, run() executes the subcommand.
142 #
143 # The class documentation doubles as the help text for the command. The
144 # first line is also used in the summary when calling the program without
145 # a subcommand.
146 #
147 # No need to document the functions each time.
148 # pylint: disable=C0111
149 class AdminServe:
150     """\
151     Start a simple web server for serving the API.
152
153     This command starts a built-in webserver to serve the website
154     from the current project directory. This webserver is only suitable
155     for testing and development. Do not use it in production setups!
156
157     There are different webservers available. The default 'php' engine
158     runs the classic PHP frontend. The other engines are Python servers
159     which run the new Python frontend code. This is highly experimental
160     at the moment and may not include the full API.
161
162     By the default, the webserver can be accessed at: http://127.0.0.1:8088
163     """
164
165     def add_args(self, parser: argparse.ArgumentParser) -> None:
166         group = parser.add_argument_group('Server arguments')
167         group.add_argument('--server', default='127.0.0.1:8088',
168                            help='The address the server will listen to.')
169         group.add_argument('--engine', default='falcon',
170                            choices=('php', 'falcon', 'starlette'),
171                            help='Webserver framework to run. (default: falcon)')
172
173
174     def run(self, args: NominatimArgs) -> int:
175         if args.engine == 'php':
176             if args.config.lib_dir.php is None:
177                 raise UsageError("PHP frontend not configured.")
178             LOG.warning('\n\nWARNING: the PHP frontend is deprecated '
179                         'and will be removed in Nominatim 5.0.\n\n')
180             run_php_server(args.server, args.project_dir / 'website')
181         else:
182             asyncio.run(self.run_uvicorn(args))
183
184         return 0
185
186
187     async def run_uvicorn(self, args: NominatimArgs) -> None:
188         import uvicorn # pylint: disable=import-outside-toplevel
189
190         server_info = args.server.split(':', 1)
191         host = server_info[0]
192         if len(server_info) > 1:
193             if not server_info[1].isdigit():
194                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
195             port = int(server_info[1])
196         else:
197             port = 8088
198
199         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
200
201         app = server_module.get_application(args.project_dir)
202
203         config = uvicorn.Config(app, host=host, port=port)
204         server = uvicorn.Server(config)
205         await server.serve()
206
207
208 def get_set_parser() -> CommandlineParser:
209     """\
210     Initializes the parser and adds various subcommands for
211     nominatim cli.
212     """
213     parser = CommandlineParser('nominatim', nominatim.__doc__)
214
215     parser.add_subcommand('import', clicmd.SetupAll())
216     parser.add_subcommand('freeze', clicmd.SetupFreeze())
217     parser.add_subcommand('replication', clicmd.UpdateReplication())
218
219     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
220
221     parser.add_subcommand('add-data', clicmd.UpdateAddData())
222     parser.add_subcommand('index', clicmd.UpdateIndex())
223     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
224
225     parser.add_subcommand('admin', clicmd.AdminFuncs())
226
227     try:
228         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
229         apicmd = importlib.import_module('nominatim_db.clicmd.api')
230         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
231
232         parser.add_subcommand('export', exportcmd.QueryExport())
233         parser.add_subcommand('convert', convertcmd.ConvertDB())
234         parser.add_subcommand('serve', AdminServe())
235
236         parser.add_subcommand('search', apicmd.APISearch())
237         parser.add_subcommand('reverse', apicmd.APIReverse())
238         parser.add_subcommand('lookup', apicmd.APILookup())
239         parser.add_subcommand('details', apicmd.APIDetails())
240         parser.add_subcommand('status', apicmd.APIStatus())
241     except ModuleNotFoundError as ex:
242         if not ex.name or 'nominatim_api' not in ex.name: # pylint: disable=E1135
243             raise ex
244
245         parser.parser.epilog = \
246             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
247             '\nThe following commands are not available:'\
248             '\n    export, convert, serve, search, reverse, lookup, details, status'\
249             "\n\nRun 'pip install nominatim-api' to install the package."
250
251
252     return parser
253
254
255 def nominatim(**kwargs: Any) -> int:
256     """\
257     Command-line tools for importing, updating, administrating and
258     querying the Nominatim database.
259     """
260     return get_set_parser().run(**kwargs)