]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/cli.py
9a54f338e0f61daecf8fe9e0de0a85a9d53f48f0
[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(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             ret = args.command.run(args)
122
123             if args.config.TOKENIZER == 'legacy':
124                 log.warning('WARNING: the "legacy" tokenizer is deprecated '
125                             'and will be removed in Nominatim 5.0.')
126
127             return ret
128         except UsageError as exception:
129             if log.isEnabledFor(logging.DEBUG):
130                 raise # use Python's exception printing
131             log.fatal('FATAL: %s', exception)
132
133         # If we get here, then execution has failed in some way.
134         return 1
135
136
137 # Subcommand classes
138 #
139 # Each class needs to implement two functions: add_args() adds the CLI parameters
140 # for the subfunction, run() executes the subcommand.
141 #
142 # The class documentation doubles as the help text for the command. The
143 # first line is also used in the summary when calling the program without
144 # a subcommand.
145 #
146 # No need to document the functions each time.
147 # pylint: disable=C0111
148 class AdminServe:
149     """\
150     Start a simple web server for serving the API.
151
152     This command starts a built-in webserver to serve the website
153     from the current project directory. This webserver is only suitable
154     for testing and development. Do not use it in production setups!
155
156     There are two different webserver implementations for Python available:
157     falcon (the default) and starlette. You need to make sure the
158     appropriate Python packages as well as the uvicorn package are
159     installed to use this function.
160
161     By the default, the webserver can be accessed at: http://127.0.0.1:8088
162     """
163
164     def add_args(self, parser: argparse.ArgumentParser) -> None:
165         group = parser.add_argument_group('Server arguments')
166         group.add_argument('--server', default='127.0.0.1:8088',
167                            help='The address the server will listen to.')
168         group.add_argument('--engine', default='falcon',
169                            choices=('falcon', 'starlette'),
170                            help='Webserver framework to run. (default: falcon)')
171
172
173     def run(self, args: NominatimArgs) -> int:
174         asyncio.run(self.run_uvicorn(args))
175
176         return 0
177
178
179     async def run_uvicorn(self, args: NominatimArgs) -> None:
180         import uvicorn # pylint: disable=import-outside-toplevel
181
182         server_info = args.server.split(':', 1)
183         host = server_info[0]
184         if len(server_info) > 1:
185             if not server_info[1].isdigit():
186                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
187             port = int(server_info[1])
188         else:
189             port = 8088
190
191         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
192
193         app = server_module.get_application(args.project_dir)
194
195         config = uvicorn.Config(app, host=host, port=port)
196         server = uvicorn.Server(config)
197         await server.serve()
198
199
200 def get_set_parser() -> CommandlineParser:
201     """\
202     Initializes the parser and adds various subcommands for
203     nominatim cli.
204     """
205     parser = CommandlineParser('nominatim', nominatim.__doc__)
206
207     parser.add_subcommand('import', clicmd.SetupAll())
208     parser.add_subcommand('freeze', clicmd.SetupFreeze())
209     parser.add_subcommand('replication', clicmd.UpdateReplication())
210
211     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
212
213     parser.add_subcommand('add-data', clicmd.UpdateAddData())
214     parser.add_subcommand('index', clicmd.UpdateIndex())
215     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
216
217     parser.add_subcommand('admin', clicmd.AdminFuncs())
218
219     try:
220         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
221         apicmd = importlib.import_module('nominatim_db.clicmd.api')
222         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
223
224         parser.add_subcommand('export', exportcmd.QueryExport())
225         parser.add_subcommand('convert', convertcmd.ConvertDB())
226         parser.add_subcommand('serve', AdminServe())
227
228         parser.add_subcommand('search', apicmd.APISearch())
229         parser.add_subcommand('reverse', apicmd.APIReverse())
230         parser.add_subcommand('lookup', apicmd.APILookup())
231         parser.add_subcommand('details', apicmd.APIDetails())
232         parser.add_subcommand('status', apicmd.APIStatus())
233     except ModuleNotFoundError as ex:
234         if not ex.name or 'nominatim_api' not in ex.name: # pylint: disable=E1135
235             raise ex
236
237         parser.parser.epilog = \
238             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
239             '\nThe following commands are not available:'\
240             '\n    export, convert, serve, search, reverse, lookup, details, status'\
241             "\n\nRun 'pip install nominatim-api' to install the package."
242
243
244     return parser
245
246
247 def nominatim(**kwargs: Any) -> int:
248     """\
249     Command-line tools for importing, updating, administrating and
250     querying the Nominatim database.
251     """
252     return get_set_parser().run(**kwargs)