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