1 # SPDX-License-Identifier: GPL-3.0-or-later
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2025 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, List, Mapping
 
  17 from pathlib import Path
 
  19 from .config import Configuration
 
  20 from .errors import UsageError
 
  23 from .clicmd.args import NominatimArgs, Subcommand
 
  25 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')
 
  60     def nominatim_version_text(self) -> str:
 
  61         """ Program name and version number as string
 
  63         text = f'Nominatim version {version.NOMINATIM_VERSION!s}'
 
  64         if version.GIT_COMMIT_HASH is not None:
 
  65             text += f' ({version.GIT_COMMIT_HASH})'
 
  68     def add_subcommand(self, name: str, cmd: Subcommand) -> None:
 
  69         """ Add a subcommand to the parser. The subcommand must be a class
 
  70             with a function add_args() that adds the parameters for the
 
  71             subcommand and a run() function that executes the command.
 
  73         assert cmd.__doc__ is not None
 
  75         parser = self.subs.add_parser(name, parents=[self.default_args],
 
  76                                       help=cmd.__doc__.split('\n', 1)[0],
 
  77                                       description=cmd.__doc__,
 
  78                                       formatter_class=argparse.RawDescriptionHelpFormatter,
 
  80         parser.set_defaults(command=cmd)
 
  83     def run(self, cli_args: Optional[List[str]],
 
  84             environ: Optional[Mapping[str, str]]) -> int:
 
  85         """ Parse the command line arguments of the program and execute the
 
  86             appropriate subcommand.
 
  88         args = NominatimArgs()
 
  90             self.parser.parse_args(args=cli_args, namespace=args)
 
  95             print(self.nominatim_version_text())
 
  98         if args.subcommand is None:
 
  99             self.parser.print_help()
 
 102         args.project_dir = Path(args.project_dir).resolve()
 
 105             logging.basicConfig(stream=sys.stderr,
 
 106                                 format='%(asctime)s: %(message)s',
 
 107                                 datefmt='%Y-%m-%d %H:%M:%S',
 
 108                                 level=max(4 - args.verbose, 1) * 10)
 
 110         args.config = Configuration(args.project_dir, environ=environ)
 
 112         log = logging.getLogger()
 
 113         log.warning('Using project directory: %s', str(args.project_dir))
 
 116             return args.command.run(args)
 
 117         except UsageError as exception:
 
 118             if log.isEnabledFor(logging.DEBUG):
 
 119                 raise  # use Python's exception printing
 
 120             log.fatal('FATAL: %s', exception)
 
 122         # If we get here, then execution has failed in some way.
 
 128 # Each class needs to implement two functions: add_args() adds the CLI parameters
 
 129 # for the subfunction, run() executes the subcommand.
 
 131 # The class documentation doubles as the help text for the command. The
 
 132 # first line is also used in the summary when calling the program without
 
 135 # No need to document the functions each time.
 
 138     Start a simple web server for serving the API.
 
 140     This command starts a built-in webserver to serve the website
 
 141     from the current project directory. This webserver is only suitable
 
 142     for testing and development. Do not use it in production setups!
 
 144     There are two different webserver implementations for Python available:
 
 145     falcon (the default) and starlette. You need to make sure the
 
 146     appropriate Python packages as well as the uvicorn package are
 
 147     installed to use this function.
 
 149     By the default, the webserver can be accessed at: http://127.0.0.1:8088
 
 152     def add_args(self, parser: argparse.ArgumentParser) -> None:
 
 153         group = parser.add_argument_group('Server arguments')
 
 154         group.add_argument('--server', default='127.0.0.1:8088',
 
 155                            help='The address the server will listen to.')
 
 156         group.add_argument('--engine', default='falcon',
 
 157                            choices=('falcon', 'starlette'),
 
 158                            help='Webserver framework to run. (default: falcon)')
 
 160     def run(self, args: NominatimArgs) -> int:
 
 161         asyncio.run(self.run_uvicorn(args))
 
 165     async def run_uvicorn(self, args: NominatimArgs) -> None:
 
 168         server_info = args.server.split(':', 1)
 
 169         host = server_info[0]
 
 170         if len(server_info) > 1:
 
 171             if not server_info[1].isdigit():
 
 172                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
 
 173             port = int(server_info[1])
 
 177         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
 
 179         app = server_module.get_application(args.project_dir)
 
 181         config = uvicorn.Config(app, host=host, port=port)
 
 182         server = uvicorn.Server(config)
 
 186 def get_set_parser() -> CommandlineParser:
 
 188     Initializes the parser and adds various subcommands for
 
 191     parser = CommandlineParser('nominatim', nominatim.__doc__)
 
 193     parser.add_subcommand('import', clicmd.SetupAll())
 
 194     parser.add_subcommand('freeze', clicmd.SetupFreeze())
 
 195     parser.add_subcommand('replication', clicmd.UpdateReplication())
 
 197     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
 
 199     parser.add_subcommand('add-data', clicmd.UpdateAddData())
 
 200     parser.add_subcommand('index', clicmd.UpdateIndex())
 
 201     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
 
 203     parser.add_subcommand('admin', clicmd.AdminFuncs())
 
 206         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
 
 207         apicmd = importlib.import_module('nominatim_db.clicmd.api')
 
 208         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
 
 210         parser.add_subcommand('export', exportcmd.QueryExport())
 
 211         parser.add_subcommand('convert', convertcmd.ConvertDB())
 
 212         parser.add_subcommand('serve', AdminServe())
 
 214         parser.add_subcommand('search', apicmd.APISearch())
 
 215         parser.add_subcommand('reverse', apicmd.APIReverse())
 
 216         parser.add_subcommand('lookup', apicmd.APILookup())
 
 217         parser.add_subcommand('details', apicmd.APIDetails())
 
 218         parser.add_subcommand('status', apicmd.APIStatus())
 
 219     except ModuleNotFoundError as ex:
 
 220         if not ex.name or 'nominatim_api' not in ex.name:
 
 223         parser.parser.epilog = \
 
 224             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
 
 225             '\nThe following commands are not available:'\
 
 226             '\n    export, convert, serve, search, reverse, lookup, details, status'\
 
 227             "\n\nRun 'pip install nominatim-api' to install the package."
 
 232 def nominatim(cli_args: Optional[List[str]] = None,
 
 233               environ: Optional[Mapping[str, str]] = None) -> int:
 
 235     Command-line tools for importing, updating, administrating and
 
 236     querying the Nominatim database.
 
 238     'cli_args' is a list of parameters for the command to run. If not given,
 
 239     sys.args will be used.
 
 241     'environ' is the dictionary of environment variables containing the
 
 242     Nominatim configuration. When None, the os.environ is inherited.
 
 244     return get_set_parser().run(cli_args=cli_args, environ=environ)