]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/cli.py
move complex typing annotations to extra file
[nominatim.git] / nominatim / cli.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Command-line interface to the Nominatim functions for import, update,
9 database administration and querying.
10 """
11 import logging
12 import os
13 import sys
14 import argparse
15 from pathlib import Path
16
17 from nominatim.config import Configuration
18 from nominatim.tools.exec_utils import run_legacy_script, run_php_server
19 from nominatim.errors import UsageError
20 from nominatim import clicmd
21 from nominatim import version
22 from nominatim.clicmd.args import NominatimArgs
23
24 LOG = logging.getLogger()
25
26
27 class CommandlineParser:
28     """ Wraps some of the common functions for parsing the command line
29         and setting up subcommands.
30     """
31     def __init__(self, prog, description):
32         self.parser = argparse.ArgumentParser(
33             prog=prog,
34             description=description,
35             formatter_class=argparse.RawDescriptionHelpFormatter)
36
37         self.subs = self.parser.add_subparsers(title='available commands',
38                                                dest='subcommand')
39
40         # Global arguments that only work if no sub-command given
41         self.parser.add_argument('--version', action='store_true',
42                                  help='Print Nominatim version and exit')
43
44         # Arguments added to every sub-command
45         self.default_args = argparse.ArgumentParser(add_help=False)
46         group = self.default_args.add_argument_group('Default arguments')
47         group.add_argument('-h', '--help', action='help',
48                            help='Show this help message and exit')
49         group.add_argument('-q', '--quiet', action='store_const', const=0,
50                            dest='verbose', default=1,
51                            help='Print only error messages')
52         group.add_argument('-v', '--verbose', action='count', default=1,
53                            help='Increase verboseness of output')
54         group.add_argument('--project-dir', metavar='DIR', default='.',
55                            help='Base directory of the Nominatim installation (default:.)')
56         group.add_argument('-j', '--threads', metavar='NUM', type=int,
57                            help='Number of parallel threads to use')
58
59     @staticmethod
60     def nominatim_version_text():
61         """ Program name and version number as string
62         """
63         text = f'Nominatim version {version.version_str()}'
64         if version.GIT_COMMIT_HASH is not None:
65             text += f' ({version.GIT_COMMIT_HASH})'
66         return text
67
68     def add_subcommand(self, name, cmd):
69         """ Add a subcommand to the parser. The subcommand must be a class
70             with a function add_args() that adds the parameters for the
71             subcommand and a run() function that executes the command.
72         """
73         parser = self.subs.add_parser(name, parents=[self.default_args],
74                                       help=cmd.__doc__.split('\n', 1)[0],
75                                       description=cmd.__doc__,
76                                       formatter_class=argparse.RawDescriptionHelpFormatter,
77                                       add_help=False)
78         parser.set_defaults(command=cmd)
79         cmd.add_args(parser)
80
81     def run(self, **kwargs):
82         """ Parse the command line arguments of the program and execute the
83             appropriate subcommand.
84         """
85         args = NominatimArgs()
86         try:
87             self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args)
88         except SystemExit:
89             return 1
90
91         if args.version:
92             print(CommandlineParser.nominatim_version_text())
93             return 0
94
95         if args.subcommand is None:
96             self.parser.print_help()
97             return 1
98
99         for arg in ('module_dir', 'osm2pgsql_path', 'phplib_dir', 'sqllib_dir',
100                     'data_dir', 'config_dir', 'phpcgi_path'):
101             setattr(args, arg, Path(kwargs[arg]))
102         args.project_dir = Path(args.project_dir).resolve()
103
104         if 'cli_args' not in kwargs:
105             logging.basicConfig(stream=sys.stderr,
106                                 format='%(asctime)s: %(message)s',
107                                 datefmt='%Y-%m-%d %H:%M:%S',
108                                 level=max(4 - args.verbose, 1) * 10)
109
110         args.config = Configuration(args.project_dir, args.config_dir,
111                                     environ=kwargs.get('environ', os.environ))
112         args.config.set_libdirs(module=args.module_dir,
113                                 osm2pgsql=args.osm2pgsql_path,
114                                 php=args.phplib_dir,
115                                 sql=args.sqllib_dir,
116                                 data=args.data_dir)
117
118         log = logging.getLogger()
119         log.warning('Using project directory: %s', str(args.project_dir))
120
121         try:
122             return args.command.run(args)
123         except UsageError as exception:
124             if log.isEnabledFor(logging.DEBUG):
125                 raise # use Python's exception printing
126             log.fatal('FATAL: %s', exception)
127
128         # If we get here, then execution has failed in some way.
129         return 1
130
131
132 # Subcommand classes
133 #
134 # Each class needs to implement two functions: add_args() adds the CLI parameters
135 # for the subfunction, run() executes the subcommand.
136 #
137 # The class documentation doubles as the help text for the command. The
138 # first line is also used in the summary when calling the program without
139 # a subcommand.
140 #
141 # No need to document the functions each time.
142 # pylint: disable=C0111
143 class QueryExport:
144     """\
145     Export addresses as CSV file from the database.
146     """
147
148     @staticmethod
149     def add_args(parser):
150         group = parser.add_argument_group('Output arguments')
151         group.add_argument('--output-type', default='street',
152                            choices=('continent', 'country', 'state', 'county',
153                                     'city', 'suburb', 'street', 'path'),
154                            help='Type of places to output (default: street)')
155         group.add_argument('--output-format',
156                            default='street;suburb;city;county;state;country',
157                            help=("Semicolon-separated list of address types "
158                                  "(see --output-type). Multiple ranks can be "
159                                  "merged into one column by simply using a "
160                                  "comma-separated list."))
161         group.add_argument('--output-all-postcodes', action='store_true',
162                            help=("List all postcodes for address instead of "
163                                  "just the most likely one"))
164         group.add_argument('--language',
165                            help=("Preferred language for output "
166                                  "(use local name, if omitted)"))
167         group = parser.add_argument_group('Filter arguments')
168         group.add_argument('--restrict-to-country', metavar='COUNTRY_CODE',
169                            help='Export only objects within country')
170         group.add_argument('--restrict-to-osm-node', metavar='ID', type=int,
171                            help='Export only children of this OSM node')
172         group.add_argument('--restrict-to-osm-way', metavar='ID', type=int,
173                            help='Export only children of this OSM way')
174         group.add_argument('--restrict-to-osm-relation', metavar='ID', type=int,
175                            help='Export only children of this OSM relation')
176
177
178     @staticmethod
179     def run(args):
180         params = ['export.php',
181                   '--output-type', args.output_type,
182                   '--output-format', args.output_format]
183         if args.output_all_postcodes:
184             params.append('--output-all-postcodes')
185         if args.language:
186             params.extend(('--language', args.language))
187         if args.restrict_to_country:
188             params.extend(('--restrict-to-country', args.restrict_to_country))
189         if args.restrict_to_osm_node:
190             params.extend(('--restrict-to-osm-node', args.restrict_to_osm_node))
191         if args.restrict_to_osm_way:
192             params.extend(('--restrict-to-osm-way', args.restrict_to_osm_way))
193         if args.restrict_to_osm_relation:
194             params.extend(('--restrict-to-osm-relation', args.restrict_to_osm_relation))
195
196         return run_legacy_script(*params, nominatim_env=args)
197
198
199 class AdminServe:
200     """\
201     Start a simple web server for serving the API.
202
203     This command starts the built-in PHP webserver to serve the website
204     from the current project directory. This webserver is only suitable
205     for testing and development. Do not use it in production setups!
206
207     By the default, the webserver can be accessed at: http://127.0.0.1:8088
208     """
209
210     @staticmethod
211     def add_args(parser):
212         group = parser.add_argument_group('Server arguments')
213         group.add_argument('--server', default='127.0.0.1:8088',
214                            help='The address the server will listen to.')
215
216     @staticmethod
217     def run(args):
218         run_php_server(args.server, args.project_dir / 'website')
219
220 def get_set_parser(**kwargs):
221     """\
222     Initializes the parser and adds various subcommands for
223     nominatim cli.
224     """
225     parser = CommandlineParser('nominatim', nominatim.__doc__)
226
227     parser.add_subcommand('import', clicmd.SetupAll)
228     parser.add_subcommand('freeze', clicmd.SetupFreeze)
229     parser.add_subcommand('replication', clicmd.UpdateReplication)
230
231     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases)
232
233     parser.add_subcommand('add-data', clicmd.UpdateAddData)
234     parser.add_subcommand('index', clicmd.UpdateIndex)
235     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
236
237     parser.add_subcommand('admin', clicmd.AdminFuncs)
238
239     parser.add_subcommand('export', QueryExport)
240     parser.add_subcommand('serve', AdminServe)
241
242     if kwargs.get('phpcgi_path'):
243         parser.add_subcommand('search', clicmd.APISearch)
244         parser.add_subcommand('reverse', clicmd.APIReverse)
245         parser.add_subcommand('lookup', clicmd.APILookup)
246         parser.add_subcommand('details', clicmd.APIDetails)
247         parser.add_subcommand('status', clicmd.APIStatus)
248     else:
249         parser.parser.epilog = 'php-cgi not found. Query commands not available.'
250
251     return parser
252
253
254 def nominatim(**kwargs):
255     """\
256     Command-line tools for importing, updating, administrating and
257     querying the Nominatim database.
258     """
259     parser = get_set_parser(**kwargs)
260
261     return parser.run(**kwargs)