]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/cli.py
Merge remote-tracking branch 'upstream/master'
[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 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(osm2pgsql=kwargs['osm2pgsql_path'])
115
116         log = logging.getLogger()
117         log.warning('Using project directory: %s', str(args.project_dir))
118
119         try:
120             ret = args.command.run(args)
121
122             return ret
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 AdminServe:
144     """\
145     Start a simple web server for serving the API.
146
147     This command starts a built-in webserver to serve the website
148     from the current project directory. This webserver is only suitable
149     for testing and development. Do not use it in production setups!
150
151     There are two different webserver implementations for Python available:
152     falcon (the default) and starlette. You need to make sure the
153     appropriate Python packages as well as the uvicorn package are
154     installed to use this function.
155
156     By the default, the webserver can be accessed at: http://127.0.0.1:8088
157     """
158
159     def add_args(self, parser: argparse.ArgumentParser) -> None:
160         group = parser.add_argument_group('Server arguments')
161         group.add_argument('--server', default='127.0.0.1:8088',
162                            help='The address the server will listen to.')
163         group.add_argument('--engine', default='falcon',
164                            choices=('falcon', 'starlette'),
165                            help='Webserver framework to run. (default: falcon)')
166
167
168     def run(self, args: NominatimArgs) -> int:
169         asyncio.run(self.run_uvicorn(args))
170
171         return 0
172
173
174     async def run_uvicorn(self, args: NominatimArgs) -> None:
175         import uvicorn # pylint: disable=import-outside-toplevel
176
177         server_info = args.server.split(':', 1)
178         host = server_info[0]
179         if len(server_info) > 1:
180             if not server_info[1].isdigit():
181                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
182             port = int(server_info[1])
183         else:
184             port = 8088
185
186         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
187
188         app = server_module.get_application(args.project_dir)
189
190         config = uvicorn.Config(app, host=host, port=port)
191         server = uvicorn.Server(config)
192         await server.serve()
193
194
195 def get_set_parser() -> CommandlineParser:
196     """\
197     Initializes the parser and adds various subcommands for
198     nominatim cli.
199     """
200     parser = CommandlineParser('nominatim', nominatim.__doc__)
201
202     parser.add_subcommand('import', clicmd.SetupAll())
203     parser.add_subcommand('freeze', clicmd.SetupFreeze())
204     parser.add_subcommand('replication', clicmd.UpdateReplication())
205
206     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
207
208     parser.add_subcommand('add-data', clicmd.UpdateAddData())
209     parser.add_subcommand('index', clicmd.UpdateIndex())
210     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
211
212     parser.add_subcommand('admin', clicmd.AdminFuncs())
213
214     try:
215         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
216         apicmd = importlib.import_module('nominatim_db.clicmd.api')
217         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
218
219         parser.add_subcommand('export', exportcmd.QueryExport())
220         parser.add_subcommand('convert', convertcmd.ConvertDB())
221         parser.add_subcommand('serve', AdminServe())
222
223         parser.add_subcommand('search', apicmd.APISearch())
224         parser.add_subcommand('reverse', apicmd.APIReverse())
225         parser.add_subcommand('lookup', apicmd.APILookup())
226         parser.add_subcommand('details', apicmd.APIDetails())
227         parser.add_subcommand('status', apicmd.APIStatus())
228     except ModuleNotFoundError as ex:
229         if not ex.name or 'nominatim_api' not in ex.name: # pylint: disable=E1135
230             raise ex
231
232         parser.parser.epilog = \
233             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
234             '\nThe following commands are not available:'\
235             '\n    export, convert, serve, search, reverse, lookup, details, status'\
236             "\n\nRun 'pip install nominatim-api' to install the package."
237
238
239     return parser
240
241
242 def nominatim(**kwargs: Any) -> int:
243     """\
244     Command-line tools for importing, updating, administrating and
245     querying the Nominatim database.
246     """
247     return get_set_parser().run(**kwargs)