2 Command-line interface to the Nominatim functions for import, update,
 
   3 database administration and querying.
 
   9 from pathlib import Path
 
  11 from .config import Configuration
 
  12 from .tools.exec_utils import run_legacy_script, run_php_server
 
  13 from .errors import UsageError
 
  15 from .clicmd.args import NominatimArgs
 
  17 LOG = logging.getLogger()
 
  20 class CommandlineParser:
 
  21     """ Wraps some of the common functions for parsing the command line
 
  22         and setting up subcommands.
 
  24     def __init__(self, prog, description):
 
  25         self.parser = argparse.ArgumentParser(
 
  27             description=description,
 
  28             formatter_class=argparse.RawDescriptionHelpFormatter)
 
  30         self.subs = self.parser.add_subparsers(title='available commands',
 
  33         # Arguments added to every sub-command
 
  34         self.default_args = argparse.ArgumentParser(add_help=False)
 
  35         group = self.default_args.add_argument_group('Default arguments')
 
  36         group.add_argument('-h', '--help', action='help',
 
  37                            help='Show this help message and exit')
 
  38         group.add_argument('-q', '--quiet', action='store_const', const=0,
 
  39                            dest='verbose', default=1,
 
  40                            help='Print only error messages')
 
  41         group.add_argument('-v', '--verbose', action='count', default=1,
 
  42                            help='Increase verboseness of output')
 
  43         group.add_argument('--project-dir', metavar='DIR', default='.',
 
  44                            help='Base directory of the Nominatim installation (default:.)')
 
  45         group.add_argument('-j', '--threads', metavar='NUM', type=int,
 
  46                            help='Number of parallel threads to use')
 
  49     def add_subcommand(self, name, cmd):
 
  50         """ Add a subcommand to the parser. The subcommand must be a class
 
  51             with a function add_args() that adds the parameters for the
 
  52             subcommand and a run() function that executes the command.
 
  54         parser = self.subs.add_parser(name, parents=[self.default_args],
 
  55                                       help=cmd.__doc__.split('\n', 1)[0],
 
  56                                       description=cmd.__doc__,
 
  57                                       formatter_class=argparse.RawDescriptionHelpFormatter,
 
  59         parser.set_defaults(command=cmd)
 
  62     def run(self, **kwargs):
 
  63         """ Parse the command line arguments of the program and execute the
 
  64             appropriate subcommand.
 
  66         args = NominatimArgs()
 
  67         self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
 
  69         if args.subcommand is None:
 
  70             self.parser.print_help()
 
  73         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'sqllib_dir',
 
  74                     'data_dir', 'config_dir', 'phpcgi_path'):
 
  75             setattr(args, arg, Path(kwargs[arg]))
 
  76         args.project_dir = Path(args.project_dir).resolve()
 
  78         if 'cli_args' not in kwargs:
 
  79             logging.basicConfig(stream=sys.stderr,
 
  80                                 format='%(asctime)s: %(message)s',
 
  81                                 datefmt='%Y-%m-%d %H:%M:%S',
 
  82                                 level=max(4 - args.verbose, 1) * 10)
 
  84         args.config = Configuration(args.project_dir, args.config_dir,
 
  85                                     environ=kwargs.get('environ', os.environ))
 
  87         log = logging.getLogger()
 
  88         log.warning('Using project directory: %s', str(args.project_dir))
 
  91             return args.command.run(args)
 
  92         except UsageError as exception:
 
  93             if log.isEnabledFor(logging.DEBUG):
 
  94                 raise # use Python's exception printing
 
  95             log.fatal('FATAL: %s', exception)
 
  97         # If we get here, then execution has failed in some way.
 
 101 ##### Subcommand classes
 
 103 # Each class needs to implement two functions: add_args() adds the CLI parameters
 
 104 # for the subfunction, run() executes the subcommand.
 
 106 # The class documentation doubles as the help text for the command. The
 
 107 # first line is also used in the summary when calling the program without
 
 110 # No need to document the functions each time.
 
 111 # pylint: disable=C0111
 
 112 # Using non-top-level imports to make pyosmium optional for replication only.
 
 113 # pylint: disable=E0012,C0415
 
 116 class SetupSpecialPhrases:
 
 118     Maintain special phrases.
 
 122     def add_args(parser):
 
 123         group = parser.add_argument_group('Input arguments')
 
 124         group.add_argument('--from-wiki', action='store_true',
 
 125                            help='Pull special phrases from the OSM wiki.')
 
 126         group = parser.add_argument_group('Output arguments')
 
 127         group.add_argument('-o', '--output', default='-',
 
 128                            help="""File to write the preprocessed phrases to.
 
 129                                    If omitted, it will be written to stdout.""")
 
 133         if args.output != '-':
 
 134             raise NotImplementedError('Only output to stdout is currently implemented.')
 
 135         return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
 
 140     Add additional data from a file or an online source.
 
 142     Data is only imported, not indexed. You need to call `nominatim-update index`
 
 143     to complete the process.
 
 147     def add_args(parser):
 
 148         group_name = parser.add_argument_group('Source')
 
 149         group = group_name.add_mutually_exclusive_group(required=True)
 
 150         group.add_argument('--file', metavar='FILE',
 
 151                            help='Import data from an OSM file')
 
 152         group.add_argument('--diff', metavar='FILE',
 
 153                            help='Import data from an OSM diff file')
 
 154         group.add_argument('--node', metavar='ID', type=int,
 
 155                            help='Import a single node from the API')
 
 156         group.add_argument('--way', metavar='ID', type=int,
 
 157                            help='Import a single way from the API')
 
 158         group.add_argument('--relation', metavar='ID', type=int,
 
 159                            help='Import a single relation from the API')
 
 160         group.add_argument('--tiger-data', metavar='DIR',
 
 161                            help='Add housenumbers from the US TIGER census database.')
 
 162         group = parser.add_argument_group('Extra arguments')
 
 163         group.add_argument('--use-main-api', action='store_true',
 
 164                            help='Use OSM API instead of Overpass to download objects')
 
 169             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
 
 170             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
 
 172         params = ['update.php']
 
 174             params.extend(('--import-file', args.file))
 
 176             params.extend(('--import-diff', args.diff))
 
 178             params.extend(('--import-node', args.node))
 
 180             params.extend(('--import-way', args.way))
 
 182             params.extend(('--import-relation', args.relation))
 
 183         if args.use_main_api:
 
 184             params.append('--use-main-api')
 
 185         return run_legacy_script(*params, nominatim_env=args)
 
 190     Export addresses as CSV file from the database.
 
 194     def add_args(parser):
 
 195         group = parser.add_argument_group('Output arguments')
 
 196         group.add_argument('--output-type', default='street',
 
 197                            choices=('continent', 'country', 'state', 'county',
 
 198                                     'city', 'suburb', 'street', 'path'),
 
 199                            help='Type of places to output (default: street)')
 
 200         group.add_argument('--output-format',
 
 201                            default='street;suburb;city;county;state;country',
 
 202                            help="""Semicolon-separated list of address types
 
 203                                    (see --output-type). Multiple ranks can be
 
 204                                    merged into one column by simply using a
 
 205                                    comma-separated list.""")
 
 206         group.add_argument('--output-all-postcodes', action='store_true',
 
 207                            help="""List all postcodes for address instead of
 
 208                                    just the most likely one""")
 
 209         group.add_argument('--language',
 
 210                            help="""Preferred language for output
 
 211                                    (use local name, if omitted)""")
 
 212         group = parser.add_argument_group('Filter arguments')
 
 213         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
 
 214                            help='Export only objects within country')
 
 215         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
 
 216                            help='Export only children of this OSM node')
 
 217         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
 
 218                            help='Export only children of this OSM way')
 
 219         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
 
 220                            help='Export only children of this OSM relation')
 
 225         params = ['export.php',
 
 226                   '--output-type', args.output_type,
 
 227                   '--output-format', args.output_format]
 
 228         if args.output_all_postcodes:
 
 229             params.append('--output-all-postcodes')
 
 231             params.extend(('--language', args.language))
 
 232         if args.restrict_to_country:
 
 233             params.extend(('--restrict-to-country', args.restrict_to_country))
 
 234         if args.restrict_to_osm_node:
 
 235             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
 
 236         if args.restrict_to_osm_way:
 
 237             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
 
 238         if args.restrict_to_osm_relation:
 
 239             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
 
 241         return run_legacy_script(*params, nominatim_env=args)
 
 246     Start a simple web server for serving the API.
 
 248     This command starts the built-in PHP webserver to serve the website
 
 249     from the current project directory. This webserver is only suitable
 
 250     for testing and develop. Do not use it in production setups!
 
 252     By the default, the webserver can be accessed at: http://127.0.0.1:8088
 
 256     def add_args(parser):
 
 257         group = parser.add_argument_group('Server arguments')
 
 258         group.add_argument('--server', default='127.0.0.1:8088',
 
 259                            help='The address the server will listen to.')
 
 263         run_php_server(args.server, args.project_dir / 'website')
 
 266 def nominatim(**kwargs):
 
 268     Command-line tools for importing, updating, administrating and
 
 269     querying the Nominatim database.
 
 271     parser = CommandlineParser('nominatim', nominatim.__doc__)
 
 273     parser.add_subcommand('import', clicmd.SetupAll)
 
 274     parser.add_subcommand('freeze', clicmd.SetupFreeze)
 
 275     parser.add_subcommand('replication', clicmd.UpdateReplication)
 
 277     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
 
 279     parser.add_subcommand('add-data', UpdateAddData)
 
 280     parser.add_subcommand('index', clicmd.UpdateIndex)
 
 281     parser.add_subcommand('refresh', clicmd.UpdateRefresh)
 
 283     parser.add_subcommand('admin', clicmd.AdminFuncs)
 
 285     parser.add_subcommand('export', QueryExport)
 
 286     parser.add_subcommand('serve', AdminServe)
 
 288     if kwargs.get('phpcgi_path'):
 
 289         parser.add_subcommand('search', clicmd.APISearch)
 
 290         parser.add_subcommand('reverse', clicmd.APIReverse)
 
 291         parser.add_subcommand('lookup', clicmd.APILookup)
 
 292         parser.add_subcommand('details', clicmd.APIDetails)
 
 293         parser.add_subcommand('status', clicmd.APIStatus)
 
 295         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
 
 297     parser.add_subcommand('transition', clicmd.AdminTransition)
 
 299     return parser.run(**kwargs)