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