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
17 from pathlib import Path
19 from nominatim_core.config import Configuration
20 from nominatim_core.errors import UsageError
21 from .tools.exec_utils import run_php_server
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 return args.command.run(args)
122 except UsageError as exception:
123 if log.isEnabledFor(logging.DEBUG):
124 raise # use Python's exception printing
125 log.fatal('FATAL: %s', exception)
127 # If we get here, then execution has failed in some way.
133 # Each class needs to implement two functions: add_args() adds the CLI parameters
134 # for the subfunction, run() executes the subcommand.
136 # The class documentation doubles as the help text for the command. The
137 # first line is also used in the summary when calling the program without
140 # No need to document the functions each time.
141 # pylint: disable=C0111
144 Start a simple web server for serving the API.
146 This command starts a built-in webserver to serve the website
147 from the current project directory. This webserver is only suitable
148 for testing and development. Do not use it in production setups!
150 There are different webservers available. The default 'php' engine
151 runs the classic PHP frontend. The other engines are Python servers
152 which run the new Python frontend code. This is highly experimental
153 at the moment and may not include the full API.
155 By the default, the webserver can be accessed at: http://127.0.0.1:8088
158 def add_args(self, parser: argparse.ArgumentParser) -> None:
159 group = parser.add_argument_group('Server arguments')
160 group.add_argument('--server', default='127.0.0.1:8088',
161 help='The address the server will listen to.')
162 group.add_argument('--engine', default='falcon',
163 choices=('php', 'falcon', 'starlette'),
164 help='Webserver framework to run. (default: falcon)')
167 def run(self, args: NominatimArgs) -> int:
168 if args.engine == 'php':
169 if args.config.lib_dir.php is None:
170 raise UsageError("PHP frontend not configured.")
171 run_php_server(args.server, args.project_dir / 'website')
173 import uvicorn # pylint: disable=import-outside-toplevel
174 server_info = args.server.split(':', 1)
175 host = server_info[0]
176 if len(server_info) > 1:
177 if not server_info[1].isdigit():
178 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
179 port = int(server_info[1])
183 server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
185 app = server_module.get_application(args.project_dir)
186 uvicorn.run(app, host=host, port=port)
191 def get_set_parser() -> CommandlineParser:
193 Initializes the parser and adds various subcommands for
196 parser = CommandlineParser('nominatim', nominatim.__doc__)
198 parser.add_subcommand('import', clicmd.SetupAll())
199 parser.add_subcommand('freeze', clicmd.SetupFreeze())
200 parser.add_subcommand('replication', clicmd.UpdateReplication())
202 parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
204 parser.add_subcommand('add-data', clicmd.UpdateAddData())
205 parser.add_subcommand('index', clicmd.UpdateIndex())
206 parser.add_subcommand('refresh', clicmd.UpdateRefresh())
208 parser.add_subcommand('admin', clicmd.AdminFuncs())
211 exportcmd = importlib.import_module('nominatim_db.clicmd.export')
212 apicmd = importlib.import_module('nominatim_db.clicmd.api')
213 convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
215 parser.add_subcommand('export', exportcmd.QueryExport())
216 parser.add_subcommand('convert', convertcmd.ConvertDB())
217 parser.add_subcommand('serve', AdminServe())
219 parser.add_subcommand('search', apicmd.APISearch())
220 parser.add_subcommand('reverse', apicmd.APIReverse())
221 parser.add_subcommand('lookup', apicmd.APILookup())
222 parser.add_subcommand('details', apicmd.APIDetails())
223 parser.add_subcommand('status', apicmd.APIStatus())
224 except ModuleNotFoundError as ex:
225 if not ex.name or 'nominatim_api' not in ex.name: # pylint: disable=E1135
228 parser.parser.epilog = \
229 '\n\nNominatim API package not found. The 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."
237 def nominatim(**kwargs: Any) -> int:
239 Command-line tools for importing, updating, administrating and
240 querying the Nominatim database.
242 return get_set_parser().run(**kwargs)