]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
845b1451db6455d15b661637e6311d0e82b5fc16
[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 QueryExport:
269     """\
270     Export addresses as CSV file from the database.
271     """
272
273     @staticmethod
274     def add_args(parser):
275         group = parser.add_argument_group('Output arguments')
276         group.add_argument('--output-type', default='street',
277                            choices=('continent', 'country', 'state', 'county',
278                                     'city', 'suburb', 'street', 'path'),
279                            help='Type of places to output (default: street)')
280         group.add_argument('--output-format',
281                            default='street;suburb;city;county;state;country',
282                            help="""Semicolon-separated list of address types
283                                    (see --output-type). Multiple ranks can be
284                                    merged into one column by simply using a
285                                    comma-separated list.""")
286         group.add_argument('--output-all-postcodes', action='store_true',
287                            help="""List all postcodes for address instead of
288                                    just the most likely one""")
289         group.add_argument('--language',
290                            help="""Preferred language for output
291                                    (use local name, if omitted)""")
292         group = parser.add_argument_group('Filter arguments')
293         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
294                            help='Export only objects within country')
295         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
296                            help='Export only children of this OSM node')
297         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
298                            help='Export only children of this OSM way')
299         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
300                            help='Export only children of this OSM relation')
301
302
303     @staticmethod
304     def run(args):
305         params = ['export.php',
306                   '--output-type', args.output_type,
307                   '--output-format', args.output_format]
308         if args.output_all_postcodes:
309             params.append('--output-all-postcodes')
310         if args.language:
311             params.extend(('--language', args.language))
312         if args.restrict_to_country:
313             params.extend(('--restrict-to-country', args.restrict_to_country))
314         if args.restrict_to_osm_node:
315             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
316         if args.restrict_to_osm_way:
317             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
318         if args.restrict_to_osm_relation:
319             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
320
321         return run_legacy_script(*params, nominatim_env=args)
322
323
324 class AdminServe:
325     """\
326     Start a simple web server for serving the API.
327
328     This command starts the built-in PHP webserver to serve the website
329     from the current project directory. This webserver is only suitable
330     for testing and develop. Do not use it in production setups!
331
332     By the default, the webserver can be accessed at: http://127.0.0.1:8088
333     """
334
335     @staticmethod
336     def add_args(parser):
337         group = parser.add_argument_group('Server arguments')
338         group.add_argument('--server', default='127.0.0.1:8088',
339                            help='The address the server will listen to.')
340
341     @staticmethod
342     def run(args):
343         run_php_server(args.server, args.project_dir / 'website')
344
345
346 def nominatim(**kwargs):
347     """\
348     Command-line tools for importing, updating, administrating and
349     querying the Nominatim database.
350     """
351     parser = CommandlineParser('nominatim', nominatim.__doc__)
352
353     parser.add_subcommand('import', SetupAll)
354     parser.add_subcommand('freeze', SetupFreeze)
355     parser.add_subcommand('replication', clicmd.UpdateReplication)
356
357     parser.add_subcommand('special-phrases', SetupSpecialPhrases)
358
359     parser.add_subcommand('add-data', UpdateAddData)
360     parser.add_subcommand('index', clicmd.UpdateIndex)
361     parser.add_subcommand('refresh', clicmd.UpdateRefresh)
362
363     parser.add_subcommand('admin', clicmd.AdminFuncs)
364
365     parser.add_subcommand('export', QueryExport)
366     parser.add_subcommand('serve', AdminServe)
367
368     if kwargs.get('phpcgi_path'):
369         parser.add_subcommand('search', clicmd.APISearch)
370         parser.add_subcommand('reverse', clicmd.APIReverse)
371         parser.add_subcommand('lookup', clicmd.APILookup)
372         parser.add_subcommand('details', clicmd.APIDetails)
373         parser.add_subcommand('status', clicmd.APIStatus)
374     else:
375         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
376
377     return parser.run(**kwargs)