1 # SPDX-License-Identifier: GPL-3.0-or-later
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Command-line interface to the Nominatim functions for import, update,
9 database administration and querying.
11 from typing import Optional, Any
18 from pathlib import Path
20 from .config import Configuration
21 from .errors import UsageError
24 from .clicmd.args import NominatimArgs, Subcommand
26 LOG = logging.getLogger()
28 class CommandlineParser:
29 """ Wraps some of the common functions for parsing the command line
30 and setting up subcommands.
32 def __init__(self, prog: str, description: Optional[str]):
33 self.parser = argparse.ArgumentParser(
35 description=description,
36 formatter_class=argparse.RawDescriptionHelpFormatter)
38 self.subs = self.parser.add_subparsers(title='available commands',
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')
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')
61 def nominatim_version_text(self) -> str:
62 """ Program name and version number as string
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})'
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.
75 assert cmd.__doc__ is not None
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,
82 parser.set_defaults(command=cmd)
86 def run(self, **kwargs: Any) -> int:
87 """ Parse the command line arguments of the program and execute the
88 appropriate subcommand.
90 args = NominatimArgs()
92 self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
97 print(self.nominatim_version_text())
100 if args.subcommand is None:
101 self.parser.print_help()
104 args.project_dir = Path(args.project_dir).resolve()
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)
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'])
117 log = logging.getLogger()
118 log.warning('Using project directory: %s', str(args.project_dir))
121 ret = args.command.run(args)
123 if args.config.TOKENIZER == 'legacy':
124 log.warning('WARNING: the "legacy" tokenizer is deprecated '
125 'and will be removed in Nominatim 5.0.')
128 except UsageError as exception:
129 if log.isEnabledFor(logging.DEBUG):
130 raise # use Python's exception printing
131 log.fatal('FATAL: %s', exception)
133 # If we get here, then execution has failed in some way.
139 # Each class needs to implement two functions: add_args() adds the CLI parameters
140 # for the subfunction, run() executes the subcommand.
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
146 # No need to document the functions each time.
147 # pylint: disable=C0111
150 Start a simple web server for serving the API.
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!
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.
161 By the default, the webserver can be accessed at: http://127.0.0.1:8088
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)')
173 def run(self, args: NominatimArgs) -> int:
174 asyncio.run(self.run_uvicorn(args))
179 async def run_uvicorn(self, args: NominatimArgs) -> None:
180 import uvicorn # pylint: disable=import-outside-toplevel
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])
191 server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
193 app = server_module.get_application(args.project_dir)
195 config = uvicorn.Config(app, host=host, port=port)
196 server = uvicorn.Server(config)
200 def get_set_parser() -> CommandlineParser:
202 Initializes the parser and adds various subcommands for
205 parser = CommandlineParser('nominatim', nominatim.__doc__)
207 parser.add_subcommand('import', clicmd.SetupAll())
208 parser.add_subcommand('freeze', clicmd.SetupFreeze())
209 parser.add_subcommand('replication', clicmd.UpdateReplication())
211 parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
213 parser.add_subcommand('add-data', clicmd.UpdateAddData())
214 parser.add_subcommand('index', clicmd.UpdateIndex())
215 parser.add_subcommand('refresh', clicmd.UpdateRefresh())
217 parser.add_subcommand('admin', clicmd.AdminFuncs())
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')
224 parser.add_subcommand('export', exportcmd.QueryExport())
225 parser.add_subcommand('convert', convertcmd.ConvertDB())
226 parser.add_subcommand('serve', AdminServe())
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
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."
247 def nominatim(**kwargs: Any) -> int:
249 Command-line tools for importing, updating, administrating and
250 querying the Nominatim database.
252 return get_set_parser().run(**kwargs)