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
22 from .tools.exec_utils import run_php_server
25 from .clicmd.args import NominatimArgs, Subcommand
27 LOG = logging.getLogger()
29 class CommandlineParser:
30 """ Wraps some of the common functions for parsing the command line
31 and setting up subcommands.
33 def __init__(self, prog: str, description: Optional[str]):
34 self.parser = argparse.ArgumentParser(
36 description=description,
37 formatter_class=argparse.RawDescriptionHelpFormatter)
39 self.subs = self.parser.add_subparsers(title='available commands',
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')
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')
62 def nominatim_version_text(self) -> str:
63 """ Program name and version number as string
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})'
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.
76 assert cmd.__doc__ is not None
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,
83 parser.set_defaults(command=cmd)
87 def run(self, **kwargs: Any) -> int:
88 """ Parse the command line arguments of the program and execute the
89 appropriate subcommand.
91 args = NominatimArgs()
93 self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
98 print(self.nominatim_version_text())
101 if args.subcommand is None:
102 self.parser.print_help()
105 args.project_dir = Path(args.project_dir).resolve()
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)
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'])
118 log = logging.getLogger()
119 log.warning('Using project directory: %s', str(args.project_dir))
122 ret = args.command.run(args)
124 if args.config.TOKENIZER == 'legacy':
125 log.warning('WARNING: the "legacy" tokenizer is deprecated '
126 'and will be removed in Nominatim 5.0.')
129 except UsageError as exception:
130 if log.isEnabledFor(logging.DEBUG):
131 raise # use Python's exception printing
132 log.fatal('FATAL: %s', exception)
134 # If we get here, then execution has failed in some way.
140 # Each class needs to implement two functions: add_args() adds the CLI parameters
141 # for the subfunction, run() executes the subcommand.
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
147 # No need to document the functions each time.
148 # pylint: disable=C0111
151 Start a simple web server for serving the API.
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!
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.
162 By the default, the webserver can be accessed at: http://127.0.0.1:8088
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)')
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')
182 asyncio.run(self.run_uvicorn(args))
187 async def run_uvicorn(self, args: NominatimArgs) -> None:
188 import uvicorn # pylint: disable=import-outside-toplevel
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])
199 server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
201 app = server_module.get_application(args.project_dir)
203 config = uvicorn.Config(app, host=host, port=port)
204 server = uvicorn.Server(config)
208 def get_set_parser() -> CommandlineParser:
210 Initializes the parser and adds various subcommands for
213 parser = CommandlineParser('nominatim', nominatim.__doc__)
215 parser.add_subcommand('import', clicmd.SetupAll())
216 parser.add_subcommand('freeze', clicmd.SetupFreeze())
217 parser.add_subcommand('replication', clicmd.UpdateReplication())
219 parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
221 parser.add_subcommand('add-data', clicmd.UpdateAddData())
222 parser.add_subcommand('index', clicmd.UpdateIndex())
223 parser.add_subcommand('refresh', clicmd.UpdateRefresh())
225 parser.add_subcommand('admin', clicmd.AdminFuncs())
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')
232 parser.add_subcommand('export', exportcmd.QueryExport())
233 parser.add_subcommand('convert', convertcmd.ConvertDB())
234 parser.add_subcommand('serve', AdminServe())
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
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."
255 def nominatim(**kwargs: Any) -> int:
257 Command-line tools for importing, updating, administrating and
258 querying the Nominatim database.
260 return get_set_parser().run(**kwargs)