1 # SPDX-License-Identifier: GPL-2.0-only
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2023 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.config import Configuration
 
  20 from nominatim.tools.exec_utils import run_php_server
 
  21 from nominatim.errors import UsageError
 
  22 from nominatim import clicmd
 
  23 from nominatim import version
 
  24 from nominatim.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='php',
 
 163                            choices=('php', 'falcon', 'starlette'),
 
 164                            help='Webserver framework to run. (default: php)')
 
 167     def run(self, args: NominatimArgs) -> int:
 
 168         if args.engine == 'php':
 
 169             run_php_server(args.server, args.project_dir / 'website')
 
 171             import uvicorn # pylint: disable=import-outside-toplevel
 
 172             server_info = args.server.split(':', 1)
 
 173             host = server_info[0]
 
 174             if len(server_info) > 1:
 
 175                 if not server_info[1].isdigit():
 
 176                     raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
 
 177                 port = int(server_info[1])
 
 181             server_module = importlib.import_module(f'nominatim.server.{args.engine}.server')
 
 183             app = server_module.get_application(args.project_dir)
 
 184             uvicorn.run(app, host=host, port=port)
 
 189 def get_set_parser() -> CommandlineParser:
 
 191     Initializes the parser and adds various subcommands for
 
 194     parser = CommandlineParser('nominatim', nominatim.__doc__)
 
 196     parser.add_subcommand('import', clicmd.SetupAll())
 
 197     parser.add_subcommand('freeze', clicmd.SetupFreeze())
 
 198     parser.add_subcommand('replication', clicmd.UpdateReplication())
 
 200     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
 
 202     parser.add_subcommand('add-data', clicmd.UpdateAddData())
 
 203     parser.add_subcommand('index', clicmd.UpdateIndex())
 
 204     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
 
 206     parser.add_subcommand('admin', clicmd.AdminFuncs())
 
 208     parser.add_subcommand('export', clicmd.QueryExport())
 
 209     parser.add_subcommand('serve', AdminServe())
 
 211     parser.add_subcommand('search', clicmd.APISearch())
 
 212     parser.add_subcommand('reverse', clicmd.APIReverse())
 
 213     parser.add_subcommand('lookup', clicmd.APILookup())
 
 214     parser.add_subcommand('details', clicmd.APIDetails())
 
 215     parser.add_subcommand('status', clicmd.APIStatus())
 
 220 def nominatim(**kwargs: Any) -> int:
 
 222     Command-line tools for importing, updating, administrating and
 
 223     querying the Nominatim database.
 
 225     return get_set_parser().run(**kwargs)