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