]> 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
16 LOG = logging.getLogger()
17
18
19 class CommandlineParser:
20     """ Wraps some of the common functions for parsing the command line
21         and setting up subcommands.
22     """
23     def __init__(self, prog, description):
24         self.parser = argparse.ArgumentParser(
25             prog=prog,
26             description=description,
27             formatter_class=argparse.RawDescriptionHelpFormatter)
28
29         self.subs = self.parser.add_subparsers(title='available commands',
30                                                dest='subcommand')
31
32         # Arguments added to every sub-command
33         self.default_args = argparse.ArgumentParser(add_help=False)
34         group = self.default_args.add_argument_group('Default arguments')
35         group.add_argument('-h', '--help', action='help',
36                            help='Show this help message and exit')
37         group.add_argument('-q', '--quiet', action='store_const', const=0,
38                            dest='verbose', default=1,
39                            help='Print only error messages')
40         group.add_argument('-v', '--verbose', action='count', default=1,
41                            help='Increase verboseness of output')
42         group.add_argument('--project-dir', metavar='DIR', default='.',
43                            help='Base directory of the Nominatim installation (default:.)')
44         group.add_argument('-j', '--threads', metavar='NUM', type=int,
45                            help='Number of parallel threads to use')
46
47
48     def add_subcommand(self, name, cmd):
49         """ Add a subcommand to the parser. The subcommand must be a class
50             with a function add_args() that adds the parameters for the
51             subcommand and a run() function that executes the command.
52         """
53         parser = self.subs.add_parser(name, parents=[self.default_args],
54                                       help=cmd.__doc__.split('\n', 1)[0],
55                                       description=cmd.__doc__,
56                                       formatter_class=argparse.RawDescriptionHelpFormatter,
57                                       add_help=False)
58         parser.set_defaults(command=cmd)
59         cmd.add_args(parser)
60
61     def run(self, **kwargs):
62         """ Parse the command line arguments of the program and execute the
63             appropriate subcommand.
64         """
65         args = self.parser.parse_args(args=kwargs.get('cli_args'))
66
67         if args.subcommand is None:
68             self.parser.print_help()
69             return 1
70
71         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'sqllib_dir',
72                     'data_dir', 'config_dir', 'phpcgi_path'):
73             setattr(args, arg, Path(kwargs[arg]))
74         args.project_dir = Path(args.project_dir).resolve()
75
76         logging.basicConfig(stream=sys.stderr,
77                             format='%(asctime)s: %(message)s',
78                             datefmt='%Y-%m-%d %H:%M:%S',
79                             level=max(4 - args.verbose, 1) * 10)
80
81         args.config = Configuration(args.project_dir, args.config_dir)
82
83         log = logging.getLogger()
84         log.warning('Using project directory: %s', str(args.project_dir))
85
86         try:
87             return args.command.run(args)
88         except UsageError as exception:
89             if log.isEnabledFor(logging.DEBUG):
90                 raise # use Python's exception printing
91             log.fatal('FATAL: %s', exception)
92
93         # If we get here, then execution has failed in some way.
94         return 1
95
96
97 ##### Subcommand classes
98 #
99 # Each class needs to implement two functions: add_args() adds the CLI parameters
100 # for the subfunction, run() executes the subcommand.
101 #
102 # The class documentation doubles as the help text for the command. The
103 # first line is also used in the summary when calling the program without
104 # a subcommand.
105 #
106 # No need to document the functions each time.
107 # pylint: disable=C0111
108 # Using non-top-level imports to make pyosmium optional for replication only.
109 # pylint: disable=E0012,C0415
110
111
112 class SetupAll:
113     """\
114     Create a new Nominatim database from an OSM file.
115     """
116
117     @staticmethod
118     def add_args(parser):
119         group_name = parser.add_argument_group('Required arguments')
120         group = group_name.add_mutually_exclusive_group(required=True)
121         group.add_argument('--osm-file',
122                            help='OSM file to be imported.')
123         group.add_argument('--continue', dest='continue_at',
124                            choices=['load-data', 'indexing', 'db-postprocess'],
125                            help='Continue an import that was interrupted')
126         group = parser.add_argument_group('Optional arguments')
127         group.add_argument('--osm2pgsql-cache', metavar='SIZE', type=int,
128                            help='Size of cache to be used by osm2pgsql (in MB)')
129         group.add_argument('--reverse-only', action='store_true',
130                            help='Do not create tables and indexes for searching')
131         group.add_argument('--enable-debug-statements', action='store_true',
132                            help='Include debug warning statements in SQL code')
133         group.add_argument('--no-partitions', action='store_true',
134                            help="""Do not partition search indices
135                                    (speeds up import of single country extracts)""")
136         group.add_argument('--no-updates', action='store_true',
137                            help="""Do not keep tables that are only needed for
138                                    updating the database later""")
139         group = parser.add_argument_group('Expert options')
140         group.add_argument('--ignore-errors', action='store_true',
141                            help='Continue import even when errors in SQL are present')
142         group.add_argument('--index-noanalyse', action='store_true',
143                            help='Do not perform analyse operations during index')
144
145
146     @staticmethod
147     def run(args):
148         params = ['setup.php']
149         if args.osm_file:
150             params.extend(('--all', '--osm-file', args.osm_file))
151         else:
152             if args.continue_at == 'load-data':
153                 params.append('--load-data')
154             if args.continue_at in ('load-data', 'indexing'):
155                 params.append('--index')
156             params.extend(('--create-search-indices', '--create-country-names',
157                            '--setup-website'))
158         if args.osm2pgsql_cache:
159             params.extend(('--osm2pgsql-cache', args.osm2pgsql_cache))
160         if args.reverse_only:
161             params.append('--reverse-only')
162         if args.enable_debug_statements:
163             params.append('--enable-debug-statements')
164         if args.no_partitions:
165             params.append('--no-partitions')
166         if args.no_updates:
167             params.append('--drop')
168         if args.ignore_errors:
169             params.append('--ignore-errors')
170         if args.index_noanalyse:
171             params.append('--index-noanalyse')
172
173         return run_legacy_script(*params, nominatim_env=args)
174
175
176 class SetupFreeze:
177     """\
178     Make database read-only.
179
180     About half of data in the Nominatim database is kept only to be able to
181     keep the data up-to-date with new changes made in OpenStreetMap. This
182     command drops all this data and only keeps the part needed for geocoding
183     itself.
184
185     This command has the same effect as the `--no-updates` option for imports.
186     """
187
188     @staticmethod
189     def add_args(parser):
190         pass # No options
191
192     @staticmethod
193     def run(args):
194         return run_legacy_script('setup.php', '--drop', nominatim_env=args)
195
196
197 class SetupSpecialPhrases:
198     """\
199     Maintain special phrases.
200     """
201
202     @staticmethod
203     def add_args(parser):
204         group = parser.add_argument_group('Input arguments')
205         group.add_argument('--from-wiki', action='store_true',
206                            help='Pull special phrases from the OSM wiki.')
207         group = parser.add_argument_group('Output arguments')
208         group.add_argument('-o', '--output', default='-',
209                            help="""File to write the preprocessed phrases to.
210                                    If omitted, it will be written to stdout.""")
211
212     @staticmethod
213     def run(args):
214         if args.output != '-':
215             raise NotImplementedError('Only output to stdout is currently implemented.')
216         return run_legacy_script('specialphrases.php', '--wiki-import', nominatim_env=args)
217
218
219 class UpdateAddData:
220     """\
221     Add additional data from a file or an online source.
222
223     Data is only imported, not indexed. You need to call `nominatim-update index`
224     to complete the process.
225     """
226
227     @staticmethod
228     def add_args(parser):
229         group_name = parser.add_argument_group('Source')
230         group = group_name.add_mutually_exclusive_group(required=True)
231         group.add_argument('--file', metavar='FILE',
232                            help='Import data from an OSM file')
233         group.add_argument('--diff', metavar='FILE',
234                            help='Import data from an OSM diff file')
235         group.add_argument('--node', metavar='ID', type=int,
236                            help='Import a single node from the API')
237         group.add_argument('--way', metavar='ID', type=int,
238                            help='Import a single way from the API')
239         group.add_argument('--relation', metavar='ID', type=int,
240                            help='Import a single relation from the API')
241         group.add_argument('--tiger-data', metavar='DIR',
242                            help='Add housenumbers from the US TIGER census database.')
243         group = parser.add_argument_group('Extra arguments')
244         group.add_argument('--use-main-api', action='store_true',
245                            help='Use OSM API instead of Overpass to download objects')
246
247     @staticmethod
248     def run(args):
249         if args.tiger_data:
250             os.environ['NOMINATIM_TIGER_DATA_PATH'] = args.tiger_data
251             return run_legacy_script('setup.php', '--import-tiger-data', nominatim_env=args)
252
253         params = ['update.php']
254         if args.file:
255             params.extend(('--import-file', args.file))
256         elif args.diff:
257             params.extend(('--import-diff', args.diff))
258         elif args.node:
259             params.extend(('--import-node', args.node))
260         elif args.way:
261             params.extend(('--import-way', args.way))
262         elif args.relation:
263             params.extend(('--import-relation', args.relation))
264         if args.use_main_api:
265             params.append('--use-main-api')
266         return run_legacy_script(*params, nominatim_env=args)
267
268
269 class QueryExport:
270     """\
271     Export addresses as CSV file from the database.
272     """
273
274     @staticmethod
275     def add_args(parser):
276         group = parser.add_argument_group('Output arguments')
277         group.add_argument('--output-type', default='street',
278                            choices=('continent', 'country', 'state', 'county',
279                                     'city', 'suburb', 'street', 'path'),
280                            help='Type of places to output (default: street)')
281         group.add_argument('--output-format',
282                            default='street;suburb;city;county;state;country',
283                            help="""Semicolon-separated list of address types
284                                    (see --output-type). Multiple ranks can be
285                                    merged into one column by simply using a
286                                    comma-separated list.""")
287         group.add_argument('--output-all-postcodes', action='store_true',
288                            help="""List all postcodes for address instead of
289                                    just the most likely one""")
290         group.add_argument('--language',
291                            help="""Preferred language for output
292                                    (use local name, if omitted)""")
293         group = parser.add_argument_group('Filter arguments')
294         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
295                            help='Export only objects within country')
296         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
297                            help='Export only children of this OSM node')
298         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
299                            help='Export only children of this OSM way')
300         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
301                            help='Export only children of this OSM relation')
302
303
304     @staticmethod
305     def run(args):
306         params = ['export.php',
307                   '--output-type', args.output_type,
308                   '--output-format', args.output_format]
309         if args.output_all_postcodes:
310             params.append('--output-all-postcodes')
311         if args.language:
312             params.extend(('--language', args.language))
313         if args.restrict_to_country:
314             params.extend(('--restrict-to-country', args.restrict_to_country))
315         if args.restrict_to_osm_node:
316             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
317         if args.restrict_to_osm_way:
318             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
319         if args.restrict_to_osm_relation:
320             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
321
322         return run_legacy_script(*params, nominatim_env=args)
323
324
325 class AdminServe:
326     """\
327     Start a simple web server for serving the API.
328
329     This command starts the built-in PHP webserver to serve the website
330     from the current project directory. This webserver is only suitable
331     for testing and develop. Do not use it in production setups!
332
333     By the default, the webserver can be accessed at: http://127.0.0.1:8088
334     """
335
336     @staticmethod
337     def add_args(parser):
338         group = parser.add_argument_group('Server arguments')
339         group.add_argument('--server', default='127.0.0.1:8088',
340                            help='The address the server will listen to.')
341
342     @staticmethod
343     def run(args):
344         run_php_server(args.server, args.project_dir / 'website')
345
346
347 def nominatim(**kwargs):
348     """\
349     Command-line tools for importing, updating, administrating and
350     querying the Nominatim database.
351     """
352     parser = CommandlineParser('nominatim', nominatim.__doc__)
353
354     parser.add_subcommand('import', SetupAll)
355     parser.add_subcommand('freeze', SetupFreeze)
356     parser.add_subcommand('replication', clicmd.UpdateReplication)
357
358     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
359
360     parser.add_subcommand('add-data', UpdateAddData)
361     parser.add_subcommand('index', clicmd.UpdateIndex)
362     parser.add_subcommand('refresh', clicmd.UpdateRefresh)
363
364     parser.add_subcommand('admin', clicmd.AdminFuncs)
365
366     parser.add_subcommand('export', QueryExport)
367     parser.add_subcommand('serve', AdminServe)
368
369     if kwargs.get('phpcgi_path'):
370         parser.add_subcommand('search', clicmd.APISearch)
371         parser.add_subcommand('reverse', clicmd.APIReverse)
372         parser.add_subcommand('lookup', clicmd.APILookup)
373         parser.add_subcommand('details', clicmd.APIDetails)
374         parser.add_subcommand('status', clicmd.APIStatus)
375     else:
376         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
377
378     return parser.run(**kwargs)