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