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