]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/cli.py
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / src / nominatim_db / cli.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 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 from typing import Optional, List, Mapping
12 import importlib
13 import logging
14 import sys
15 import argparse
16 import asyncio
17 from pathlib import Path
18
19 from .config import Configuration
20 from .errors import UsageError
21 from . import clicmd
22 from . import version
23 from .clicmd.args import NominatimArgs, Subcommand
24
25 LOG = logging.getLogger()
26
27
28 class CommandlineParser:
29     """ Wraps some of the common functions for parsing the command line
30         and setting up subcommands.
31     """
32     def __init__(self, prog: str, description: Optional[str]):
33         self.parser = argparse.ArgumentParser(
34             prog=prog,
35             description=description,
36             formatter_class=argparse.RawDescriptionHelpFormatter)
37
38         self.subs = self.parser.add_subparsers(title='available commands',
39                                                dest='subcommand')
40
41         # Global arguments that only work if no sub-command given
42         self.parser.add_argument('--version', action='store_true',
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     def nominatim_version_text(self) -> str:
61         """ Program name and version number as string
62         """
63         text = f'Nominatim version {version.NOMINATIM_VERSION!s}'
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: str, cmd: Subcommand) -> None:
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         assert cmd.__doc__ is not None
74
75         parser = self.subs.add_parser(name, parents=[self.default_args],
76                                       help=cmd.__doc__.split('\n', 1)[0],
77                                       description=cmd.__doc__,
78                                       formatter_class=argparse.RawDescriptionHelpFormatter,
79                                       add_help=False)
80         parser.set_defaults(command=cmd)
81         cmd.add_args(parser)
82
83     def run(self, cli_args: Optional[List[str]],
84             environ: Optional[Mapping[str, str]]) -> int:
85         """ Parse the command line arguments of the program and execute the
86             appropriate subcommand.
87         """
88         args = NominatimArgs()
89         try:
90             self.parser.parse_args(args=cli_args, namespace=args)
91         except SystemExit:
92             return 1
93
94         if args.version:
95             print(self.nominatim_version_text())
96             return 0
97
98         if args.subcommand is None:
99             self.parser.print_help()
100             return 1
101
102         args.project_dir = Path(args.project_dir).resolve()
103
104         if cli_args is None:
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, environ=environ)
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 class AdminServe:
137     """\
138     Start a simple web server for serving the API.
139
140     This command starts a built-in webserver to serve the website
141     from the current project directory. This webserver is only suitable
142     for testing and development. Do not use it in production setups!
143
144     There are two different webserver implementations for Python available:
145     falcon (the default) and starlette. You need to make sure the
146     appropriate Python packages as well as the uvicorn package are
147     installed to use this function.
148
149     By the default, the webserver can be accessed at: http://127.0.0.1:8088
150     """
151
152     def add_args(self, parser: argparse.ArgumentParser) -> None:
153         group = parser.add_argument_group('Server arguments')
154         group.add_argument('--server', default='127.0.0.1:8088',
155                            help='The address the server will listen to.')
156         group.add_argument('--engine', default='falcon',
157                            choices=('falcon', 'starlette'),
158                            help='Webserver framework to run. (default: falcon)')
159
160     def run(self, args: NominatimArgs) -> int:
161         asyncio.run(self.run_uvicorn(args))
162
163         return 0
164
165     async def run_uvicorn(self, args: NominatimArgs) -> None:
166         import uvicorn
167
168         server_info = args.server.split(':', 1)
169         host = server_info[0]
170         if len(server_info) > 1:
171             if not server_info[1].isdigit():
172                 raise UsageError('Invalid format for --server parameter. Use <host>:<port>')
173             port = int(server_info[1])
174         else:
175             port = 8088
176
177         server_module = importlib.import_module(f'nominatim_api.server.{args.engine}.server')
178
179         app = server_module.get_application(args.project_dir)
180
181         config = uvicorn.Config(app, host=host, port=port)
182         server = uvicorn.Server(config)
183         await server.serve()
184
185
186 def get_set_parser() -> CommandlineParser:
187     """\
188     Initializes the parser and adds various subcommands for
189     nominatim cli.
190     """
191     parser = CommandlineParser('nominatim', nominatim.__doc__)
192
193     parser.add_subcommand('import', clicmd.SetupAll())
194     parser.add_subcommand('freeze', clicmd.SetupFreeze())
195     parser.add_subcommand('replication', clicmd.UpdateReplication())
196
197     parser.add_subcommand('special-phrases', clicmd.ImportSpecialPhrases())
198
199     parser.add_subcommand('add-data', clicmd.UpdateAddData())
200     parser.add_subcommand('index', clicmd.UpdateIndex())
201     parser.add_subcommand('refresh', clicmd.UpdateRefresh())
202
203     parser.add_subcommand('admin', clicmd.AdminFuncs())
204
205     try:
206         exportcmd = importlib.import_module('nominatim_db.clicmd.export')
207         apicmd = importlib.import_module('nominatim_db.clicmd.api')
208         convertcmd = importlib.import_module('nominatim_db.clicmd.convert')
209
210         parser.add_subcommand('export', exportcmd.QueryExport())
211         parser.add_subcommand('convert', convertcmd.ConvertDB())
212         parser.add_subcommand('serve', AdminServe())
213
214         parser.add_subcommand('search', apicmd.APISearch())
215         parser.add_subcommand('reverse', apicmd.APIReverse())
216         parser.add_subcommand('lookup', apicmd.APILookup())
217         parser.add_subcommand('details', apicmd.APIDetails())
218         parser.add_subcommand('status', apicmd.APIStatus())
219     except ModuleNotFoundError as ex:
220         if not ex.name or 'nominatim_api' not in ex.name:
221             raise ex
222
223         parser.parser.epilog = \
224             f'\n\nNominatim API package not found (was looking for module: {ex.name}).'\
225             '\nThe following commands are not available:'\
226             '\n    export, convert, serve, search, reverse, lookup, details, status'\
227             "\n\nRun 'pip install nominatim-api' to install the package."
228
229     return parser
230
231
232 def nominatim(cli_args: Optional[List[str]] = None,
233               environ: Optional[Mapping[str, str]] = None) -> int:
234     """\
235     Command-line tools for importing, updating, administrating and
236     querying the Nominatim database.
237
238     'cli_args' is a list of parameters for the command to run. If not given,
239     sys.args will be used.
240
241     'environ' is the dictionary of environment variables containing the
242     Nominatim configuration. When None, the os.environ is inherited.
243     """
244     return get_set_parser().run(cli_args=cli_args, environ=environ)