]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/cli.py
Merge pull request #3586 from lonvia/reduce-lookup-calls
[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 . import clicmd
23 from . import version
24 from .clicmd.args import NominatimArgs, Subcommand
25
26 LOG = logging.getLogger()
27
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     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     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     def run(self, **kwargs: Any) -> int:
85         """ Parse the command line arguments of the program and execute the
86             appropriate subcommand.
87         """
88         args = NominatimArgs()
89         try:
90             self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
91         except SystemExit:
92             return 1
93
94         if args.version:
95             print(self.nominatim_version_text())
96             return 0
97
98         if args.subcommand is None:
99             self.parser.print_help()
100             return 1
101
102         args.project_dir = Path(args.project_dir).resolve()
103
104         if 'cli_args' not in kwargs:
105             logging.basicConfig(stream=sys.stderr,
106                                 format='%(asctime)s: %(message)s',
107                                 datefmt='%Y-%m-%d %H:%M:%S',
108                                 level=max(4 - args.verbose, 1) * 10)
109
110         args.config = Configuration(args.project_dir,
111                                     environ=kwargs.get('environ', os.environ))
112         args.config.set_libdirs(osm2pgsql=kwargs['osm2pgsql_path'])
113
114         log = logging.getLogger()
115         log.warning('Using project directory: %s', str(args.project_dir))
116
117         try:
118             ret = args.command.run(args)
119
120             return ret
121         except UsageError as exception:
122             if log.isEnabledFor(logging.DEBUG):
123                 raise  # use Python's exception printing
124             log.fatal('FATAL: %s', exception)
125
126         # If we get here, then execution has failed in some way.
127         return 1
128
129
130 # Subcommand classes
131 #
132 # Each class needs to implement two functions: add_args() adds the CLI parameters
133 # for the subfunction, run() executes the subcommand.
134 #
135 # The class documentation doubles as the help text for the command. The
136 # first line is also used in the summary when calling the program without
137 # a subcommand.
138 #
139 # No need to document the functions each time.
140 class AdminServe:
141     """\
142     Start a simple web server for serving the API.
143
144     This command starts a built-in webserver to serve the website
145     from the current project directory. This webserver is only suitable
146     for testing and development. Do not use it in production setups!
147
148     There are two different webserver implementations for Python available:
149     falcon (the default) and starlette. You need to make sure the
150     appropriate Python packages as well as the uvicorn package are
151     installed to use this function.
152
153     By the default, the webserver can be accessed at: http://127.0.0.1:8088
154     """
155
156     def add_args(self, parser: argparse.ArgumentParser) -> None:
157         group = parser.add_argument_group('Server arguments')
158         group.add_argument('--server', default='127.0.0.1:8088',
159                            help='The address the server will listen to.')
160         group.add_argument('--engine', default='falcon',
161                            choices=('falcon', 'starlette'),
162                            help='Webserver framework to run. (default: falcon)')
163
164     def run(self, args: NominatimArgs) -> int:
165         asyncio.run(self.run_uvicorn(args))
166
167         return 0
168
169     async def run_uvicorn(self, args: NominatimArgs) -> None:
170         import uvicorn
171
172         server_info = args.server.split(':', 1)
173         host = server_info[0]
174         if len(server_info) > 1:
175             if not server_info[1].isdigit():
176                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
177             port = int(server_info[1])
178         else:
179             port = 8088
180
181         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
182
183         app = server_module.get_application(args.project_dir)
184
185         config = uvicorn.Config(app, host=host, port=port)
186         server = uvicorn.Server(config)
187         await server.serve()
188
189
190 def get_set_parser() -> CommandlineParser:
191     """\
192     Initializes the parser and adds various subcommands for
193     nominatim cli.
194     """
195     parser = CommandlineParser('nominatim', nominatim.__doc__)
196
197     parser.add_subcommand('import', clicmd.SetupAll())
198     parser.add_subcommand('freeze', clicmd.SetupFreeze())
199     parser.add_subcommand('replication', clicmd.UpdateReplication())
200
201     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
202
203     parser.add_subcommand('add-data', clicmd.UpdateAddData())
204     parser.add_subcommand('index', clicmd.UpdateIndex())
205     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
206
207     parser.add_subcommand('admin', clicmd.AdminFuncs())
208
209     try:
210         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
211         apicmd = importlib.import_module('nominatim_db.clicmd.api')
212         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
213
214         parser.add_subcommand('export', exportcmd.QueryExport())
215         parser.add_subcommand('convert', convertcmd.ConvertDB())
216         parser.add_subcommand('serve', AdminServe())
217
218         parser.add_subcommand('search', apicmd.APISearch())
219         parser.add_subcommand('reverse', apicmd.APIReverse())
220         parser.add_subcommand('lookup', apicmd.APILookup())
221         parser.add_subcommand('details', apicmd.APIDetails())
222         parser.add_subcommand('status', apicmd.APIStatus())
223     except ModuleNotFoundError as ex:
224         if not ex.name or 'nominatim_api' not in ex.name:
225             raise ex
226
227         parser.parser.epilog = \
228             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
229             '\nThe following commands are not available:'\
230             '\n    export, convert, serve, search, reverse, lookup, details, status'\
231             "\n\nRun 'pip install nominatim-api' to install the package."
232
233     return parser
234
235
236 def nominatim(**kwargs: Any) -> int:
237     """\
238     Command-line tools for importing, updating, administrating and
239     querying the Nominatim database.
240     """
241     return get_set_parser().run(**kwargs)